Complete step462 ARM assembly parser with tests
This commit is contained in:
@@ -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)
|
||||
|
||||
245
editor/src/ast/ArmAssemblyParser.h
Normal file
245
editor/src/ast/ArmAssemblyParser.h
Normal file
@@ -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 <algorithm>
|
||||
#include <cctype>
|
||||
#include <regex>
|
||||
#include <set>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
|
||||
class ArmAssemblyParser {
|
||||
public:
|
||||
static std::unique_ptr<Module> 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>();
|
||||
module->id = IdGenerator::next("mod");
|
||||
module->name = "parsed_arm_module";
|
||||
module->targetLanguage = "arm_asm";
|
||||
|
||||
std::set<std::string> 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<std::string> splitOperands(const std::string& s) {
|
||||
std::vector<std::string> 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<unsigned char>(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<std::string> 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<char>(std::tolower(c)); });
|
||||
return s;
|
||||
}
|
||||
|
||||
static bool isKnownOpcode(const std::string& op) {
|
||||
static const std::set<std::string> known = {
|
||||
"mov", "add", "sub", "ldr", "str", "b", "bl", "cmp",
|
||||
"beq", "bne", "bgt", "blt", "bge", "ble", "ret"
|
||||
};
|
||||
return known.count(op) > 0;
|
||||
}
|
||||
};
|
||||
173
editor/tests/step462_test.cpp
Normal file
173
editor/tests/step462_test.cpp
Normal file
@@ -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 <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_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<Function*>(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<Function*>(mod->getChildren("functions")[0]);
|
||||
auto* i = static_cast<AssemblyInstruction*>(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<Function*>(mod->getChildren("functions")[0]);
|
||||
auto* i = static_cast<AssemblyInstruction*>(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<Function*>(mod->getChildren("functions")[0]);
|
||||
int special = 0;
|
||||
for (auto* n : fn->getChildren("body")) {
|
||||
if (n->conceptType != "AssemblyInstruction") continue;
|
||||
auto* i = static_cast<AssemblyInstruction*>(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<Function*>(mod->getChildren("functions")[0]);
|
||||
auto* i = static_cast<AssemblyInstruction*>(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<Function*>(mod->getChildren("functions")[0]);
|
||||
auto* i = static_cast<AssemblyInstruction*>(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<Function*>(mod->getChildren("functions")[0]);
|
||||
auto* i = static_cast<AssemblyInstruction*>(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<NamespaceDeclaration*>(s);
|
||||
directives += static_cast<int>(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<Function*>(mod->getChildren("functions")[0]);
|
||||
auto* lbl = static_cast<AssemblyLabel*>(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;
|
||||
}
|
||||
36
progress.md
36
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)
|
||||
|
||||
Reference in New Issue
Block a user