diff --git a/editor/CMakeLists.txt b/editor/CMakeLists.txt index b673e4a..c50f5cf 100644 --- a/editor/CMakeLists.txt +++ b/editor/CMakeLists.txt @@ -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) diff --git a/editor/src/ast/AssemblyGenerator.h b/editor/src/ast/AssemblyGenerator.h new file mode 100644 index 0000000..c864049 --- /dev/null +++ b/editor/src/ast/AssemblyGenerator.h @@ -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 +#include +#include +#include + +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(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(fnNode); + emitFunctionArm(*fn, out); + } + return out.str(); + } + + static std::vector translateX86ToArmSimple( + const std::vector& x86Instructions) { + std::vector 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(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(o[0]))) { + bool maybeReg = true; + for (char c : o) { + if (!(std::isalnum(static_cast(c)) || c == '_')) { + maybeReg = false; + break; + } + } + if (maybeReg) o = "%" + o; + } + if (!o.empty() && std::isdigit(static_cast(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(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(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(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(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(s); + out << emitDirective(*d) << "\n"; + } else if (s->conceptType == "NamespaceDeclaration") { + auto* ns = static_cast(s); + if (!ns->name.empty()) out << ns->name << "\n"; + for (auto* b : ns->getChildren("body")) { + if (b->conceptType == "AssemblyDirective") { + auto* d = static_cast(b); + out << emitDirective(*d) << "\n"; + } + } + } + } + } +}; diff --git a/editor/tests/step463_test.cpp b/editor/tests/step463_test.cpp new file mode 100644 index 0000000..a082d5b --- /dev/null +++ b/editor/tests/step463_test.cpp @@ -0,0 +1,147 @@ +// Step 463: Assembly Generator Tests (12 tests) + +#include "ast/ArmAssemblyParser.h" +#include "ast/AssemblyGenerator.h" +#include "ast/X86AssemblyParser.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_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 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 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 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 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(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; +} diff --git a/progress.md b/progress.md index eab75b0..d48c896 100644 --- a/progress.md +++ b/progress.md @@ -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)