Sprint 2 Step 6: JSON deserialization and round-trip verification
fromJson(json) reconstructs heap-allocated AST from JSON. createNode factory handles all 33 concepts. deleteTree for cleanup. Test verifies save→load→save produces byte-identical JSON for the full Calculator model. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -29,3 +29,7 @@ target_include_directories(step4_test PRIVATE src)
|
||||
add_executable(step5_test tests/step5_test.cpp)
|
||||
target_include_directories(step5_test PRIVATE src)
|
||||
target_link_libraries(step5_test PRIVATE nlohmann_json::nlohmann_json)
|
||||
|
||||
add_executable(step6_test tests/step6_test.cpp)
|
||||
target_include_directories(step6_test PRIVATE src)
|
||||
target_link_libraries(step6_test PRIVATE nlohmann_json::nlohmann_json)
|
||||
|
||||
@@ -136,3 +136,165 @@ inline json toJson(const ASTNode* node) {
|
||||
|
||||
return j;
|
||||
}
|
||||
|
||||
// --- Deserialization ---
|
||||
|
||||
inline ASTNode* createNode(const std::string& conceptName) {
|
||||
if (conceptName == "Module") return new Module();
|
||||
if (conceptName == "Function") return new Function();
|
||||
if (conceptName == "Variable") return new Variable();
|
||||
if (conceptName == "Parameter") return new Parameter();
|
||||
if (conceptName == "Block") return new Block();
|
||||
if (conceptName == "Assignment") return new Assignment();
|
||||
if (conceptName == "IfStatement") return new IfStatement();
|
||||
if (conceptName == "WhileLoop") return new WhileLoop();
|
||||
if (conceptName == "ForLoop") return new ForLoop();
|
||||
if (conceptName == "Return") return new Return();
|
||||
if (conceptName == "ExpressionStatement") return new ExpressionStatement();
|
||||
if (conceptName == "BinaryOperation") return new BinaryOperation();
|
||||
if (conceptName == "UnaryOperation") return new UnaryOperation();
|
||||
if (conceptName == "FunctionCall") return new FunctionCall();
|
||||
if (conceptName == "VariableReference") return new VariableReference();
|
||||
if (conceptName == "IntegerLiteral") return new IntegerLiteral();
|
||||
if (conceptName == "FloatLiteral") return new FloatLiteral();
|
||||
if (conceptName == "StringLiteral") return new StringLiteral();
|
||||
if (conceptName == "BooleanLiteral") return new BooleanLiteral();
|
||||
if (conceptName == "NullLiteral") return new NullLiteral();
|
||||
if (conceptName == "ListLiteral") return new ListLiteral();
|
||||
if (conceptName == "IndexAccess") return new IndexAccess();
|
||||
if (conceptName == "MemberAccess") return new MemberAccess();
|
||||
if (conceptName == "PrimitiveType") return new PrimitiveType();
|
||||
if (conceptName == "ListType") return new ListType();
|
||||
if (conceptName == "SetType") return new SetType();
|
||||
if (conceptName == "MapType") return new MapType();
|
||||
if (conceptName == "TupleType") return new TupleType();
|
||||
if (conceptName == "ArrayType") return new ArrayType();
|
||||
if (conceptName == "OptionalType") return new OptionalType();
|
||||
if (conceptName == "CustomType") return new CustomType();
|
||||
if (conceptName == "DerefStrategy") return new DerefStrategy();
|
||||
if (conceptName == "OptimizationLock") return new OptimizationLock();
|
||||
if (conceptName == "LangSpecific") return new LangSpecific();
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
inline void setPropertiesFromJson(ASTNode* node, const json& props) {
|
||||
const auto& ct = node->conceptType;
|
||||
|
||||
if (ct == "Module") {
|
||||
auto* n = static_cast<Module*>(node);
|
||||
if (props.contains("name")) n->name = props["name"].get<std::string>();
|
||||
if (props.contains("targetLanguage")) n->targetLanguage = props["targetLanguage"].get<std::string>();
|
||||
}
|
||||
else if (ct == "Function") {
|
||||
auto* n = static_cast<Function*>(node);
|
||||
if (props.contains("name")) n->name = props["name"].get<std::string>();
|
||||
}
|
||||
else if (ct == "Variable") {
|
||||
auto* n = static_cast<Variable*>(node);
|
||||
if (props.contains("name")) n->name = props["name"].get<std::string>();
|
||||
}
|
||||
else if (ct == "Parameter") {
|
||||
auto* n = static_cast<Parameter*>(node);
|
||||
if (props.contains("name")) n->name = props["name"].get<std::string>();
|
||||
}
|
||||
else if (ct == "ForLoop") {
|
||||
auto* n = static_cast<ForLoop*>(node);
|
||||
if (props.contains("iteratorName")) n->iteratorName = props["iteratorName"].get<std::string>();
|
||||
}
|
||||
else if (ct == "BinaryOperation") {
|
||||
auto* n = static_cast<BinaryOperation*>(node);
|
||||
if (props.contains("op")) n->op = props["op"].get<std::string>();
|
||||
}
|
||||
else if (ct == "UnaryOperation") {
|
||||
auto* n = static_cast<UnaryOperation*>(node);
|
||||
if (props.contains("op")) n->op = props["op"].get<std::string>();
|
||||
}
|
||||
else if (ct == "FunctionCall") {
|
||||
auto* n = static_cast<FunctionCall*>(node);
|
||||
if (props.contains("functionName")) n->functionName = props["functionName"].get<std::string>();
|
||||
}
|
||||
else if (ct == "VariableReference") {
|
||||
auto* n = static_cast<VariableReference*>(node);
|
||||
if (props.contains("variableName")) n->variableName = props["variableName"].get<std::string>();
|
||||
}
|
||||
else if (ct == "IntegerLiteral") {
|
||||
auto* n = static_cast<IntegerLiteral*>(node);
|
||||
if (props.contains("value")) n->value = props["value"].get<int>();
|
||||
}
|
||||
else if (ct == "FloatLiteral") {
|
||||
auto* n = static_cast<FloatLiteral*>(node);
|
||||
if (props.contains("value")) n->value = props["value"].get<std::string>();
|
||||
}
|
||||
else if (ct == "StringLiteral") {
|
||||
auto* n = static_cast<StringLiteral*>(node);
|
||||
if (props.contains("value")) n->value = props["value"].get<std::string>();
|
||||
}
|
||||
else if (ct == "BooleanLiteral") {
|
||||
auto* n = static_cast<BooleanLiteral*>(node);
|
||||
if (props.contains("value")) n->value = props["value"].get<bool>();
|
||||
}
|
||||
else if (ct == "MemberAccess") {
|
||||
auto* n = static_cast<MemberAccess*>(node);
|
||||
if (props.contains("memberName")) n->memberName = props["memberName"].get<std::string>();
|
||||
}
|
||||
else if (ct == "PrimitiveType") {
|
||||
auto* n = static_cast<PrimitiveType*>(node);
|
||||
if (props.contains("kind")) n->kind = props["kind"].get<std::string>();
|
||||
}
|
||||
else if (ct == "CustomType") {
|
||||
auto* n = static_cast<CustomType*>(node);
|
||||
if (props.contains("typeName")) n->typeName = props["typeName"].get<std::string>();
|
||||
}
|
||||
else if (ct == "DerefStrategy") {
|
||||
auto* n = static_cast<DerefStrategy*>(node);
|
||||
if (props.contains("strategy")) n->strategy = props["strategy"].get<std::string>();
|
||||
if (props.contains("derefLocation")) n->derefLocation = props["derefLocation"].get<std::string>();
|
||||
if (props.contains("owner")) n->owner = props["owner"].get<std::string>();
|
||||
}
|
||||
else if (ct == "OptimizationLock") {
|
||||
auto* n = static_cast<OptimizationLock*>(node);
|
||||
if (props.contains("lockedBy")) n->lockedBy = props["lockedBy"].get<std::string>();
|
||||
if (props.contains("lockReason")) n->lockReason = props["lockReason"].get<std::string>();
|
||||
if (props.contains("lockLevel")) n->lockLevel = props["lockLevel"].get<std::string>();
|
||||
if (props.contains("affectedStrategies")) n->affectedStrategies = props["affectedStrategies"].get<std::string>();
|
||||
if (props.contains("timestamp")) n->timestamp = props["timestamp"].get<std::string>();
|
||||
}
|
||||
else if (ct == "LangSpecific") {
|
||||
auto* n = static_cast<LangSpecific*>(node);
|
||||
if (props.contains("language")) n->language = props["language"].get<std::string>();
|
||||
if (props.contains("idiomType")) n->idiomType = props["idiomType"].get<std::string>();
|
||||
if (props.contains("rawSyntax")) n->rawSyntax = props["rawSyntax"].get<std::string>();
|
||||
if (props.contains("semanticHint")) n->semanticHint = props["semanticHint"].get<std::string>();
|
||||
if (props.contains("position")) n->position = props["position"].get<std::string>();
|
||||
}
|
||||
}
|
||||
|
||||
inline ASTNode* fromJson(const json& j) {
|
||||
ASTNode* node = createNode(j["concept"].get<std::string>());
|
||||
if (!node) return nullptr;
|
||||
|
||||
node->id = j["id"].get<std::string>();
|
||||
|
||||
if (j.contains("properties")) {
|
||||
setPropertiesFromJson(node, j["properties"]);
|
||||
}
|
||||
|
||||
if (j.contains("children")) {
|
||||
for (auto& [role, arr] : j["children"].items()) {
|
||||
for (auto& childJson : arr) {
|
||||
ASTNode* child = fromJson(childJson);
|
||||
if (child) node->addChild(role, child);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
inline void deleteTree(ASTNode* node) {
|
||||
if (!node) return;
|
||||
for (auto* child : node->allChildren()) {
|
||||
deleteTree(child);
|
||||
}
|
||||
delete node;
|
||||
}
|
||||
|
||||
127
editor/tests/step6_test.cpp
Normal file
127
editor/tests/step6_test.cpp
Normal file
@@ -0,0 +1,127 @@
|
||||
// Step 6: JSON round-trip. Save Calculator → load → save again → compare (byte-identical).
|
||||
|
||||
#include "../src/ast/Serialization.h"
|
||||
#include <cassert>
|
||||
#include <iostream>
|
||||
|
||||
int main() {
|
||||
// Build Calculator (same as step5)
|
||||
Module calc("Calc_M001", "Calculator", "cpp");
|
||||
|
||||
Variable pi("Calc_V001", "PI");
|
||||
PrimitiveType piType("Calc_VT001", "float");
|
||||
pi.setChild("type", &piType);
|
||||
calc.addChild("variables", &pi);
|
||||
|
||||
Function add("Calc_F001", "add");
|
||||
PrimitiveType addRet("Calc_RT001", "int");
|
||||
add.setChild("returnType", &addRet);
|
||||
|
||||
Parameter px("Calc_P001", "x");
|
||||
PrimitiveType pxType("Calc_PT001", "int");
|
||||
px.setChild("type", &pxType);
|
||||
add.addChild("parameters", &px);
|
||||
|
||||
Parameter py("Calc_P002", "y");
|
||||
PrimitiveType pyType("Calc_PT002", "int");
|
||||
py.setChild("type", &pyType);
|
||||
add.addChild("parameters", &py);
|
||||
|
||||
Assignment assign;
|
||||
assign.id = "Calc_A001";
|
||||
VariableReference assignTarget("c_VR_result", "result");
|
||||
assign.setChild("target", &assignTarget);
|
||||
BinaryOperation addOp("Calc_E001", "+");
|
||||
VariableReference vrX("Calc_VR_x", "x");
|
||||
VariableReference vrY("Calc_VR_y", "y");
|
||||
addOp.setChild("left", &vrX);
|
||||
addOp.setChild("right", &vrY);
|
||||
assign.setChild("value", &addOp);
|
||||
add.addChild("body", &assign);
|
||||
|
||||
Return addReturn;
|
||||
addReturn.id = "Calc_R001";
|
||||
VariableReference vrRetResult("c_VR_return", "result");
|
||||
addReturn.setChild("value", &vrRetResult);
|
||||
add.addChild("body", &addReturn);
|
||||
calc.addChild("functions", &add);
|
||||
|
||||
Function multiply("Calc_F002", "multiply");
|
||||
PrimitiveType mulRet("Calc_RT002", "int");
|
||||
multiply.setChild("returnType", &mulRet);
|
||||
|
||||
Parameter pa("Calc_P003", "a");
|
||||
PrimitiveType paType("Calc_PT003", "int");
|
||||
pa.setChild("type", &paType);
|
||||
multiply.addChild("parameters", &pa);
|
||||
|
||||
Parameter pb("Calc_P004", "b");
|
||||
PrimitiveType pbType("Calc_PT004", "int");
|
||||
pb.setChild("type", &pbType);
|
||||
multiply.addChild("parameters", &pb);
|
||||
|
||||
Return mulReturn;
|
||||
mulReturn.id = "Calc_R002";
|
||||
BinaryOperation mulOp("Calc_E005", "*");
|
||||
VariableReference vrA("Calc_VR_a", "a");
|
||||
VariableReference vrB("Calc_VR_b", "b");
|
||||
mulOp.setChild("left", &vrA);
|
||||
mulOp.setChild("right", &vrB);
|
||||
mulReturn.setChild("value", &mulOp);
|
||||
multiply.addChild("body", &mulReturn);
|
||||
calc.addChild("functions", &multiply);
|
||||
|
||||
// --- Round-trip ---
|
||||
|
||||
// Save 1
|
||||
json j1 = toJson(&calc);
|
||||
std::string s1 = j1.dump(2);
|
||||
|
||||
// Load
|
||||
ASTNode* loaded = fromJson(j1);
|
||||
assert(loaded != nullptr);
|
||||
|
||||
// Verify loaded tree structure
|
||||
assert(loaded->conceptType == "Module");
|
||||
auto* lm = static_cast<Module*>(loaded);
|
||||
assert(lm->name == "Calculator");
|
||||
assert(lm->targetLanguage == "cpp");
|
||||
assert(lm->getChildren("functions").size() == 2);
|
||||
assert(lm->getChildren("variables").size() == 1);
|
||||
|
||||
// Verify deep node
|
||||
auto* lf = static_cast<Function*>(lm->getChildren("functions")[0]);
|
||||
assert(lf->name == "add");
|
||||
assert(lf->parent == loaded);
|
||||
assert(lf->getChildren("parameters").size() == 2);
|
||||
assert(lf->getChildren("body").size() == 2);
|
||||
|
||||
auto* lp = static_cast<Parameter*>(lf->getChildren("parameters")[0]);
|
||||
assert(lp->name == "x");
|
||||
assert(lp->parent == lf);
|
||||
auto* lpt = static_cast<PrimitiveType*>(lp->getChild("type"));
|
||||
assert(lpt->kind == "int");
|
||||
assert(lpt->parent == lp);
|
||||
|
||||
auto* la = static_cast<Assignment*>(lf->getChildren("body")[0]);
|
||||
auto* lbo = static_cast<BinaryOperation*>(la->getChild("value"));
|
||||
assert(lbo->op == "+");
|
||||
assert(static_cast<VariableReference*>(lbo->getChild("left"))->variableName == "x");
|
||||
|
||||
// Save 2
|
||||
json j2 = toJson(loaded);
|
||||
std::string s2 = j2.dump(2);
|
||||
|
||||
// Compare: must be byte-identical
|
||||
if (s1 != s2) {
|
||||
std::cerr << "MISMATCH!\n--- Save 1 ---\n" << s1
|
||||
<< "\n--- Save 2 ---\n" << s2 << std::endl;
|
||||
assert(false && "Round-trip JSON not identical");
|
||||
}
|
||||
|
||||
// Cleanup heap-allocated tree
|
||||
deleteTree(loaded);
|
||||
|
||||
std::cout << "Step 6: PASS — JSON round-trip (save/load/save identical)" << std::endl;
|
||||
return 0;
|
||||
}
|
||||
Reference in New Issue
Block a user