diff --git a/editor/CMakeLists.txt b/editor/CMakeLists.txt index dec06cb..fc9da61 100644 --- a/editor/CMakeLists.txt +++ b/editor/CMakeLists.txt @@ -2138,4 +2138,13 @@ add_executable(step360_test tests/step360_test.cpp) target_include_directories(step360_test PRIVATE src) target_link_libraries(step360_test PRIVATE nlohmann_json::nlohmann_json) +add_executable(step361_test tests/step361_test.cpp) +target_include_directories(step361_test PRIVATE src) +target_link_libraries(step361_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/Pipeline.h b/editor/src/Pipeline.h index d0758f4..667aa8f 100644 --- a/editor/src/Pipeline.h +++ b/editor/src/Pipeline.h @@ -118,6 +118,10 @@ public: auto pr = CSharpParser::parseCSharpWithDiagnostics(source); diags = std::move(pr.diagnostics); return std::move(pr.module); + } else if (language == "c") { + auto pr = CParser::parseCWithDiagnostics(source); + diags = std::move(pr.diagnostics); + return std::move(pr.module); } return nullptr; } diff --git a/editor/src/ast/CParser.h b/editor/src/ast/CParser.h new file mode 100644 index 0000000..a660f04 --- /dev/null +++ b/editor/src/ast/CParser.h @@ -0,0 +1,288 @@ +#pragma once +#include "ASTNode.h" +#include "Module.h" +#include "Function.h" +#include "Variable.h" +#include "Parameter.h" +#include "Type.h" +#include "ClassDeclaration.h" +#include "PreprocessorNodes.h" +#include "EnumNamespaceNodes.h" +#include "../ast/Parser.h" +#include +#include +#include +#include +#include +#include + +class CParser { +public: + static std::unique_ptr parseC(const std::string& source) { + auto module = std::make_unique(); + module->id = IdGenerator::next("mod"); + module->name = "parsed_c_module"; + module->targetLanguage = "c"; + + parsePreprocessor(source, module.get()); + std::string cleaned = stripComments(source); + parseTypedefStructs(cleaned, module.get()); + parseStructs(cleaned, module.get()); + parseEnums(cleaned, module.get()); + parseFunctions(cleaned, module.get()); + parseTopLevelVariables(cleaned, module.get()); + return module; + } + + static ParseResult parseCWithDiagnostics(const std::string& source) { + ParseResult result; + result.module = parseC(source); + return result; + } + +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 bool startsWith(const std::string& s, const std::string& prefix) { + return s.rfind(prefix, 0) == 0; + } + + static std::string stripComments(const std::string& source) { + std::string out; + out.reserve(source.size()); + bool line = false, block = false, str = false, chr = false; + for (size_t i = 0; i < source.size(); ++i) { + char c = source[i]; + char n = (i + 1 < source.size()) ? source[i + 1] : '\0'; + if (line) { + if (c == '\n') { line = false; out.push_back('\n'); } else out.push_back(' '); + continue; + } + if (block) { + if (c == '*' && n == '/') { block = false; out += " "; ++i; } + else out.push_back(c == '\n' ? '\n' : ' '); + continue; + } + if (!str && !chr && c == '/' && n == '/') { line = true; out += " "; ++i; continue; } + if (!str && !chr && c == '/' && n == '*') { block = true; out += " "; ++i; continue; } + if (c == '"' && !chr && !(i > 0 && source[i - 1] == '\\')) str = !str; + if (c == '\'' && !str && !(i > 0 && source[i - 1] == '\\')) chr = !chr; + out.push_back(c); + } + return out; + } + + static std::vector splitTopLevel(const std::string& input, char sep) { + std::vector parts; + std::string cur; + int depth = 0; + for (char c : input) { + if (c == '(') depth++; + if (c == ')') depth--; + if (c == sep && depth == 0) { parts.push_back(trim(cur)); cur.clear(); } + else cur.push_back(c); + } + if (!trim(cur).empty()) parts.push_back(trim(cur)); + return parts; + } + + static void setType(ASTNode* owner, const std::string& role, const std::string& raw) { + std::string t = trim(raw); + if (t.empty()) return; + static const std::vector p = { + "void","char","short","int","long","float","double","signed","unsigned", + "size_t","ssize_t","bool","_Bool","const","static","extern" + }; + bool primitive = false; + for (const auto& k : p) { + if (t == k || startsWith(t, k + " ") || startsWith(t, k + "*")) { primitive = true; break; } + } + if (primitive) owner->setChild(role, new PrimitiveType(IdGenerator::next("type"), t)); + else owner->setChild(role, new CustomType(IdGenerator::next("type"), t)); + } + + static void parsePreprocessor(const std::string& source, Module* module) { + std::istringstream in(source); + std::string line; + std::regex inc(R"(^\s*#\s*include\s*([<"])([^>"]+)[>"]\s*$)"); + std::regex defFn(R"(^\s*#\s*define\s+([A-Za-z_]\w*)\s*\(([^)]*)\)\s*(.*)$)"); + std::regex defObj(R"(^\s*#\s*define\s+([A-Za-z_]\w*)\s*(.*)$)"); + std::regex prag(R"(^\s*#\s*pragma\s+(.*)$)"); + std::smatch m; + while (std::getline(in, line)) { + std::string t = trim(line); + if (std::regex_match(line, m, inc)) { + auto* node = new IncludeDirective(IdGenerator::next("inc"), trim(m[2].str()), m[1].str() == "<"); + module->addChild("statements", node); + } else if (std::regex_match(line, m, defFn)) { + auto* node = new MacroDefinition(IdGenerator::next("mac"), m[1].str()); + node->isFunctionLike = true; + node->body = trim(m[3].str()); + for (const auto& part : splitTopLevel(m[2].str(), ',')) if (!part.empty()) node->parameters.push_back(part); + module->addChild("statements", node); + } else if (std::regex_match(line, m, defObj)) { + auto* node = new MacroDefinition(IdGenerator::next("mac"), m[1].str()); + node->isFunctionLike = false; + node->body = trim(m[2].str()); + module->addChild("statements", node); + } else if (std::regex_match(line, m, prag)) { + auto* node = new PragmaDirective(IdGenerator::next("prag"), trim(m[1].str())); + module->addChild("statements", node); + } else if (startsWith(t, "#ifndef") || startsWith(t, "#ifdef") || startsWith(t, "#endif")) { + auto* node = new PragmaDirective(IdGenerator::next("prag"), t.substr(1)); + module->addChild("statements", node); + } + } + } + + static void parseStructBody(const std::string& body, ClassDeclaration* cls) { + std::istringstream in(body); + std::string line; + std::regex field(R"((?:const\s+)?([A-Za-z_]\w*(?:\s+\w+)?(?:\s*\*+)?)\s+([A-Za-z_]\w*)\s*(?:\[[^\]]*\])?\s*;)"); + while (std::getline(in, line)) { + for (auto it = std::sregex_iterator(line.begin(), line.end(), field); it != std::sregex_iterator(); ++it) { + std::smatch m = *it; + auto* v = new Variable(IdGenerator::next("var"), m[2].str()); + setType(v, "type", m[1].str()); + cls->addChild("fields", v); + } + } + } + + static bool insideTypedefStruct(const std::string& src, size_t pos) { + std::regex rx(R"(typedef\s+struct\s*([A-Za-z_]\w*)?\s*\{[\s\S]*?\}\s*([A-Za-z_]\w*)\s*;)"); + for (auto it = std::sregex_iterator(src.begin(), src.end(), rx); it != std::sregex_iterator(); ++it) { + size_t p = static_cast((*it).position()); + size_t len = static_cast((*it).length()); + if (pos >= p && pos < p + len) return true; + } + return false; + } + + static void parseTypedefStructs(const std::string& src, Module* module) { + std::regex rx(R"(typedef\s+struct\s*([A-Za-z_]\w*)?\s*\{([\s\S]*?)\}\s*([A-Za-z_]\w*)\s*;)"); + for (auto it = std::sregex_iterator(src.begin(), src.end(), rx); it != std::sregex_iterator(); ++it) { + std::smatch m = *it; + std::string sName = trim(m[1].str()); + std::string alias = trim(m[3].str()); + std::string className = sName.empty() ? alias : sName; + auto* cls = new ClassDeclaration(IdGenerator::next("cls"), className); + parseStructBody(m[2].str(), cls); + module->addChild("classes", cls); + auto* ta = new TypeAlias(IdGenerator::next("typedef"), alias, + sName.empty() ? ("struct " + alias) : ("struct " + sName), false); + module->addChild("statements", ta); + } + } + + static void parseStructs(const std::string& src, Module* module) { + std::regex rx(R"(struct\s+([A-Za-z_]\w*)\s*\{([\s\S]*?)\}\s*;)"); + for (auto it = std::sregex_iterator(src.begin(), src.end(), rx); it != std::sregex_iterator(); ++it) { + size_t pos = static_cast((*it).position()); + if (insideTypedefStruct(src, pos)) continue; + std::smatch m = *it; + auto* cls = new ClassDeclaration(IdGenerator::next("cls"), trim(m[1].str())); + parseStructBody(m[2].str(), cls); + module->addChild("classes", cls); + } + } + + static void parseEnums(const std::string& src, Module* module) { + std::regex rx(R"(enum\s*([A-Za-z_]\w*)?\s*\{([\s\S]*?)\}\s*;)"); + int anon = 0; + for (auto it = std::sregex_iterator(src.begin(), src.end(), rx); it != std::sregex_iterator(); ++it) { + std::smatch m = *it; + auto* e = new EnumDeclaration(); + e->id = IdGenerator::next("enum"); + e->name = trim(m[1].str()); + if (e->name.empty()) e->name = "anonymous_enum_" + std::to_string(++anon); + for (const auto& part : splitTopLevel(m[2].str(), ',')) { + if (part.empty()) continue; + size_t eq = part.find('='); + std::string n = trim(eq == std::string::npos ? part : part.substr(0, eq)); + std::string v = trim(eq == std::string::npos ? "" : part.substr(eq + 1)); + if (!n.empty()) e->addChild("members", new EnumMember(IdGenerator::next("em"), n, v)); + } + module->addChild("statements", e); + } + } + + static std::vector> parseParams(const std::string& txt) { + std::vector> out; + std::string p = trim(txt); + if (p.empty() || p == "void") return out; + std::regex fnPtr(R"(^(.+?)\(\s*\*\s*([A-Za-z_]\w*)\s*\)\s*\((.*)\)\s*$)"); + for (const auto& raw : splitTopLevel(p, ',')) { + std::string t = trim(raw); + if (t.empty()) continue; + std::smatch m; + if (std::regex_match(t, m, fnPtr)) { + out.push_back({trim(m[2].str()), trim(m[1].str()) + " (*)(" + trim(m[3].str()) + ")"}); + continue; + } + size_t sp = t.find_last_of(" \t"); + if (sp == std::string::npos) { out.push_back({t, "int"}); continue; } + std::string name = trim(t.substr(sp + 1)); + std::string type = trim(t.substr(0, sp)); + while (!name.empty() && name[0] == '*') { type += "*"; name.erase(name.begin()); } + out.push_back({trim(name.empty() ? "param" : name), trim(type.empty() ? "int" : type)}); + } + return out; + } + + static void parseFunctions(const std::string& src, Module* module) { + std::regex rx( + R"((^|[\n\r])\s*((?:static|extern|const|inline|volatile|register|signed|unsigned|\w|\s|\*)+?)\s+([A-Za-z_]\w*)\s*\(([^;{}]*)\)\s*\{)", + std::regex::ECMAScript); + for (auto it = std::sregex_iterator(src.begin(), src.end(), rx); it != std::sregex_iterator(); ++it) { + std::smatch m = *it; + std::string ret = trim(m[2].str()); + std::string name = trim(m[3].str()); + if (name == "if" || name == "for" || name == "while" || name == "switch") continue; + if (ret.find("typedef") != std::string::npos) continue; + auto* fn = new Function(IdGenerator::next("fn"), name); + setType(fn, "returnType", ret); + for (const auto& [pn, pt] : parseParams(m[4].str())) { + auto* p = new Parameter(IdGenerator::next("param"), pn); + setType(p, "type", pt); + fn->addChild("parameters", p); + } + module->addChild("functions", fn); + } + } + + static bool looksLikeType(const std::string& t) { + static const std::vector k = { + "const","static","extern","unsigned","signed","volatile","register", + "char","short","int","long","float","double","void","size_t","struct","enum" + }; + for (const auto& x : k) if (t == x || startsWith(t, x + " ") || startsWith(t, x + "*")) return true; + return false; + } + + static void parseTopLevelVariables(const std::string& src, Module* module) { + std::istringstream in(src); + std::string line; + int depth = 0; + std::regex rx(R"(^\s*([A-Za-z_]\w*(?:\s+\w+)?(?:\s*\*+)?)\s+([A-Za-z_]\w*)\s*(=\s*[^;]+)?\s*;\s*$)"); + std::smatch m; + while (std::getline(in, line)) { + for (char c : line) { if (c == '{') depth++; else if (c == '}') depth--; } + if (depth != 0) continue; + std::string t = trim(line); + if (t.empty() || startsWith(t, "#") || t.find('(') != std::string::npos) continue; + if (t.find("typedef") != std::string::npos || t.find("enum ") != std::string::npos) continue; + if (!std::regex_match(t, m, rx)) continue; + std::string type = trim(m[1].str()); + if (!looksLikeType(type)) continue; + auto* v = new Variable(IdGenerator::next("var"), trim(m[2].str())); + setType(v, "type", type); + module->addChild("variables", v); + } + } +}; diff --git a/editor/src/ast/Parser.h b/editor/src/ast/Parser.h index b6ae73d..dfad5f3 100644 --- a/editor/src/ast/Parser.h +++ b/editor/src/ast/Parser.h @@ -164,3 +164,4 @@ private: // Standalone parsers (not tree-sitter fragment includes) #include "ast/KotlinParser.h" #include "ast/CSharpParser.h" +#include "ast/CParser.h" diff --git a/editor/tests/step361_test.cpp b/editor/tests/step361_test.cpp new file mode 100644 index 0000000..918342d --- /dev/null +++ b/editor/tests/step361_test.cpp @@ -0,0 +1,186 @@ +// Step 361: C Parser — Functions, Structs, Enums (12 tests) + +#include +#include +#include +#include "Pipeline.h" +#include "ast/Parser.h" +#include "ast/ClassDeclaration.h" +#include "ast/EnumNamespaceNodes.h" + +int main() { + int passed = 0; + + // Test 1: function parsing + { + auto mod = CParser::parseC("int add(int x, int y) { return x + y; }\n"); + auto fns = mod->getChildren("functions"); + assert(fns.size() == 1); + auto* fn = static_cast(fns[0]); + assert(fn->name == "add"); + assert(fn->getChildren("parameters").size() == 2); + std::cout << "Test 1 PASSED: function parsing\n"; + passed++; + } + + // Test 2: struct parsing + { + auto mod = CParser::parseC("struct Point { int x; int y; };\n"); + auto classes = mod->getChildren("classes"); + assert(classes.size() == 1); + auto* cls = static_cast(classes[0]); + assert(cls->name == "Point"); + assert(cls->getChildren("fields").size() == 2); + std::cout << "Test 2 PASSED: struct parsing\n"; + passed++; + } + + // Test 3: typedef struct parsing + { + auto mod = CParser::parseC("typedef struct { int id; char* name; } User;\n"); + auto classes = mod->getChildren("classes"); + assert(classes.size() == 1); + auto* cls = static_cast(classes[0]); + assert(cls->name == "User"); + bool foundAlias = false; + for (auto* s : mod->getChildren("statements")) { + if (s->conceptType != "TypeAlias") continue; + auto* ta = static_cast(s); + if (ta->aliasName == "User" && !ta->isUsing) foundAlias = true; + } + assert(foundAlias); + std::cout << "Test 3 PASSED: typedef struct parsing\n"; + passed++; + } + + // Test 4: enum parsing + { + std::string src = "enum Color { RED=1, GREEN=2, BLUE=3 }; enum { A=1, B=2 };"; + auto mod = CParser::parseC(src); + int enums = 0; + bool named = false, anon = false; + for (auto* s : mod->getChildren("statements")) { + if (s->conceptType != "EnumDeclaration") continue; + enums++; + auto* e = static_cast(s); + if (e->name == "Color") named = true; + if (e->name.find("anonymous_enum_") == 0) anon = true; + } + assert(enums == 2 && named && anon); + std::cout << "Test 4 PASSED: enum parsing\n"; + passed++; + } + + // Test 5: preprocessor directives + { + std::string src = + "#include \n#include \"x.h\"\n#define MAX 10\n" + "#define MIN(a,b) ((a)<(b)?(a):(b))\n#pragma once\n"; + auto mod = CParser::parseC(src); + int includes = 0, macros = 0, pragmas = 0; + for (auto* s : mod->getChildren("statements")) { + if (s->conceptType == "IncludeDirective") includes++; + if (s->conceptType == "MacroDefinition") macros++; + if (s->conceptType == "PragmaDirective") pragmas++; + } + assert(includes == 2 && macros == 2 && pragmas == 1); + std::cout << "Test 5 PASSED: preprocessor directives\n"; + passed++; + } + + // Test 6: static/extern qualifiers + { + auto mod = CParser::parseC("static int h(){return 1;}\nextern int api(){return 2;}\n"); + auto fns = mod->getChildren("functions"); + assert(fns.size() == 2); + bool hasStatic = false, hasExtern = false; + for (auto* f : fns) { + auto* fn = static_cast(f); + auto* t = fn->getChild("returnType"); + if (!t || t->conceptType != "PrimitiveType") continue; + auto kind = static_cast(t)->kind; + if (fn->name == "h" && kind.find("static") != std::string::npos) hasStatic = true; + if (fn->name == "api" && kind.find("extern") != std::string::npos) hasExtern = true; + } + assert(hasStatic && hasExtern); + std::cout << "Test 6 PASSED: qualifiers parsed\n"; + passed++; + } + + // Test 7: function pointers + { + auto mod = CParser::parseC("void set_callback(void (*callback)(int)) { }"); + auto fns = mod->getChildren("functions"); + assert(fns.size() == 1); + auto* fn = static_cast(fns[0]); + auto params = fn->getChildren("parameters"); + assert(params.size() == 1); + auto* p = static_cast(params[0]); + assert(p->name == "callback"); + auto* t = p->getChild("type"); + assert(t != nullptr); + std::cout << "Test 7 PASSED: function pointers\n"; + passed++; + } + + // Test 8: pointer variable + { + auto mod = CParser::parseC("char* name;"); + auto vars = mod->getChildren("variables"); + assert(vars.size() == 1); + auto* v = static_cast(vars[0]); + assert(v->name == "name"); + assert(v->getChild("type") != nullptr); + std::cout << "Test 8 PASSED: pointer variable\n"; + passed++; + } + + // Test 9: multiple functions + { + auto mod = CParser::parseC("int a(){return 1;}\nint b(){return 2;}\nint c(){return 3;}\n"); + assert(mod->getChildren("functions").size() == 3); + std::cout << "Test 9 PASSED: multiple functions\n"; + passed++; + } + + // Test 10: backward compat c++ parser + { + auto mod = TreeSitterParser::parseCpp("int cpp_fn() { return 7; }"); + assert(mod != nullptr); + assert(mod->getChildren("functions").size() == 1); + std::cout << "Test 10 PASSED: C++ parser unaffected\n"; + passed++; + } + + // Test 11: C-style comments + { + std::string src = "/* x */\n// y\nint live(int z) { return z; } // tail\n"; + auto mod = CParser::parseC(src); + auto fns = mod->getChildren("functions"); + assert(fns.size() == 1); + assert(static_cast(fns[0])->name == "live"); + std::cout << "Test 11 PASSED: C comments handled\n"; + passed++; + } + + // Test 12: header guard + pipeline route + { + std::string src = "#ifndef X_H\n#define X_H\nint f(void){return 0;}\n#endif\n"; + auto mod = CParser::parseC(src); + int pragmaLike = 0; + for (auto* s : mod->getChildren("statements")) if (s->conceptType == "PragmaDirective") pragmaLike++; + assert(pragmaLike >= 2); + + Pipeline p; + std::vector diags; + auto parsed = p.parse("int z(void){return 0;}", "c", diags); + assert(parsed != nullptr && parsed->targetLanguage == "c"); + assert(diags.empty()); + std::cout << "Test 12 PASSED: header guard + pipeline route\n"; + passed++; + } + + std::cout << "\nResults: " << passed << "/12\n"; + assert(passed == 12); + return 0; +} diff --git a/progress.md b/progress.md index 4db3c6b..3c645ae 100644 --- a/progress.md +++ b/progress.md @@ -2043,6 +2043,48 @@ state transitions, key symbol visibility, and panel layout persistence. helper functions to satisfy the max function length constraint (<=80 lines), with `step354_test`, `step355_test`, and `step360_test` revalidated +--- + +# Sprint 14 Progress — Language Batch 1 + +### Step 361: C Parser — Functions, Structs, Enums +**Status:** PASS (12/12 tests) + +Added a standalone C parser (regex/text-based, no new tree-sitter dependency) +that handles core C declarations and preprocessor constructs needed for the +pipeline entry point in Sprint 14. + +**Files created:** +- `editor/src/ast/CParser.h` — C parser support: + - `parseC(...)` and `parseCWithDiagnostics(...)` + - preprocessor parsing: `#include`, `#define` (object/function-like), `#pragma` + - header-guard shape recognition (`#ifndef/#ifdef/#endif`) into pragma-style nodes + - C declarations: functions, structs, typedef-struct, enums (named + anonymous), + top-level variables, function-pointer parameters + - comment stripping for C line/block comments before declaration parsing +- `editor/tests/step361_test.cpp` — 12 tests covering: + 1. function parsing + 2. struct parsing + 3. typedef struct parsing + 4. enum parsing (named + anonymous) + 5. preprocessor directives + 6. static/extern qualifiers + 7. function-pointer parameter parsing + 8. pointer variable parsing + 9. multiple functions per file + 10. C++ parser backward compatibility + 11. C comment handling + 12. pipeline routing for `"c"` language + header-guard pattern + +**Files modified:** +- `editor/src/ast/Parser.h` — include `ast/CParser.h` +- `editor/src/Pipeline.h` — add parse routing for language `"c"` +- `editor/CMakeLists.txt` — `step361_test` target + +**Verification run:** +- `step361_test` — PASS (12/12) new step coverage +- `step360_test` — PASS (8/8) regression coverage + # Roadmap Planning — Sprints 12-25+ ## Status: Planning Complete (Sprints 12-19 detailed, 20-25 in roadmap.md)