From f385eebc992049bfdce231d2c92d6b40f525c246 Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Sun, 22 Feb 2026 23:01:52 +0300 Subject: [PATCH 01/12] tests: initial benchmarking setup Signed-off-by: NotAShelf Change-Id: If0ed2dd4279abf155a8ddc678ca047736a6a6964 --- tests/benchmark/large.nix | 47 +++++++++++++++++++++ tests/benchmark/medium.nix | 31 ++++++++++++++ tests/benchmark/run_benchmarks.sh | 69 +++++++++++++++++++++++++++++++ tests/benchmark/simple.nix | 13 ++++++ 4 files changed, 160 insertions(+) create mode 100644 tests/benchmark/large.nix create mode 100644 tests/benchmark/medium.nix create mode 100755 tests/benchmark/run_benchmarks.sh create mode 100644 tests/benchmark/simple.nix diff --git a/tests/benchmark/large.nix b/tests/benchmark/large.nix new file mode 100644 index 0000000..ffd7e22 --- /dev/null +++ b/tests/benchmark/large.nix @@ -0,0 +1,47 @@ +# Large :b:oke ---I mean...benchmark, for stress testing +let + # Generate large list + range = start: end: + if start >= end + then [] + else [start] ++ range (start + 1) end; + + # Deep nesting + deepNest = {a = {b = {c = {d = {e = {f = 42;};};};};};}; + + # Large attrset + largeAttrs = { + a1 = 1; + a2 = 2; + a3 = 3; + a4 = 4; + a5 = 5; + a6 = 6; + a7 = 7; + a8 = 8; + a9 = 9; + a10 = 10; + b1 = 11; + b2 = 12; + b3 = 13; + b4 = 14; + b5 = 15; + b6 = 16; + b7 = 17; + b8 = 18; + b9 = 19; + b10 = 20; + }; + + # Recursive attrset + recursive = rec { + x = 10; + y = x * 2; + z = y + x; + result = z * 3; + }; +in { + list_100 = range 1 100; + deep = deepNest.a.b.c.d.e.f; + inherit largeAttrs recursive; +} diff --git a/tests/benchmark/medium.nix b/tests/benchmark/medium.nix new file mode 100644 index 0000000..f8db9e9 --- /dev/null +++ b/tests/benchmark/medium.nix @@ -0,0 +1,31 @@ +let + # Recursive fibonacci (not memoized) + fib = n: + if n <= 1 + then n + else fib (n - 1) + fib (n - 2); + + # List operations + numbers = [1 2 3 4 5 6 7 8 9 10]; + doubled = builtins.map (x: x * 2) numbers; + + # Attrset operations + base = { + a = 1; + b = 2; + c = 3; + }; + extended = + base + // { + d = 4; + e = 5; + }; + + # String operations + greeting = "Hello"; + message = "${greeting}, World!"; +in { + fibonacci_10 = fib 10; + inherit doubled extended message; +} diff --git a/tests/benchmark/run_benchmarks.sh b/tests/benchmark/run_benchmarks.sh new file mode 100755 index 0000000..49c9ba8 --- /dev/null +++ b/tests/benchmark/run_benchmarks.sh @@ -0,0 +1,69 @@ +#!/usr/bin/env bash +set -e + +echo "" + +PLUGIN_PATH="$(pwd)/build/nix-ir-plugin.so" +BENCH_DIR="$(pwd)/tests/benchmark" +IRC_BIN="$(pwd)/build/nix-irc" + +# Colors for output +GREEN='\033[0;32m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +run_benchmark() { + local name="$1" + local file="$2" + + echo -e "${BLUE}Benchmark: $name${NC}" + echo "----------------------------------------" + + # 1. Parse + Compile Time (nix-irc) + echo -n " Parse + Compile: " + local compile_output=$( (time "$IRC_BIN" "$file" /tmp/bench.nixir 2>&1) 2>&1) + local compile_time=$(echo "$compile_output" | grep "real" | awk '{print $2}') + echo -e "${GREEN}$compile_time${NC}" + + # 2. IR Size + if [ -f /tmp/bench.nixir ]; then + local ir_size=$(stat -f%z /tmp/bench.nixir 2>/dev/null || stat -c%s /tmp/bench.nixir 2>/dev/null) + echo -e " IR Bundle Size: ${GREEN}${ir_size} bytes${NC}" + fi + + # 3. Native Nix evaluation time + echo -n " Native Eval: " + local native_time=$( (time nix-instantiate --eval --strict "$file" >/dev/null 2>&1) 2>&1 | grep "real" | awk '{print $2}') + echo -e "${GREEN}$native_time${NC}" + + # 4. With Plugin evaluation time + echo -n " Plugin Eval: " + local plugin_time=$( (time nix-instantiate --plugin-files "$PLUGIN_PATH" --eval --strict "$file" >/dev/null 2>&1) 2>&1 | grep "real" | awk '{print $2}') + echo -e "${GREEN}$plugin_time${NC}" + + echo "" +} + +echo "# Running benchmarks..." +echo "" + +run_benchmark "Simple Expressions" "$BENCH_DIR/simple.nix" +run_benchmark "Medium Complexity" "$BENCH_DIR/medium.nix" +run_benchmark "Large/Complex" "$BENCH_DIR/large.nix" + +# File size comparison +echo -e "${BLUE}File Size Comparison${NC}" +echo "----------------------------------------" +testdir=$(mktemp -d) + +for f in "$BENCH_DIR"/*.nix; do + nixsize=$(stat -c%s "$f" 2>/dev/null || stat -f%z "$f" 2>/dev/null) + base=$(basename "$f" .nix) + irfile="${testdir}/${base}.nixir" + $IRC_BIN "$f" "$irfile" >/dev/null 2>&1 + if [ -f "$irfile" ]; then + irsize=$(stat -c%s "$irfile" 2>/dev/null || stat -f%z "$irfile" 2>/dev/null) + ratio=$((irsize * 100 / nixsize)) + echo " $base: ${nixsize}B => ${irsize}B (${ratio}% of source)" + fi +done diff --git a/tests/benchmark/simple.nix b/tests/benchmark/simple.nix new file mode 100644 index 0000000..aec2fc9 --- /dev/null +++ b/tests/benchmark/simple.nix @@ -0,0 +1,13 @@ +let + x = 10; + y = 20; + z = x + y; +in { + result = z * 2; + list = [1 2 3 4 5]; + attrs = { + a = 1; + b = 2; + c = 3; + }; +} From 68873352f9e11239f010db50e482ed0cf0653915 Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Sun, 22 Feb 2026 23:27:57 +0300 Subject: [PATCH 02/12] tests/benchmark: rename runner script; compare compilation with native eval Signed-off-by: NotAShelf Change-Id: I6ef30732f875ab134a35282eb2cd66a36a6a6964 --- tests/benchmark/run.sh | 119 ++++++++++++++++++++++++++++++ tests/benchmark/run_benchmarks.sh | 69 ----------------- 2 files changed, 119 insertions(+), 69 deletions(-) create mode 100755 tests/benchmark/run.sh delete mode 100755 tests/benchmark/run_benchmarks.sh diff --git a/tests/benchmark/run.sh b/tests/benchmark/run.sh new file mode 100755 index 0000000..d59b497 --- /dev/null +++ b/tests/benchmark/run.sh @@ -0,0 +1,119 @@ +#!/usr/bin/env bash +set -e + +echo "# Running benchmarks..." +echo "" + +BENCH_DIR="$(pwd)/tests/benchmark" +IRC_BIN="$(pwd)/build/nix-irc" + +GREEN='\033[0;32m' +BLUE='\033[0;34m' +YELLOW='\033[0;33m' +NC='\033[0m' + +get_ms() { + local time_str="$1" + if [[ $time_str =~ ([0-9]+)m([0-9.]+)s ]]; then + local mins="${BASH_REMATCH[1]}" + local secs="${BASH_REMATCH[2]}" + local ms + ms=$(awk "BEGIN {printf \"%.1f\", ($mins * 60000) + ($secs * 1000)}") + echo "$ms" + else + echo "0" + fi +} + +run_benchmark() { + local name="$1" + local file="$2" + + echo -e "${BLUE}=== $name ===${NC}" + echo "" + + # Measure compilation time + echo -n " Compilation time: " + local compile_start + compile_start=$(date +%s%N) + "$IRC_BIN" "$file" /tmp/bench.nixir >/dev/null 2>&1 + local compile_end + compile_end=$(date +%s%N) + local compile_ms=$(((compile_end - compile_start) / 1000000)) + echo -e "${YELLOW}${compile_ms}ms${NC}" + + # Source and IR sizes + local src_size + src_size=$(stat -c%s "$file" 2>/dev/null || stat -f%z "$file" 2>/dev/null) + local ir_size + ir_size=$(stat -c%s /tmp/bench.nixir 2>/dev/null || stat -f%z /tmp/bench.nixir 2>/dev/null) + local ratio=$((ir_size * 100 / src_size)) + echo -e " Source size: ${src_size}B" + echo -e " IR bundle size: ${ir_size}B (${ratio}% of source)" + + echo "" + + # Native Nix evaluation (baseline) + echo -n " Native Nix eval: " + local native_total=0 + for _ in {1..5}; do + local t + t=$( (time nix-instantiate --eval --strict "$file" >/dev/null 2>&1) 2>&1 | grep "real" | awk '{print $2}') + local ms + ms=$(get_ms "$t") + native_total=$(awk "BEGIN {print $native_total + $ms}") + done + local native_avg + native_avg=$(awk "BEGIN {printf \"%.1f\", $native_total / 5}") + echo -e "${GREEN}${native_avg}ms${NC} avg (5 runs)" + + echo "" +} + +echo "Measuring IR compilation speed and bundle size characteristics." +echo "" + +run_benchmark "Simple Expression" "$BENCH_DIR/simple.nix" +run_benchmark "Medium Complexity" "$BENCH_DIR/medium.nix" +run_benchmark "Large Expression" "$BENCH_DIR/large.nix" + +# Overall statistics +echo -e "${BLUE}=== Overall Statistics ===${NC}" +echo "" + +testdir=$(mktemp -d) +total_nix=0 +total_ir=0 +total_compile_time=0 + +for f in "$BENCH_DIR"/*.nix; do + nixsize=$(stat -c%s "$f" 2>/dev/null || stat -f%z "$f" 2>/dev/null) + base=$(basename "$f" .nix) + irfile="${testdir}/${base}.nixir" + + start=$(date +%s%N) + "$IRC_BIN" "$f" "$irfile" >/dev/null 2>&1 + end=$(date +%s%N) + compile_time=$(((end - start) / 1000000)) + + if [ -f "$irfile" ]; then + irsize=$(stat -c%s "$irfile" 2>/dev/null || stat -f%z "$irfile" 2>/dev/null) + total_nix=$((total_nix + nixsize)) + total_ir=$((total_ir + irsize)) + total_compile_time=$((total_compile_time + compile_time)) + fi +done + +total_ratio=$((total_ir * 100 / total_nix)) +avg_compile_time=$((total_compile_time / 3)) + +# TBH those are entirely unnecessary. However, I'm a sucker for data +# and those are trivial to compile. Might as well. Who knows, maybe it'll +# come in handy in the future. +echo " Total source size: ${total_nix}B" +echo " Total IR size: ${total_ir}B" +echo " Compression ratio: ${total_ratio}% of source" +echo " Average compile time: ${avg_compile_time}ms" +echo "" + +rm -rf "$testdir" diff --git a/tests/benchmark/run_benchmarks.sh b/tests/benchmark/run_benchmarks.sh deleted file mode 100755 index 49c9ba8..0000000 --- a/tests/benchmark/run_benchmarks.sh +++ /dev/null @@ -1,69 +0,0 @@ -#!/usr/bin/env bash -set -e - -echo "" - -PLUGIN_PATH="$(pwd)/build/nix-ir-plugin.so" -BENCH_DIR="$(pwd)/tests/benchmark" -IRC_BIN="$(pwd)/build/nix-irc" - -# Colors for output -GREEN='\033[0;32m' -BLUE='\033[0;34m' -NC='\033[0m' # No Color - -run_benchmark() { - local name="$1" - local file="$2" - - echo -e "${BLUE}Benchmark: $name${NC}" - echo "----------------------------------------" - - # 1. Parse + Compile Time (nix-irc) - echo -n " Parse + Compile: " - local compile_output=$( (time "$IRC_BIN" "$file" /tmp/bench.nixir 2>&1) 2>&1) - local compile_time=$(echo "$compile_output" | grep "real" | awk '{print $2}') - echo -e "${GREEN}$compile_time${NC}" - - # 2. IR Size - if [ -f /tmp/bench.nixir ]; then - local ir_size=$(stat -f%z /tmp/bench.nixir 2>/dev/null || stat -c%s /tmp/bench.nixir 2>/dev/null) - echo -e " IR Bundle Size: ${GREEN}${ir_size} bytes${NC}" - fi - - # 3. Native Nix evaluation time - echo -n " Native Eval: " - local native_time=$( (time nix-instantiate --eval --strict "$file" >/dev/null 2>&1) 2>&1 | grep "real" | awk '{print $2}') - echo -e "${GREEN}$native_time${NC}" - - # 4. With Plugin evaluation time - echo -n " Plugin Eval: " - local plugin_time=$( (time nix-instantiate --plugin-files "$PLUGIN_PATH" --eval --strict "$file" >/dev/null 2>&1) 2>&1 | grep "real" | awk '{print $2}') - echo -e "${GREEN}$plugin_time${NC}" - - echo "" -} - -echo "# Running benchmarks..." -echo "" - -run_benchmark "Simple Expressions" "$BENCH_DIR/simple.nix" -run_benchmark "Medium Complexity" "$BENCH_DIR/medium.nix" -run_benchmark "Large/Complex" "$BENCH_DIR/large.nix" - -# File size comparison -echo -e "${BLUE}File Size Comparison${NC}" -echo "----------------------------------------" -testdir=$(mktemp -d) - -for f in "$BENCH_DIR"/*.nix; do - nixsize=$(stat -c%s "$f" 2>/dev/null || stat -f%z "$f" 2>/dev/null) - base=$(basename "$f" .nix) - irfile="${testdir}/${base}.nixir" - $IRC_BIN "$f" "$irfile" >/dev/null 2>&1 - if [ -f "$irfile" ]; then - irsize=$(stat -c%s "$irfile" 2>/dev/null || stat -f%z "$irfile" 2>/dev/null) - ratio=$((irsize * 100 / nixsize)) - echo " $base: ${nixsize}B => ${irsize}B (${ratio}% of source)" - fi -done From 347175bb86dcf0e8f4496ff57410b749e91c1cd3 Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Sun, 22 Feb 2026 23:47:41 +0300 Subject: [PATCH 03/12] tests/benchmark: fine-grain timing reports Signed-off-by: NotAShelf Change-Id: Ia481b0129193540636665340bd257d136a6a6964 --- tests/benchmark/run.sh | 40 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 38 insertions(+), 2 deletions(-) diff --git a/tests/benchmark/run.sh b/tests/benchmark/run.sh index d59b497..8e43fe8 100755 --- a/tests/benchmark/run.sh +++ b/tests/benchmark/run.sh @@ -32,8 +32,8 @@ run_benchmark() { echo -e "${BLUE}=== $name ===${NC}" echo "" - # Measure compilation time - echo -n " Compilation time: " + # Measure compilation time only + echo -n " Compilation only: " local compile_start compile_start=$(date +%s%N) "$IRC_BIN" "$file" /tmp/bench.nixir >/dev/null 2>&1 @@ -42,6 +42,42 @@ run_benchmark() { local compile_ms=$(((compile_end - compile_start) / 1000000)) echo -e "${YELLOW}${compile_ms}ms${NC}" + # Measure IR loading only (deserialization + evaluation) + echo -n " IR load only: " + PLUGIN_PATH="$(pwd)/build/nix-ir-plugin.so" + if [ ! -f "$PLUGIN_PATH" ]; then + echo -e "${YELLOW}skipped${NC} (plugin not built)" + else + # Pre-compile the IR + "$IRC_BIN" "$file" /tmp/bench.nixir >/dev/null 2>&1 + + # Measure just the loading (average of 10 runs to reduce noise) + local total_load_us=0 + for _ in {1..10}; do + local load_output + load_output=$(nix-instantiate --plugin-files "$PLUGIN_PATH" --eval --expr "builtins.nixIR_loadIR \"/tmp/bench.nixir\"" 2>&1 >/dev/null | grep "nixIR timing" | grep -oP 'total=\K[0-9]+') + total_load_us=$((total_load_us + load_output)) + done + local avg_load_us=$((total_load_us / 10)) + local avg_load_ms_frac=$(awk "BEGIN {printf \"%.3f\", $avg_load_us / 1000}") + echo -e "${GREEN}${avg_load_ms_frac}ms${NC} avg (10 runs)" + fi + + # Measure full pipeline (compile + nix-instantiate overhead + IR load) + echo -n " Full pipeline: " + if [ ! -f "$PLUGIN_PATH" ]; then + echo -e "${YELLOW}skipped${NC}" + else + local pipeline_start + pipeline_start=$(date +%s%N) + "$IRC_BIN" "$file" /tmp/bench.nixir >/dev/null 2>&1 + nix-instantiate --plugin-files "$PLUGIN_PATH" --eval --expr "builtins.nixIR_loadIR \"/tmp/bench.nixir\"" >/dev/null 2>&1 + local pipeline_end + pipeline_end=$(date +%s%N) + local pipeline_ms=$(((pipeline_end - pipeline_start) / 1000000)) + echo -e "${YELLOW}${pipeline_ms}ms${NC}" + fi + # Source and IR sizes local src_size src_size=$(stat -c%s "$file" 2>/dev/null || stat -f%z "$file" 2>/dev/null) From 8bce6c27b578edc666b658fb41920f23560d3495 Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Mon, 23 Feb 2026 01:27:17 +0300 Subject: [PATCH 04/12] tests: initial integration tests Signed-off-by: NotAShelf Change-Id: I09ed2eea568edfaecdb800197bc36c416a6a6964 --- tests/integration/import_test.nix | 7 ++ tests/integration/imported_module.nix | 7 ++ tests/integration/ir_builtins_test.nix | 13 ++++ tests/integration/regression_normal_nix.nix | 39 +++++++++++ tests/integration/run.sh | 76 +++++++++++++++++++++ tests/integration/simple_eval.nix | 6 ++ 6 files changed, 148 insertions(+) create mode 100644 tests/integration/import_test.nix create mode 100644 tests/integration/imported_module.nix create mode 100644 tests/integration/ir_builtins_test.nix create mode 100644 tests/integration/regression_normal_nix.nix create mode 100755 tests/integration/run.sh create mode 100644 tests/integration/simple_eval.nix diff --git a/tests/integration/import_test.nix b/tests/integration/import_test.nix new file mode 100644 index 0000000..7914eea --- /dev/null +++ b/tests/integration/import_test.nix @@ -0,0 +1,7 @@ +# Test that import builtin still works +let + imported = import ./imported_module.nix; +in { + value = imported.foo + 100; + nested = imported.bar.baz; +} diff --git a/tests/integration/imported_module.nix b/tests/integration/imported_module.nix new file mode 100644 index 0000000..a9b969c --- /dev/null +++ b/tests/integration/imported_module.nix @@ -0,0 +1,7 @@ +# Module to be imported +{ + foo = 42; + bar = { + baz = "hello"; + }; +} diff --git a/tests/integration/ir_builtins_test.nix b/tests/integration/ir_builtins_test.nix new file mode 100644 index 0000000..4aa28e3 --- /dev/null +++ b/tests/integration/ir_builtins_test.nix @@ -0,0 +1,13 @@ +# Test our custom IR builtins +let + # Test nixIR_info + info = builtins.nixIR_info; + + # Test nixIR_compile + compiled = builtins.nixIR_compile "let x = 10; in x + 5"; + + # Test that normal builtins still work + list = builtins.map (x: x * 2) [1 2 3]; +in { + inherit info compiled list; +} diff --git a/tests/integration/regression_normal_nix.nix b/tests/integration/regression_normal_nix.nix new file mode 100644 index 0000000..2eb73f6 --- /dev/null +++ b/tests/integration/regression_normal_nix.nix @@ -0,0 +1,39 @@ +# Test that normal Nix evaluation is not broken +# This file should work identically with or without the plugin +let + # Basic arithmetic + math = 1 + 2 * 3; + + # String operations + str = "hello" + " " + "world"; + + # List operations + list = [1 2 3] ++ [4 5 6]; + + # Attrset operations + attrs = + { + a = 1; + b = 2; + } + // {c = 3;}; + + # Functions + double = x: x * 2; + result = double 21; + + # Conditionals + cond = + if true + then "yes" + else "no"; + + # Let bindings + nested = let + x = 10; + y = 20; + in + x + y; +in { + inherit math str list attrs result cond nested; +} diff --git a/tests/integration/run.sh b/tests/integration/run.sh new file mode 100755 index 0000000..8645388 --- /dev/null +++ b/tests/integration/run.sh @@ -0,0 +1,76 @@ +#!/usr/bin/env bash +set -euo pipefail + +echo "=== Phase 6: Integration Testing ===" +echo "" + +PLUGIN_PATH="$(pwd)/build/nix-ir-plugin.so" +TEST_DIR="$(pwd)/tests/integration" + +if [ ! -f "$PLUGIN_PATH" ]; then + echo "ERROR: Plugin not found at $PLUGIN_PATH" + exit 1 +fi + +echo "Plugin path: $PLUGIN_PATH" +echo "" + +echo "Test 1: Plugin Loading" +echo "----------------------" +if nix-instantiate --plugin-files "$PLUGIN_PATH" --eval "$TEST_DIR/simple_eval.nix" 2>&1 | grep -q "30"; then + echo "[PASS] Plugin loads and evaluates correctly" +else + echo "[FAIL] Plugin failed to load or evaluate" + exit 1 +fi +echo "" + +echo "Test 2: Normal Nix Evaluation (No Plugin)" +echo "------------------------------------------" +result=$(nix-instantiate --eval --strict --json "$TEST_DIR/regression_normal_nix.nix" 2>&1) +if echo "$result" | grep -q '"math":7'; then + echo "[PASS] Normal Nix evaluation works without plugin" +else + echo "[FAIL] Normal Nix evaluation broken" + echo "$result" + exit 1 +fi +echo "" + +echo "Test 3: Normal Nix Evaluation (With Plugin)" +echo "--------------------------------------------" +result=$(nix-instantiate --plugin-files "$PLUGIN_PATH" --eval --strict --json "$TEST_DIR/regression_normal_nix.nix" 2>&1) +if echo "$result" | grep -q '"math":7'; then + echo "[PASS] Normal Nix evaluation works with plugin loaded" +else + echo "[FAIL] Plugin breaks normal Nix evaluation" + echo "$result" + exit 1 +fi +echo "" + +echo "Test 4: Import Builtin" +echo "----------------------" +cd "$TEST_DIR" +result=$(nix-instantiate --plugin-files "$PLUGIN_PATH" --eval --strict --json import_test.nix 2>&1) +if echo "$result" | grep -q '"value":142'; then + echo "[PASS] Import builtin works correctly" +else + echo "[FAIL] Import builtin broken" + echo "$result" + exit 1 +fi +cd - >/dev/null +echo "" + +echo "Test 5: IR Builtins Available" +echo "------------------------------" +result=$(nix-instantiate --plugin-files "$PLUGIN_PATH" --eval "$TEST_DIR/ir_builtins_test.nix" 2>&1) +if echo "$result" | grep -q "info.*="; then + echo "[PASS] IR builtins (nixIR_info, nixIR_compile, nixIR_loadIR) available" +else + echo "[WARN] IR builtins may not be available (check plugin initialization)" +fi +echo "" + +echo "Integration Tests Complete" diff --git a/tests/integration/simple_eval.nix b/tests/integration/simple_eval.nix new file mode 100644 index 0000000..9dc2744 --- /dev/null +++ b/tests/integration/simple_eval.nix @@ -0,0 +1,6 @@ +# Simple expression to test plugin loading +let + x = 10; + y = 20; +in + x + y From 6587d0783300005e7bbdfd821c1d1c6012279eef Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Mon, 23 Feb 2026 02:10:34 +0300 Subject: [PATCH 05/12] irc/parser: fix list parsing and function application Fixes bug where `concat [1 2 3] [4 5 6]` tried to apply integer 1 as a function. Signed-off-by: NotAShelf Change-Id: I6f373dd83bcac9e59286b0448472200b6a6a6964 --- src/irc/parser.cpp | 79 ++++++++++++++++++++++++++++++++++++---------- 1 file changed, 62 insertions(+), 17 deletions(-) diff --git a/src/irc/parser.cpp b/src/irc/parser.cpp index 8d92617..2bb31ed 100644 --- a/src/irc/parser.cpp +++ b/src/irc/parser.cpp @@ -636,8 +636,9 @@ private: void tokenize_ident() { size_t start = pos; - while (pos < input.size() && (isalnum(input[pos]) || input[pos] == '_' || input[pos] == '-' || - input[pos] == '+' || input[pos] == '.')) + // Note: Don't include '.' here - it's used for selection (a.b.c) + // URIs are handled separately by checking for '://' pattern + while (pos < input.size() && (isalnum(input[pos]) || input[pos] == '_' || input[pos] == '-')) pos++; std::string ident = input.substr(start, pos - start); @@ -927,16 +928,25 @@ public: std::shared_ptr left = parse_expr3(); while (true) { - if (current().type == Token::LBRACKET) { - advance(); - auto arg = parse_expr(); - expect(Token::RBRACKET); - left = std::make_shared(AppNode(left, arg)); - } else if (current().type == Token::STRING) { + if (current().type == Token::STRING) { Token s = current(); advance(); auto arg = std::make_shared(ConstStringNode(s.value)); left = std::make_shared(AppNode(left, arg)); + } else if (current().type == Token::LPAREN) { + // Function application with parenthesized argument: func (expr) + advance(); + auto arg = parse_expr(); + expect(Token::RPAREN); + left = std::make_shared(AppNode(left, arg)); + } else if (current().type == Token::IDENT || current().type == Token::INT || + current().type == Token::FLOAT || current().type == Token::BOOL || + current().type == Token::PATH || current().type == Token::LOOKUP_PATH || + current().type == Token::URI || current().type == Token::LBRACKET) { + // Juxtaposition application: f x + // Parse the argument as a primary expression (which handles lists, etc.) + auto arg = parse_expr3(); + left = std::make_shared(AppNode(left, arg)); } else { break; } @@ -969,6 +979,16 @@ public: return expr; } + // Handle rec { ... } syntax + if (consume(Token::REC)) { + expect(Token::LBRACE); + auto attrs = parse_attrs(); + if (auto* attrset = attrs->get_if()) { + attrset->recursive = true; + } + return attrs; + } + if (consume(Token::LBRACE)) { return parse_attrs(); } @@ -1151,6 +1171,35 @@ public: return std::make_shared(std::move(attrs)); } + // Parse a list element: supports selections but NOT juxtaposition application + // This prevents [1 2 3] from being parsed as ((1 2) 3) + std::shared_ptr parse_list_element() { + auto left = parse_expr3(); + + // Handle selections (a.b.c) + while (current().type == Token::DOT) { + advance(); + Token name = current(); + if (name.type == Token::IDENT) { + advance(); + auto attr = std::make_shared(ConstStringNode(name.value)); + left = std::make_shared(SelectNode(left, attr)); + continue; + } + break; + } + + // Check for 'or' default value + if (left->get_if() && current().type == Token::IDENT && current().value == "or") { + advance(); + auto default_expr = parse_expr3(); + auto* select = left->get_if(); + select->default_expr = default_expr; + } + + return left; + } + std::shared_ptr parse_list() { std::vector> elements; @@ -1158,18 +1207,14 @@ public: return std::make_shared(ListNode(elements)); } - while (current().type != Token::RBRACKET) { - elements.push_back(parse_expr()); - if (!consume(Token::RBRACKET)) { - // Elements are whitespace-separated in Nix, no comma required - // But we'll continue parsing until we hit ] - } else { - // Found closing bracket - return std::make_shared(ListNode(elements)); + while (current().type != Token::RBRACKET && current().type != Token::EOF_) { + elements.push_back(parse_list_element()); + if (current().type == Token::RBRACKET) { + break; } } - // Unreachable, but for safety + expect(Token::RBRACKET); return std::make_shared(ListNode(elements)); } From 6612479286cbaae87e8cafd2c7b6cbc177c39d27 Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Mon, 23 Feb 2026 02:22:37 +0300 Subject: [PATCH 06/12] irc: add ListNode support; fix recursive attrset scoping Signed-off-by: NotAShelf Change-Id: I1657bc6a05c264f0ae0dd2c94d32b1046a6a6964 --- src/irc/ir_gen.cpp | 30 ++++++++++++++++++++++++------ 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/src/irc/ir_gen.cpp b/src/irc/ir_gen.cpp index ff9f18c..90175f9 100644 --- a/src/irc/ir_gen.cpp +++ b/src/irc/ir_gen.cpp @@ -1,5 +1,6 @@ #include "ir_gen.h" #include +#include #include #include @@ -97,7 +98,8 @@ struct IRGenerator::Impl { return std::make_shared(*n); } if (auto* n = node.get_if()) { - uint32_t idx = name_resolver.resolve(n->name.value_or("")); + std::string var_name = n->name.value_or(""); + uint32_t idx = name_resolver.resolve(var_name); VarNode converted(idx); converted.name = n->name; converted.line = n->line; @@ -121,12 +123,17 @@ struct IRGenerator::Impl { } if (auto* n = node.get_if()) { AttrsetNode attrs(n->recursive, n->line); - name_resolver.enter_scope(); - for (const auto& binding : n->attrs) { - if (!binding.is_dynamic()) { - name_resolver.bind(binding.static_name.value()); + + // Only enter a new scope for recursive attrsets + if (n->recursive) { + name_resolver.enter_scope(); + for (const auto& binding : n->attrs) { + if (!binding.is_dynamic()) { + name_resolver.bind(binding.static_name.value()); + } } } + for (const auto& binding : n->attrs) { if (binding.is_dynamic()) { attrs.attrs.push_back(AttrBinding(convert(binding.dynamic_name), convert(binding.value))); @@ -134,7 +141,10 @@ struct IRGenerator::Impl { attrs.attrs.push_back(AttrBinding(binding.static_name.value(), convert(binding.value))); } } - name_resolver.exit_scope(); + + if (n->recursive) { + name_resolver.exit_scope(); + } return std::make_shared(attrs); } if (auto* n = node.get_if()) { @@ -208,6 +218,14 @@ struct IRGenerator::Impl { auto operand = convert(n->operand); return std::make_shared(UnaryOpNode(n->op, operand, n->line)); } + if (auto* n = node.get_if()) { + std::vector> elements; + elements.reserve(n->elements.size()); + for (const auto& elem : n->elements) { + elements.push_back(convert(elem)); + } + return std::make_shared(ListNode(std::move(elements), n->line)); + } return std::make_shared(ConstNullNode{}); } }; From b6fd2326a6f439221f82ebc6f505ae611cc04663 Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Mon, 23 Feb 2026 02:22:59 +0300 Subject: [PATCH 07/12] irc/evaluator: fix variable lookup, recursive let, and value handling Bunch of things: - Decode depth and offset from encoded variable indices - Pre-allocate Values for recursive let bindings before eval - Use mk* methods for value copying instead of direct assignment - Evaluate attrset values immediately to avoid dangling thunks Signed-off-by: NotAShelf Change-Id: I4dd40c93d74df5973a642fb9f123e70e6a6a6964 --- src/irc/evaluator.cpp | 107 ++++++++++++++++++++++++++++++++++++------ 1 file changed, 92 insertions(+), 15 deletions(-) diff --git a/src/irc/evaluator.cpp b/src/irc/evaluator.cpp index 7a50dc7..987e38a 100644 --- a/src/irc/evaluator.cpp +++ b/src/irc/evaluator.cpp @@ -21,15 +21,20 @@ struct IREnvironment { void bind(Value* val) { bindings.push_back(val); } - Value* lookup(uint32_t index) { + Value* lookup(uint32_t encoded_index) { + // Decode the index: high 16 bits = depth, low 16 bits = offset + uint32_t depth = encoded_index >> 16; + uint32_t offset = encoded_index & 0xFFFF; + IREnvironment* env = this; - while (env) { - if (index < env->bindings.size()) { - return env->bindings[index]; - } - index -= env->bindings.size(); + // Skip 'depth' levels to get to the right scope + for (uint32_t i = 0; i < depth && env; i++) { env = env->parent; } + + if (env && offset < env->bindings.size()) { + return env->bindings[offset]; + } return nullptr; } @@ -147,7 +152,35 @@ struct Evaluator::Impl { state.error("variable not found").debugThrow(); } force(bound); - v = *bound; + // Copy the forced value's data into v + // For simple types, use mk* methods to ensure proper initialization + // For complex types (attrs, lists, functions), direct assignment is safe + state.forceValue(*bound, noPos); + switch (bound->type()) { + case nInt: + v.mkInt(bound->integer()); + break; + case nBool: + v.mkBool(bound->boolean()); + break; + case nString: + v.mkString(bound->c_str()); + break; + case nPath: + v.mkPath(bound->path()); + break; + case nNull: + v.mkNull(); + break; + case nFloat: + v.mkFloat(bound->fpoint()); + break; + default: + // For attrs, lists, functions, etc., direct assignment is safe + // as they use reference counting internally + v = *bound; + break; + } } else if (auto* n = node->get_if()) { auto lambda_env = env; auto body = n->body; @@ -422,20 +455,33 @@ struct Evaluator::Impl { } } else if (auto* n = node->get_if()) { auto let_env = make_env(env); + // Nix's let is recursive: bind all names first, then evaluate + // We allocate Values immediately and evaluate into them + std::vector values; for (const auto& [name, expr] : n->bindings) { - // Create thunks in let_env so bindings can reference each other - Value* val = make_thunk(expr, let_env); + Value* val = state.allocValue(); + values.push_back(val); let_env->bind(val); } + // Now evaluate each binding expression into its pre-allocated Value + size_t idx = 0; + for (const auto& [name, expr] : n->bindings) { + eval_node(expr, *values[idx++], let_env); + } eval_node(n->body, v, let_env); } else if (auto* n = node->get_if()) { auto letrec_env = make_env(env); - + // Same as LetNode - both are recursive in Nix + std::vector values; for (const auto& [name, expr] : n->bindings) { - Value* val = make_thunk(expr, letrec_env); + Value* val = state.allocValue(); + values.push_back(val); letrec_env->bind(val); } - + size_t idx = 0; + for (const auto& [name, expr] : n->bindings) { + eval_node(expr, *values[idx++], letrec_env); + } eval_node(n->body, v, letrec_env); } else if (auto* n = node->get_if()) { auto bindings = state.buildBindings(n->attrs.size()); @@ -453,9 +499,12 @@ struct Evaluator::Impl { } } - // Attributes should be lazy, so store as thunks and not evaluated values + // Evaluate attribute values immediately to avoid dangling thunks + // Our thunk system is tied to the Evaluator lifetime, so we can't + // return lazy thunks that outlive the evaluator for (const auto& binding : n->attrs) { - Value* attr_val = make_thunk(binding.value, attr_env); + Value* attr_val = state.allocValue(); + eval_node(binding.value, *attr_val, attr_env); if (binding.is_dynamic()) { // Evaluate key expression to get attribute name @@ -498,7 +547,35 @@ struct Evaluator::Impl { if (attr) { Value* val = attr->value; force(val); - v = *val; + // Copy the forced value's data into v + // For simple types, use mk* methods to ensure proper initialization + // For complex types (attrs, lists, functions), direct assignment is safe + state.forceValue(*val, noPos); + switch (val->type()) { + case nInt: + v.mkInt(val->integer()); + break; + case nBool: + v.mkBool(val->boolean()); + break; + case nString: + v.mkString(val->c_str()); + break; + case nPath: + v.mkPath(val->path()); + break; + case nNull: + v.mkNull(); + break; + case nFloat: + v.mkFloat(val->fpoint()); + break; + default: + // For attrs, lists, functions, etc., direct assignment is safe + // as they use reference counting internally + v = *val; + break; + } } else if (n->default_expr) { eval_node(*n->default_expr, v, env); } else { From 84cf5fdf6883fc3d031c97c7e4466cb803d60545 Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Mon, 23 Feb 2026 02:23:44 +0300 Subject: [PATCH 08/12] irc: add timing measurements; formatting Signed-off-by: NotAShelf Change-Id: Id4402547e18b6569464850c3753383396a6a6964 --- src/irc/serializer.cpp | 2 +- src/plugin.cpp | 17 +++++++++++++++-- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/src/irc/serializer.cpp b/src/irc/serializer.cpp index f5b1982..73051a7 100644 --- a/src/irc/serializer.cpp +++ b/src/irc/serializer.cpp @@ -388,7 +388,7 @@ struct Deserializer::Impl { uint32_t num_elements = read_u32(); std::vector> elements; elements.reserve(num_elements); -for (uint32_t i = 0; i < num_elements; i++) { + for (uint32_t i = 0; i < num_elements; i++) { elements.push_back(read_node()); } return std::make_shared(ListNode(std::move(elements), line)); diff --git a/src/plugin.cpp b/src/plugin.cpp index 18a832c..4a821f5 100644 --- a/src/plugin.cpp +++ b/src/plugin.cpp @@ -12,6 +12,7 @@ #include "irc/serializer.h" #include "irc/types.h" +#include #include namespace nix_ir_plugin { @@ -29,6 +30,8 @@ static void prim_loadIR(EvalState& state, const PosIdx pos, Value** args, Value& std::string pathStr(path); + auto t_start = std::chrono::high_resolution_clock::now(); + Deserializer deserializer; IRModule module; @@ -38,6 +41,8 @@ static void prim_loadIR(EvalState& state, const PosIdx pos, Value** args, Value& state.error("failed to deserialize IR bundle: %s", e.what()).atPos(pos).debugThrow(); } + auto t_deser = std::chrono::high_resolution_clock::now(); + if (!module.entry) { state.error("IR bundle has no entry point").atPos(pos).debugThrow(); } @@ -48,6 +53,14 @@ static void prim_loadIR(EvalState& state, const PosIdx pos, Value** args, Value& } catch (const std::exception& e) { state.error("failed to evaluate IR: %s", e.what()).atPos(pos).debugThrow(); } + + auto t_eval = std::chrono::high_resolution_clock::now(); + + auto deser_us = std::chrono::duration_cast(t_deser - t_start).count(); + auto eval_us = std::chrono::duration_cast(t_eval - t_deser).count(); + + std::cerr << "nixIR timing: deser=" << deser_us << "us eval=" << eval_us + << "us total=" << (deser_us + eval_us) << "us" << std::endl; } /** @@ -139,7 +152,7 @@ static RegisterPrimOp rp_info({ } // namespace nix_ir_plugin -// Plugin initialization message +// Plugin initialization __attribute__((constructor)) static void init_plugin() { - std::cerr << "nix-ir-plugin loaded" << std::endl; + // Plugin loads silently... } From 3347699a8ce0c8ba6f767906aaee6fc0a3d145ba Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Mon, 23 Feb 2026 02:24:10 +0300 Subject: [PATCH 09/12] tests/benchmark: make benchmark cases... bigger Signed-off-by: NotAShelf Change-Id: Iabd307b475f6568cff4d1ae6e5ae56ef6a6a6964 --- tests/benchmark/large.nix | 218 ++++++++++++++++++++++++++++++++++--- tests/benchmark/medium.nix | 74 ++++++++++--- tests/benchmark/run.sh | 5 +- 3 files changed, 267 insertions(+), 30 deletions(-) diff --git a/tests/benchmark/large.nix b/tests/benchmark/large.nix index ffd7e22..bf29a77 100644 --- a/tests/benchmark/large.nix +++ b/tests/benchmark/large.nix @@ -1,15 +1,50 @@ -# Large :b:oke ---I mean...benchmark, for stress testing +# Large benchmark for comprehensive stress testing let - # Generate large list range = start: end: if start >= end then [] else [start] ++ range (start + 1) end; - # Deep nesting - deepNest = {a = {b = {c = {d = {e = {f = 42;};};};};};}; + concat = a: b: a ++ b; + + factorial = n: + if n <= 1 + then 1 + else n * factorial (n - 1); + + # Ackermann function (highly recursive) + ackermann = m: n: + if m == 0 + then n + 1 + else if n == 0 + then ackermann (m - 1) 1 + else ackermann (m - 1) (ackermann m (n - 1)); + + # Greatest common divisor + gcd = a: b: + if b == 0 + then a + else gcd b (a - (a / b) * b); + + # Power function + pow = base: exp: + if exp == 0 + then 1 + else if exp == 1 + then base + else base * pow base (exp - 1); + + compose = f: g: x: f (g x); + double = x: x * 2; + addTen = x: x + 10; + square = x: x * x; + + pipeline = compose square (compose double addTen); + + list_100 = range 1 101; + list_50 = range 1 51; + list_25 = range 1 26; - # Large attrset largeAttrs = { a1 = 1; a2 = 2; @@ -31,17 +66,172 @@ let b8 = 18; b9 = 19; b10 = 20; + c1 = 21; + c2 = 22; + c3 = 23; + c4 = 24; + c5 = 25; + c6 = 26; + c7 = 27; + c8 = 28; + c9 = 29; + c10 = 30; + d1 = 31; + d2 = 32; + d3 = 33; + d4 = 34; + d5 = 35; + d6 = 36; + d7 = 37; + d8 = 38; + d9 = 39; + d10 = 40; + e1 = 41; + e2 = 42; + e3 = 43; + e4 = 44; + e5 = 45; + e6 = 46; + e7 = 47; + e8 = 48; + e9 = 49; + e10 = 50; }; - # Recursive attrset - recursive = rec { - x = 10; - y = x * 2; - z = y + x; - result = z * 3; + # Very deep nesting (10 levels) + deepNest = { + level1 = { + level2 = { + level3 = { + level4 = { + level5 = { + level6 = { + level7 = { + level8 = { + level9 = { + level10 = { + treasure = "found"; + value = 12345; + }; + }; + }; + }; + }; + }; + }; + }; + }; + }; }; + + recursiveComplex = rec { + base = 10; + doubled = base * 2; + tripled = base * 3; + + sum = doubled + tripled; + product = doubled * tripled; + + x = base * 4; + y = x + doubled; + z = y * tripled; + + total = sum + product + z; + final = total * base; + }; + + config1 = rec { + multiplier = 5; + base = 100; + result = base * multiplier; + }; + + config2 = rec { + offset = 50; + scaled = config1.result + offset; + doubled = scaled * 2; + }; + + config3 = rec { + factor = 3; + combined = config2.doubled * factor; + final = combined + config1.multiplier; + }; + + baseConfig = { + system = { + arch = "x86_64"; + os = "linux"; + }; + settings = { + enabled = true; + level = 5; + }; + }; + + overrides = { + system = { + kernel = "6.1"; + }; + settings = { + level = 10; + extra = "custom"; + }; + newSection = { + value = 42; + }; + }; + + merged = + baseConfig + // overrides + // { + system = baseConfig.system // overrides.system; + settings = + baseConfig.settings + // overrides.settings + // { + combined = baseConfig.settings.level + overrides.settings.level; + }; + }; + + fact10 = factorial 10; + fact7 = factorial 7; + ack_3_3 = ackermann 3 3; + gcd_48_18 = gcd 48 18; + gcd_100_35 = gcd 100 35; + pow_2_10 = pow 2 10; + pow_3_5 = pow 3 5; + + pipelineResult = pipeline 5; # ((5 + 10) * 2)^2 = 900 + + # List operations + concatenated = concat [1 2 3] [4 5 6]; + multilevel = concat (concat [1] [2 3]) [4 5]; in { - list_100 = range 1 100; - deep = deepNest.a.b.c.d.e.f; - inherit largeAttrs recursive; + # Lists + inherit list_100 list_50 list_25 concatenated multilevel; + + # Math results + inherit fact10 fact7 ack_3_3 gcd_48_18 gcd_100_35 pow_2_10 pow_3_5 pipelineResult; + + # Data structures + inherit largeAttrs merged; + deepValue = deepNest.level1.level2.level3.level4.level5.level6.level7.level8.level9.level10.value; + deepTreasure = deepNest.level1.level2.level3.level4.level5.level6.level7.level8.level9.level10.treasure; + + # Recursive attrsets + recursiveTotal = recursiveComplex.total; + recursiveFinal = recursiveComplex.final; + computedZ = recursiveComplex.z; + + # Config chain + config1Result = config1.result; + config2Doubled = config2.doubled; + config3Final = config3.final; + + # Merged config + mergedCombined = merged.settings.combined; + mergedArch = merged.system.arch; + mergedKernel = merged.system.kernel; } diff --git a/tests/benchmark/medium.nix b/tests/benchmark/medium.nix index f8db9e9..6234dca 100644 --- a/tests/benchmark/medium.nix +++ b/tests/benchmark/medium.nix @@ -1,31 +1,75 @@ let - # Recursive fibonacci (not memoized) + # Recursive factorial + factorial = n: + if n <= 1 + then 1 + else n * factorial (n - 1); + + # Fibonacci sequence generator fib = n: if n <= 1 then n else fib (n - 1) + fib (n - 2); - # List operations - numbers = [1 2 3 4 5 6 7 8 9 10]; - doubled = builtins.map (x: x * 2) numbers; + # List concatenation test + range = start: end: + if start >= end + then [] + else [start] ++ range (start + 1) end; - # Attrset operations + # Curried function application + add = x: y: x + y; + add5 = add 5; + + # Complex computation + compute = x: y: let + a = x * 2; + b = y + 10; + c = a * b; + in + c / 2; + + # Data structures + numbers = range 1 11; + + # Nested attribute operations base = { - a = 1; - b = 2; - c = 3; + config = { + enable = true; + value = 42; + }; + data = { + items = [1 2 3]; + }; }; + extended = base // { - d = 4; - e = 5; + config = + base.config + // { + extra = "test"; + multiplied = base.config.value * 2; + }; + computed = base.config.value + 100; }; - # String operations - greeting = "Hello"; - message = "${greeting}, World!"; + # Recursive attrset with selections + recursive = rec { + x = 10; + y = x * 2; + z = y + x; + result = z * 3; + final = result + x; + }; in { - fibonacci_10 = fib 10; - inherit doubled extended message; + fact5 = factorial 5; + fib7 = fib 7; + sum15 = add5 10; + computed = compute 10 20; + inherit numbers extended; + deepValue = extended.config.multiplied; + recursiveResult = recursive.result; + recursiveFinal = recursive.final; } diff --git a/tests/benchmark/run.sh b/tests/benchmark/run.sh index 8e43fe8..c392139 100755 --- a/tests/benchmark/run.sh +++ b/tests/benchmark/run.sh @@ -83,7 +83,10 @@ run_benchmark() { src_size=$(stat -c%s "$file" 2>/dev/null || stat -f%z "$file" 2>/dev/null) local ir_size ir_size=$(stat -c%s /tmp/bench.nixir 2>/dev/null || stat -f%z /tmp/bench.nixir 2>/dev/null) - local ratio=$((ir_size * 100 / src_size)) + local ratio=0 + if [[ "$src_size" -gt 0 ]]; then + ratio=$((ir_size * 100 / src_size)) + fi echo -e " Source size: ${src_size}B" echo -e " IR bundle size: ${ir_size}B (${ratio}% of source)" From ae505188fc9892ec91f75a659b259b9d945b61f9 Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Mon, 23 Feb 2026 02:24:48 +0300 Subject: [PATCH 10/12] tests: move fixtures to dedicated dir Signed-off-by: NotAShelf Change-Id: I9d6ce6a264780f215b1b57d947b5264c6a6a6964 --- tests/block_comments.nix | 12 -- tests/{ => fixtures}/ancient_let.nix | 0 tests/{ => fixtures}/attrset.nix | 0 tests/{ => fixtures}/attrset_var.nix | 0 tests/fixtures/block_comments.nix | 24 ++++ tests/{ => fixtures}/comparison.nix | 0 tests/fixtures/dynamic_attr_full.nix | 14 ++ tests/{ => fixtures}/dynamic_attrs.nix | 0 tests/{ => fixtures}/float_test.nix | 0 tests/{ => fixtures}/home_path.nix | 0 tests/{ => fixtures}/if.nix | 0 tests/{ => fixtures}/import_lookup.nix | 0 tests/{ => fixtures}/import_simple.nix | 4 +- tests/{ => fixtures}/indented_string.nix | 0 tests/{ => fixtures}/inherit.nix | 0 tests/{ => fixtures}/inherit_from.nix | 0 tests/{ => fixtures}/inherit_simple.nix | 0 tests/{ => fixtures}/lambda_pattern.nix | 0 tests/{ => fixtures}/let.nix | 0 tests/{ => fixtures}/list_concat.nix | 0 tests/fixtures/list_simple.nix | 8 ++ tests/{ => fixtures}/logical.nix | 0 tests/{ => fixtures}/lookup_path.nix | 3 +- tests/{ => fixtures}/lookup_path_nested.nix | 0 tests/{ => fixtures}/merge.nix | 0 tests/{ => fixtures}/nested_attrs.nix | 0 tests/{ => fixtures}/operators.nix | 0 tests/{ => fixtures}/or_in_attrset.nix | 2 +- tests/fixtures/or_simple.nix | 5 + tests/{ => fixtures}/path_concat.nix | 0 tests/{ => fixtures}/precedence.nix | 0 tests/{ => fixtures}/select_or_default.nix | 5 +- tests/{ => fixtures}/shortcircuit.nix | 0 tests/{ => fixtures}/shortcircuit2.nix | 0 tests/{ => fixtures}/simple.nix | 0 tests/{ => fixtures}/simple_op.nix | 0 tests/{ => fixtures}/string_interp.nix | 0 tests/{ => fixtures}/unary.nix | 0 tests/{ => fixtures}/uri_test.nix | 0 tests/or_simple.nix | 4 - tests/regression_test.cpp | 147 ++++++++++---------- 41 files changed, 134 insertions(+), 94 deletions(-) delete mode 100644 tests/block_comments.nix rename tests/{ => fixtures}/ancient_let.nix (100%) rename tests/{ => fixtures}/attrset.nix (100%) rename tests/{ => fixtures}/attrset_var.nix (100%) create mode 100644 tests/fixtures/block_comments.nix rename tests/{ => fixtures}/comparison.nix (100%) create mode 100644 tests/fixtures/dynamic_attr_full.nix rename tests/{ => fixtures}/dynamic_attrs.nix (100%) rename tests/{ => fixtures}/float_test.nix (100%) rename tests/{ => fixtures}/home_path.nix (100%) rename tests/{ => fixtures}/if.nix (100%) rename tests/{ => fixtures}/import_lookup.nix (100%) rename tests/{ => fixtures}/import_simple.nix (99%) rename tests/{ => fixtures}/indented_string.nix (100%) rename tests/{ => fixtures}/inherit.nix (100%) rename tests/{ => fixtures}/inherit_from.nix (100%) rename tests/{ => fixtures}/inherit_simple.nix (100%) rename tests/{ => fixtures}/lambda_pattern.nix (100%) rename tests/{ => fixtures}/let.nix (100%) rename tests/{ => fixtures}/list_concat.nix (100%) create mode 100644 tests/fixtures/list_simple.nix rename tests/{ => fixtures}/logical.nix (100%) rename tests/{ => fixtures}/lookup_path.nix (99%) rename tests/{ => fixtures}/lookup_path_nested.nix (100%) rename tests/{ => fixtures}/merge.nix (100%) rename tests/{ => fixtures}/nested_attrs.nix (100%) rename tests/{ => fixtures}/operators.nix (100%) rename tests/{ => fixtures}/or_in_attrset.nix (75%) create mode 100644 tests/fixtures/or_simple.nix rename tests/{ => fixtures}/path_concat.nix (100%) rename tests/{ => fixtures}/precedence.nix (100%) rename tests/{ => fixtures}/select_or_default.nix (90%) rename tests/{ => fixtures}/shortcircuit.nix (100%) rename tests/{ => fixtures}/shortcircuit2.nix (100%) rename tests/{ => fixtures}/simple.nix (100%) rename tests/{ => fixtures}/simple_op.nix (100%) rename tests/{ => fixtures}/string_interp.nix (100%) rename tests/{ => fixtures}/unary.nix (100%) rename tests/{ => fixtures}/uri_test.nix (100%) delete mode 100644 tests/or_simple.nix diff --git a/tests/block_comments.nix b/tests/block_comments.nix deleted file mode 100644 index b5de60f..0000000 --- a/tests/block_comments.nix +++ /dev/null @@ -1,12 +0,0 @@ -# Test block comments /* */ -/* This is a block comment */ -let - x = 42; /* inline block comment */ - /* Multi-line - block - comment */ - y = 100; -in -/* Comment before expression */ -x + y -/* Trailing comment */ diff --git a/tests/ancient_let.nix b/tests/fixtures/ancient_let.nix similarity index 100% rename from tests/ancient_let.nix rename to tests/fixtures/ancient_let.nix diff --git a/tests/attrset.nix b/tests/fixtures/attrset.nix similarity index 100% rename from tests/attrset.nix rename to tests/fixtures/attrset.nix diff --git a/tests/attrset_var.nix b/tests/fixtures/attrset_var.nix similarity index 100% rename from tests/attrset_var.nix rename to tests/fixtures/attrset_var.nix diff --git a/tests/fixtures/block_comments.nix b/tests/fixtures/block_comments.nix new file mode 100644 index 0000000..301297d --- /dev/null +++ b/tests/fixtures/block_comments.nix @@ -0,0 +1,24 @@ +# Test block comments /* */ +/* +This is a block comment +*/ +let + x = 42; + /* + inline block comment + */ + /* + Multi-line + block + comment + */ + y = 100; +in + /* + Comment before expression + */ + x + y +/* +Trailing comment +*/ + diff --git a/tests/comparison.nix b/tests/fixtures/comparison.nix similarity index 100% rename from tests/comparison.nix rename to tests/fixtures/comparison.nix diff --git a/tests/fixtures/dynamic_attr_full.nix b/tests/fixtures/dynamic_attr_full.nix new file mode 100644 index 0000000..3c7b39b --- /dev/null +++ b/tests/fixtures/dynamic_attr_full.nix @@ -0,0 +1,14 @@ +# Test dynamic attribute names +let + key = "mykey"; + value = 42; +in { + # Dynamic attribute with string interpolation + "${key}" = value; + + # Another dynamic attribute + "${key}_suffix" = value + 1; + + # Static attribute for comparison + static = 100; +} diff --git a/tests/dynamic_attrs.nix b/tests/fixtures/dynamic_attrs.nix similarity index 100% rename from tests/dynamic_attrs.nix rename to tests/fixtures/dynamic_attrs.nix diff --git a/tests/float_test.nix b/tests/fixtures/float_test.nix similarity index 100% rename from tests/float_test.nix rename to tests/fixtures/float_test.nix diff --git a/tests/home_path.nix b/tests/fixtures/home_path.nix similarity index 100% rename from tests/home_path.nix rename to tests/fixtures/home_path.nix diff --git a/tests/if.nix b/tests/fixtures/if.nix similarity index 100% rename from tests/if.nix rename to tests/fixtures/if.nix diff --git a/tests/import_lookup.nix b/tests/fixtures/import_lookup.nix similarity index 100% rename from tests/import_lookup.nix rename to tests/fixtures/import_lookup.nix diff --git a/tests/import_simple.nix b/tests/fixtures/import_simple.nix similarity index 99% rename from tests/import_simple.nix rename to tests/fixtures/import_simple.nix index 023b49d..1e024cc 100644 --- a/tests/import_simple.nix +++ b/tests/fixtures/import_simple.nix @@ -1,11 +1,9 @@ # Test import expression # Import evaluates the file and returns its value - # Import a file that returns a simple value (42) import ./simple.nix - # Can also import lookup paths: # import { } - # Import with path expressions: # import (./dir + "/file.nix") + diff --git a/tests/indented_string.nix b/tests/fixtures/indented_string.nix similarity index 100% rename from tests/indented_string.nix rename to tests/fixtures/indented_string.nix diff --git a/tests/inherit.nix b/tests/fixtures/inherit.nix similarity index 100% rename from tests/inherit.nix rename to tests/fixtures/inherit.nix diff --git a/tests/inherit_from.nix b/tests/fixtures/inherit_from.nix similarity index 100% rename from tests/inherit_from.nix rename to tests/fixtures/inherit_from.nix diff --git a/tests/inherit_simple.nix b/tests/fixtures/inherit_simple.nix similarity index 100% rename from tests/inherit_simple.nix rename to tests/fixtures/inherit_simple.nix diff --git a/tests/lambda_pattern.nix b/tests/fixtures/lambda_pattern.nix similarity index 100% rename from tests/lambda_pattern.nix rename to tests/fixtures/lambda_pattern.nix diff --git a/tests/let.nix b/tests/fixtures/let.nix similarity index 100% rename from tests/let.nix rename to tests/fixtures/let.nix diff --git a/tests/list_concat.nix b/tests/fixtures/list_concat.nix similarity index 100% rename from tests/list_concat.nix rename to tests/fixtures/list_concat.nix diff --git a/tests/fixtures/list_simple.nix b/tests/fixtures/list_simple.nix new file mode 100644 index 0000000..7167967 --- /dev/null +++ b/tests/fixtures/list_simple.nix @@ -0,0 +1,8 @@ +# Test basic list support +let + x = [1 2 3]; + y = [4 5 6]; + z = x ++ y; # List concatenation +in { + inherit x y z; +} diff --git a/tests/logical.nix b/tests/fixtures/logical.nix similarity index 100% rename from tests/logical.nix rename to tests/fixtures/logical.nix diff --git a/tests/lookup_path.nix b/tests/fixtures/lookup_path.nix similarity index 99% rename from tests/lookup_path.nix rename to tests/fixtures/lookup_path.nix index e8bb4ca..a770360 100644 --- a/tests/lookup_path.nix +++ b/tests/fixtures/lookup_path.nix @@ -1,9 +1,8 @@ # Test lookup path syntax # Lookup paths resolve via NIX_PATH environment variable # Example: -> /nix/var/nix/profiles/per-user/root/channels/nixpkgs - # Simple lookup path - # Nested lookup path (common pattern) # + diff --git a/tests/lookup_path_nested.nix b/tests/fixtures/lookup_path_nested.nix similarity index 100% rename from tests/lookup_path_nested.nix rename to tests/fixtures/lookup_path_nested.nix diff --git a/tests/merge.nix b/tests/fixtures/merge.nix similarity index 100% rename from tests/merge.nix rename to tests/fixtures/merge.nix diff --git a/tests/nested_attrs.nix b/tests/fixtures/nested_attrs.nix similarity index 100% rename from tests/nested_attrs.nix rename to tests/fixtures/nested_attrs.nix diff --git a/tests/operators.nix b/tests/fixtures/operators.nix similarity index 100% rename from tests/operators.nix rename to tests/fixtures/operators.nix diff --git a/tests/or_in_attrset.nix b/tests/fixtures/or_in_attrset.nix similarity index 75% rename from tests/or_in_attrset.nix rename to tests/fixtures/or_in_attrset.nix index 406149b..d1ab7f9 100644 --- a/tests/or_in_attrset.nix +++ b/tests/fixtures/or_in_attrset.nix @@ -1,6 +1,6 @@ # Test 'or' in attrset context let - attrs = { a = 1; }; + attrs = {a = 1;}; in { test = attrs.a or 999; } diff --git a/tests/fixtures/or_simple.nix b/tests/fixtures/or_simple.nix new file mode 100644 index 0000000..d4de5be --- /dev/null +++ b/tests/fixtures/or_simple.nix @@ -0,0 +1,5 @@ +# Simplest 'or' test +let + x = {a = 1;}; +in + x.a or 2 diff --git a/tests/path_concat.nix b/tests/fixtures/path_concat.nix similarity index 100% rename from tests/path_concat.nix rename to tests/fixtures/path_concat.nix diff --git a/tests/precedence.nix b/tests/fixtures/precedence.nix similarity index 100% rename from tests/precedence.nix rename to tests/fixtures/precedence.nix diff --git a/tests/select_or_default.nix b/tests/fixtures/select_or_default.nix similarity index 90% rename from tests/select_or_default.nix rename to tests/fixtures/select_or_default.nix index df91875..6144ff5 100644 --- a/tests/select_or_default.nix +++ b/tests/fixtures/select_or_default.nix @@ -1,6 +1,9 @@ # Test selection with 'or' default let - attrs = { a = 1; b = 2; }; + attrs = { + a = 1; + b = 2; + }; in { # Attribute exists - should use value from attrs has_attr = attrs.a or 999; diff --git a/tests/shortcircuit.nix b/tests/fixtures/shortcircuit.nix similarity index 100% rename from tests/shortcircuit.nix rename to tests/fixtures/shortcircuit.nix diff --git a/tests/shortcircuit2.nix b/tests/fixtures/shortcircuit2.nix similarity index 100% rename from tests/shortcircuit2.nix rename to tests/fixtures/shortcircuit2.nix diff --git a/tests/simple.nix b/tests/fixtures/simple.nix similarity index 100% rename from tests/simple.nix rename to tests/fixtures/simple.nix diff --git a/tests/simple_op.nix b/tests/fixtures/simple_op.nix similarity index 100% rename from tests/simple_op.nix rename to tests/fixtures/simple_op.nix diff --git a/tests/string_interp.nix b/tests/fixtures/string_interp.nix similarity index 100% rename from tests/string_interp.nix rename to tests/fixtures/string_interp.nix diff --git a/tests/unary.nix b/tests/fixtures/unary.nix similarity index 100% rename from tests/unary.nix rename to tests/fixtures/unary.nix diff --git a/tests/uri_test.nix b/tests/fixtures/uri_test.nix similarity index 100% rename from tests/uri_test.nix rename to tests/fixtures/uri_test.nix diff --git a/tests/or_simple.nix b/tests/or_simple.nix deleted file mode 100644 index 6025a4d..0000000 --- a/tests/or_simple.nix +++ /dev/null @@ -1,4 +0,0 @@ -# Simplest 'or' test -let - x = { a = 1; }; -in x.a or 2 diff --git a/tests/regression_test.cpp b/tests/regression_test.cpp index 1b4c4ce..f9f9914 100644 --- a/tests/regression_test.cpp +++ b/tests/regression_test.cpp @@ -1,3 +1,4 @@ +#include "irc/parser.h" #include "irc/serializer.h" #include "irc/types.h" #include @@ -7,21 +8,21 @@ 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; \ - } \ +#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++; \ +#define TEST_FAIL(msg) \ + do { \ + std::cerr << " FAIL: " << msg << std::endl; \ + failures++; \ } while (0) void test_enum_compatibility() { @@ -30,33 +31,28 @@ void test_enum_compatibility() { 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; + 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; + 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) + 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; + 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; + 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; + std::cerr << " FAIL: Either bump IR_VERSION or fix enum values" << std::endl; } } @@ -80,19 +76,16 @@ void test_serializer_select_with_default() { 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(); + 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; + 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; + std::cerr << " FAIL: default_expr not deserialized (missing u8 flag read)" << std::endl; } } @@ -114,11 +107,9 @@ void test_serializer_select_without_default() { 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; + 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; } @@ -127,34 +118,53 @@ void test_serializer_select_without_default() { 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::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; + try { + Parser parser; + auto ast = parser.parse(test_input); + TEST_PASS("Brace inside string does not confuse parser"); + } catch (const std::exception& e) { + TEST_FAIL("Parser should handle '}' inside strings"); + } } 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; + std::string with_ellipsis = "{ a, ... }: a"; + std::string without_ellipsis = "{ a, b }: a + b"; + + try { + Parser parser1; + auto ast1 = parser1.parse(with_ellipsis); + TEST_PASS("Pattern with ellipsis parses correctly"); + + Parser parser2; + auto ast2 = parser2.parse(without_ellipsis); + TEST_PASS("Pattern without ellipsis parses correctly"); + } catch (const std::exception& e) { + TEST_FAIL("Pattern parsing failed"); + } } 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; + std::string not_a_lambda = "1 + 2"; + std::string actual_lambda = "x: x + 1"; + + try { + Parser parser1; + auto ast1 = parser1.parse(not_a_lambda); + TEST_PASS("Non-lambda input does not cause parser to throw"); + + Parser parser2; + auto ast2 = parser2.parse(actual_lambda); + TEST_PASS("Actual lambda parses correctly"); + } catch (const std::exception& e) { + TEST_FAIL("Parser should handle both lambda and non-lambda input"); + } } void test_lookup_path_node() { @@ -170,10 +180,9 @@ void test_lookup_path_node() { Deserializer deser; auto loaded = deser.deserialize(bytes); - auto *loaded_lookup = loaded.entry->get_if(); + auto* loaded_lookup = loaded.entry->get_if(); TEST_CHECK(loaded_lookup != nullptr, "Deserialized node is ConstLookupPathNode"); - TEST_CHECK(loaded_lookup && loaded_lookup->value == "nixpkgs", - "Lookup path value is 'nixpkgs'"); + TEST_CHECK(loaded_lookup && loaded_lookup->value == "nixpkgs", "Lookup path value is 'nixpkgs'"); } void test_import_node() { @@ -190,16 +199,14 @@ void test_import_node() { Deserializer deser; auto loaded = deser.deserialize(bytes); - auto *loaded_import = loaded.entry->get_if(); + auto* loaded_import = loaded.entry->get_if(); TEST_CHECK(loaded_import != nullptr, "Deserialized node is ImportNode"); - TEST_CHECK(loaded_import && loaded_import->path != nullptr, - "Import node has path"); + TEST_CHECK(loaded_import && loaded_import->path != nullptr, "Import node has path"); if (loaded_import && loaded_import->path) { - auto *path_node = loaded_import->path->get_if(); + auto* path_node = loaded_import->path->get_if(); TEST_CHECK(path_node != nullptr, "Import path is ConstPathNode"); - TEST_CHECK(path_node && path_node->value == "./test.nix", - "Import path value is './test.nix'"); + TEST_CHECK(path_node && path_node->value == "./test.nix", "Import path value is './test.nix'"); } } @@ -217,14 +224,13 @@ void test_import_with_lookup_path() { Deserializer deser; auto loaded = deser.deserialize(bytes); - auto *loaded_import = loaded.entry->get_if(); + auto* loaded_import = loaded.entry->get_if(); TEST_CHECK(loaded_import != nullptr, "Deserialized node is ImportNode"); if (loaded_import && loaded_import->path) { - auto *lookup_node = loaded_import->path->get_if(); + auto* lookup_node = loaded_import->path->get_if(); TEST_CHECK(lookup_node != nullptr, "Import path is ConstLookupPathNode"); - TEST_CHECK(lookup_node && lookup_node->value == "nixpkgs", - "Lookup path value is 'nixpkgs'"); + TEST_CHECK(lookup_node && lookup_node->value == "nixpkgs", "Lookup path value is 'nixpkgs'"); } } @@ -241,7 +247,7 @@ void test_uri_node() { Deserializer deser; auto loaded = deser.deserialize(bytes); - auto *loaded_uri = loaded.entry->get_if(); + auto* loaded_uri = loaded.entry->get_if(); TEST_CHECK(loaded_uri != nullptr, "Deserialized node is ConstURINode"); TEST_CHECK(loaded_uri && loaded_uri->value == "https://example.com", "URI value is 'https://example.com'"); @@ -260,10 +266,9 @@ void test_float_node() { Deserializer deser; auto loaded = deser.deserialize(bytes); - auto *loaded_float = loaded.entry->get_if(); + auto* loaded_float = loaded.entry->get_if(); TEST_CHECK(loaded_float != nullptr, "Deserialized node is ConstFloatNode"); - TEST_CHECK(loaded_float && loaded_float->value > 3.14 && - loaded_float->value < 3.15, + TEST_CHECK(loaded_float && loaded_float->value > 3.14 && loaded_float->value < 3.15, "Float value is approximately 3.14159"); } From 7584eb76e125913b824bc6807c52f04e4d716bf5 Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Mon, 23 Feb 2026 02:25:06 +0300 Subject: [PATCH 11/12] meta: switch to justfile for task organization Signed-off-by: NotAShelf Change-Id: Ib4000ab597f94b2dd3dccf1e31fce3a76a6a6964 --- CMakeLists.txt | 1 + flake.nix | 2 ++ justfile | 98 ++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 101 insertions(+) create mode 100644 justfile diff --git a/CMakeLists.txt b/CMakeLists.txt index edb503c..10ed5a2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -78,6 +78,7 @@ install(TARGETS nix-ir-plugin LIBRARY DESTINATION "${CMAKE_INSTALL_PREFIX}/lib/n add_executable(regression_test tests/regression_test.cpp src/irc/serializer.cpp + src/irc/parser.cpp ) target_include_directories(regression_test PRIVATE diff --git a/flake.nix b/flake.nix index a127312..4f98a7f 100644 --- a/flake.nix +++ b/flake.nix @@ -30,6 +30,8 @@ ninja bear clang-tools + just + entr ]; env.NIX_PLUGINABI = "0.2"; diff --git a/justfile b/justfile new file mode 100644 index 0000000..9ca92f8 --- /dev/null +++ b/justfile @@ -0,0 +1,98 @@ +# Default recipe, show available commands +default: + @just --list + +# Build all targets +build: + cmake --build build + +# Clean build artifacts +clean: + rm -rf build + find tests -name '*.nixir' -delete + +# Configure and build from scratch +rebuild: clean + cmake -B build -G Ninja + cmake --build build + +# Run unit tests +test-unit: + ./build/regression_test + +# Run compilation tests (do all fixtures compile?) +test-compile: + #!/usr/bin/env bash + total=0 + success=0 + for f in tests/fixtures/*.nix; do + total=$((total+1)) + if ./build/nix-irc "$f" "${f%.nix}.nixir" 2>&1 | grep -q "Done!"; then + success=$((success+1)) + fi + done + echo "Compiled: $success/$total test files" + [ $success -eq $total ] + +# Run integration tests +test-integration: + ./tests/integration/run.sh + +# Run all tests +test: test-unit test-compile test-integration + @echo "All tests passed" + +# Run benchmarks +bench: + ./tests/benchmark/run.sh + +# Compile a single Nix file to IR +compile FILE OUTPUT="": + #!/usr/bin/env bash + if [ -z "{{OUTPUT}}" ]; then + file="{{FILE}}" + output="${file%.nix}.nixir" + else + output="{{OUTPUT}}" + fi + ./build/nix-irc "{{FILE}}" "$output" + +# Load plugin and evaluate Nix expression +eval FILE: + nix-instantiate --plugin-files ./build/nix-ir-plugin.so --eval --strict "{{FILE}}" + +# Format C++ code with clang-format +format: + find src tests -name '*.cpp' -o -name '*.h' | xargs clang-format -i + +# Run clang-tidy on source files +lint: + find src -name '*.cpp' | xargs clang-tidy --fix + +# Show project statistics +stats: + @echo "Lines of code:" + @find src -name '*.cpp' -o -name '*.h' | xargs wc -l | tail -1 + @echo "" + @echo "Test files:" + @find tests/fixtures -name '*.nix' | wc -l + @echo "" + @echo "Build status:" + @ls -lh build/nix-irc build/nix-ir-plugin.so build/regression_test 2>/dev/null || echo "Not built" + +# Run a quick smoke test +smoke: + ./build/nix-irc tests/fixtures/simple.nix /tmp/smoke.nixir + nix-instantiate --plugin-files ./build/nix-ir-plugin.so --eval tests/integration/simple_eval.nix + +# Generate IR from a Nix file and inspect it +inspect FILE: + ./build/nix-irc "{{FILE}}" /tmp/inspect.nixir + @echo "IR bundle size:" + @ls -lh /tmp/inspect.nixir | awk '{print $5}' + @echo "Magic number:" + @xxd -l 4 /tmp/inspect.nixir + +# Watch mode, rebuild on file changes +watch: + find src tests -name '*.cpp' -o -name '*.h' | entr -c just build test-unit From 815f4a4725f6c84f6609815ebb5a4a33658e8265 Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Mon, 23 Feb 2026 02:25:46 +0300 Subject: [PATCH 12/12] docs: document using `just` in the codebase Signed-off-by: NotAShelf Change-Id: I56d1de8a88bb28e49e6387a320f318c86a6a6964 --- README.md | 51 ++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 38 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 1048d9c..9ff280b 100644 --- a/README.md +++ b/README.md @@ -169,27 +169,44 @@ Entry: ### Building ```bash -# Configure -$ cmake -B build +# Using just (recommended) +$ just build -# Build -$ make +# Or manually with CMake +$ cmake -B build -G Ninja +$ cmake --build build -# The nix-irc executable will be in the project root -$ ./nix-irc --help +# 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 ```bash # Basic compilation -$ nix-irc input.nix output.nixir +$ ./build/nix-irc input.nix output.nixir # With import search paths -$ nix-irc -I ./lib -I /nix/store/... input.nix output.nixir +$ ./build/nix-irc -I ./lib -I /nix/store/... input.nix output.nixir # Disable import resolution -$ nix-irc --no-imports input.nix output.nixir +$ ./build/nix-irc --no-imports input.nix output.nixir + +# Using just +$ just compile input.nix output.nixir ``` ### Runtime Evaluation (Plugin) @@ -212,13 +229,21 @@ $ 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" +# 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/simple.nixir | head -3 +$ hexdump -C tests/fixtures/simple.nixir | head -3 ``` ## Contributing