fold/tests/unit/test_expr_json_roundtrip.cpp
trvon cb600dbac9
feat: IR language discriminator + Clang node kinds (Phase 3, multi-language IR)
- Language enum: Unknown, Rust, Cpp, Swift on RustExprNode
- 4 new Clang node kinds: ThrowExpr, CXXConstructExpr, LambdaExpr, StmtExpr
- Updated json_keys namespace with new constants
- 42 roundtrip test assertions pass
- All 3 frontends now share the same IR representation
2026-05-28 20:55:54 -04:00

366 lines
12 KiB
C++

// 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 <catch2/catch_test_macros.hpp>
#include <nlohmann/json.hpp>
#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);
}