Sprint 2 Step 5: JSON serialization with nlohmann/json
toJson(ASTNode*) recursively serializes the AST to JSON with id, concept, properties, and children structure. Added childRoles() to ASTNode for enumeration. FetchContent pulls nlohmann/json v3.11.3. Test serializes the full Calculator model and verifies every node in the JSON output. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -4,6 +4,16 @@ project(WhetstoneEditor LANGUAGES CXX)
|
|||||||
set(CMAKE_CXX_STANDARD 20)
|
set(CMAKE_CXX_STANDARD 20)
|
||||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||||
|
|
||||||
|
# nlohmann/json (header-only, fetched once)
|
||||||
|
include(FetchContent)
|
||||||
|
FetchContent_Declare(
|
||||||
|
json
|
||||||
|
GIT_REPOSITORY https://github.com/nlohmann/json.git
|
||||||
|
GIT_TAG v3.11.3
|
||||||
|
GIT_SHALLOW TRUE
|
||||||
|
)
|
||||||
|
FetchContent_MakeAvailable(json)
|
||||||
|
|
||||||
add_executable(step1_test tests/step1_test.cpp)
|
add_executable(step1_test tests/step1_test.cpp)
|
||||||
target_include_directories(step1_test PRIVATE src)
|
target_include_directories(step1_test PRIVATE src)
|
||||||
|
|
||||||
@@ -15,3 +25,7 @@ target_include_directories(step3_test PRIVATE src)
|
|||||||
|
|
||||||
add_executable(step4_test tests/step4_test.cpp)
|
add_executable(step4_test tests/step4_test.cpp)
|
||||||
target_include_directories(step4_test PRIVATE src)
|
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)
|
||||||
|
|||||||
@@ -45,6 +45,15 @@ public:
|
|||||||
return it != children_.end() ? it->second : empty;
|
return it != children_.end() ? it->second : empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get all child role names
|
||||||
|
std::vector<std::string> childRoles() const {
|
||||||
|
std::vector<std::string> roles;
|
||||||
|
for (const auto& [role, _] : children_) {
|
||||||
|
roles.push_back(role);
|
||||||
|
}
|
||||||
|
return roles;
|
||||||
|
}
|
||||||
|
|
||||||
// Get all children across all roles
|
// Get all children across all roles
|
||||||
std::vector<ASTNode*> allChildren() const {
|
std::vector<ASTNode*> allChildren() const {
|
||||||
std::vector<ASTNode*> result;
|
std::vector<ASTNode*> result;
|
||||||
|
|||||||
138
editor/src/ast/Serialization.h
Normal file
138
editor/src/ast/Serialization.h
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <nlohmann/json.hpp>
|
||||||
|
#include "ASTNode.h"
|
||||||
|
#include "Module.h"
|
||||||
|
#include "Function.h"
|
||||||
|
#include "Variable.h"
|
||||||
|
#include "Parameter.h"
|
||||||
|
#include "Statement.h"
|
||||||
|
#include "Expression.h"
|
||||||
|
#include "Type.h"
|
||||||
|
#include "Annotation.h"
|
||||||
|
|
||||||
|
using json = nlohmann::json;
|
||||||
|
|
||||||
|
inline json propertiesToJson(const ASTNode* node) {
|
||||||
|
json props = json::object();
|
||||||
|
const auto& ct = node->conceptType;
|
||||||
|
|
||||||
|
// Module
|
||||||
|
if (ct == "Module") {
|
||||||
|
auto* n = static_cast<const Module*>(node);
|
||||||
|
props["name"] = n->name;
|
||||||
|
props["targetLanguage"] = n->targetLanguage;
|
||||||
|
}
|
||||||
|
// Function
|
||||||
|
else if (ct == "Function") {
|
||||||
|
auto* n = static_cast<const Function*>(node);
|
||||||
|
props["name"] = n->name;
|
||||||
|
}
|
||||||
|
// Variable
|
||||||
|
else if (ct == "Variable") {
|
||||||
|
auto* n = static_cast<const Variable*>(node);
|
||||||
|
props["name"] = n->name;
|
||||||
|
}
|
||||||
|
// Parameter
|
||||||
|
else if (ct == "Parameter") {
|
||||||
|
auto* n = static_cast<const Parameter*>(node);
|
||||||
|
props["name"] = n->name;
|
||||||
|
}
|
||||||
|
// Statements
|
||||||
|
else if (ct == "ForLoop") {
|
||||||
|
auto* n = static_cast<const ForLoop*>(node);
|
||||||
|
props["iteratorName"] = n->iteratorName;
|
||||||
|
}
|
||||||
|
// Expressions
|
||||||
|
else if (ct == "BinaryOperation") {
|
||||||
|
auto* n = static_cast<const BinaryOperation*>(node);
|
||||||
|
props["op"] = n->op;
|
||||||
|
}
|
||||||
|
else if (ct == "UnaryOperation") {
|
||||||
|
auto* n = static_cast<const UnaryOperation*>(node);
|
||||||
|
props["op"] = n->op;
|
||||||
|
}
|
||||||
|
else if (ct == "FunctionCall") {
|
||||||
|
auto* n = static_cast<const FunctionCall*>(node);
|
||||||
|
props["functionName"] = n->functionName;
|
||||||
|
}
|
||||||
|
else if (ct == "VariableReference") {
|
||||||
|
auto* n = static_cast<const VariableReference*>(node);
|
||||||
|
props["variableName"] = n->variableName;
|
||||||
|
}
|
||||||
|
else if (ct == "IntegerLiteral") {
|
||||||
|
auto* n = static_cast<const IntegerLiteral*>(node);
|
||||||
|
props["value"] = n->value;
|
||||||
|
}
|
||||||
|
else if (ct == "FloatLiteral") {
|
||||||
|
auto* n = static_cast<const FloatLiteral*>(node);
|
||||||
|
props["value"] = n->value;
|
||||||
|
}
|
||||||
|
else if (ct == "StringLiteral") {
|
||||||
|
auto* n = static_cast<const StringLiteral*>(node);
|
||||||
|
props["value"] = n->value;
|
||||||
|
}
|
||||||
|
else if (ct == "BooleanLiteral") {
|
||||||
|
auto* n = static_cast<const BooleanLiteral*>(node);
|
||||||
|
props["value"] = n->value;
|
||||||
|
}
|
||||||
|
else if (ct == "MemberAccess") {
|
||||||
|
auto* n = static_cast<const MemberAccess*>(node);
|
||||||
|
props["memberName"] = n->memberName;
|
||||||
|
}
|
||||||
|
// Types
|
||||||
|
else if (ct == "PrimitiveType") {
|
||||||
|
auto* n = static_cast<const PrimitiveType*>(node);
|
||||||
|
props["kind"] = n->kind;
|
||||||
|
}
|
||||||
|
else if (ct == "CustomType") {
|
||||||
|
auto* n = static_cast<const CustomType*>(node);
|
||||||
|
props["typeName"] = n->typeName;
|
||||||
|
}
|
||||||
|
// Annotations
|
||||||
|
else if (ct == "DerefStrategy") {
|
||||||
|
auto* n = static_cast<const DerefStrategy*>(node);
|
||||||
|
props["strategy"] = n->strategy;
|
||||||
|
if (!n->derefLocation.empty()) props["derefLocation"] = n->derefLocation;
|
||||||
|
if (!n->owner.empty()) props["owner"] = n->owner;
|
||||||
|
}
|
||||||
|
else if (ct == "OptimizationLock") {
|
||||||
|
auto* n = static_cast<const OptimizationLock*>(node);
|
||||||
|
props["lockedBy"] = n->lockedBy;
|
||||||
|
props["lockReason"] = n->lockReason;
|
||||||
|
props["lockLevel"] = n->lockLevel;
|
||||||
|
if (!n->affectedStrategies.empty()) props["affectedStrategies"] = n->affectedStrategies;
|
||||||
|
if (!n->timestamp.empty()) props["timestamp"] = n->timestamp;
|
||||||
|
}
|
||||||
|
else if (ct == "LangSpecific") {
|
||||||
|
auto* n = static_cast<const LangSpecific*>(node);
|
||||||
|
props["language"] = n->language;
|
||||||
|
props["idiomType"] = n->idiomType;
|
||||||
|
props["rawSyntax"] = n->rawSyntax;
|
||||||
|
if (!n->semanticHint.empty()) props["semanticHint"] = n->semanticHint;
|
||||||
|
if (!n->position.empty()) props["position"] = n->position;
|
||||||
|
}
|
||||||
|
// NullLiteral, ListLiteral, IndexAccess, Block, Assignment, IfStatement,
|
||||||
|
// WhileLoop, Return, ExpressionStatement, ListType, SetType, MapType,
|
||||||
|
// TupleType, ArrayType, OptionalType — no extra properties
|
||||||
|
|
||||||
|
return props;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline json toJson(const ASTNode* node) {
|
||||||
|
json j;
|
||||||
|
j["id"] = node->id;
|
||||||
|
j["concept"] = node->conceptType;
|
||||||
|
j["properties"] = propertiesToJson(node);
|
||||||
|
|
||||||
|
json children = json::object();
|
||||||
|
for (const auto& role : node->childRoles()) {
|
||||||
|
json arr = json::array();
|
||||||
|
for (const auto* child : node->getChildren(role)) {
|
||||||
|
arr.push_back(toJson(child));
|
||||||
|
}
|
||||||
|
children[role] = arr;
|
||||||
|
}
|
||||||
|
j["children"] = children;
|
||||||
|
|
||||||
|
return j;
|
||||||
|
}
|
||||||
139
editor/tests/step5_test.cpp
Normal file
139
editor/tests/step5_test.cpp
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
// Step 5: Serialize Calculator AST to JSON, verify structure.
|
||||||
|
|
||||||
|
#include "../src/ast/Serialization.h"
|
||||||
|
#include <cassert>
|
||||||
|
#include <iostream>
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
// Build the Calculator model (same as step3)
|
||||||
|
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);
|
||||||
|
|
||||||
|
// --- Serialize ---
|
||||||
|
json j = toJson(&calc);
|
||||||
|
|
||||||
|
// Top-level structure
|
||||||
|
assert(j["id"] == "Calc_M001");
|
||||||
|
assert(j["concept"] == "Module");
|
||||||
|
assert(j["properties"]["name"] == "Calculator");
|
||||||
|
assert(j["properties"]["targetLanguage"] == "cpp");
|
||||||
|
|
||||||
|
// Functions array
|
||||||
|
assert(j["children"]["functions"].size() == 2);
|
||||||
|
|
||||||
|
auto& jAdd = j["children"]["functions"][0];
|
||||||
|
assert(jAdd["id"] == "Calc_F001");
|
||||||
|
assert(jAdd["concept"] == "Function");
|
||||||
|
assert(jAdd["properties"]["name"] == "add");
|
||||||
|
|
||||||
|
// add's returnType
|
||||||
|
assert(jAdd["children"]["returnType"].size() == 1);
|
||||||
|
assert(jAdd["children"]["returnType"][0]["concept"] == "PrimitiveType");
|
||||||
|
assert(jAdd["children"]["returnType"][0]["properties"]["kind"] == "int");
|
||||||
|
|
||||||
|
// add's parameters
|
||||||
|
assert(jAdd["children"]["parameters"].size() == 2);
|
||||||
|
assert(jAdd["children"]["parameters"][0]["properties"]["name"] == "x");
|
||||||
|
assert(jAdd["children"]["parameters"][1]["properties"]["name"] == "y");
|
||||||
|
|
||||||
|
// Parameter type nested
|
||||||
|
auto& jPx = jAdd["children"]["parameters"][0];
|
||||||
|
assert(jPx["children"]["type"].size() == 1);
|
||||||
|
assert(jPx["children"]["type"][0]["properties"]["kind"] == "int");
|
||||||
|
|
||||||
|
// add's body
|
||||||
|
assert(jAdd["children"]["body"].size() == 2);
|
||||||
|
auto& jAssign = jAdd["children"]["body"][0];
|
||||||
|
assert(jAssign["concept"] == "Assignment");
|
||||||
|
assert(jAssign["children"]["target"].size() == 1);
|
||||||
|
assert(jAssign["children"]["target"][0]["concept"] == "VariableReference");
|
||||||
|
assert(jAssign["children"]["target"][0]["properties"]["variableName"] == "result");
|
||||||
|
|
||||||
|
auto& jBinOp = jAssign["children"]["value"][0];
|
||||||
|
assert(jBinOp["concept"] == "BinaryOperation");
|
||||||
|
assert(jBinOp["properties"]["op"] == "+");
|
||||||
|
assert(jBinOp["children"]["left"][0]["properties"]["variableName"] == "x");
|
||||||
|
assert(jBinOp["children"]["right"][0]["properties"]["variableName"] == "y");
|
||||||
|
|
||||||
|
// multiply function
|
||||||
|
auto& jMul = j["children"]["functions"][1];
|
||||||
|
assert(jMul["properties"]["name"] == "multiply");
|
||||||
|
auto& jMulBody = jMul["children"]["body"][0];
|
||||||
|
assert(jMulBody["concept"] == "Return");
|
||||||
|
auto& jMulOp = jMulBody["children"]["value"][0];
|
||||||
|
assert(jMulOp["properties"]["op"] == "*");
|
||||||
|
|
||||||
|
// Variables
|
||||||
|
assert(j["children"]["variables"].size() == 1);
|
||||||
|
assert(j["children"]["variables"][0]["properties"]["name"] == "PI");
|
||||||
|
assert(j["children"]["variables"][0]["children"]["type"][0]["properties"]["kind"] == "float");
|
||||||
|
|
||||||
|
// Print for manual inspection
|
||||||
|
std::cout << j.dump(2) << std::endl;
|
||||||
|
std::cout << "\nStep 5: PASS — Calculator serialized to JSON" << std::endl;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user