diff --git a/editor/CMakeLists.txt b/editor/CMakeLists.txt index 592d5ad..b673e4a 100644 --- a/editor/CMakeLists.txt +++ b/editor/CMakeLists.txt @@ -3037,4 +3037,13 @@ target_link_libraries(step461_test PRIVATE tree_sitter_javascript tree_sitter_typescript tree_sitter_java tree_sitter_rust tree_sitter_go) +add_executable(step462_test tests/step462_test.cpp) +target_include_directories(step462_test PRIVATE src) +target_link_libraries(step462_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/ast/ArmAssemblyParser.h b/editor/src/ast/ArmAssemblyParser.h new file mode 100644 index 0000000..eb8a979 --- /dev/null +++ b/editor/src/ast/ArmAssemblyParser.h @@ -0,0 +1,245 @@ +#pragma once + +// Step 462: ARM Assembly Parser +// Supports ARM/AArch64 core instruction/directive/label forms. + +#include "AssemblyNodes.h" +#include "EnumNamespaceNodes.h" +#include "Function.h" +#include "Module.h" +#include "Parser.h" + +#include +#include +#include +#include +#include +#include + +class ArmAssemblyParser { +public: + static std::unique_ptr parseArm(const std::string& source) { + auto result = parseArmWithDiagnostics(source); + return std::move(result.module); + } + + static ParseResult parseArmWithDiagnostics(const std::string& source) { + ParseResult out; + auto module = std::make_unique(); + module->id = IdGenerator::next("mod"); + module->name = "parsed_arm_module"; + module->targetLanguage = "arm_asm"; + + std::set globalLabels; + NamespaceDeclaration* currentSection = nullptr; + Function* currentFunction = nullptr; + + std::istringstream in(source); + std::string raw; + int lineNo = 0; + while (std::getline(in, raw)) { + ++lineNo; + std::string line = trim(stripComment(raw)); + if (line.empty()) continue; + + if (isDirective(line)) { + auto dir = parseDirective(line); + if (dir.type == AssemblyDirectiveType::Global) { + std::string g = trim(dir.value); + if (!g.empty()) globalLabels.insert(stripSigils(g)); + } + if (dir.type == AssemblyDirectiveType::Text || + dir.type == AssemblyDirectiveType::Data || + dir.type == AssemblyDirectiveType::Section) { + std::string sectionName = dir.type == AssemblyDirectiveType::Section + ? trim(dir.value) : directiveTypeToString(dir.type); + currentSection = new NamespaceDeclaration(IdGenerator::next("ns"), sectionName); + module->addChild("statements", currentSection); + currentFunction = nullptr; + } + auto* d = new AssemblyDirective(dir.type, dir.value); + if (currentSection) currentSection->addChild("body", d); + else module->addChild("statements", d); + continue; + } + + if (isLabel(line)) { + std::string name = parseLabelName(line); + bool isGlobal = globalLabels.count(name) > 0; + auto* label = new AssemblyLabel(name, isGlobal); + currentFunction = new Function(IdGenerator::next("fn"), name); + currentFunction->addChild("body", label); + module->addChild("functions", currentFunction); + continue; + } + + auto inst = parseInstruction(line); + if (inst.opcode.empty()) { + out.diagnostics.push_back({lineNo, 1, "Unrecognized assembly line: " + line, "warning"}); + continue; + } + if (!isKnownOpcode(inst.opcode)) { + out.diagnostics.push_back({lineNo, 1, "Unknown opcode: " + inst.opcode, "warning"}); + } + auto* node = new AssemblyInstruction(inst.opcode, inst.operands); + node->registers = std::move(inst.registers); + node->memoryOperands = std::move(inst.memoryOperands); + if (!currentFunction) { + currentFunction = new Function(IdGenerator::next("fn"), "_entry"); + module->addChild("functions", currentFunction); + } + currentFunction->addChild("body", node); + } + + out.module = std::move(module); + return out; + } + +private: + static std::string trim(const std::string& s) { + size_t start = s.find_first_not_of(" \t\r\n"); + if (start == std::string::npos) return ""; + size_t end = s.find_last_not_of(" \t\r\n"); + return s.substr(start, end - start + 1); + } + + static std::string stripComment(const std::string& line) { + size_t at = line.find('@'); + size_t sem = line.find(';'); + size_t slash = line.find("//"); + size_t cut = std::string::npos; + if (at != std::string::npos) cut = at; + if (sem != std::string::npos) cut = cut == std::string::npos ? sem : std::min(cut, sem); + if (slash != std::string::npos) cut = cut == std::string::npos ? slash : std::min(cut, slash); + return cut == std::string::npos ? line : line.substr(0, cut); + } + + static bool isDirective(const std::string& line) { + return !line.empty() && line[0] == '.'; + } + + static bool isLabel(const std::string& line) { + return !line.empty() && line.back() == ':'; + } + + static std::string stripSigils(const std::string& token) { + std::string out = token; + while (!out.empty() && (out[0] == '#' || out[0] == '%')) out.erase(out.begin()); + return out; + } + + static std::string parseLabelName(const std::string& line) { + std::string n = line.substr(0, line.size() - 1); + return stripSigils(trim(n)); + } + + static AssemblyDirective parseDirective(const std::string& line) { + size_t sp = line.find_first_of(" \t"); + std::string head = sp == std::string::npos ? line : line.substr(0, sp); + std::string tail = sp == std::string::npos ? "" : trim(line.substr(sp + 1)); + return AssemblyDirective(directiveTypeFromString(head), tail); + } + + static std::vector splitOperands(const std::string& s) { + std::vector out; + std::string cur; + int bracketDepth = 0; + for (char c : s) { + if (c == '[') ++bracketDepth; + if (c == ']') --bracketDepth; + if (c == ',' && bracketDepth == 0) { + out.push_back(trim(cur)); + cur.clear(); + } else { + cur.push_back(c); + } + } + if (!trim(cur).empty()) out.push_back(trim(cur)); + return out; + } + + static bool isRegisterName(const std::string& raw) { + std::string r = toLower(stripSigils(raw)); + if (r == "sp" || r == "lr" || r == "pc") return true; + if (r.size() >= 2 && (r[0] == 'r' || r[0] == 'x')) { + for (size_t i = 1; i < r.size(); ++i) { + if (!std::isdigit(static_cast(r[i]))) return false; + } + int v = std::stoi(r.substr(1)); + if (r[0] == 'r') return v >= 0 && v <= 15; + return v >= 0 && v <= 30; + } + return false; + } + + static int registerSizeBits(const std::string& raw) { + std::string r = toLower(stripSigils(raw)); + if (r == "sp" || r == "lr" || r == "pc") return 64; + if (!r.empty() && r[0] == 'x') return 64; + return 32; + } + + static int parseInt(const std::string& raw) { + std::string t = trim(stripSigils(raw)); + if (t.empty()) return 0; + try { + size_t idx = 0; + int base = 10; + if (t.size() > 2 && t[0] == '0' && (t[1] == 'x' || t[1] == 'X')) base = 16; + return std::stoi(t, &idx, base); + } catch (...) { + return 0; + } + } + + static bool parseMemoryOperand(const std::string& op, AssemblyMemoryOperand& out) { + if (op.size() < 2 || op.front() != '[' || op.back() != ']') return false; + std::string inner = trim(op.substr(1, op.size() - 2)); // r0, #4 OR r0, r1, LSL #2 + std::vector parts = splitOperands(inner); + if (parts.empty()) return false; + out.baseRegister = stripSigils(trim(parts[0])); + if (parts.size() >= 2) { + std::string p1 = trim(parts[1]); + if (isRegisterName(p1)) out.indexRegister = stripSigils(p1); + else out.offset = parseInt(p1); + } + if (parts.size() >= 3) { + std::string p2 = toLower(trim(parts[2])); + std::regex lslRx(R"(lsl\s*#?(\d+))"); + std::smatch m; + if (std::regex_search(p2, m, lslRx)) out.scale = parseInt(m[1].str()); + } + return true; + } + + static AssemblyInstruction parseInstruction(const std::string& line) { + size_t sp = line.find_first_of(" \t"); + std::string op = toLower(trim(sp == std::string::npos ? line : line.substr(0, sp))); + std::string ops = sp == std::string::npos ? "" : trim(line.substr(sp + 1)); + AssemblyInstruction inst(op, splitOperands(ops)); + + for (const auto& operand : inst.operands) { + if (isRegisterName(operand)) { + inst.registers.emplace_back(stripSigils(operand), registerSizeBits(operand)); + continue; + } + AssemblyMemoryOperand mem; + if (parseMemoryOperand(operand, mem)) inst.memoryOperands.push_back(mem); + } + return inst; + } + + static std::string toLower(std::string s) { + std::transform(s.begin(), s.end(), s.begin(), + [](unsigned char c){ return static_cast(std::tolower(c)); }); + return s; + } + + static bool isKnownOpcode(const std::string& op) { + static const std::set known = { + "mov", "add", "sub", "ldr", "str", "b", "bl", "cmp", + "beq", "bne", "bgt", "blt", "bge", "ble", "ret" + }; + return known.count(op) > 0; + } +}; diff --git a/editor/tests/step462_test.cpp b/editor/tests/step462_test.cpp new file mode 100644 index 0000000..a579436 --- /dev/null +++ b/editor/tests/step462_test.cpp @@ -0,0 +1,173 @@ +// Step 462: ARM Assembly Parser Tests (12 tests) + +#include "ast/ArmAssemblyParser.h" +#include "ast/AssemblyNodes.h" +#include "ast/EnumNamespaceNodes.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_parse_arm_basic_instructions() { + TEST(parse_arm_basic_instructions); + std::string src = ".text\nmain:\n MOV r0, r1\n ADD r0, r0, #1\n RET\n"; + auto mod = ArmAssemblyParser::parseArm(src); + CHECK(mod->getChildren("functions").size() == 1, "expected one function"); + auto* fn = static_cast(mod->getChildren("functions")[0]); + CHECK(fn->name == "main", "expected main function"); + CHECK(fn->getChildren("body").size() >= 3, "expected label + instructions"); + PASS(); +} + +void test_parse_aarch64_registers() { + TEST(parse_aarch64_registers); + std::string src = ".text\nmain:\n MOV x0, x1\n RET\n"; + auto mod = ArmAssemblyParser::parseArm(src); + auto* fn = static_cast(mod->getChildren("functions")[0]); + auto* i = static_cast(fn->getChildren("body")[1]); + CHECK(i->registers.size() >= 2, "expected register extraction"); + CHECK(i->registers[0].sizeBits == 64, "expected 64-bit x register"); + PASS(); +} + +void test_parse_arm32_registers() { + TEST(parse_arm32_registers); + std::string src = ".text\nmain:\n MOV r0, r1\n RET\n"; + auto mod = ArmAssemblyParser::parseArm(src); + auto* fn = static_cast(mod->getChildren("functions")[0]); + auto* i = static_cast(fn->getChildren("body")[1]); + CHECK(i->registers.size() >= 2, "expected register extraction"); + CHECK(i->registers[0].sizeBits == 32, "expected 32-bit r register"); + PASS(); +} + +void test_parse_special_registers_sp_lr_pc() { + TEST(parse_special_registers_sp_lr_pc); + std::string src = ".text\nmain:\n MOV sp, lr\n MOV x0, pc\n RET\n"; + auto mod = ArmAssemblyParser::parseArm(src); + auto* fn = static_cast(mod->getChildren("functions")[0]); + int special = 0; + for (auto* n : fn->getChildren("body")) { + if (n->conceptType != "AssemblyInstruction") continue; + auto* i = static_cast(n); + for (const auto& r : i->registers) { + if (r.name == "sp" || r.name == "lr" || r.name == "pc") ++special; + } + } + CHECK(special >= 2, "expected special register recognition"); + PASS(); +} + +void test_parse_memory_mode_base_only() { + TEST(parse_memory_mode_base_only); + std::string src = ".text\nmain:\n LDR r0, [r1]\n RET\n"; + auto mod = ArmAssemblyParser::parseArm(src); + auto* fn = static_cast(mod->getChildren("functions")[0]); + auto* i = static_cast(fn->getChildren("body")[1]); + CHECK(i->memoryOperands.size() == 1, "expected memory operand"); + CHECK(i->memoryOperands[0].baseRegister == "r1", "expected base r1"); + PASS(); +} + +void test_parse_memory_mode_base_plus_offset() { + TEST(parse_memory_mode_base_plus_offset); + std::string src = ".text\nmain:\n LDR r0, [r1, #4]\n RET\n"; + auto mod = ArmAssemblyParser::parseArm(src); + auto* fn = static_cast(mod->getChildren("functions")[0]); + auto* i = static_cast(fn->getChildren("body")[1]); + CHECK(i->memoryOperands.size() == 1, "expected memory operand"); + CHECK(i->memoryOperands[0].offset == 4, "expected offset 4"); + PASS(); +} + +void test_parse_memory_mode_index_shifted() { + TEST(parse_memory_mode_index_shifted); + std::string src = ".text\nmain:\n STR x0, [x1, x2, LSL #2]\n RET\n"; + auto mod = ArmAssemblyParser::parseArm(src); + auto* fn = static_cast(mod->getChildren("functions")[0]); + auto* i = static_cast(fn->getChildren("body")[1]); + CHECK(i->memoryOperands.size() == 1, "expected memory operand"); + CHECK(i->memoryOperands[0].indexRegister == "x2", "expected index x2"); + CHECK(i->memoryOperands[0].scale == 2, "expected scale 2"); + PASS(); +} + +void test_parse_directives_global_text_data_word_align() { + TEST(parse_directives_global_text_data_word_align); + std::string src = ".global main\n.text\nmain:\n RET\n.data\nval: .word 1\n.align 4\n"; + auto mod = ArmAssemblyParser::parseArm(src); + int directives = 0; + for (auto* s : mod->getChildren("statements")) { + if (s->conceptType == "AssemblyDirective") ++directives; + if (s->conceptType == "NamespaceDeclaration") { + auto* ns = static_cast(s); + directives += static_cast(ns->getChildren("body").size()); + } + } + CHECK(directives >= 4, "expected directive parsing"); + PASS(); +} + +void test_parse_branch_and_call_opcodes() { + TEST(parse_branch_and_call_opcodes); + std::string src = ".text\nmain:\n B loop\n BL helper\n CMP r0, r1\nloop:\n RET\nhelper:\n RET\n"; + auto res = ArmAssemblyParser::parseArmWithDiagnostics(src); + CHECK(res.diagnostics.empty(), "expected known opcodes only"); + PASS(); +} + +void test_labels_and_global_flags() { + TEST(labels_and_global_flags); + std::string src = ".global main\n.text\nmain:\n RET\n"; + auto mod = ArmAssemblyParser::parseArm(src); + auto* fn = static_cast(mod->getChildren("functions")[0]); + auto* lbl = static_cast(fn->getChildren("body")[0]); + CHECK(lbl->isGlobal, "expected global label"); + PASS(); +} + +void test_section_namespaces_created() { + TEST(section_namespaces_created); + std::string src = ".text\nmain:\n RET\n.data\nx: .word 1\n"; + auto mod = ArmAssemblyParser::parseArm(src); + int nsCount = 0; + for (auto* s : mod->getChildren("statements")) { + if (s->conceptType == "NamespaceDeclaration") ++nsCount; + } + CHECK(nsCount >= 2, "expected .text and .data namespaces"); + PASS(); +} + +void test_unknown_opcode_warning_emitted() { + TEST(unknown_opcode_warning_emitted); + std::string src = ".text\nmain:\n FROB r0, r1\n RET\n"; + auto res = ArmAssemblyParser::parseArmWithDiagnostics(src); + CHECK(!res.diagnostics.empty(), "expected warning diagnostics"); + CHECK(res.diagnostics[0].severity == "warning", "expected warning severity"); + PASS(); +} + +int main() { + std::cout << "Step 462: ARM Assembly Parser Tests\n"; + + test_parse_arm_basic_instructions(); // 1 + test_parse_aarch64_registers(); // 2 + test_parse_arm32_registers(); // 3 + test_parse_special_registers_sp_lr_pc(); // 4 + test_parse_memory_mode_base_only(); // 5 + test_parse_memory_mode_base_plus_offset(); // 6 + test_parse_memory_mode_index_shifted(); // 7 + test_parse_directives_global_text_data_word_align(); // 8 + test_parse_branch_and_call_opcodes(); // 9 + test_labels_and_global_flags(); // 10 + test_section_namespaces_created(); // 11 + test_unknown_opcode_warning_emitted(); // 12 + + std::cout << "\nResults: " << passed << "/" << (passed + failed) + << " passed\n"; + return failed == 0 ? 0 : 1; +} diff --git a/progress.md b/progress.md index a7d34f4..eab75b0 100644 --- a/progress.md +++ b/progress.md @@ -6102,3 +6102,39 @@ addressing extraction. **Architecture gate check:** - `editor/src/ast/X86AssemblyParser.h` within header-size limit (`299` <= `600`) - `editor/tests/step461_test.cpp` within test-file size guidance (`186` lines) + +### Step 462: ARM Assembly Parser +**Status:** PASS (12/12 tests) + +Implements ARM/AArch64 assembly parsing for core instructions, registers, +addressing modes, directives, and label/function shaping. + +**Files added:** +- `editor/src/ast/ArmAssemblyParser.h` — ARM parser: + - parses ARM/AArch64 instructions (`mov`, `add`, `sub`, `ldr`, `str`, `b`, + `bl`, `cmp`, conditional branches, `ret`) + - register recognition for `r0-r15`, `x0-x30`, `sp`, `lr`, `pc` + - ARM memory addressing extraction: + - `[r0]` + - `[r0, #4]` + - `[r0, r1, LSL #2]` + - directive handling (`.global`, `.text`, `.data`, `.word`, `.align`, `.section`) + - section-as-namespace shaping via `NamespaceDeclaration` + - warning diagnostics for unknown opcodes +- `editor/tests/step462_test.cpp` — 12 tests covering: + - ARM and AArch64 instruction/register parsing + - special register handling + - all required addressing forms + - directive and section handling + - branch/call opcode coverage + - unknown-opcode warning behavior +- `editor/CMakeLists.txt` — `step462_test` target + +**Verification run:** +- `cmake --build editor/build-native --target step462_test` — PASS +- `./editor/build-native/step462_test` — PASS (12/12) +- `./editor/build-native/step461_test` — PASS (12/12) regression coverage + +**Architecture gate check:** +- `editor/src/ast/ArmAssemblyParser.h` within header-size limit (`245` <= `600`) +- `editor/tests/step462_test.cpp` within test-file size guidance (`173` lines)