Complete step463 assembly generators and x86-to-arm mapping tests

This commit is contained in:
Bill
2026-02-16 20:14:03 -07:00
parent 068bd71a31
commit c3d8e1cd0c
4 changed files with 355 additions and 0 deletions

View File

@@ -3046,4 +3046,13 @@ target_link_libraries(step462_test PRIVATE
tree_sitter_javascript tree_sitter_typescript
tree_sitter_java tree_sitter_rust tree_sitter_go)
add_executable(step463_test tests/step463_test.cpp)
target_include_directories(step463_test PRIVATE src)
target_link_libraries(step463_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,167 @@
#pragma once
// Step 463: Assembly Generator (x86 + ARM)
// Generates assembly text from Assembly AST nodes and provides a simple
// cross-architecture mapper for basic instruction sequences.
#include "AssemblyNodes.h"
#include "EnumNamespaceNodes.h"
#include "Function.h"
#include "Module.h"
#include <algorithm>
#include <sstream>
#include <string>
#include <vector>
enum class X86OutputSyntax {
Intel,
ATT
};
class AssemblyGenerator {
public:
static std::string commentPrefix(const std::string& arch) {
if (arch == "x86") return "; ";
if (arch == "arm") return "@ ";
return "; ";
}
static std::string generateX86(const Module& module,
X86OutputSyntax syntax = X86OutputSyntax::Intel) {
std::ostringstream out;
emitModuleDirectives(module, out);
for (auto* fnNode : module.getChildren("functions")) {
if (fnNode->conceptType != "Function") continue;
auto* fn = static_cast<Function*>(fnNode);
emitFunctionX86(*fn, out, syntax);
}
return out.str();
}
static std::string generateArm(const Module& module) {
std::ostringstream out;
emitModuleDirectives(module, out);
for (auto* fnNode : module.getChildren("functions")) {
if (fnNode->conceptType != "Function") continue;
auto* fn = static_cast<Function*>(fnNode);
emitFunctionArm(*fn, out);
}
return out.str();
}
static std::vector<AssemblyInstruction> translateX86ToArmSimple(
const std::vector<AssemblyInstruction>& x86Instructions) {
std::vector<AssemblyInstruction> out;
for (const auto& i : x86Instructions) {
std::string op = toLower(i.opcode);
AssemblyInstruction mapped;
mapped.operands = i.operands;
if (op == "mov" || op == "movq" || op == "movl") mapped.opcode = "mov";
else if (op == "add" || op == "addq" || op == "addl") mapped.opcode = "add";
else if (op == "sub" || op == "subq" || op == "subl") mapped.opcode = "sub";
else if (op == "cmp" || op == "cmpq" || op == "cmpl") mapped.opcode = "cmp";
else if (op == "jmp") mapped.opcode = "b";
else if (op == "call") mapped.opcode = "bl";
else if (op == "ret") mapped.opcode = "ret";
else mapped.opcode = op;
out.push_back(std::move(mapped));
}
return out;
}
private:
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 std::string emitDirective(const AssemblyDirective& d) {
if (d.value.empty()) return directiveTypeToString(d.type);
return directiveTypeToString(d.type) + " " + d.value;
}
static std::string formatOperandX86(const std::string& operand, X86OutputSyntax syntax) {
if (syntax == X86OutputSyntax::Intel) return operand;
std::string o = operand;
// naive register sigil conversion for AT&T
if (!o.empty() && o[0] != '[' && o[0] != '%' && std::isalpha(static_cast<unsigned char>(o[0]))) {
bool maybeReg = true;
for (char c : o) {
if (!(std::isalnum(static_cast<unsigned char>(c)) || c == '_')) {
maybeReg = false;
break;
}
}
if (maybeReg) o = "%" + o;
}
if (!o.empty() && std::isdigit(static_cast<unsigned char>(o[0]))) {
o = "$" + o;
}
return o;
}
static void emitFunctionX86(const Function& fn, std::ostringstream& out, X86OutputSyntax syntax) {
out << fn.name << ":\n";
for (auto* n : fn.getChildren("body")) {
if (n->conceptType == "AssemblyLabel") continue; // function name already emitted
if (n->conceptType == "AssemblyInstruction") {
auto* i = static_cast<AssemblyInstruction*>(n);
out << " " << i->opcode;
if (!i->operands.empty()) {
out << " ";
for (size_t k = 0; k < i->operands.size(); ++k) {
out << formatOperandX86(i->operands[k], syntax);
if (k + 1 < i->operands.size()) out << ", ";
}
}
out << "\n";
} else if (n->conceptType == "AssemblyDirective") {
auto* d = static_cast<AssemblyDirective*>(n);
out << " " << emitDirective(*d) << "\n";
}
}
}
static void emitFunctionArm(const Function& fn, std::ostringstream& out) {
out << fn.name << ":\n";
for (auto* n : fn.getChildren("body")) {
if (n->conceptType == "AssemblyLabel") continue;
if (n->conceptType == "AssemblyInstruction") {
auto* i = static_cast<AssemblyInstruction*>(n);
out << " " << toLower(i->opcode);
if (!i->operands.empty()) {
out << " ";
for (size_t k = 0; k < i->operands.size(); ++k) {
out << i->operands[k];
if (k + 1 < i->operands.size()) out << ", ";
}
}
out << "\n";
} else if (n->conceptType == "AssemblyDirective") {
auto* d = static_cast<AssemblyDirective*>(n);
out << " " << emitDirective(*d) << "\n";
}
}
}
static void emitModuleDirectives(const Module& module, std::ostringstream& out) {
for (auto* s : module.getChildren("statements")) {
if (s->conceptType == "AssemblyDirective") {
auto* d = static_cast<AssemblyDirective*>(s);
out << emitDirective(*d) << "\n";
} else if (s->conceptType == "NamespaceDeclaration") {
auto* ns = static_cast<NamespaceDeclaration*>(s);
if (!ns->name.empty()) out << ns->name << "\n";
for (auto* b : ns->getChildren("body")) {
if (b->conceptType == "AssemblyDirective") {
auto* d = static_cast<AssemblyDirective*>(b);
out << emitDirective(*d) << "\n";
}
}
}
}
}
};

View File

@@ -0,0 +1,147 @@
// Step 463: Assembly Generator Tests (12 tests)
#include "ast/ArmAssemblyParser.h"
#include "ast/AssemblyGenerator.h"
#include "ast/X86AssemblyParser.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_generate_x86_intel_contains_expected_instructions() {
TEST(generate_x86_intel_contains_expected_instructions);
auto mod = X86AssemblyParser::parseX86(".text\nmain:\n mov rax, rbx\n ret\n");
auto out = AssemblyGenerator::generateX86(*mod, X86OutputSyntax::Intel);
CHECK(out.find("main:") != std::string::npos, "missing label");
CHECK(out.find("mov rax, rbx") != std::string::npos, "missing intel mov");
PASS();
}
void test_generate_x86_att_adds_register_sigils() {
TEST(generate_x86_att_adds_register_sigils);
auto mod = X86AssemblyParser::parseX86(".text\nmain:\n mov rax, rbx\n ret\n");
auto out = AssemblyGenerator::generateX86(*mod, X86OutputSyntax::ATT);
CHECK(out.find("mov %rax, %rbx") != std::string::npos, "missing AT&T register sigils");
PASS();
}
void test_generate_x86_preserves_directives() {
TEST(generate_x86_preserves_directives);
auto mod = X86AssemblyParser::parseX86(".global main\n.text\nmain:\n ret\n");
auto out = AssemblyGenerator::generateX86(*mod);
CHECK(out.find(".global main") != std::string::npos, "missing .global");
CHECK(out.find(".text") != std::string::npos, "missing .text");
PASS();
}
void test_generate_arm_contains_lowercase_opcodes() {
TEST(generate_arm_contains_lowercase_opcodes);
auto mod = ArmAssemblyParser::parseArm(".text\nmain:\n MOV r0, r1\n RET\n");
auto out = AssemblyGenerator::generateArm(*mod);
CHECK(out.find("mov r0, r1") != std::string::npos, "missing arm mov");
CHECK(out.find("ret") != std::string::npos, "missing arm ret");
PASS();
}
void test_generate_arm_preserves_sections() {
TEST(generate_arm_preserves_sections);
auto mod = ArmAssemblyParser::parseArm(".text\nmain:\n RET\n.data\nv: .word 1\n");
auto out = AssemblyGenerator::generateArm(*mod);
CHECK(out.find(".text") != std::string::npos, "missing .text");
CHECK(out.find(".data") != std::string::npos, "missing .data");
PASS();
}
void test_translate_x86_to_arm_maps_mov_add_sub_cmp() {
TEST(translate_x86_to_arm_maps_mov_add_sub_cmp);
std::vector<AssemblyInstruction> x86 = {
AssemblyInstruction("mov", {"rax", "rbx"}),
AssemblyInstruction("add", {"rax", "1"}),
AssemblyInstruction("sub", {"rax", "2"}),
AssemblyInstruction("cmp", {"rax", "rbx"})
};
auto arm = AssemblyGenerator::translateX86ToArmSimple(x86);
CHECK(arm.size() == 4, "wrong mapped size");
CHECK(arm[0].opcode == "mov", "mov mapping failed");
CHECK(arm[1].opcode == "add", "add mapping failed");
CHECK(arm[2].opcode == "sub", "sub mapping failed");
CHECK(arm[3].opcode == "cmp", "cmp mapping failed");
PASS();
}
void test_translate_x86_to_arm_maps_jmp_and_call() {
TEST(translate_x86_to_arm_maps_jmp_and_call);
std::vector<AssemblyInstruction> x86 = {
AssemblyInstruction("jmp", {"label"}),
AssemblyInstruction("call", {"helper"})
};
auto arm = AssemblyGenerator::translateX86ToArmSimple(x86);
CHECK(arm[0].opcode == "b", "jmp should map to b");
CHECK(arm[1].opcode == "bl", "call should map to bl");
PASS();
}
void test_translate_x86_to_arm_maps_ret() {
TEST(translate_x86_to_arm_maps_ret);
std::vector<AssemblyInstruction> x86 = { AssemblyInstruction("ret", {}) };
auto arm = AssemblyGenerator::translateX86ToArmSimple(x86);
CHECK(arm[0].opcode == "ret", "ret mapping failed");
PASS();
}
void test_translate_unknown_opcode_passthrough() {
TEST(translate_unknown_opcode_passthrough);
std::vector<AssemblyInstruction> x86 = { AssemblyInstruction("nop", {}) };
auto arm = AssemblyGenerator::translateX86ToArmSimple(x86);
CHECK(arm[0].opcode == "nop", "unknown opcode should pass through");
PASS();
}
void test_comment_prefix_x86() {
TEST(comment_prefix_x86);
CHECK(AssemblyGenerator::commentPrefix("x86") == "; ", "wrong x86 comment prefix");
PASS();
}
void test_comment_prefix_arm() {
TEST(comment_prefix_arm);
CHECK(AssemblyGenerator::commentPrefix("arm") == "@ ", "wrong arm comment prefix");
PASS();
}
void test_roundtrip_parse_generate_parse_x86() {
TEST(roundtrip_parse_generate_parse_x86);
std::string src = ".text\nmain:\n mov rax, rbx\n add rax, 1\n ret\n";
auto mod1 = X86AssemblyParser::parseX86(src);
auto gen = AssemblyGenerator::generateX86(*mod1);
auto mod2 = X86AssemblyParser::parseX86(gen);
CHECK(mod2->getChildren("functions").size() == 1, "roundtrip function count mismatch");
auto* fn = static_cast<Function*>(mod2->getChildren("functions")[0]);
CHECK(fn->getChildren("body").size() >= 4, "roundtrip body too small");
PASS();
}
int main() {
std::cout << "Step 463: Assembly Generator Tests\n";
test_generate_x86_intel_contains_expected_instructions(); // 1
test_generate_x86_att_adds_register_sigils(); // 2
test_generate_x86_preserves_directives(); // 3
test_generate_arm_contains_lowercase_opcodes(); // 4
test_generate_arm_preserves_sections(); // 5
test_translate_x86_to_arm_maps_mov_add_sub_cmp(); // 6
test_translate_x86_to_arm_maps_jmp_and_call(); // 7
test_translate_x86_to_arm_maps_ret(); // 8
test_translate_unknown_opcode_passthrough(); // 9
test_comment_prefix_x86(); // 10
test_comment_prefix_arm(); // 11
test_roundtrip_parse_generate_parse_x86(); // 12
std::cout << "\nResults: " << passed << "/" << (passed + failed)
<< " passed\n";
return failed == 0 ? 0 : 1;
}

