#include "irc/lexer.h" #include "irc/parser.h" #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 == 3) { std::cout << " PASS: IR_VERSION is 3" << std::endl; } else { std::cerr << " FAIL: IR_VERSION should be 3, got " << IR_VERSION << std::endl; failures++; } } 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)"; 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::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::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_implication_right_associativity() { std::cout << "> Implication right associativity..." << std::endl; Parser parser; auto ast = parser.parse("a -> b -> c"); auto* outer = ast->get_if(); TEST_CHECK(outer != nullptr, "Top-level node is BinaryOpNode"); TEST_CHECK(outer && outer->op == BinaryOp::IMPL, "Top-level operator is implication"); if (outer) { auto* left = outer->left->get_if(); auto* right = outer->right->get_if(); TEST_CHECK(left != nullptr && left->name && *left->name == "a", "Left branch is variable 'a'"); TEST_CHECK(right != nullptr && right->op == BinaryOp::IMPL, "Right branch is nested implication"); } } void test_lookup_path_lexer_position() { std::cout << "> Lookup path lexer position..." << std::endl; Lexer lexer(" x"); auto tokens = lexer.tokenize(); TEST_CHECK(tokens.size() >= 3, "Lexer produced lookup path, identifier, and EOF"); TEST_CHECK(tokens[0].type == Token::LOOKUP_PATH, "First token is LOOKUP_PATH"); TEST_CHECK(tokens[1].type == Token::IDENT && tokens[1].value == "x", "Second token is identifier 'x'"); TEST_CHECK(tokens[1].col == 11, "Identifier column reflects consumed lookup path width"); } void test_unterminated_block_comment_rejected() { std::cout << "> Unterminated block comment rejection..." << std::endl; try { Lexer lexer("/* unterminated"); auto tokens = lexer.tokenize(); (void) tokens; TEST_FAIL("Lexer should reject unterminated block comments"); } catch (const std::exception& e) { TEST_PASS("Lexer rejects unterminated block comments"); } } void test_unknown_character_rejected() { std::cout << "> Unknown character rejection..." << std::endl; try { Lexer lexer("1 $ 2"); auto tokens = lexer.tokenize(); (void) tokens; TEST_FAIL("Lexer should reject unexpected characters"); } catch (const std::exception& e) { TEST_PASS("Lexer rejects unexpected characters"); } } void test_lookup_path_node() { std::cout << "> Lookup path serialization..." << std::endl; auto lookup = std::make_shared(ConstLookupPathNode("nixpkgs")); IRModule module; module.entry = lookup; Serializer ser; auto bytes = ser.serialize_to_bytes(module); Deserializer deser; auto loaded = deser.deserialize(bytes); 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'"); } void test_import_node() { std::cout << "> Import node serialization..." << std::endl; auto path = std::make_shared(ConstPathNode("./test.nix")); auto import_node = std::make_shared(ImportNode(path)); IRModule module; module.entry = import_node; Serializer ser; auto bytes = ser.serialize_to_bytes(module); Deserializer deser; auto loaded = deser.deserialize(bytes); 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"); if (loaded_import && loaded_import->path) { 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'"); } } void test_import_with_lookup_path() { std::cout << "> Import with lookup path..." << std::endl; auto lookup = std::make_shared(ConstLookupPathNode("nixpkgs")); auto import_node = std::make_shared(ImportNode(lookup)); IRModule module; module.entry = import_node; Serializer ser; auto bytes = ser.serialize_to_bytes(module); Deserializer deser; auto loaded = deser.deserialize(bytes); 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(); TEST_CHECK(lookup_node != nullptr, "Import path is ConstLookupPathNode"); TEST_CHECK(lookup_node && lookup_node->value == "nixpkgs", "Lookup path value is 'nixpkgs'"); } } void test_relative_path_import_parsing() { std::cout << "> Relative path import parsing..." << std::endl; Parser parser; auto ast = parser.parse("import ./simple.nix"); auto* import_node = ast->get_if(); TEST_CHECK(import_node != nullptr, "Parsed expression is ImportNode"); if (import_node && import_node->path) { auto* path_node = import_node->path->get_if(); TEST_CHECK(path_node != nullptr, "Import argument is ConstPathNode"); TEST_CHECK(path_node && path_node->value == "./simple.nix", "Relative path is preserved as './simple.nix'"); } } void test_builtin_call_node() { std::cout << "> BuiltinCallNode serialization..." << std::endl; auto arg = std::make_shared(ConstStringNode("/tmp/example-flake")); auto builtin = std::make_shared(BuiltinCallNode("getFlake", std::vector>{arg})); IRModule module; module.entry = builtin; Serializer ser; auto bytes = ser.serialize_to_bytes(module); Deserializer deser; auto loaded = deser.deserialize(bytes); auto* loaded_builtin = loaded.entry->get_if(); TEST_CHECK(loaded_builtin != nullptr, "Deserialized node is BuiltinCallNode"); TEST_CHECK(loaded_builtin && loaded_builtin->builtin_name == "getFlake", "Builtin name is 'getFlake'"); TEST_CHECK(loaded_builtin && loaded_builtin->args.size() == 1, "Builtin has one argument"); if (loaded_builtin && loaded_builtin->args.size() == 1) { auto* loaded_arg = loaded_builtin->args[0]->get_if(); TEST_CHECK(loaded_arg != nullptr, "Builtin argument is ConstStringNode"); TEST_CHECK(loaded_arg && loaded_arg->value == "/tmp/example-flake", "Builtin argument value round-trips"); } } void test_uri_node() { std::cout << "> URI node serialization..." << std::endl; auto uri = std::make_shared(ConstURINode("https://example.com")); IRModule module; module.entry = uri; Serializer ser; auto bytes = ser.serialize_to_bytes(module); Deserializer deser; auto loaded = deser.deserialize(bytes); 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'"); } void test_float_node() { std::cout << "> Float node serialization..." << std::endl; auto float_val = std::make_shared(ConstFloatNode(3.14159)); IRModule module; module.entry = float_val; Serializer ser; auto bytes = ser.serialize_to_bytes(module); Deserializer deser; auto loaded = deser.deserialize(bytes); 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, "Float value is approximately 3.14159"); } // LambdaPatternNode Tests void test_lambda_pattern_simple() { std::cout << "> LambdaPatternNode simple ({ a, b }: a + b)..." << std::endl; // Body: a + b (using VarNode for a and b) auto var_a = std::make_shared(VarNode(0, "a")); auto var_b = std::make_shared(VarNode(0, "b")); auto body = std::make_shared(BinaryOpNode(BinaryOp::ADD, var_a, var_b)); // Create lambda pattern with two required fields LambdaPatternNode lambda_pattern(body); lambda_pattern.required_fields.emplace_back("a", std::nullopt); lambda_pattern.required_fields.emplace_back("b", std::nullopt); lambda_pattern.allow_extra = false; auto node = std::make_shared(std::move(lambda_pattern)); // Serialize IRModule module; module.entry = node; Serializer ser; auto bytes = ser.serialize_to_bytes(module); // Deserialize Deserializer deser; auto loaded = deser.deserialize(bytes); // Verify auto* loaded_node = loaded.entry->get_if(); TEST_CHECK(loaded_node != nullptr, "Type is LambdaPatternNode"); TEST_CHECK(loaded_node && loaded_node->required_fields.size() == 2, "Has 2 required fields"); TEST_CHECK(loaded_node && loaded_node->optional_fields.size() == 0, "Has 0 optional fields"); TEST_CHECK(loaded_node && loaded_node->required_fields[0].name == "a", "First field is 'a'"); TEST_CHECK(loaded_node && loaded_node->required_fields[1].name == "b", "Second field is 'b'"); TEST_CHECK(loaded_node && !loaded_node->at_binding.has_value(), "No at-binding"); TEST_CHECK(loaded_node && !loaded_node->allow_extra, "No ellipsis"); TEST_CHECK(loaded_node && loaded_node->body != nullptr, "Has body"); } void test_lambda_pattern_with_defaults() { std::cout << "> LambdaPatternNode with defaults ({ a, b ? 10 }: a + b)..." << std::endl; // Default value for b auto default_b = std::make_shared(ConstIntNode(10)); // Body: a + b auto var_a = std::make_shared(VarNode(0, "a")); auto var_b = std::make_shared(VarNode(0, "b")); auto body = std::make_shared(BinaryOpNode(BinaryOp::ADD, var_a, var_b)); // Create lambda pattern LambdaPatternNode lambda_pattern(body); lambda_pattern.required_fields.emplace_back("a", std::nullopt); lambda_pattern.optional_fields.emplace_back("b", default_b); lambda_pattern.allow_extra = false; auto node = std::make_shared(std::move(lambda_pattern)); // Serialize IRModule module; module.entry = node; Serializer ser; auto bytes = ser.serialize_to_bytes(module); // Deserialize Deserializer deser; auto loaded = deser.deserialize(bytes); // Verify auto* loaded_node = loaded.entry->get_if(); TEST_CHECK(loaded_node != nullptr, "Type is LambdaPatternNode"); TEST_CHECK(loaded_node && loaded_node->required_fields.size() == 1, "Has 1 required field"); TEST_CHECK(loaded_node && loaded_node->optional_fields.size() == 1, "Has 1 optional field"); TEST_CHECK(loaded_node && loaded_node->required_fields[0].name == "a", "Required field is 'a'"); TEST_CHECK(loaded_node && loaded_node->optional_fields[0].name == "b", "Optional field is 'b'"); TEST_CHECK(loaded_node && loaded_node->optional_fields[0].default_value.has_value(), "Optional field has default"); if (loaded_node && loaded_node->optional_fields[0].default_value) { auto* def_val = (*loaded_node->optional_fields[0].default_value)->get_if(); TEST_CHECK(def_val && def_val->value == 10, "Default value is 10"); } } void test_lambda_pattern_at_binding() { std::cout << "> LambdaPatternNode with at-binding (args@{ a, b }: args.a)..." << std::endl; // Body: args.a (select expression) auto var_args = std::make_shared(VarNode(0, "args")); auto attr = std::make_shared(ConstStringNode("a")); auto body = std::make_shared(SelectNode(var_args, attr)); // Create lambda pattern with at-binding LambdaPatternNode lambda_pattern(body); lambda_pattern.required_fields.emplace_back("a", std::nullopt); lambda_pattern.required_fields.emplace_back("b", std::nullopt); lambda_pattern.at_binding = "args"; lambda_pattern.allow_extra = false; auto node = std::make_shared(std::move(lambda_pattern)); // Serialize IRModule module; module.entry = node; Serializer ser; auto bytes = ser.serialize_to_bytes(module); // Deserialize Deserializer deser; auto loaded = deser.deserialize(bytes); // Verify auto* loaded_node = loaded.entry->get_if(); TEST_CHECK(loaded_node != nullptr, "Type is LambdaPatternNode"); TEST_CHECK(loaded_node && loaded_node->at_binding.has_value(), "Has at-binding"); TEST_CHECK(loaded_node && loaded_node->at_binding.value() == "args", "At-binding is 'args'"); } void test_lambda_pattern_ellipsis() { std::cout << "> LambdaPatternNode with ellipsis ({ a, ... }: a)..." << std::endl; // Body: a auto body = std::make_shared(VarNode(0, "a")); // Create lambda pattern with ellipsis LambdaPatternNode lambda_pattern(body); lambda_pattern.required_fields.emplace_back("a", std::nullopt); lambda_pattern.allow_extra = true; auto node = std::make_shared(std::move(lambda_pattern)); // Serialize IRModule module; module.entry = node; Serializer ser; auto bytes = ser.serialize_to_bytes(module); // Deserialize Deserializer deser; auto loaded = deser.deserialize(bytes); // Verify auto* loaded_node = loaded.entry->get_if(); TEST_CHECK(loaded_node != nullptr, "Type is LambdaPatternNode"); TEST_CHECK(loaded_node && loaded_node->allow_extra, "Has ellipsis (allow_extra=true)"); } void test_lambda_pattern_complete() { std::cout << "> LambdaPatternNode complete (args@{ a, b ? 5, ... }: body)..." << std::endl; // Default value for b auto default_b = std::make_shared(ConstIntNode(5)); // Body: simple var auto body = std::make_shared(VarNode(0, "x")); // Create lambda pattern with all features LambdaPatternNode lambda_pattern(body); lambda_pattern.required_fields.emplace_back("a", std::nullopt); lambda_pattern.optional_fields.emplace_back("b", default_b); lambda_pattern.at_binding = "args"; lambda_pattern.allow_extra = true; auto node = std::make_shared(std::move(lambda_pattern)); // Serialize IRModule module; module.entry = node; Serializer ser; auto bytes = ser.serialize_to_bytes(module); // Deserialize Deserializer deser; auto loaded = deser.deserialize(bytes); // Verify all fields auto* loaded_node = loaded.entry->get_if(); TEST_CHECK(loaded_node != nullptr, "Type is LambdaPatternNode"); TEST_CHECK(loaded_node && loaded_node->required_fields.size() == 1, "Has 1 required field"); TEST_CHECK(loaded_node && loaded_node->optional_fields.size() == 1, "Has 1 optional field"); TEST_CHECK(loaded_node && loaded_node->at_binding.has_value(), "Has at-binding"); TEST_CHECK(loaded_node && loaded_node->at_binding.value() == "args", "At-binding is 'args'"); TEST_CHECK(loaded_node && loaded_node->allow_extra, "Has ellipsis"); } void test_lambda_pattern_empty() { std::cout << "> LambdaPatternNode empty ({ }: body)..." << std::endl; // Body: simple constant auto body = std::make_shared(ConstIntNode(42)); // Create empty lambda pattern LambdaPatternNode lambda_pattern(body); lambda_pattern.allow_extra = false; auto node = std::make_shared(std::move(lambda_pattern)); // Serialize IRModule module; module.entry = node; Serializer ser; auto bytes = ser.serialize_to_bytes(module); // Deserialize Deserializer deser; auto loaded = deser.deserialize(bytes); // Verify auto* loaded_node = loaded.entry->get_if(); TEST_CHECK(loaded_node != nullptr, "Type is LambdaPatternNode"); TEST_CHECK(loaded_node && loaded_node->required_fields.size() == 0, "Has 0 required fields"); TEST_CHECK(loaded_node && loaded_node->optional_fields.size() == 0, "Has 0 optional fields"); TEST_CHECK(loaded_node && !loaded_node->at_binding.has_value(), "No at-binding"); TEST_CHECK(loaded_node && !loaded_node->allow_extra, "No ellipsis"); } // StringInterpolationNode Tests void test_string_interpolation_simple() { std::cout << "> StringInterpolationNode simple (\"hello ${name}\")..." << std::endl; // "hello ${name}" = literal "hello " + expr(name) std::vector parts; parts.push_back(StringPart::make_literal("hello ")); parts.push_back(StringPart::make_expr(std::make_shared(VarNode(0, "name")))); auto node = std::make_shared(StringInterpolationNode(std::move(parts))); // Serialize IRModule module; module.entry = node; Serializer ser; auto bytes = ser.serialize_to_bytes(module); // Deserialize Deserializer deser; auto loaded = deser.deserialize(bytes); // Verify auto* loaded_node = loaded.entry->get_if(); TEST_CHECK(loaded_node != nullptr, "Type is StringInterpolationNode"); TEST_CHECK(loaded_node && loaded_node->parts.size() == 2, "Has 2 parts"); TEST_CHECK(loaded_node && loaded_node->parts[0].type == StringPart::Type::LITERAL, "First part is LITERAL"); TEST_CHECK(loaded_node && loaded_node->parts[0].literal == "hello ", "First part is 'hello '"); TEST_CHECK(loaded_node && loaded_node->parts[1].type == StringPart::Type::EXPR, "Second part is EXPR"); TEST_CHECK(loaded_node && loaded_node->parts[1].expr != nullptr, "Second part has expression"); } void test_string_interpolation_multiple() { std::cout << "> StringInterpolationNode multiple (\"${a} and ${b}\")..." << std::endl; // "${a} and ${b}" = expr(a) + literal " and " + expr(b) std::vector parts; parts.push_back(StringPart::make_expr(std::make_shared(VarNode(0, "a")))); parts.push_back(StringPart::make_literal(" and ")); parts.push_back(StringPart::make_expr(std::make_shared(VarNode(0, "b")))); auto node = std::make_shared(StringInterpolationNode(std::move(parts))); // Serialize IRModule module; module.entry = node; Serializer ser; auto bytes = ser.serialize_to_bytes(module); // Deserialize Deserializer deser; auto loaded = deser.deserialize(bytes); // Verify auto* loaded_node = loaded.entry->get_if(); TEST_CHECK(loaded_node != nullptr, "Type is StringInterpolationNode"); TEST_CHECK(loaded_node && loaded_node->parts.size() == 3, "Has 3 parts"); TEST_CHECK(loaded_node && loaded_node->parts[0].type == StringPart::Type::EXPR, "Part 0 is EXPR"); TEST_CHECK(loaded_node && loaded_node->parts[1].type == StringPart::Type::LITERAL, "Part 1 is LITERAL"); TEST_CHECK(loaded_node && loaded_node->parts[1].literal == " and ", "Part 1 is ' and '"); TEST_CHECK(loaded_node && loaded_node->parts[2].type == StringPart::Type::EXPR, "Part 2 is EXPR"); } void test_string_interpolation_complex() { std::cout << "> StringInterpolationNode complex (\"result: ${a + b}\")..." << std::endl; // "result: ${a + b}" = literal "result: " + expr(a + b) auto expr_a = std::make_shared(VarNode(0, "a")); auto expr_b = std::make_shared(VarNode(0, "b")); auto add_expr = std::make_shared(BinaryOpNode(BinaryOp::ADD, expr_a, expr_b)); std::vector parts; parts.push_back(StringPart::make_literal("result: ")); parts.push_back(StringPart::make_expr(add_expr)); auto node = std::make_shared(StringInterpolationNode(std::move(parts))); // Serialize IRModule module; module.entry = node; Serializer ser; auto bytes = ser.serialize_to_bytes(module); // Deserialize Deserializer deser; auto loaded = deser.deserialize(bytes); // Verify auto* loaded_node = loaded.entry->get_if(); TEST_CHECK(loaded_node != nullptr, "Type is StringInterpolationNode"); TEST_CHECK(loaded_node && loaded_node->parts.size() == 2, "Has 2 parts"); TEST_CHECK(loaded_node && loaded_node->parts[1].type == StringPart::Type::EXPR, "Part 1 is EXPR"); // Verify the expression is a BinaryOpNode if (loaded_node && loaded_node->parts[1].expr) { auto* bin_op = loaded_node->parts[1].expr->get_if(); TEST_CHECK(bin_op != nullptr, "Expression is BinaryOpNode"); TEST_CHECK(bin_op && bin_op->op == BinaryOp::ADD, "Operation is ADD"); } } void test_string_interpolation_nested() { std::cout << "> StringInterpolationNode nested (\"${prefix}/${path}\")..." << std::endl; // "${prefix}/${path}" = expr(prefix) + literal "/" + expr(path) std::vector parts; parts.push_back(StringPart::make_expr(std::make_shared(VarNode(0, "prefix")))); parts.push_back(StringPart::make_literal("/")); parts.push_back(StringPart::make_expr(std::make_shared(VarNode(0, "path")))); auto node = std::make_shared(StringInterpolationNode(std::move(parts))); // Serialize IRModule module; module.entry = node; Serializer ser; auto bytes = ser.serialize_to_bytes(module); // Deserialize Deserializer deser; auto loaded = deser.deserialize(bytes); // Verify auto* loaded_node = loaded.entry->get_if(); TEST_CHECK(loaded_node != nullptr, "Type is StringInterpolationNode"); TEST_CHECK(loaded_node && loaded_node->parts.size() == 3, "Has 3 parts"); TEST_CHECK(loaded_node && loaded_node->parts[1].type == StringPart::Type::LITERAL, "Middle part is LITERAL"); TEST_CHECK(loaded_node && loaded_node->parts[1].literal == "/", "Middle part is '/'"); } int main() { std::cout << "=== Regression Tests ===" << 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; test_implication_right_associativity(); std::cout << std::endl; test_lookup_path_lexer_position(); std::cout << std::endl; test_unterminated_block_comment_rejected(); std::cout << std::endl; test_unknown_character_rejected(); std::cout << std::endl; test_lookup_path_node(); std::cout << std::endl; test_import_node(); std::cout << std::endl; test_import_with_lookup_path(); std::cout << std::endl; test_relative_path_import_parsing(); std::cout << std::endl; test_builtin_call_node(); std::cout << std::endl; test_uri_node(); std::cout << std::endl; test_float_node(); std::cout << std::endl; test_lambda_pattern_simple(); std::cout << std::endl; test_lambda_pattern_with_defaults(); std::cout << std::endl; test_lambda_pattern_at_binding(); std::cout << std::endl; test_lambda_pattern_ellipsis(); std::cout << std::endl; test_lambda_pattern_complete(); std::cout << std::endl; test_lambda_pattern_empty(); std::cout << std::endl; test_string_interpolation_simple(); std::cout << std::endl; test_string_interpolation_multiple(); std::cout << std::endl; test_string_interpolation_complex(); std::cout << std::endl; test_string_interpolation_nested(); std::cout << std::endl; std::cout << "=== Tests Complete ===" << std::endl; std::cout << "Failures: " << failures << std::endl; return failures > 0 ? 1 : 0; }