// RF-2: wire-format round-trip test for fold::ir::Expr. // // Build a RustExprNode, serialize to JSON, deserialize back, assert // structural equality. Exercises every Expr::Kind the RF-2 schema // supports. #include #include #include "fold/ir/expr.hpp" #include "fold/ir/expr_json.hpp" #include "fold/ir/opcode.hpp" #include "fold/ir/source_loc.hpp" using fold::ir::BinaryOpcode; using fold::ir::Expr; using fold::ir::fromJson; using fold::ir::RustExprNode; using fold::ir::RustNodeArena; using fold::ir::SourceLoc; using fold::ir::toJson; using fold::ir::UnaryOpcode; namespace { // Structural equality, ignoring distinct pointer identity. Two nodes // match when their kind + payload + recursively their children match. bool nodesEqual(const RustExprNode* a, const RustExprNode* b) { if (a == nullptr || b == nullptr) return a == b; if (a->kind != b->kind) return false; if (a->language != b->language) return false; if (a->loc.file != b->loc.file || a->loc.line != b->loc.line || a->loc.column != b->loc.column) return false; switch (a->kind) { case Expr::Kind::DeclRef: return a->declref_symbol == b->declref_symbol; case Expr::Kind::IntegerLiteral: return a->int_value == b->int_value && a->int_width == b->int_width && a->int_signed == b->int_signed; case Expr::Kind::BoolLiteral: return a->bool_value == b->bool_value; case Expr::Kind::NullPtr: return true; case Expr::Kind::BinaryOp: return a->binary_op == b->binary_op && nodesEqual(a->binary_lhs, b->binary_lhs) && nodesEqual(a->binary_rhs, b->binary_rhs); case Expr::Kind::UnaryOp: case Expr::Kind::AddrOf: case Expr::Kind::Deref: case Expr::Kind::Cast: return a->unary_op == b->unary_op && nodesEqual(a->unary_sub, b->unary_sub); case Expr::Kind::Call: case Expr::Kind::MemberCall: case Expr::Kind::ConstructorCall: { if (a->callee_name != b->callee_name || a->call_args.size() != b->call_args.size()) return false; for (std::size_t i = 0; i < a->call_args.size(); ++i) if (!nodesEqual(a->call_args[i], b->call_args[i])) return false; return true; } case Expr::Kind::Member: { return a->member_field == b->member_field && nodesEqual(a->member_base, b->member_base); } case Expr::Kind::ArraySubscript: return nodesEqual(a->array_base, b->array_base) && nodesEqual(a->array_index, b->array_index); case Expr::Kind::HeapNew: case Expr::Kind::HeapDelete: return a->heap_is_array == b->heap_is_array && nodesEqual(a->heap_alloc_arg, b->heap_alloc_arg); case Expr::Kind::ThrowExpr: return nodesEqual(a->throw_sub, b->throw_sub); case Expr::Kind::CXXConstructExpr: if (a->callee_name != b->callee_name || a->call_args.size() != b->call_args.size()) return false; for (std::size_t i = 0; i < a->call_args.size(); ++i) if (!nodesEqual(a->call_args[i], b->call_args[i])) return false; return true; case Expr::Kind::LambdaExpr: return nodesEqual(a->lambda_body, b->lambda_body); case Expr::Kind::StmtExpr: return nodesEqual(a->stmt_expr_body, b->stmt_expr_body); default: return false; } } // Round-trip helper: serialize → deserialize → compare. void requireRoundTrip(const RustExprNode& original) { auto j = toJson(original); RustNodeArena arena; auto* parsed = fromJson(j, arena); REQUIRE(parsed != nullptr); REQUIRE(nodesEqual(parsed, &original)); } } // namespace TEST_CASE("expr_json: DeclRef round-trip", "[expr_json][roundtrip]") { RustExprNode node; node.kind = Expr::Kind::DeclRef; node.loc = SourceLoc{"foo.rs", 12, 4}; node.declref_symbol = "crate::foo::x"; requireRoundTrip(node); } TEST_CASE("expr_json: IntegerLiteral round-trip", "[expr_json][roundtrip]") { RustExprNode node; node.kind = Expr::Kind::IntegerLiteral; node.loc = SourceLoc{"foo.rs", 3, 7}; node.int_value = -42; node.int_width = 32; node.int_signed = true; requireRoundTrip(node); } TEST_CASE("expr_json: BoolLiteral round-trip", "[expr_json][roundtrip]") { RustExprNode node; node.kind = Expr::Kind::BoolLiteral; node.loc = SourceLoc{"foo.rs", 1, 1}; node.bool_value = true; requireRoundTrip(node); } TEST_CASE("expr_json: NullPtr round-trip", "[expr_json][roundtrip]") { RustExprNode node; node.kind = Expr::Kind::NullPtr; node.loc = SourceLoc{"foo.rs", 9, 2}; requireRoundTrip(node); } TEST_CASE("expr_json: BinaryOp Add of two literals round-trip", "[expr_json][roundtrip]") { RustNodeArena scratch; auto* lhs = scratch.alloc(); lhs->kind = Expr::Kind::IntegerLiteral; lhs->int_value = 7; lhs->int_width = 32; auto* rhs = scratch.alloc(); rhs->kind = Expr::Kind::DeclRef; rhs->declref_symbol = "crate::y"; RustExprNode root; root.kind = Expr::Kind::BinaryOp; root.loc = SourceLoc{"foo.rs", 5, 9}; root.binary_op = BinaryOpcode::Add; root.binary_lhs = lhs; root.binary_rhs = rhs; requireRoundTrip(root); } TEST_CASE("expr_json: Deref of DeclRef round-trip", "[expr_json][roundtrip]") { RustNodeArena scratch; auto* sub = scratch.alloc(); sub->kind = Expr::Kind::DeclRef; sub->declref_symbol = "crate::p"; RustExprNode root; root.kind = Expr::Kind::Deref; root.loc = SourceLoc{"foo.rs", 11, 13}; root.unary_op = UnaryOpcode::Deref; root.unary_sub = sub; requireRoundTrip(root); } TEST_CASE("expr_json: AddrOf of DeclRef round-trip", "[expr_json][roundtrip]") { RustNodeArena scratch; auto* sub = scratch.alloc(); sub->kind = Expr::Kind::DeclRef; sub->declref_symbol = "crate::q"; RustExprNode root; root.kind = Expr::Kind::AddrOf; root.unary_op = UnaryOpcode::AddrOf; root.unary_sub = sub; requireRoundTrip(root); } TEST_CASE("expr_json: Call of DeclRef round-trip", "[expr_json][roundtrip]") { RustNodeArena scratch; auto* arg = scratch.alloc(); arg->kind = Expr::Kind::IntegerLiteral; arg->int_value = 42; arg->int_width = 32; RustExprNode root; root.kind = Expr::Kind::Call; root.loc = SourceLoc{"foo.rs", 10, 5}; root.callee_name = "my_func"; root.call_args = {arg}; requireRoundTrip(root); } TEST_CASE("expr_json: Member access round-trip", "[expr_json][roundtrip]") { RustNodeArena scratch; auto* base = scratch.alloc(); base->kind = Expr::Kind::DeclRef; base->declref_symbol = "crate::obj"; RustExprNode root; root.kind = Expr::Kind::Member; root.loc = SourceLoc{"foo.rs", 7, 3}; root.member_base = base; root.member_field = "size"; requireRoundTrip(root); } TEST_CASE("expr_json: ArraySubscript round-trip", "[expr_json][roundtrip]") { RustNodeArena scratch; auto* base = scratch.alloc(); base->kind = Expr::Kind::DeclRef; base->declref_symbol = "crate::arr"; auto* idx = scratch.alloc(); idx->kind = Expr::Kind::IntegerLiteral; idx->int_value = 0; idx->int_width = 64; RustExprNode root; root.kind = Expr::Kind::ArraySubscript; root.loc = SourceLoc{"foo.rs", 8, 4}; root.array_base = base; root.array_index = idx; requireRoundTrip(root); } TEST_CASE("expr_json: Cast round-trip", "[expr_json][roundtrip]") { RustNodeArena scratch; auto* sub = scratch.alloc(); sub->kind = Expr::Kind::DeclRef; sub->declref_symbol = "crate::x"; RustExprNode root; root.kind = Expr::Kind::Cast; root.loc = SourceLoc{"foo.rs", 6, 2}; root.unary_sub = sub; root.cast_type_name = "i32"; requireRoundTrip(root); } TEST_CASE("expr_json: HeapNew round-trip", "[expr_json][roundtrip]") { RustNodeArena scratch; auto* arg = scratch.alloc(); arg->kind = Expr::Kind::IntegerLiteral; arg->int_value = 16; arg->int_width = 64; RustExprNode root; root.kind = Expr::Kind::HeapNew; root.loc = SourceLoc{"foo.rs", 15, 3}; root.heap_alloc_arg = arg; root.heap_is_array = true; requireRoundTrip(root); } TEST_CASE("expr_json: language field round-trip (Rust)", "[expr_json][roundtrip][language]") { RustExprNode node; node.kind = Expr::Kind::DeclRef; node.language = fold::ir::Language::Rust; node.declref_symbol = "crate::x"; requireRoundTrip(node); } TEST_CASE("expr_json: language field round-trip (Cpp)", "[expr_json][roundtrip][language]") { RustExprNode node; node.kind = Expr::Kind::DeclRef; node.language = fold::ir::Language::Cpp; node.declref_symbol = "::my_var"; requireRoundTrip(node); } TEST_CASE("expr_json: language field round-trip (Swift)", "[expr_json][roundtrip][language]") { RustExprNode node; node.kind = Expr::Kind::DeclRef; node.language = fold::ir::Language::Swift; node.declref_symbol = "myVar"; requireRoundTrip(node); } TEST_CASE("expr_json: language field defaults to Unknown", "[expr_json][roundtrip][language]") { RustNodeArena arena; nlohmann::json j; j["kind"] = "DeclRef"; j["symbol"] = "x"; // No language key -- should default to Unknown. auto* parsed = fromJson(j, arena); REQUIRE(parsed != nullptr); REQUIRE(parsed->language == fold::ir::Language::Unknown); } TEST_CASE("expr_json: ThrowExpr round-trip", "[expr_json][roundtrip][clang]") { RustNodeArena scratch; auto* sub = scratch.alloc(); sub->kind = Expr::Kind::DeclRef; sub->declref_symbol = "std::runtime_error"; RustExprNode root; root.kind = Expr::Kind::ThrowExpr; root.language = fold::ir::Language::Cpp; root.loc = SourceLoc{"throw.cpp", 42, 5}; root.throw_sub = sub; requireRoundTrip(root); } TEST_CASE("expr_json: CXXConstructExpr round-trip", "[expr_json][roundtrip][clang]") { RustNodeArena scratch; auto* arg = scratch.alloc(); arg->kind = Expr::Kind::IntegerLiteral; arg->int_value = 42; arg->int_width = 32; RustExprNode root; root.kind = Expr::Kind::CXXConstructExpr; root.language = fold::ir::Language::Cpp; root.loc = SourceLoc{"construct.cpp", 10, 3}; root.callee_name = "std::string"; root.call_args = {arg}; requireRoundTrip(root); } TEST_CASE("expr_json: LambdaExpr round-trip", "[expr_json][roundtrip][clang]") { RustNodeArena scratch; auto* body = scratch.alloc(); body->kind = Expr::Kind::DeclRef; body->declref_symbol = "captured_x"; RustExprNode root; root.kind = Expr::Kind::LambdaExpr; root.language = fold::ir::Language::Cpp; root.loc = SourceLoc{"lambda.cpp", 7, 12}; root.lambda_body = body; requireRoundTrip(root); } TEST_CASE("expr_json: StmtExpr round-trip", "[expr_json][roundtrip][clang]") { RustNodeArena scratch; auto* body = scratch.alloc(); body->kind = Expr::Kind::IntegerLiteral; body->int_value = 1; body->int_width = 32; RustExprNode root; root.kind = Expr::Kind::StmtExpr; root.language = fold::ir::Language::Cpp; root.loc = SourceLoc{"stmtexpr.c", 15, 3}; root.stmt_expr_body = body; requireRoundTrip(root); } TEST_CASE("expr_json: unknown kind decodes to nullptr", "[expr_json][roundtrip][error]") { nlohmann::json j; j["kind"] = "SomethingFromTheFuture"; RustNodeArena arena; REQUIRE(fromJson(j, arena) == nullptr); } TEST_CASE("expr_json: malformed input (no kind) decodes to nullptr", "[expr_json][roundtrip][error]") { nlohmann::json j; j["op"] = "Add"; RustNodeArena arena; REQUIRE(fromJson(j, arena) == nullptr); }