diff --git a/editor/src/SemannoSidecar.h b/editor/src/SemannoSidecar.h new file mode 100644 index 0000000..3a0c77a --- /dev/null +++ b/editor/src/SemannoSidecar.h @@ -0,0 +1,160 @@ +#pragma once +#include +#include +#include +#include +#include +#include "ast/ASTNode.h" +#include "ast/Module.h" +#include "SemannoFormat.h" + +struct SemannoSidecarSaveResult { + bool success = false; + std::string path; + int annotationCount = 0; + std::string error; +}; + +struct SemannoSidecarLoadResult { + bool success = false; + std::vector entries; + int count = 0; + std::string error; +}; + +class SemannoSidecar { +public: + // Generates the path for a .semanno sidecar file. + // e.g., /path/to/project/src/file.py -> /path/to/project/.whetstone/src/file.py.semanno + static std::filesystem::path getSidecarPath(const std::filesystem::path& workspaceRoot, const std::filesystem::path& filePath) { + std::filesystem::path relativePath; + if (filePath.is_absolute()) { + std::error_code ec; + relativePath = std::filesystem::relative(filePath, workspaceRoot, ec); + if (ec || relativePath.empty()) { + relativePath = filePath.filename(); + } + } else { + relativePath = filePath; + } + + std::filesystem::path sidecarDir = workspaceRoot / ".whetstone"; + return sidecarDir / (relativePath.generic_string() + ".semanno"); + } + + // Saves all semantic annotations from an AST module to a .semanno sidecar file. + static SemannoSidecarSaveResult saveSemannoSidecar(const std::string& workspaceRoot, const std::string& filePath, const Module* module) { + SemannoSidecarSaveResult result; + if (!module) { + result.error = "No AST to save"; + return result; + } + + std::vector> entries; + collectEntries(module, entries); + result.annotationCount = static_cast(entries.size()); + + auto path = getSidecarPath(workspaceRoot, filePath); + result.path = path.string(); + if (entries.empty()) { + try { + std::filesystem::create_directories(path.parent_path()); + std::ofstream file(path); + if (!file.is_open()) { + result.error = "Cannot write to " + path.string(); + return result; + } + } catch (const std::filesystem::filesystem_error& e) { + result.error = e.what(); + return result; + } + result.success = true; + return result; + } + + // Sort by line number to ensure a stable format + std::sort(entries.begin(), entries.end(), [](const auto& a, const auto& b) { + return a.first < b.first; + }); + + try { + std::filesystem::create_directories(path.parent_path()); + + std::ofstream file(path); + if (!file.is_open()) { + result.error = "Cannot write to " + path.string(); + return result; + } + + for (const auto& entry : entries) { + file << "L" << entry.first << ": " << entry.second << "\n"; + } + result.success = true; + return result; + } catch (const std::filesystem::filesystem_error& e) { + result.error = e.what(); + return result; + } + } + + // Loads semantic annotations from a .semanno sidecar file. + static SemannoSidecarLoadResult loadSemannoSidecar(const std::string& workspaceRoot, const std::string& filePath) { + SemannoSidecarLoadResult result; + auto path = getSidecarPath(workspaceRoot, filePath); + + std::ifstream file(path); + if (!file.is_open()) { + result.error = "No sidecar file: " + path.string(); + return result; + } + + std::string line; + while (std::getline(file, line)) { + if (line.empty() || line[0] != 'L') continue; + + auto semanno = SemannoParser::parse(line); + if (!semanno.type.empty()) { + result.entries.push_back(semanno); + } + } + result.count = static_cast(result.entries.size()); + result.success = true; + return result; + } + +private: + // Helper to recursively traverse the AST and collect annotation entries. + static void collectEntries(const ASTNode* node, std::vector>& entries) { + if (!node) return; + + // An annotation's line number is determined by its parent node + if (node->parent && node->parent->spanStartLine >= 0) { + std::string semannoString = SemannoEmitter::emit(node); + if (!semannoString.empty()) { + // Link annotation comments to their parent source line in the sidecar. + entries.push_back({node->parent->spanStartLine, semannoString}); + } + } + + // Recurse through all children + for (const auto* child : node->allChildren()) { + collectEntries(child, entries); + } + } +}; + +inline std::string semannoSidecarPath(const std::string& workspaceRoot, + const std::string& filePath) { + return SemannoSidecar::getSidecarPath(workspaceRoot, filePath).string(); +} + +inline SemannoSidecarSaveResult saveSemannoSidecar(const std::string& workspaceRoot, + const std::string& filePath, + const Module* module) { + return SemannoSidecar::saveSemannoSidecar(workspaceRoot, filePath, module); +} + +inline SemannoSidecarLoadResult loadSemannoSidecar(const std::string& workspaceRoot, + const std::string& filePath) { + return SemannoSidecar::loadSemannoSidecar(workspaceRoot, filePath); +} diff --git a/editor/src/SidecarPersistence.h b/editor/src/SidecarPersistence.h index 723c566..3cb7758 100644 --- a/editor/src/SidecarPersistence.h +++ b/editor/src/SidecarPersistence.h @@ -7,6 +7,9 @@ #include "ast/ASTNode.h" #include "ast/Serialization.h" #include "ast/Annotation.h" +#include "ASTUtils.h" +#include "CompactAST.h" +#include "SemannoSidecar.h" #include #include #include diff --git a/editor/tests/step295_test.cpp b/editor/tests/step295_test.cpp new file mode 100644 index 0000000..7f13437 --- /dev/null +++ b/editor/tests/step295_test.cpp @@ -0,0 +1,305 @@ +// Step 295: Semanno Sidecar Integration (12 tests) +#include "SemannoSidecar.h" +#include "SidecarPersistence.h" +#include "ast/Module.h" +#include "ast/Function.h" +#include "ast/Annotation.h" +#include "EnvironmentSpec.h" +#include +#include +#include + +static int passed = 0, failed = 0; +#define TEST(name) { std::cout << " " << #name << "... "; } +#define PASS() { std::cout << "PASS\n"; ++passed; } +#define FAIL(msg) { std::cout << "FAIL: " << msg << "\n"; ++failed; } +#define CHECK(cond, msg) if (!(cond)) { FAIL(msg); return; } else {} + +static std::string tmpDir() { + auto p = std::filesystem::temp_directory_path() / "whetstone_test_295"; + std::filesystem::create_directories(p); + return p.string(); +} + +static void cleanup() { + std::filesystem::remove_all(std::filesystem::temp_directory_path() / "whetstone_test_295"); +} + +// Helper: create a module with annotations +static std::unique_ptr makeAnnotatedModule() { + auto mod = std::make_unique(); + mod->name = "test_module"; + + auto fn = std::make_unique(); + fn->name = "compute"; + fn->spanStartLine = 5; + + auto intent = std::make_unique(); + intent->summary = "computes result"; + intent->category = "pure"; + fn->addChild("annotations", intent.release()); + + auto risk = std::make_unique(); + risk->level = "low"; + risk->reason = "simple math"; + fn->addChild("annotations", risk.release()); + + mod->addChild("functions", fn.release()); + return mod; +} + +// 1. Sidecar path generation +void test_sidecar_path() { + TEST(sidecar_path); + std::string path = semannoSidecarPath("/workspace", "src/main.py"); + CHECK(path.find(".whetstone") != std::string::npos, "missing .whetstone dir"); + CHECK(path.find("src/main.py.semanno") != std::string::npos, "wrong extension"); + PASS(); +} + +// 2. Save creates file on disk +void test_save_creates_file() { + TEST(save_creates_file); + cleanup(); + auto mod = makeAnnotatedModule(); + auto result = saveSemannoSidecar(tmpDir(), "test.py", mod.get()); + CHECK(result.success, "save failed: " + result.error); + CHECK(result.annotationCount == 2, "wrong count: " + std::to_string(result.annotationCount)); + CHECK(std::filesystem::exists(result.path), "file not created"); + PASS(); +} + +// 3. Save content format (L: @semanno:...) +void test_save_content_format() { + TEST(save_content_format); + cleanup(); + auto mod = makeAnnotatedModule(); + auto result = saveSemannoSidecar(tmpDir(), "test.py", mod.get()); + CHECK(result.success, "save failed"); + + std::ifstream in(result.path); + std::string content((std::istreambuf_iterator(in)), + std::istreambuf_iterator()); + CHECK(content.find("L5: @semanno:intent") != std::string::npos, "missing L5 intent"); + CHECK(content.find("L5: @semanno:risk") != std::string::npos, "missing L5 risk"); + PASS(); +} + +// 4. Load roundtrip +void test_load_roundtrip() { + TEST(load_roundtrip); + cleanup(); + auto mod = makeAnnotatedModule(); + saveSemannoSidecar(tmpDir(), "test.py", mod.get()); + + auto loaded = loadSemannoSidecar(tmpDir(), "test.py"); + CHECK(loaded.success, "load failed: " + loaded.error); + CHECK(loaded.count == 2, "wrong count: " + std::to_string(loaded.count)); + // Verify entries + bool hasIntent = false, hasRisk = false; + for (const auto& entry : loaded.entries) { + if (entry.type == "intent") hasIntent = true; + if (entry.type == "risk") hasRisk = true; + } + CHECK(hasIntent, "missing intent entry"); + CHECK(hasRisk, "missing risk entry"); + PASS(); +} + +// 5. Load nonexistent file returns error +void test_load_nonexistent() { + TEST(load_nonexistent); + cleanup(); + auto loaded = loadSemannoSidecar(tmpDir(), "nonexistent.py"); + CHECK(!loaded.success, "should fail"); + CHECK(loaded.count == 0, "should have 0 entries"); + PASS(); +} + +// 6. Save null AST returns error +void test_save_null_ast() { + TEST(save_null_ast); + auto result = saveSemannoSidecar(tmpDir(), "test.py", nullptr); + CHECK(!result.success, "should fail"); + CHECK(result.error.find("No AST") != std::string::npos, "wrong error"); + PASS(); +} + +// 7. Multiple annotation types in sidecar +void test_multiple_types() { + TEST(multiple_types); + cleanup(); + auto mod = std::make_unique(); + mod->name = "multi"; + + auto fn = std::make_unique(); + fn->name = "process"; + fn->spanStartLine = 10; + + fn->addChild("annotations", new IntentAnnotation()); + fn->addChild("annotations", new ComplexityAnnotation()); + auto contract = new ContractAnnotation(); + contract->preconditions = "x > 0"; + fn->addChild("annotations", contract); + auto tags = new SemanticTagAnnotation(); + tags->tags = {"io", "network"}; + fn->addChild("annotations", tags); + + mod->addChild("functions", fn.release()); + + auto result = saveSemannoSidecar(tmpDir(), "multi.py", mod.get()); + CHECK(result.success, "save failed"); + CHECK(result.annotationCount == 4, "wrong count: " + std::to_string(result.annotationCount)); + + auto loaded = loadSemannoSidecar(tmpDir(), "multi.py"); + CHECK(loaded.count == 4, "loaded wrong count: " + std::to_string(loaded.count)); + PASS(); +} + +// 8. Sidecar alongside .ast.json +void test_alongside_ast_sidecar() { + TEST(alongside_ast_sidecar); + cleanup(); + auto mod = makeAnnotatedModule(); + + // Save both types + auto semannoResult = saveSemannoSidecar(tmpDir(), "test.py", mod.get()); + CHECK(semannoResult.success, "semanno save failed"); + + // Also save AST sidecar using SidecarPersistence + auto astPath = sidecarPath(tmpDir(), "test.py"); + namespace fs = std::filesystem; + fs::create_directories(fs::path(astPath).parent_path()); + saveSidecarAST(tmpDir(), "test.py", mod.get()); + + // Both files should exist in same .whetstone dir + CHECK(fs::exists(semannoResult.path), "semanno file missing"); + CHECK(fs::exists(astPath), "ast file missing"); + // Both under .whetstone/ + CHECK(semannoResult.path.find(".whetstone") != std::string::npos, "wrong dir"); + CHECK(astPath.find(".whetstone") != std::string::npos, "wrong dir"); + PASS(); +} + +// 9. Environment annotations in sidecar +void test_environment_annotations() { + TEST(environment_annotations); + cleanup(); + auto mod = std::make_unique(); + mod->name = "env_test"; + + auto fn = std::make_unique(); + fn->name = "net_call"; + fn->spanStartLine = 1; + + auto cap = new CapabilityRequirement(); + cap->capability = "io.net"; + cap->required = true; + fn->addChild("annotations", cap); + + mod->addChild("functions", fn.release()); + + auto result = saveSemannoSidecar(tmpDir(), "net.py", mod.get()); + CHECK(result.success, "save failed"); + CHECK(result.annotationCount == 1, "wrong count"); + + auto loaded = loadSemannoSidecar(tmpDir(), "net.py"); + CHECK(loaded.count == 1, "loaded wrong count"); + CHECK(loaded.entries[0].type == "capability", "wrong type"); + CHECK(loaded.entries[0].properties["capability"] == "io.net", "wrong cap"); + PASS(); +} + +// 10. Overwrite existing sidecar +void test_overwrite() { + TEST(overwrite); + cleanup(); + auto mod1 = makeAnnotatedModule(); + saveSemannoSidecar(tmpDir(), "test.py", mod1.get()); + + // Save again with different content + auto mod2 = std::make_unique(); + mod2->name = "changed"; + auto fn = std::make_unique(); + fn->name = "new_func"; + fn->spanStartLine = 1; + fn->addChild("annotations", new PureAnnotation()); + mod2->addChild("functions", fn.release()); + + auto result = saveSemannoSidecar(tmpDir(), "test.py", mod2.get()); + CHECK(result.success, "save failed"); + // PureAnnotation is Subject 1 memory — check if isSemanticAnnotation includes it + // PureAnnotation is from Sprint 3 original set, check what isSemanticAnnotation says + // Either way the save should succeed + auto loaded = loadSemannoSidecar(tmpDir(), "test.py"); + CHECK(loaded.success, "load failed"); + PASS(); +} + +// 11. Properties preserved in roundtrip +void test_properties_preserved() { + TEST(properties_preserved); + cleanup(); + auto mod = std::make_unique(); + mod->name = "props"; + + auto fn = std::make_unique(); + fn->name = "f"; + fn->spanStartLine = 3; + + auto contract = new ContractAnnotation(); + contract->preconditions = "len(arr) > 0"; + contract->postconditions = "result >= 0"; + contract->returnShape = "int"; + contract->sideEffects = "none"; + fn->addChild("annotations", contract); + + mod->addChild("functions", fn.release()); + + saveSemannoSidecar(tmpDir(), "props.py", mod.get()); + auto loaded = loadSemannoSidecar(tmpDir(), "props.py"); + CHECK(loaded.success, "load failed"); + CHECK(loaded.count == 1, "wrong count"); + + auto& entry = loaded.entries[0]; + CHECK(entry.type == "contract", "wrong type"); + CHECK(entry.properties["preconditions"] == "len(arr) > 0", "preconditions lost"); + CHECK(entry.properties["postconditions"] == "result >= 0", "postconditions lost"); + PASS(); +} + +// 12. Empty module produces empty sidecar +void test_empty_module() { + TEST(empty_module); + cleanup(); + auto mod = std::make_unique(); + mod->name = "empty"; + + auto result = saveSemannoSidecar(tmpDir(), "empty.py", mod.get()); + CHECK(result.success, "save failed"); + CHECK(result.annotationCount == 0, "should be empty"); + + auto loaded = loadSemannoSidecar(tmpDir(), "empty.py"); + CHECK(loaded.success, "load failed"); + CHECK(loaded.count == 0, "should be empty"); + PASS(); +} + +int main() { + std::cout << "=== Step 295: Semanno Sidecar Integration Tests ===\n"; + test_sidecar_path(); + test_save_creates_file(); + test_save_content_format(); + test_load_roundtrip(); + test_load_nonexistent(); + test_save_null_ast(); + test_multiple_types(); + test_alongside_ast_sidecar(); + test_environment_annotations(); + test_overwrite(); + test_properties_preserved(); + test_empty_module(); + cleanup(); + std::cout << "\nResults: " << passed << "/" << (passed + failed) << " passed\n"; + return failed > 0 ? 1 : 0; +} diff --git a/editor/tests/step296_test.cpp b/editor/tests/step296_test.cpp new file mode 100644 index 0000000..86a4fe7 --- /dev/null +++ b/editor/tests/step296_test.cpp @@ -0,0 +1,336 @@ +// Step 296: Phase 11a Integration Tests (8 tests) +#include "ast/PythonGenerator.h" +#include "ast/CppGenerator.h" +#include "ast/RustGenerator.h" +#include "ast/GoGenerator.h" +#include "ast/JavaGenerator.h" +#include "ast/JavaScriptGenerator.h" +#include "ast/ElispGenerator.h" +#include "SemannoFormat.h" +#include "SemannoSidecar.h" +#include "ast/Module.h" +#include "ast/Function.h" +#include "ast/Annotation.h" +#include "EnvironmentSpec.h" +#include +#include +#include +#include +#include + +static int passed = 0, failed = 0; +#define TEST(name) { std::cout << " " << #name << "... "; } +#define PASS() { std::cout << "PASS\n"; ++passed; } +#define FAIL(msg) { std::cout << "FAIL: " << msg << "\n"; ++failed; } +#define CHECK(cond, msg) if (!(cond)) { FAIL(msg); return; } else {} + +static std::string tmpDir() { + auto p = std::filesystem::temp_directory_path() / "whetstone_test_296"; + std::filesystem::create_directories(p); + return p.string(); +} + +static void cleanup() { + std::filesystem::remove_all(std::filesystem::temp_directory_path() / "whetstone_test_296"); +} + +// 1. Module with Subject 2-8 annotations generates Semanno in all generators +void test_multi_subject_generation() { + TEST(multi_subject_generation); + auto mod = std::make_unique(); + mod->name = "annotated"; + + auto fn = std::make_unique(); + fn->name = "process"; + + // One from each subject + auto bw = new BitWidthAnnotation(); bw->width = 32; // S2 + auto atomic = new AtomicAnnotation(); atomic->consistency = "seq_cst"; // S3 + auto cap = new CaptureAnnotation(); cap->strategy = "move"; // S4 + auto shim = new ShimAnnotation(); shim->strategy = "vtable"; // S5 + auto loop = new LoopAnnotation(); loop->hint = "unroll"; // S6 + auto meta = new MetaAnnotation(); meta->state = "quoted"; meta->phase = "compile"; // S7 + auto policy = new PolicyAnnotation(); policy->strictness = "high"; // S8 + + fn->addChild("annotations", bw); + fn->addChild("annotations", atomic); + fn->addChild("annotations", cap); + fn->addChild("annotations", shim); + fn->addChild("annotations", loop); + fn->addChild("annotations", meta); + fn->addChild("annotations", policy); + + mod->addChild("functions", fn.release()); + + PythonGenerator gen; + std::string result = gen.generate(mod.get()); + + CHECK(result.find("@semanno:bitwidth") != std::string::npos, "missing S2"); + CHECK(result.find("@semanno:atomic") != std::string::npos, "missing S3"); + CHECK(result.find("@semanno:capture") != std::string::npos, "missing S4"); + CHECK(result.find("@semanno:shim") != std::string::npos, "missing S5"); + CHECK(result.find("@semanno:loop") != std::string::npos, "missing S6"); + CHECK(result.find("@semanno:meta") != std::string::npos, "missing S7"); + CHECK(result.find("@semanno:policy") != std::string::npos, "missing S8"); + PASS(); +} + +// 2. Cross-language: annotations preserved through projection (generate same module with diff generators) +void test_cross_language_preservation() { + TEST(cross_language_preservation); + auto mod = std::make_unique(); + mod->name = "cross"; + auto fn = std::make_unique(); + fn->name = "sort"; + fn->addChild("annotations", new LoopAnnotation()); + fn->addChild("annotations", new PureAnnotation()); + mod->addChild("functions", fn.release()); + + PythonGenerator pyGen; + CppGenerator cppGen; + RustGenerator rsGen; + + std::string py = pyGen.generate(mod.get()); + std::string cpp = cppGen.generate(mod.get()); + std::string rs = rsGen.generate(mod.get()); + + CHECK(py.find("@semanno:loop") != std::string::npos, "py missing loop"); + CHECK(cpp.find("@semanno:loop") != std::string::npos, "cpp missing loop"); + CHECK(rs.find("@semanno:loop") != std::string::npos, "rs missing loop"); + PASS(); +} + +// 3. All 7 generators produce non-empty Semanno for every annotation type (exhaustive) +void test_exhaustive_no_unknown() { + TEST(exhaustive_no_unknown); + + // Create one of each annotation type + std::vector> annotations; + // Subject 2 + { auto a = std::make_unique(); a->width = 8; annotations.push_back(std::move(a)); } + { auto a = std::make_unique(); a->order = "little"; annotations.push_back(std::move(a)); } + { auto a = std::make_unique(); a->mode = "aligned"; annotations.push_back(std::move(a)); } + { auto a = std::make_unique(); a->nullable = true; annotations.push_back(std::move(a)); } + { auto a = std::make_unique(); a->variance = "invariant"; annotations.push_back(std::move(a)); } + { auto a = std::make_unique(); a->mode = "structural"; annotations.push_back(std::move(a)); } + { auto a = std::make_unique(); a->depth = "shallow"; annotations.push_back(std::move(a)); } + { auto a = std::make_unique(); a->state = "erased"; annotations.push_back(std::move(a)); } + // Subject 3 + { auto a = std::make_unique(); a->consistency = "relaxed"; annotations.push_back(std::move(a)); } + { auto a = std::make_unique(); a->primitive = "spin"; annotations.push_back(std::move(a)); } + { auto a = std::make_unique(); a->model = "os"; annotations.push_back(std::move(a)); } + { annotations.push_back(std::make_unique()); } + { auto a = std::make_unique(); a->mode = "event"; annotations.push_back(std::move(a)); } + { auto a = std::make_unique(); a->kind = "compute"; annotations.push_back(std::move(a)); } + { auto a = std::make_unique(); a->kind = "task"; annotations.push_back(std::move(a)); } + { auto a = std::make_unique(); a->signal = "SIGSEGV"; annotations.push_back(std::move(a)); } + { auto a = std::make_unique(); a->style = "unchecked"; annotations.push_back(std::move(a)); } + { auto a = std::make_unique(); a->behavior = "unwind"; annotations.push_back(std::move(a)); } + // Subject 4 + { auto a = std::make_unique(); a->time = "dynamic"; annotations.push_back(std::move(a)); } + { auto a = std::make_unique(); a->mode = "hoisted"; annotations.push_back(std::move(a)); } + { auto a = std::make_unique(); a->strategy = "ref"; annotations.push_back(std::move(a)); } + { auto a = std::make_unique(); a->level = "internal"; annotations.push_back(std::move(a)); } + { auto a = std::make_unique(); a->style = "flat"; annotations.push_back(std::move(a)); } + { auto a = std::make_unique(); a->kind = "global_leaked"; annotations.push_back(std::move(a)); } + // Subject 5 + { auto a = std::make_unique(); a->instruction = "nop"; annotations.push_back(std::move(a)); } + { auto a = std::make_unique(); a->language = "c"; a->code = "x"; annotations.push_back(std::move(a)); } + { auto a = std::make_unique(); a->convention = "fastcall"; annotations.push_back(std::move(a)); } + { auto a = std::make_unique(); a->symbolName = "s"; annotations.push_back(std::move(a)); } + { auto a = std::make_unique(); a->strategy = "cast"; annotations.push_back(std::move(a)); } + { annotations.push_back(std::make_unique()); } + { auto a = std::make_unique(); a->reason = "r"; annotations.push_back(std::move(a)); } + { auto a = std::make_unique(); a->platform = "linux"; annotations.push_back(std::move(a)); } + { auto a = std::make_unique(); a->flag = "sse2"; a->enabled = true; annotations.push_back(std::move(a)); } + { auto a = std::make_unique(); a->sourceCode = "x"; a->sourceLanguage = "py"; annotations.push_back(std::move(a)); } + { auto a = std::make_unique(); a->history = {"a"}; annotations.push_back(std::move(a)); } + // Subject 6 + { annotations.push_back(std::make_unique()); } + { auto a = std::make_unique(); a->hint = "vectorize"; annotations.push_back(std::move(a)); } + { auto a = std::make_unique(); a->hint = "restrict"; annotations.push_back(std::move(a)); } + { auto a = std::make_unique(); a->bytes = 16; annotations.push_back(std::move(a)); } + { annotations.push_back(std::make_unique()); } + { auto a = std::make_unique(); a->enabled = true; annotations.push_back(std::move(a)); } + { auto a = std::make_unique(); a->behavior = "saturation"; annotations.push_back(std::move(a)); } + // Subject 7 + { auto a = std::make_unique(); a->state = "quoted"; annotations.push_back(std::move(a)); } + { auto a = std::make_unique(); a->mode = "gensym"; annotations.push_back(std::move(a)); } + { auto a = std::make_unique(); a->phase = "compile_time"; annotations.push_back(std::move(a)); } + { auto a = std::make_unique(); a->specialization = "erasure"; annotations.push_back(std::move(a)); } + { auto a = std::make_unique(); a->generator = "macro"; annotations.push_back(std::move(a)); } + // Subject 8 + { auto a = std::make_unique(); a->strictness = "low"; annotations.push_back(std::move(a)); } + { auto a = std::make_unique(); a->intent = "x"; annotations.push_back(std::move(a)); } + { auto a = std::make_unique(); a->inferredTypes = {"int"}; annotations.push_back(std::move(a)); } + { auto a = std::make_unique(); a->reason = "r"; annotations.push_back(std::move(a)); } + { auto a = std::make_unique(); a->choiceId = "c"; annotations.push_back(std::move(a)); } + { auto a = std::make_unique(); a->choiceId = "c"; a->selection = "a"; annotations.push_back(std::move(a)); } + // Semantic Core + { auto a = std::make_unique(); a->summary = "s"; annotations.push_back(std::move(a)); } + { auto a = std::make_unique(); a->timeComplexity = "O(1)"; annotations.push_back(std::move(a)); } + { auto a = std::make_unique(); a->level = "low"; annotations.push_back(std::move(a)); } + { auto a = std::make_unique(); a->preconditions = "x"; annotations.push_back(std::move(a)); } + { auto a = std::make_unique(); a->tags = {"io"}; annotations.push_back(std::move(a)); } + // Environment + { auto a = std::make_unique(); a->capability = "gc"; a->required = true; annotations.push_back(std::move(a)); } + + PythonGenerator py; + CppGenerator cpp; + int unknownCount = 0; + for (const auto& a : annotations) { + std::string pyRes = py.generate(a.get()); + std::string cppRes = cpp.generate(a.get()); + if (pyRes.find("Unknown") != std::string::npos) { + std::cout << "Python Unknown: " << a->conceptType << "\n"; + ++unknownCount; + } + if (cppRes.find("Unknown") != std::string::npos) { + std::cout << "C++ Unknown: " << a->conceptType << "\n"; + ++unknownCount; + } + } + CHECK(unknownCount == 0, std::to_string(unknownCount) + " Unknown results"); + CHECK(annotations.size() >= 56, "expected 56+ types, got " + std::to_string(annotations.size())); + PASS(); +} + +// 4. Semanno roundtrip: generate → parse back → match +void test_semanno_roundtrip() { + TEST(semanno_roundtrip); + CppGenerator gen; + + auto anno = std::make_unique(); + anno->strictness = "high"; + anno->perf = "critical"; + anno->style = "idiomatic"; + anno->binaryStable = false; + + std::string generated = gen.generate(anno.get()); + auto entry = SemannoParser::parse(generated); + CHECK(entry.type == "policy", "wrong type"); + CHECK(entry.properties["strictness"] == "high", "strictness lost"); + CHECK(entry.properties["perf"] == "critical", "perf lost"); + CHECK(entry.properties["style"] == "idiomatic", "style lost"); + PASS(); +} + +// 5. Mixed legacy + new annotations in single module +void test_mixed_legacy_new() { + TEST(mixed_legacy_new); + auto mod = std::make_unique(); + mod->name = "mixed"; + auto fn = std::make_unique(); + fn->name = "calc"; + + // Legacy (Subject 1 memory) + auto reclaim = new ReclaimAnnotation(); + reclaim->strategy = "Tracing"; + fn->addChild("annotations", reclaim); + + // New (Subject 6) + auto loop = new LoopAnnotation(); + loop->hint = "vectorize"; + fn->addChild("annotations", loop); + + // Semantic core + auto intent = new IntentAnnotation(); + intent->summary = "calculates sum"; + fn->addChild("annotations", intent); + + mod->addChild("functions", fn.release()); + + CppGenerator gen; + std::string result = gen.generate(mod.get()); + // Legacy memory annotations may use their own format (not semanno) + // New annotations should use semanno + CHECK(result.find("@semanno:loop") != std::string::npos, "missing loop"); + CHECK(result.find("@semanno:intent") != std::string::npos, "missing intent"); + PASS(); +} + +// 6. Semanno sidecar roundtrip with all subjects +void test_sidecar_all_subjects() { + TEST(sidecar_all_subjects); + cleanup(); + auto mod = std::make_unique(); + mod->name = "sidecar_test"; + auto fn = std::make_unique(); + fn->name = "f"; + fn->spanStartLine = 1; + + fn->addChild("annotations", new IntentAnnotation()); + auto bw = new BitWidthAnnotation(); bw->width = 32; + fn->addChild("annotations", bw); + auto atomic = new AtomicAnnotation(); atomic->consistency = "seq_cst"; + fn->addChild("annotations", atomic); + + mod->addChild("functions", fn.release()); + + auto saved = saveSemannoSidecar(tmpDir(), "all.py", mod.get()); + CHECK(saved.success, "save failed"); + CHECK(saved.annotationCount == 3, "wrong save count: " + std::to_string(saved.annotationCount)); + + auto loaded = loadSemannoSidecar(tmpDir(), "all.py"); + CHECK(loaded.success, "load failed"); + CHECK(loaded.count == 3, "wrong load count: " + std::to_string(loaded.count)); + PASS(); +} + +// 7. Generator comment prefix matches language convention +void test_all_prefixes() { + TEST(all_prefixes); + PythonGenerator py; + CppGenerator cpp; + RustGenerator rs; + GoGenerator go; + JavaGenerator java; + JavaScriptGenerator js; + ElispGenerator elisp; + + CHECK(py.commentPrefix() == "# ", "py"); + CHECK(cpp.commentPrefix() == "// ", "cpp"); + CHECK(rs.commentPrefix() == "// ", "rs"); + CHECK(go.commentPrefix() == "// ", "go"); + CHECK(java.commentPrefix() == "// ", "java"); + CHECK(js.commentPrefix() == "// ", "js"); + CHECK(elisp.commentPrefix() == ";; ", "elisp"); + PASS(); +} + +// 8. Marker annotations produce correct Semanno (no parentheses) +void test_marker_semanno() { + TEST(marker_semanno); + CppGenerator gen; + + auto pure = std::make_unique(); + auto tailcall = std::make_unique(); + auto pack = std::make_unique(); + auto barrier = std::make_unique(); + auto ptrArith = std::make_unique(); + + std::string pureResult = gen.generate(pure.get()); + std::string tailResult = gen.generate(tailcall.get()); + std::string packResult = gen.generate(pack.get()); + + // Marker annotations should NOT have parentheses + CHECK(pureResult.find("(") == std::string::npos, "pure has parens: " + pureResult); + CHECK(tailResult.find("(") == std::string::npos, "tailcall has parens: " + tailResult); + CHECK(packResult.find("(") == std::string::npos, "pack has parens: " + packResult); + PASS(); +} + +int main() { + std::cout << "=== Step 296: Phase 11a Integration Tests ===\n"; + test_multi_subject_generation(); + test_cross_language_preservation(); + test_exhaustive_no_unknown(); + test_semanno_roundtrip(); + test_mixed_legacy_new(); + test_sidecar_all_subjects(); + test_all_prefixes(); + test_marker_semanno(); + cleanup(); + std::cout << "\nResults: " << passed << "/" << (passed + failed) << " passed\n"; + return failed > 0 ? 1 : 0; +} diff --git a/progress.md b/progress.md index 666593a..a73502b 100644 --- a/progress.md +++ b/progress.md @@ -843,3 +843,40 @@ generators. Prefix verification, annotation output, exhaustive no-Unknown check. and `SemannoAnnotationImpl : public virtual AnnotationVisitorExtended` to resolve diamond inheritance ambiguity in generator classes - Removed circular include: `EnvironmentSpec.h` no longer includes `ast/Serialization.h` + +### Step 295: Semanno Sidecar Integration (12 tests) +**Status:** PASS (12/12 tests) + +Semanno sidecar persistence now supports full save/load roundtrip behavior, +including empty sidecar file handling. + +**Key files:** +- `editor/src/SemannoSidecar.h` — implemented sidecar API with structured + save/load results, path helper, line-keyed `L: @semanno:...` emission, + and empty-module sidecar creation. +- `editor/src/SidecarPersistence.h` — include fixes for AST helper symbols used + by sidecar merge logic. +- `editor/tests/step295_test.cpp` — includes sidecar persistence integration + checks alongside Semanno sidecar tests. + +### Step 296: Phase 11a Integration Tests (8 tests) +**Status:** PASS (8/8 tests) + +Full Phase 11a integration validation now passes end-to-end. + +**Key files:** +- `editor/src/SemannoSidecar.h` — added result-based free-function API used by + Sprint 11 tests (`semannoSidecarPath`, `saveSemannoSidecar`, + `loadSemannoSidecar`), path/error/count reporting, and corrected line mapping + to use parent `spanStartLine`. +- `editor/tests/step296_test.cpp` — integration suite executed and passing. + +**Verified checks:** +- Parse/annotate/generate path preserves Subject 2-8 Semanno output. +- Cross-language generators preserve Semanno annotations. +- Exhaustive no-Unknown emission for 56+ annotation types. +- Semanno parse roundtrip for structured properties. +- Mixed legacy + new annotation handling in one module. +- Semanno sidecar save/load integration with expected counts. +- Language comment prefix conventions. +- Marker annotation emission without property parentheses.