From 34eddc7da528c1d8d75b4d71ab08747fad963ec5 Mon Sep 17 00:00:00 2001 From: Bill Date: Tue, 17 Feb 2026 10:44:31 -0700 Subject: [PATCH] Step 564: add memory snapshot model --- editor/CMakeLists.txt | 9 ++ editor/src/MemorySnapshotModel.h | 149 +++++++++++++++++++++++++ editor/tests/step564_test.cpp | 179 +++++++++++++++++++++++++++++++ progress.md | 35 ++++++ 4 files changed, 372 insertions(+) create mode 100644 editor/src/MemorySnapshotModel.h create mode 100644 editor/tests/step564_test.cpp diff --git a/editor/CMakeLists.txt b/editor/CMakeLists.txt index 2a5ae93..3db096c 100644 --- a/editor/CMakeLists.txt +++ b/editor/CMakeLists.txt @@ -3955,4 +3955,13 @@ target_link_libraries(step563_test PRIVATE tree_sitter_javascript tree_sitter_typescript tree_sitter_java tree_sitter_rust tree_sitter_go) +add_executable(step564_test tests/step564_test.cpp) +target_include_directories(step564_test PRIVATE src) +target_link_libraries(step564_test PRIVATE + nlohmann_json::nlohmann_json + unofficial::tree-sitter::tree-sitter + tree_sitter_python tree_sitter_cpp tree_sitter_elisp + tree_sitter_javascript tree_sitter_typescript + tree_sitter_java tree_sitter_rust tree_sitter_go) + # Step 12: Dear ImGui shell scaffolding created (main.cpp exists but not built due to dependencies) diff --git a/editor/src/MemorySnapshotModel.h b/editor/src/MemorySnapshotModel.h new file mode 100644 index 0000000..69266d6 --- /dev/null +++ b/editor/src/MemorySnapshotModel.h @@ -0,0 +1,149 @@ +#pragma once +// Step 564: Memory Snapshot Model + +#include "DebugValidationUtil.h" + +#include +#include +#include +#include + +enum class MemoryRegionKind { + Stack, + Heap, + Global +}; + +struct MemoryRegionRecord { + std::string id; + MemoryRegionKind kind = MemoryRegionKind::Heap; + std::uint64_t startAddress = 0; + std::uint64_t sizeBytes = 0; + bool readOnly = false; +}; + +struct MemoryReferenceRecord { + std::uint64_t fromAddress = 0; + std::uint64_t toAddress = 0; + std::string label; +}; + +struct MemorySnapshot { + std::string snapshotId; + std::string sessionId; + std::string threadId; + std::uint64_t sequence = 0; + std::vector regions; + std::vector references; +}; + +class MemorySnapshotModel { +public: + static bool createSnapshot(const std::string& snapshotId, + const std::string& sessionId, + const std::string& threadId, + std::uint64_t sequence, + MemorySnapshot* outSnapshot, + std::string* error) { + if (!outSnapshot || !error) return false; + error->clear(); + if (!hasRequiredDebugIds(snapshotId, sessionId)) { + *error = "snapshot_id_or_session_id_missing"; + return false; + } + if (threadId.empty()) { + *error = "thread_id_missing"; + return false; + } + + MemorySnapshot snap; + snap.snapshotId = snapshotId; + snap.sessionId = sessionId; + snap.threadId = threadId; + snap.sequence = sequence; + *outSnapshot = snap; + return true; + } + + static bool addRegion(MemorySnapshot* snapshot, + const MemoryRegionRecord& region, + std::string* error) { + if (!snapshot || !error) return false; + error->clear(); + if (region.id.empty()) { + *error = "region_id_missing"; + return false; + } + if (region.startAddress == 0) { + *error = "region_start_address_invalid"; + return false; + } + if (region.sizeBytes == 0) { + *error = "region_size_invalid"; + return false; + } + if (findRegionIndexById(*snapshot, region.id) != -1) { + *error = "region_id_duplicate"; + return false; + } + + snapshot->regions.push_back(region); + stableSortRegions(snapshot); + return true; + } + + static bool addReference(MemorySnapshot* snapshot, + const MemoryReferenceRecord& reference, + std::string* error) { + if (!snapshot || !error) return false; + error->clear(); + if (reference.fromAddress == 0 || reference.toAddress == 0) { + *error = "reference_address_invalid"; + return false; + } + if (reference.label.empty()) { + *error = "reference_label_missing"; + return false; + } + + snapshot->references.push_back(reference); + return true; + } + + static const MemoryRegionRecord* findRegionByAddress(const MemorySnapshot& snapshot, + std::uint64_t address) { + if (address == 0) return nullptr; + for (const auto& region : snapshot.regions) { + const std::uint64_t regionEnd = region.startAddress + region.sizeBytes; + if (address >= region.startAddress && address < regionEnd) return ®ion; + } + return nullptr; + } + + static std::vector outgoingReferences(const MemorySnapshot& snapshot, + std::uint64_t fromAddress) { + std::vector filtered; + if (fromAddress == 0) return filtered; + for (const auto& reference : snapshot.references) { + if (reference.fromAddress == fromAddress) filtered.push_back(reference); + } + return filtered; + } + +private: + static int findRegionIndexById(const MemorySnapshot& snapshot, const std::string& id) { + for (size_t i = 0; i < snapshot.regions.size(); ++i) { + if (snapshot.regions[i].id == id) return static_cast(i); + } + return -1; + } + + static void stableSortRegions(MemorySnapshot* snapshot) { + std::stable_sort(snapshot->regions.begin(), + snapshot->regions.end(), + [](const MemoryRegionRecord& a, const MemoryRegionRecord& b) { + if (a.startAddress != b.startAddress) return a.startAddress < b.startAddress; + return a.id < b.id; + }); + } +}; diff --git a/editor/tests/step564_test.cpp b/editor/tests/step564_test.cpp new file mode 100644 index 0000000..f28dc24 --- /dev/null +++ b/editor/tests/step564_test.cpp @@ -0,0 +1,179 @@ +// Step 564: Memory Snapshot Model (12 tests) + +#include "MemorySnapshotModel.h" + +#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 {} + +void test_create_snapshot_success() { + TEST(create_snapshot_success); + MemorySnapshot snapshot; + std::string error; + const bool ok = MemorySnapshotModel::createSnapshot("snap-1", "sess-1", "thread-1", 7, &snapshot, &error); + CHECK(ok, "snapshot creation should succeed"); + CHECK(snapshot.snapshotId == "snap-1", "snapshot id mismatch"); + CHECK(snapshot.sessionId == "sess-1", "session id mismatch"); + CHECK(snapshot.threadId == "thread-1", "thread id mismatch"); + CHECK(snapshot.sequence == 7, "sequence mismatch"); + PASS(); +} + +void test_create_snapshot_fails_with_missing_ids() { + TEST(create_snapshot_fails_with_missing_ids); + MemorySnapshot snapshot; + std::string error; + const bool ok = MemorySnapshotModel::createSnapshot("", "sess-1", "thread-1", 1, &snapshot, &error); + CHECK(!ok, "creation should fail with missing snapshot id"); + CHECK(error == "snapshot_id_or_session_id_missing", "wrong error"); + PASS(); +} + +void test_create_snapshot_fails_with_missing_thread() { + TEST(create_snapshot_fails_with_missing_thread); + MemorySnapshot snapshot; + std::string error; + const bool ok = MemorySnapshotModel::createSnapshot("snap-1", "sess-1", "", 1, &snapshot, &error); + CHECK(!ok, "creation should fail with missing thread id"); + CHECK(error == "thread_id_missing", "wrong error"); + PASS(); +} + +void test_add_region_success() { + TEST(add_region_success); + MemorySnapshot snapshot; + std::string error; + CHECK(MemorySnapshotModel::createSnapshot("snap-1", "sess-1", "thread-1", 1, &snapshot, &error), "create failed"); + MemoryRegionRecord stackRegion{"r-stack", MemoryRegionKind::Stack, 0x1000, 64, false}; + CHECK(MemorySnapshotModel::addRegion(&snapshot, stackRegion, &error), "add region failed"); + CHECK(snapshot.regions.size() == 1, "region count should be one"); + CHECK(snapshot.regions[0].id == "r-stack", "region id mismatch"); + PASS(); +} + +void test_add_region_rejects_duplicate_id() { + TEST(add_region_rejects_duplicate_id); + MemorySnapshot snapshot; + std::string error; + CHECK(MemorySnapshotModel::createSnapshot("snap-1", "sess-1", "thread-1", 1, &snapshot, &error), "create failed"); + MemoryRegionRecord first{"dup", MemoryRegionKind::Heap, 0x2000, 32, false}; + MemoryRegionRecord second{"dup", MemoryRegionKind::Heap, 0x3000, 32, false}; + CHECK(MemorySnapshotModel::addRegion(&snapshot, first, &error), "first add failed"); + CHECK(!MemorySnapshotModel::addRegion(&snapshot, second, &error), "duplicate id should fail"); + CHECK(error == "region_id_duplicate", "wrong duplicate error"); + PASS(); +} + +void test_add_region_rejects_invalid_bounds() { + TEST(add_region_rejects_invalid_bounds); + MemorySnapshot snapshot; + std::string error; + CHECK(MemorySnapshotModel::createSnapshot("snap-1", "sess-1", "thread-1", 1, &snapshot, &error), "create failed"); + MemoryRegionRecord badStart{"bad-start", MemoryRegionKind::Heap, 0, 32, false}; + CHECK(!MemorySnapshotModel::addRegion(&snapshot, badStart, &error), "zero start should fail"); + CHECK(error == "region_start_address_invalid", "wrong start-address error"); + MemoryRegionRecord badSize{"bad-size", MemoryRegionKind::Heap, 0x2000, 0, false}; + CHECK(!MemorySnapshotModel::addRegion(&snapshot, badSize, &error), "zero size should fail"); + CHECK(error == "region_size_invalid", "wrong size error"); + PASS(); +} + +void test_regions_are_sorted_by_start_address() { + TEST(regions_are_sorted_by_start_address); + MemorySnapshot snapshot; + std::string error; + CHECK(MemorySnapshotModel::createSnapshot("snap-1", "sess-1", "thread-1", 1, &snapshot, &error), "create failed"); + CHECK(MemorySnapshotModel::addRegion(&snapshot, {"r2", MemoryRegionKind::Heap, 0x3000, 16, false}, &error), "add r2 failed"); + CHECK(MemorySnapshotModel::addRegion(&snapshot, {"r1", MemoryRegionKind::Stack, 0x1000, 16, false}, &error), "add r1 failed"); + CHECK(MemorySnapshotModel::addRegion(&snapshot, {"r3", MemoryRegionKind::Global, 0x2000, 16, true}, &error), "add r3 failed"); + CHECK(snapshot.regions[0].id == "r1", "first region should be r1"); + CHECK(snapshot.regions[1].id == "r3", "second region should be r3"); + CHECK(snapshot.regions[2].id == "r2", "third region should be r2"); + PASS(); +} + +void test_find_region_by_address_within_bounds() { + TEST(find_region_by_address_within_bounds); + MemorySnapshot snapshot; + std::string error; + CHECK(MemorySnapshotModel::createSnapshot("snap-1", "sess-1", "thread-1", 1, &snapshot, &error), "create failed"); + CHECK(MemorySnapshotModel::addRegion(&snapshot, {"heap-a", MemoryRegionKind::Heap, 0x5000, 128, false}, &error), "add failed"); + const MemoryRegionRecord* found = MemorySnapshotModel::findRegionByAddress(snapshot, 0x507F); + CHECK(found != nullptr, "region should be found"); + CHECK(found->id == "heap-a", "wrong region resolved"); + PASS(); +} + +void test_find_region_by_address_at_exclusive_end_returns_null() { + TEST(find_region_by_address_at_exclusive_end_returns_null); + MemorySnapshot snapshot; + std::string error; + CHECK(MemorySnapshotModel::createSnapshot("snap-1", "sess-1", "thread-1", 1, &snapshot, &error), "create failed"); + CHECK(MemorySnapshotModel::addRegion(&snapshot, {"heap-a", MemoryRegionKind::Heap, 0x5000, 128, false}, &error), "add failed"); + const MemoryRegionRecord* found = MemorySnapshotModel::findRegionByAddress(snapshot, 0x5080); + CHECK(found == nullptr, "exclusive upper-bound should not match"); + PASS(); +} + +void test_add_reference_success() { + TEST(add_reference_success); + MemorySnapshot snapshot; + std::string error; + CHECK(MemorySnapshotModel::createSnapshot("snap-1", "sess-1", "thread-1", 1, &snapshot, &error), "create failed"); + MemoryReferenceRecord ref{0x4000, 0x7000, "next"}; + CHECK(MemorySnapshotModel::addReference(&snapshot, ref, &error), "add reference failed"); + CHECK(snapshot.references.size() == 1, "reference count should be one"); + CHECK(snapshot.references[0].label == "next", "label mismatch"); + PASS(); +} + +void test_add_reference_rejects_invalid_fields() { + TEST(add_reference_rejects_invalid_fields); + MemorySnapshot snapshot; + std::string error; + CHECK(MemorySnapshotModel::createSnapshot("snap-1", "sess-1", "thread-1", 1, &snapshot, &error), "create failed"); + CHECK(!MemorySnapshotModel::addReference(&snapshot, {0, 0x7000, "next"}, &error), "zero source should fail"); + CHECK(error == "reference_address_invalid", "wrong address error"); + CHECK(!MemorySnapshotModel::addReference(&snapshot, {0x4000, 0x7000, ""}, &error), "empty label should fail"); + CHECK(error == "reference_label_missing", "wrong label error"); + PASS(); +} + +void test_outgoing_references_filter_by_source_address() { + TEST(outgoing_references_filter_by_source_address); + MemorySnapshot snapshot; + std::string error; + CHECK(MemorySnapshotModel::createSnapshot("snap-1", "sess-1", "thread-1", 1, &snapshot, &error), "create failed"); + CHECK(MemorySnapshotModel::addReference(&snapshot, {0x1000, 0x2000, "a"}, &error), "add ref a failed"); + CHECK(MemorySnapshotModel::addReference(&snapshot, {0x1000, 0x3000, "b"}, &error), "add ref b failed"); + CHECK(MemorySnapshotModel::addReference(&snapshot, {0x5000, 0x6000, "c"}, &error), "add ref c failed"); + const auto refs = MemorySnapshotModel::outgoingReferences(snapshot, 0x1000); + CHECK(refs.size() == 2, "expected two outgoing refs"); + CHECK(refs[0].label == "a", "first outgoing label mismatch"); + CHECK(refs[1].label == "b", "second outgoing label mismatch"); + PASS(); +} + +int main() { + std::cout << "Step 564: Memory Snapshot Model\n"; + + test_create_snapshot_success(); // 1 + test_create_snapshot_fails_with_missing_ids(); // 2 + test_create_snapshot_fails_with_missing_thread(); // 3 + test_add_region_success(); // 4 + test_add_region_rejects_duplicate_id(); // 5 + test_add_region_rejects_invalid_bounds(); // 6 + test_regions_are_sorted_by_start_address(); // 7 + test_find_region_by_address_within_bounds(); // 8 + test_find_region_by_address_at_exclusive_end_returns_null(); // 9 + test_add_reference_success(); // 10 + test_add_reference_rejects_invalid_fields(); // 11 + test_outgoing_references_filter_by_source_address(); // 12 + + std::cout << "\nResults: " << passed << "/" << (passed + failed) << " passed\n"; + return failed == 0 ? 0 : 1; +} diff --git a/progress.md b/progress.md index d027417..1f4cd37 100644 --- a/progress.md +++ b/progress.md @@ -10155,3 +10155,38 @@ header-only design and deterministic behavior. - `editor/src/BreakpointSystem.h` within header-size limit (`120` <= `600`) - `editor/src/ExceptionStackTraceCapture.h` within header-size limit (`73` <= `600`) - Shared validation centralization reduces duplicated guard logic and keeps module boundaries aligned with `ARCHITECTURE.md` + +### Step 564: Memory Snapshot Model +**Status:** PASS (12/12 tests) + +Implements a structured memory snapshot schema with region/reference capture, +validation, and query helpers for stack/heap/global memory reflection. + +**Files added:** +- `editor/src/MemorySnapshotModel.h` - memory snapshot module: + - snapshot/session/thread identity schema + - typed memory region records (stack/heap/global) + - pointer/reference record schema + - region insertion validation + deterministic ordering + - address-to-region and outgoing-reference query helpers +- `editor/tests/step564_test.cpp` - 12 tests covering: + - snapshot identity validation behavior + - region add/duplicate/bounds guard behavior + - deterministic region ordering behavior + - inclusive/exclusive address lookup behavior + - reference add/validation behavior + - outgoing reference filtering behavior + +**Files modified:** +- `editor/CMakeLists.txt` - `step564_test` target + +**Verification run:** +- `cmake -S editor -B editor/build-native` - PASS +- `cmake --build editor/build-native --target step564_test step563_test` - PASS +- `./editor/build-native/step564_test` - PASS (12/12) +- `./editor/build-native/step563_test` - PASS (8/8) regression coverage + +**Architecture gate check:** +- `editor/src/MemorySnapshotModel.h` within header-size limit (`149` <= `600`) +- `editor/tests/step564_test.cpp` within test-file size guidance (`179` lines) +- Header-only architecture and naming conventions remain aligned with `ARCHITECTURE.md`