Step 565: add memory inspector UI model

This commit is contained in:
Bill
2026-02-17 10:46:11 -07:00
parent 34eddc7da5
commit d5fe9090bd
4 changed files with 425 additions and 0 deletions

View File

@@ -3964,4 +3964,13 @@ target_link_libraries(step564_test PRIVATE
tree_sitter_javascript tree_sitter_typescript
tree_sitter_java tree_sitter_rust tree_sitter_go)
add_executable(step565_test tests/step565_test.cpp)
target_include_directories(step565_test PRIVATE src)
target_link_libraries(step565_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)

View File

@@ -0,0 +1,194 @@
#pragma once
// Step 565: Memory Inspector UI Model
#include "MemorySnapshotModel.h"
#include <cstdint>
#include <string>
#include <vector>
enum class MemoryValueKind {
Integer,
Floating,
Boolean,
String,
Pointer,
Container,
Struct
};
struct MemoryValueNode {
std::string nodeId;
std::string label;
MemoryValueKind kind = MemoryValueKind::Integer;
std::string valueText;
bool expanded = false;
std::vector<MemoryValueNode> children;
};
struct MemoryInspectorRow {
std::string nodeId;
std::string label;
std::string typeName;
std::string valueText;
int depth = 0;
bool expandable = false;
bool expanded = false;
};
class MemoryInspectorUiModel {
public:
static bool createLeaf(const std::string& nodeId,
const std::string& label,
MemoryValueKind kind,
const std::string& valueText,
MemoryValueNode* outNode,
std::string* error) {
if (!outNode || !error) return false;
error->clear();
if (nodeId.empty() || label.empty()) {
*error = "node_id_or_label_missing";
return false;
}
if (isCompositeKind(kind)) {
*error = "leaf_kind_must_be_primitive";
return false;
}
MemoryValueNode node;
node.nodeId = nodeId;
node.label = label;
node.kind = kind;
node.valueText = valueText;
*outNode = node;
return true;
}
static bool createComposite(const std::string& nodeId,
const std::string& label,
MemoryValueKind kind,
MemoryValueNode* outNode,
std::string* error) {
if (!outNode || !error) return false;
error->clear();
if (nodeId.empty() || label.empty()) {
*error = "node_id_or_label_missing";
return false;
}
if (!isCompositeKind(kind)) {
*error = "composite_kind_required";
return false;
}
MemoryValueNode node;
node.nodeId = nodeId;
node.label = label;
node.kind = kind;
node.expanded = true;
*outNode = node;
return true;
}
static bool appendChild(MemoryValueNode* parent,
const MemoryValueNode& child,
std::string* error) {
if (!parent || !error) return false;
error->clear();
if (!isCompositeKind(parent->kind)) {
*error = "parent_is_not_composite";
return false;
}
if (child.nodeId.empty()) {
*error = "child_node_id_missing";
return false;
}
parent->children.push_back(child);
return true;
}
static std::vector<MemoryInspectorRow> collectVisibleRows(const MemoryValueNode& root,
std::size_t maxRows) {
std::vector<MemoryInspectorRow> rows;
if (maxRows == 0) return rows;
appendNodeRows(root, 0, maxRows, &rows);
return rows;
}
static std::string typeNameForKind(MemoryValueKind kind) {
switch (kind) {
case MemoryValueKind::Integer: return "int";
case MemoryValueKind::Floating: return "float";
case MemoryValueKind::Boolean: return "bool";
case MemoryValueKind::String: return "string";
case MemoryValueKind::Pointer: return "pointer";
case MemoryValueKind::Container: return "container";
case MemoryValueKind::Struct: return "struct";
}
return "unknown";
}
static bool resolvePointerRegion(const MemorySnapshot& snapshot,
const MemoryValueNode& pointerNode,
const MemoryRegionRecord** outRegion,
std::string* error) {
if (!outRegion || !error) return false;
*outRegion = nullptr;
error->clear();
if (pointerNode.kind != MemoryValueKind::Pointer) {
*error = "node_is_not_pointer";
return false;
}
std::uint64_t address = 0;
if (!parseAddress(pointerNode.valueText, &address)) {
*error = "pointer_value_parse_failed";
return false;
}
*outRegion = MemorySnapshotModel::findRegionByAddress(snapshot, address);
return *outRegion != nullptr;
}
private:
static bool isCompositeKind(MemoryValueKind kind) {
return kind == MemoryValueKind::Container || kind == MemoryValueKind::Struct;
}
static bool parseAddress(const std::string& text, std::uint64_t* outAddress) {
if (!outAddress || text.empty()) return false;
try {
size_t parsed = 0;
int base = 10;
if (text.size() > 2 && text[0] == '0' && (text[1] == 'x' || text[1] == 'X')) base = 16;
const auto value = std::stoull(text, &parsed, base);
if (parsed != text.size() || value == 0) return false;
*outAddress = static_cast<std::uint64_t>(value);
return true;
} catch (...) {
return false;
}
}
static void appendNodeRows(const MemoryValueNode& node,
int depth,
std::size_t maxRows,
std::vector<MemoryInspectorRow>* rows) {
if (rows->size() >= maxRows) return;
MemoryInspectorRow row;
row.nodeId = node.nodeId;
row.label = node.label;
row.typeName = typeNameForKind(node.kind);
row.valueText = node.valueText;
row.depth = depth;
row.expandable = isCompositeKind(node.kind);
row.expanded = node.expanded;
rows->push_back(row);
if (!row.expandable || !node.expanded) return;
for (const auto& child : node.children) {
appendNodeRows(child, depth + 1, maxRows, rows);
if (rows->size() >= maxRows) return;
}
}
};