View File

@@ -6138,3 +6138,35 @@ addressing modes, directives, and label/function shaping.
**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)
### Step 463: Assembly Generator — x86 + ARM
**Status:** PASS (12/12 tests)
Adds assembly code generation for x86 and ARM from AST, plus a simple
cross-architecture instruction mapper for straightforward x86→ARM projection.
**Files added:**
- `editor/src/ast/AssemblyGenerator.h` — generator and mapper:
- `generateX86` with Intel/AT&T syntax mode support
- `generateArm` output path
- `translateX86ToArmSimple` mapping for core opcodes (`mov/add/sub/cmp/jmp/call/ret`)
- architecture comment prefixes (`x86 -> "; "`, `arm -> "@ "`)
- directive/section emission from module and namespace-like section nodes
- `editor/tests/step463_test.cpp` — 12 tests covering:
- x86 Intel/AT&T generation behavior
- ARM generation behavior
- directive/section emission
- x86→ARM opcode mapping including control flow
- unknown-opcode passthrough
- comment prefix semantics
- parse→generate→parse roundtrip sanity check for x86
- `editor/CMakeLists.txt``step463_test` target
**Verification run:**
- `cmake --build editor/build-native --target step463_test` — PASS
- `./editor/build-native/step463_test` — PASS (12/12)
- `./editor/build-native/step462_test` — PASS (12/12) regression coverage
**Architecture gate check:**
- `editor/src/ast/AssemblyGenerator.h` within header-size limit (`167` <= `600`)
- `editor/tests/step463_test.cpp` within test-file size guidance (`147` lines)