From 1fd567fb20fbc83f8b4844fd6f8a56e23c8f1b07 Mon Sep 17 00:00:00 2001 From: Bill Date: Mon, 9 Feb 2026 15:00:26 -0700 Subject: [PATCH] Step 121: add import/external module AST concepts --- editor/CMakeLists.txt | 4 ++ editor/src/ast/ExternalModule.h | 19 ++++++++ editor/src/ast/Import.h | 18 +++++++ editor/src/ast/Schema.h | 15 +++++- editor/src/ast/Serialization.h | 41 ++++++++++++++++ editor/src/ast/TypeSignature.h | 17 +++++++ editor/tests/step121_test.cpp | 84 +++++++++++++++++++++++++++++++++ sprint4_plan.md | 2 +- 8 files changed, 198 insertions(+), 2 deletions(-) create mode 100644 editor/src/ast/ExternalModule.h create mode 100644 editor/src/ast/Import.h create mode 100644 editor/src/ast/TypeSignature.h create mode 100644 editor/tests/step121_test.cpp diff --git a/editor/CMakeLists.txt b/editor/CMakeLists.txt index 53b7683..459845c 100644 --- a/editor/CMakeLists.txt +++ b/editor/CMakeLists.txt @@ -685,6 +685,10 @@ add_executable(step120_test tests/step120_test.cpp) target_include_directories(step120_test PRIVATE src) target_link_libraries(step120_test PRIVATE nlohmann_json::nlohmann_json) +add_executable(step121_test tests/step121_test.cpp) +target_include_directories(step121_test PRIVATE src) +target_link_libraries(step121_test PRIVATE nlohmann_json::nlohmann_json) + find_package(SDL2 CONFIG REQUIRED) find_package(OpenGL REQUIRED) find_package(glad CONFIG REQUIRED) diff --git a/editor/src/ast/ExternalModule.h b/editor/src/ast/ExternalModule.h new file mode 100644 index 0000000..54e53ac --- /dev/null +++ b/editor/src/ast/ExternalModule.h @@ -0,0 +1,19 @@ +#pragma once +#include "ASTNode.h" + +class ExternalModule : public ASTNode { +public: + std::string name; + std::string version; + std::string language; + + ExternalModule() { conceptType = "ExternalModule"; } + ExternalModule(const std::string& id, const std::string& name, + const std::string& language, + const std::string& version = "") + : name(name), version(version), language(language) { + this->id = id; + this->conceptType = "ExternalModule"; + } + // children: signatures (0..n) via addChild("signatures", TypeSignature*) +}; diff --git a/editor/src/ast/Import.h b/editor/src/ast/Import.h new file mode 100644 index 0000000..06a5364 --- /dev/null +++ b/editor/src/ast/Import.h @@ -0,0 +1,18 @@ +#pragma once +#include "ASTNode.h" + +class Import : public ASTNode { +public: + std::string moduleName; + std::string alias; + std::string importKind; // "module", "include", "require" + + Import() { conceptType = "Import"; } + Import(const std::string& id, const std::string& module, + const std::string& kind = "module", + const std::string& alias = "") + : moduleName(module), alias(alias), importKind(kind) { + this->id = id; + this->conceptType = "Import"; + } +}; diff --git a/editor/src/ast/Schema.h b/editor/src/ast/Schema.h index 9f0fe0b..c9092cb 100644 --- a/editor/src/ast/Schema.h +++ b/editor/src/ast/Schema.h @@ -80,6 +80,8 @@ private: addRule("Module", "variables", {"Variable"}, false); // multi-valued addRule("Module", "targetLanguage", {}, true); // property, not used for AST nodes addRule("Module", "annotations", {"DerefStrategy", "OptimizationLock", "LangSpecific"}, false); // multi-valued + addRule("Module", "imports", {"Import"}, false); // multi-valued + addRule("Module", "externalModules", {"ExternalModule"}, false); // multi-valued // Function concept rules addRule("Function", "parameters", {"Parameter"}, false); // multi-valued @@ -151,6 +153,17 @@ private: // Type concepts - no child rules needed as they're leaf nodes for types // Annotation concepts - no child rules needed as they're leaf nodes for annotations + + // External module concepts + addRule("ExternalModule", "signatures", {"TypeSignature"}, false); // multi-valued + addRule("TypeSignature", "returnType", + {"PrimitiveType", "ListType", "SetType", "MapType", "TupleType", + "ArrayType", "OptionalType", "CustomType"}, + true); + addRule("TypeSignature", "paramTypes", + {"PrimitiveType", "ListType", "SetType", "MapType", "TupleType", + "ArrayType", "OptionalType", "CustomType"}, + false); } void addRule(const std::string& parentConcept, const std::string& role, const std::vector& allowedConcepts, bool isSingleValued) { @@ -159,4 +172,4 @@ private: rule.isSingleValued = isSingleValued; schema_[parentConcept][role] = rule; } -}; \ No newline at end of file +}; diff --git a/editor/src/ast/Serialization.h b/editor/src/ast/Serialization.h index a072d56..fec1c51 100644 --- a/editor/src/ast/Serialization.h +++ b/editor/src/ast/Serialization.h @@ -9,6 +9,9 @@ #include "Expression.h" #include "Type.h" #include "Annotation.h" +#include "Import.h" +#include "ExternalModule.h" +#include "TypeSignature.h" using json = nlohmann::json; @@ -88,6 +91,24 @@ inline json propertiesToJson(const ASTNode* node) { auto* n = static_cast(node); props["typeName"] = n->typeName; } + // Imports / External modules + else if (ct == "Import") { + auto* n = static_cast(node); + props["moduleName"] = n->moduleName; + if (!n->alias.empty()) props["alias"] = n->alias; + if (!n->importKind.empty()) props["importKind"] = n->importKind; + } + else if (ct == "ExternalModule") { + auto* n = static_cast(node); + props["name"] = n->name; + if (!n->version.empty()) props["version"] = n->version; + if (!n->language.empty()) props["language"] = n->language; + } + else if (ct == "TypeSignature") { + auto* n = static_cast(node); + props["name"] = n->name; + props["variadic"] = n->variadic; + } // Annotations else if (ct == "DerefStrategy") { auto* n = static_cast(node); @@ -177,6 +198,9 @@ inline ASTNode* createNode(const std::string& conceptName) { if (conceptName == "ArrayType") return new ArrayType(); if (conceptName == "OptionalType") return new OptionalType(); if (conceptName == "CustomType") return new CustomType(); + if (conceptName == "Import") return new Import(); + if (conceptName == "ExternalModule") return new ExternalModule(); + if (conceptName == "TypeSignature") return new TypeSignature(); if (conceptName == "DerefStrategy") return new DerefStrategy(); if (conceptName == "OptimizationLock") return new OptimizationLock(); if (conceptName == "LangSpecific") return new LangSpecific(); @@ -251,6 +275,23 @@ inline void setPropertiesFromJson(ASTNode* node, const json& props) { auto* n = static_cast(node); if (props.contains("typeName")) n->typeName = props["typeName"].get(); } + else if (ct == "Import") { + auto* n = static_cast(node); + if (props.contains("moduleName")) n->moduleName = props["moduleName"].get(); + if (props.contains("alias")) n->alias = props["alias"].get(); + if (props.contains("importKind")) n->importKind = props["importKind"].get(); + } + else if (ct == "ExternalModule") { + auto* n = static_cast(node); + if (props.contains("name")) n->name = props["name"].get(); + if (props.contains("version")) n->version = props["version"].get(); + if (props.contains("language")) n->language = props["language"].get(); + } + else if (ct == "TypeSignature") { + auto* n = static_cast(node); + if (props.contains("name")) n->name = props["name"].get(); + if (props.contains("variadic")) n->variadic = props["variadic"].get(); + } else if (ct == "DerefStrategy") { auto* n = static_cast(node); if (props.contains("strategy")) n->strategy = props["strategy"].get(); diff --git a/editor/src/ast/TypeSignature.h b/editor/src/ast/TypeSignature.h new file mode 100644 index 0000000..b8feeb9 --- /dev/null +++ b/editor/src/ast/TypeSignature.h @@ -0,0 +1,17 @@ +#pragma once +#include "ASTNode.h" + +class TypeSignature : public ASTNode { +public: + std::string name; + bool variadic = false; + + TypeSignature() { conceptType = "TypeSignature"; } + TypeSignature(const std::string& id, const std::string& name, bool variadic = false) + : name(name), variadic(variadic) { + this->id = id; + this->conceptType = "TypeSignature"; + } + // children: returnType (1) via setChild("returnType", Type*) + // children: paramTypes (0..n) via addChild("paramTypes", Type*) +}; diff --git a/editor/tests/step121_test.cpp b/editor/tests/step121_test.cpp new file mode 100644 index 0000000..06de045 --- /dev/null +++ b/editor/tests/step121_test.cpp @@ -0,0 +1,84 @@ +// Step 121 TDD Test: Import/ExternalModule/TypeSignature serialization +#include "ast/Serialization.h" +#include "ast/Import.h" +#include "ast/ExternalModule.h" +#include "ast/TypeSignature.h" +#include "ast/Type.h" +#include "ast/Module.h" +#include + +static void expect(bool cond, const std::string& name, int& passed, int& failed) { + if (cond) { + std::cout << "Test " << (passed + failed + 1) << " PASS: " << name << "\n"; + ++passed; + } else { + std::cout << "Test " << (passed + failed + 1) << " FAIL: " << name << "\n"; + ++failed; + } +} + +int main() { + int passed = 0; + int failed = 0; + + // Test 1: Import round-trip + Module root("m1", "Root", "python"); + auto* imp = new Import("imp1", "numpy", "module", "np"); + root.addChild("imports", imp); + + json j = toJson(&root); + ASTNode* round = fromJson(j); + expect(round && round->conceptType == "Module", "module round-trip", passed, failed); + auto* roundMod = static_cast(round); + auto imports = roundMod->getChildren("imports"); + expect(imports.size() == 1 && imports[0]->conceptType == "Import", "import child preserved", passed, failed); + if (!imports.empty()) { + auto* ri = static_cast(imports[0]); + expect(ri->moduleName == "numpy" && ri->alias == "np" && ri->importKind == "module", + "import properties", passed, failed); + } + + deleteTree(round); + + // Test 2: ExternalModule + TypeSignature + Type children round-trip + Module root2("m2", "Root2", "python"); + auto* ext = new ExternalModule("ext1", "numpy", "python", "1.26"); + auto* sig = new TypeSignature("sig1", "array", true); + auto* retType = new CustomType("t1", "ndarray"); + auto* paramType = new PrimitiveType("t2", "float"); + sig->setChild("returnType", retType); + sig->addChild("paramTypes", paramType); + ext->addChild("signatures", sig); + root2.addChild("externalModules", ext); + + json j2 = toJson(&root2); + ASTNode* round2 = fromJson(j2); + expect(round2 && round2->conceptType == "Module", "module round-trip (external)", passed, failed); + auto* roundMod2 = static_cast(round2); + auto externals = roundMod2->getChildren("externalModules"); + expect(externals.size() == 1 && externals[0]->conceptType == "ExternalModule", + "external module child", passed, failed); + if (!externals.empty()) { + auto* re = static_cast(externals[0]); + expect(re->name == "numpy" && re->version == "1.26" && re->language == "python", + "external module properties", passed, failed); + auto sigs = re->getChildren("signatures"); + expect(sigs.size() == 1 && sigs[0]->conceptType == "TypeSignature", + "type signature child", passed, failed); + if (!sigs.empty()) { + auto* rs = static_cast(sigs[0]); + expect(rs->name == "array" && rs->variadic == true, + "type signature properties", passed, failed); + auto* rret = rs->getChild("returnType"); + auto params = rs->getChildren("paramTypes"); + expect(rret && rret->conceptType == "CustomType", "return type child", passed, failed); + expect(params.size() == 1 && params[0]->conceptType == "PrimitiveType", + "param type child", passed, failed); + } + } + + deleteTree(round2); + + std::cout << "\n=== Step 121 Results: " << passed << " passed, " << failed << " failed ===\n"; + return failed == 0 ? 0 : 1; +} diff --git a/sprint4_plan.md b/sprint4_plan.md index db96ada..91325f5 100644 --- a/sprint4_plan.md +++ b/sprint4_plan.md @@ -391,7 +391,7 @@ AST extensions for Sprint 5. AST atomically. Status bar shows undo depth. *Modifies:* `Orchestrator.h`, `main.cpp` -- [ ] **Step 121: AST Import/ExternalModule concepts** +- [x] **Step 121: AST Import/ExternalModule concepts** Extend `ASTNode.h` with new concept types: `Import` (represents `import numpy`, `#include `, `(require 'cl-lib)`), `ExternalModule` (represents an external library with its public API surface), `TypeSignature` (function