View File

@@ -0,0 +1,188 @@
// Step 565: Memory Inspector UI Model (12 tests)
#include "MemoryInspectorUiModel.h"
#include <iostream>
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_leaf_success() {
TEST(create_leaf_success);
MemoryValueNode n;
std::string error;
const bool ok = MemoryInspectorUiModel::createLeaf("n1", "counter", MemoryValueKind::Integer, "42", &n, &error);
CHECK(ok, "create leaf should succeed");
CHECK(n.nodeId == "n1", "id mismatch");
CHECK(n.label == "counter", "label mismatch");
CHECK(n.kind == MemoryValueKind::Integer, "kind mismatch");
CHECK(n.valueText == "42", "value mismatch");
PASS();
}
void test_create_leaf_rejects_composite_kind() {
TEST(create_leaf_rejects_composite_kind);
MemoryValueNode n;
std::string error;
const bool ok = MemoryInspectorUiModel::createLeaf("n1", "arr", MemoryValueKind::Container, "[]", &n, &error);
CHECK(!ok, "create leaf should fail for composite kind");
CHECK(error == "leaf_kind_must_be_primitive", "wrong error");
PASS();
}
void test_create_composite_success() {
TEST(create_composite_success);
MemoryValueNode n;
std::string error;
const bool ok = MemoryInspectorUiModel::createComposite("n2", "record", MemoryValueKind::Struct, &n, &error);
CHECK(ok, "create composite should succeed");
CHECK(n.expanded, "composite defaults to expanded");
CHECK(n.kind == MemoryValueKind::Struct, "kind mismatch");
PASS();
}
void test_create_composite_rejects_primitive_kind() {
TEST(create_composite_rejects_primitive_kind);
MemoryValueNode n;
std::string error;
const bool ok = MemoryInspectorUiModel::createComposite("n2", "count", MemoryValueKind::Integer, &n, &error);
CHECK(!ok, "create composite should fail for primitive kind");
CHECK(error == "composite_kind_required", "wrong error");
PASS();
}
void test_append_child_success() {
TEST(append_child_success);
MemoryValueNode parent;
MemoryValueNode child;
std::string error;
CHECK(MemoryInspectorUiModel::createComposite("p", "root", MemoryValueKind::Struct, &parent, &error), "parent create failed");
CHECK(MemoryInspectorUiModel::createLeaf("c", "x", MemoryValueKind::Integer, "3", &child, &error), "child create failed");
CHECK(MemoryInspectorUiModel::appendChild(&parent, child, &error), "append child should succeed");
CHECK(parent.children.size() == 1, "expected one child");
CHECK(parent.children[0].nodeId == "c", "child id mismatch");
PASS();
}
void test_append_child_rejects_non_composite_parent() {
TEST(append_child_rejects_non_composite_parent);
MemoryValueNode parent;
MemoryValueNode child;
std::string error;
CHECK(MemoryInspectorUiModel::createLeaf("p", "value", MemoryValueKind::Integer, "1", &parent, &error), "parent create failed");
CHECK(MemoryInspectorUiModel::createLeaf("c", "x", MemoryValueKind::Integer, "3", &child, &error), "child create failed");
CHECK(!MemoryInspectorUiModel::appendChild(&parent, child, &error), "append should fail");
CHECK(error == "parent_is_not_composite", "wrong error");
PASS();
}
void test_collect_visible_rows_honors_expanded_state() {
TEST(collect_visible_rows_honors_expanded_state);
MemoryValueNode root;
MemoryValueNode child;
std::string error;
CHECK(MemoryInspectorUiModel::createComposite("root", "Root", MemoryValueKind::Struct, &root, &error), "root create failed");
CHECK(MemoryInspectorUiModel::createLeaf("child", "counter", MemoryValueKind::Integer, "9", &child, &error), "child create failed");
CHECK(MemoryInspectorUiModel::appendChild(&root, child, &error), "append failed");
root.expanded = false;
const auto rows = MemoryInspectorUiModel::collectVisibleRows(root, 10);
CHECK(rows.size() == 1, "collapsed root should hide children");
CHECK(rows[0].nodeId == "root", "root row expected");
PASS();
}
void test_collect_visible_rows_depth_and_limit() {
TEST(collect_visible_rows_depth_and_limit);
MemoryValueNode root;
MemoryValueNode childA;
MemoryValueNode childB;
std::string error;
CHECK(MemoryInspectorUiModel::createComposite("root", "Root", MemoryValueKind::Container, &root, &error), "root create failed");
CHECK(MemoryInspectorUiModel::createLeaf("a", "a", MemoryValueKind::Integer, "1", &childA, &error), "a create failed");
CHECK(MemoryInspectorUiModel::createLeaf("b", "b", MemoryValueKind::Integer, "2", &childB, &error), "b create failed");
CHECK(MemoryInspectorUiModel::appendChild(&root, childA, &error), "append a failed");
CHECK(MemoryInspectorUiModel::appendChild(&root, childB, &error), "append b failed");
const auto limitedRows = MemoryInspectorUiModel::collectVisibleRows(root, 2);
CHECK(limitedRows.size() == 2, "row limit should apply");
CHECK(limitedRows[1].depth == 1, "child row depth should be one");
PASS();
}
void test_type_name_mapping_includes_pointer_and_struct() {
TEST(type_name_mapping_includes_pointer_and_struct);
CHECK(MemoryInspectorUiModel::typeNameForKind(MemoryValueKind::Pointer) == "pointer", "pointer type mismatch");
CHECK(MemoryInspectorUiModel::typeNameForKind(MemoryValueKind::Struct) == "struct", "struct type mismatch");
PASS();
}
void test_resolve_pointer_region_success() {
TEST(resolve_pointer_region_success);
MemorySnapshot snapshot;
std::string error;
CHECK(MemorySnapshotModel::createSnapshot("snap", "sess", "thread", 1, &snapshot, &error), "snapshot create failed");
CHECK(MemorySnapshotModel::addRegion(&snapshot, {"heap-1", MemoryRegionKind::Heap, 0x1000, 64, false}, &error), "region add failed");
MemoryValueNode pointerNode;
CHECK(MemoryInspectorUiModel::createLeaf("ptr", "next", MemoryValueKind::Pointer, "0x1010", &pointerNode, &error), "pointer node create failed");
const MemoryRegionRecord* resolved = nullptr;
const bool ok = MemoryInspectorUiModel::resolvePointerRegion(snapshot, pointerNode, &resolved, &error);
CHECK(ok, "pointer region resolve should succeed");
CHECK(resolved != nullptr && resolved->id == "heap-1", "resolved region mismatch");
PASS();
}
void test_resolve_pointer_region_rejects_non_pointer_node() {
TEST(resolve_pointer_region_rejects_non_pointer_node);
MemorySnapshot snapshot;
MemoryValueNode valueNode;
std::string error;
CHECK(MemorySnapshotModel::createSnapshot("snap", "sess", "thread", 1, &snapshot, &error), "snapshot create failed");
CHECK(MemoryInspectorUiModel::createLeaf("v", "x", MemoryValueKind::Integer, "7", &valueNode, &error), "value node create failed");
const MemoryRegionRecord* resolved = nullptr;
const bool ok = MemoryInspectorUiModel::resolvePointerRegion(snapshot, valueNode, &resolved, &error);
CHECK(!ok, "resolve should fail for non-pointer");
CHECK(error == "node_is_not_pointer", "wrong error");
PASS();
}
void test_resolve_pointer_region_rejects_bad_pointer_text() {
TEST(resolve_pointer_region_rejects_bad_pointer_text);
MemorySnapshot snapshot;
MemoryValueNode pointerNode;
std::string error;
CHECK(MemorySnapshotModel::createSnapshot("snap", "sess", "thread", 1, &snapshot, &error), "snapshot create failed");
CHECK(MemoryInspectorUiModel::createLeaf("ptr", "next", MemoryValueKind::Pointer, "not-an-address", &pointerNode, &error), "pointer node create failed");
const MemoryRegionRecord* resolved = nullptr;
const bool ok = MemoryInspectorUiModel::resolvePointerRegion(snapshot, pointerNode, &resolved, &error);
CHECK(!ok, "resolve should fail for parse error");
CHECK(error == "pointer_value_parse_failed", "wrong error");
PASS();
}
int main() {
std::cout << "Step 565: Memory Inspector UI Model\n";
test_create_leaf_success(); // 1
test_create_leaf_rejects_composite_kind(); // 2
test_create_composite_success(); // 3
test_create_composite_rejects_primitive_kind(); // 4
test_append_child_success(); // 5
test_append_child_rejects_non_composite_parent(); // 6
test_collect_visible_rows_honors_expanded_state(); // 7
test_collect_visible_rows_depth_and_limit(); // 8
test_type_name_mapping_includes_pointer_and_struct(); // 9
test_resolve_pointer_region_success(); // 10
test_resolve_pointer_region_rejects_non_pointer_node();// 11
test_resolve_pointer_region_rejects_bad_pointer_text();// 12
std::cout << "\nResults: " << passed << "/" << (passed + failed) << " passed\n";
return failed == 0 ? 0 : 1;
}

View File

@@ -10190,3 +10190,37 @@ validation, and query helpers for stack/heap/global memory reflection.
- `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`
### Step 565: Memory Inspector UI Model
**Status:** PASS (12/12 tests)
Implements a memory inspector UI data model with typed primitive/composite nodes,
visible-row flattening, and pointer-to-region correlation against memory snapshots.
**Files added:**
- `editor/src/MemoryInspectorUiModel.h` - memory inspector UI model:
- typed value-node schema (primitive + container/struct)
- primitive/composite creation guards
- composite child-attachment behavior
- visible row flattening with expansion + row limit handling
- pointer text parsing and region-resolution against `MemorySnapshot`
- `editor/tests/step565_test.cpp` - 12 tests covering:
- primitive/composite creation success/failure behavior
- child attachment behavior and parent-kind guards
- visible-row expansion/limit/depth behavior
- type renderer naming behavior
- pointer-region resolve success and failure behavior
**Files modified:**
- `editor/CMakeLists.txt` - `step565_test` target
**Verification run:**
- `cmake -S editor -B editor/build-native` - PASS
- `cmake --build editor/build-native --target step565_test step564_test` - PASS
- `./editor/build-native/step565_test` - PASS (12/12)
- `./editor/build-native/step564_test` - PASS (12/12) regression coverage
**Architecture gate check:**
- `editor/src/MemoryInspectorUiModel.h` within header-size limit (`194` <= `600`)
- `editor/tests/step565_test.cpp` within test-file size guidance (`188` lines)
- Header-only architecture and naming conventions remain aligned with `ARCHITECTURE.md`