From febde86430c68cf4db3af5d701753e5d49c0afb5 Mon Sep 17 00:00:00 2001 From: maelstrom Date: Sat, 26 Apr 2025 01:16:44 +0200 Subject: [PATCH] feat(autogen): scan for props --- .vscode/launch.json | 4 +- autogen/CMakeLists.txt | 1 + autogen/src/analysis.cpp | 218 ++++++++++++++++++++++++++++++++++ autogen/src/analysis.h | 23 ++++ autogen/src/main.cpp | 98 ++++----------- core/src/objects/annotation.h | 7 ++ core/src/objects/part.h | 6 +- 7 files changed, 279 insertions(+), 78 deletions(-) create mode 100644 autogen/src/analysis.cpp create mode 100644 autogen/src/analysis.h create mode 100644 core/src/objects/annotation.h diff --git a/.vscode/launch.json b/.vscode/launch.json index 010f65f..96ca399 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -17,8 +17,8 @@ "request": "launch", "name": "Debug Autogen", "program": "${workspaceFolder}/autogen/build/autogen", - "args": ["core/src", "core/src/objects", "autogen/build/generated"], - "cwd": "${workspaceFolder}", + "args": ["../core/src", "../core/src/objects", "build/generated"], + "cwd": "${workspaceFolder}/autogen", } ] } \ No newline at end of file diff --git a/autogen/CMakeLists.txt b/autogen/CMakeLists.txt index 510981e..b143633 100644 --- a/autogen/CMakeLists.txt +++ b/autogen/CMakeLists.txt @@ -5,6 +5,7 @@ add_executable(autogen src/main.cpp src/util.cpp src/cache.cpp + src/analysis.cpp ) set_target_properties(autogen PROPERTIES OUTPUT_NAME "autogen") diff --git a/autogen/src/analysis.cpp b/autogen/src/analysis.cpp new file mode 100644 index 0000000..128d215 --- /dev/null +++ b/autogen/src/analysis.cpp @@ -0,0 +1,218 @@ +#include "analysis.h" +#include "util.h" +#include +#include +#include + + +// Very simple parser +// Example format: +// name="Hello!", world=Test, read_only +// Result: +// "name": "Hello!", "world": "Test", "read_only": "" +std::map parseAnnotationString(std::string src) { + std::map result; + + std::string currentIdent = ""; + std::string currentValue = ""; + int stage = 0; + bool quoted = false; + + int i = 0; + for (; i < src.length(); i++) { + if (src[i] == ' ' && (stage != 2 || !quoted)) continue; // Ignore spaces if not in stage 2 and quoted + if (stage < 2 && (src[i] >= 'a' && src[i] <= 'z' || src[i] >= 'A' && src[i] <= 'Z' || src[i] >= '0' && src[i] <= '9' || src[i] == '_')) { + currentIdent += src[i]; + stage = 1; + continue; + } + if (stage == 1 && src[i] == '=') { // What follows is a value + stage = 2; + continue; + } + if (stage == 1 && src[i] == ',') { // Value-less key + stage = 0; + result[currentIdent] = ""; + currentIdent = ""; + continue; + } + if (stage == 2 && quoted && src[i] == '"') { // Close a quoted string + quoted = false; + continue; + } + if (stage == 2 && !quoted && src[i] == '"') { // Start a quoted string + quoted = true; + continue; + } + if (stage == 2 && !quoted && (src[i] == ' ' || src[i] == ',')) { // Terminate the string + stage = 0; + result[currentIdent] = currentValue; + currentIdent = ""; + currentValue = ""; + continue; + } + if (stage == 2) { // Otherwise if in stage 2, simply add the character + currentValue += src[i]; + continue; + } + fprintf(stderr, "Unexpected symbol: %c at index %d\n", src[i], i); + fprintf(stderr, "\t%s\n", src.c_str()); + fprintf(stderr, "\t%s^\n", i > 0 ? std::string(i, '~').c_str() : ""); + abort(); + } + + // Add the remaining value + if (stage == 1) { + result[currentIdent] = ""; + } else if (stage == 2) { + result[currentIdent] = currentValue; + } + + return result; +} + + +bool findInstanceAnnotation(CXCursor cur) { + bool found = false; + // Look for annotations in the class itself + x_clang_visitChildren(cur, [&](CXCursor cur, CXCursor parent) { + CXCursorKind kind = clang_getCursorKind(cur); + if (kind != CXCursor_AnnotateAttr) return CXChildVisit_Continue; + + std::string annString = x_clang_toString(clang_getCursorDisplayName(cur)); + if (annString == "OB::INSTANCE") found = true; + + return CXChildVisit_Break; + }); + return found; +} + +std::optional findAnnotation(CXCursor cur, std::string annotationName) { + std::optional ret = std::nullopt; + + x_clang_visitChildren(cur, [&](CXCursor cur, CXCursor parent) { + CXCursorKind kind = clang_getCursorKind(cur); + if (kind != CXCursor_AnnotateAttr) return CXChildVisit_Continue; + + std::string annString = x_clang_toString(clang_getCursorDisplayName(cur)); + if (annString != annotationName) return CXChildVisit_Continue; + + // Look inside for a StringLiteral + + x_clang_visitChildren(cur, [&](CXCursor cur, CXCursor parent) { + CXCursorKind kind = clang_getCursorKind(cur); + // if (kind != CXCursor_StringLiteral) return CXChildVisit_Recurse; + // String literals cannot be parsed as CXCursor_StringLiteral. I don't know why. + if (kind != CXCursor_UnexposedExpr) return CXChildVisit_Recurse; + + // https://stackoverflow.com/a/63859988/16255372 + auto res = clang_Cursor_Evaluate(cur); + ret = clang_EvalResult_getAsStr(res); + clang_EvalResult_dispose(res); + + return CXChildVisit_Break; + }); + + return CXChildVisit_Break; + }); + return ret; +} + +std::string findBaseClass(CXCursor cur) { + std::string baseClass = ""; + x_clang_visitChildren(cur, [&](CXCursor cur, CXCursor parent) { + CXCursorKind kind = clang_getCursorKind(cur); + if (kind != CXCursor_CXXBaseSpecifier) return CXChildVisit_Continue; + + baseClass = x_clang_toString(clang_getCursorDisplayName(cur)); + + return CXChildVisit_Break; + }); + return baseClass; +} + +void processField(CXCursor cur, ClassAnalysis* state) { + std::optional propertyDef = findAnnotation(cur, "OB::def_prop"); + if (!propertyDef) return; + + PropertyAnalysis anly; + + auto result = parseAnnotationString(propertyDef.value()); + std::string fieldName = x_clang_toString(clang_getCursorDisplayName(cur)); + + anly.name = result["name"]; + anly.fieldName = fieldName; + + state->properties.push_back(anly); +} + +void processClass(CXCursor cur, AnalysisState* state, std::string className) { + ClassAnalysis anly; + + // Find base class + std::string baseClass = findBaseClass(cur); + + anly.name = className; + anly.baseClass = baseClass; + + // Find annotated fields + x_clang_visitChildren(cur, [&](CXCursor cur, CXCursor parent) { + CXCursorKind kind = clang_getCursorKind(cur); + if (kind != CXCursor_FieldDecl) return CXChildVisit_Continue; + + processField(cur, &anly); + + return CXChildVisit_Continue; + }); + + state->classes[className] = anly; +} + +// https://clang.llvm.org/docs/LibClang.html +bool analyzeClasses(std::string path, std::string srcRoot, AnalysisState* state) { + const char* cargs[] = { "-x", "c++", "-I", srcRoot.c_str(), 0 }; + // THANK YOU SO MUCH THIS STACKOVERFLOW ANSWER IS SO HELPFUL + // https://stackoverflow.com/a/59206378/16255372 + CXIndex index = clang_createIndex(0, 0); + CXTranslationUnit unit = clang_parseTranslationUnit( + index, + path.c_str(), cargs, 4, + nullptr, 0, + CXTranslationUnit_None); + + if (!unit) { + fprintf(stderr, "Failed to parse file\n"); + return 1; + } + + // Print errors + int ndiags = clang_getNumDiagnostics(unit); + for (int i = 0; i < ndiags; i++) { + CXDiagnostic diag = clang_getDiagnostic(unit, i); + CXString str = clang_formatDiagnostic(diag, 0); + fprintf(stderr, "diag: %s\n", clang_getCString(str)); + + clang_disposeString(str); + clang_disposeDiagnostic(diag); + } + + CXCursor cursor = clang_getTranslationUnitCursor(unit); + + bool flag = false; + // Search for classes + x_clang_visitChildren(cursor, [&](CXCursor cur, CXCursor parent) { + CXCursorKind kind = clang_getCursorKind(cur); + if (kind != CXCursor_ClassDecl) return CXChildVisit_Continue; + + std::string className = x_clang_toString(clang_getCursorDisplayName(cur)); + // Forward-decls can slip through the cracks, this prevents that, but also allows us to filter non-instance classes in the src/objects directory + if (!findInstanceAnnotation(cur)) return CXChildVisit_Continue; // Class is not "primary" declaration/is not instance, skip + if (state->classes.count(className) > 0) return CXChildVisit_Continue; // Class has already been analyzed, skip... + + processClass(cur, state, className); + + return CXChildVisit_Continue; + }); + + return true; +} \ No newline at end of file diff --git a/autogen/src/analysis.h b/autogen/src/analysis.h new file mode 100644 index 0000000..bafeab2 --- /dev/null +++ b/autogen/src/analysis.h @@ -0,0 +1,23 @@ +#pragma once + +#include +#include +#include + +struct PropertyAnalysis { + std::string name; + std::string fieldName; + std::string backingFieldType; +}; + +struct ClassAnalysis { + std::string name; + std::string baseClass; + std::vector properties; +}; + +struct AnalysisState { + std::map classes; +}; + +bool analyzeClasses(std::string path, std::string srcRoot, AnalysisState* state); \ No newline at end of file diff --git a/autogen/src/main.cpp b/autogen/src/main.cpp index 9fc03d8..8d2d7db 100644 --- a/autogen/src/main.cpp +++ b/autogen/src/main.cpp @@ -4,21 +4,14 @@ #include #include #include -#include -#include #include #include #include +#include "analysis.h" #include "cache.h" -#include "util.h" namespace fs = std::filesystem; -bool analyzeFirstPass(std::string path, std::string srcRoot); - -std::map> SUBCLASSES; -std::map SUPERCLASS; - int main(int argc, char** argv) { if (argc < 4) { fprintf(stderr, "Usage: autogen \n"); @@ -32,7 +25,7 @@ int main(int argc, char** argv) { // Scrape directory for header files for (const fs::directory_entry& file : fs::recursive_directory_iterator(argv[2])) { if (!file.is_regular_file()) continue; // Not a file, skip - if (file.path().extension() != ".cpp") continue; // Not a header file, skip + if (file.path().extension() != ".h") continue; // Not a header file, skip if (!hasFileBeenUpdated(file.path())) { fs::path relpath = fs::relative(file.path(), argv[1]); printf("[AUTOGEN] Skipping file %s...\n", relpath.c_str()); @@ -43,73 +36,28 @@ int main(int argc, char** argv) { headerFiles.push_back(file.path()); } - // First-pass: Analyze type hierarchy - for (std::string path : headerFiles) { - fs::path relpath = fs::relative(path, argv[1]); - printf("[AUTOGEN] [Stage 1] Analyzing file %s...\n", relpath.c_str()); - if (!analyzeFirstPass(path, argv[1])) - return 1; + AnalysisState state; + + analyzeClasses("../core/src/objects/part.h", argv[1], &state); + + for (auto& [_, clazz] : state.classes) { + printf("Class: %s\n", clazz.name.c_str()); + if (clazz.baseClass != "") + printf("==> Base class: %s\n", clazz.baseClass.c_str()); + + if (!clazz.properties.empty()) + printf("==> Properties:\n"); + for (auto prop : clazz.properties) { + printf("====> %s (%s)\n", prop.name.c_str(), prop.fieldName.c_str()); + } } + // First-pass: Analyze type hierarchy + // for (std::string path : headerFiles) { + // fs::path relpath = fs::relative(path, argv[1]); + // printf("[AUTOGEN] Processing file %s...\n", relpath.c_str()); + // analyzeClasses(path, argv[1], &state); + // } + flushCaches(argv[3]); -} - -// https://clang.llvm.org/docs/LibClang.html -bool analyzeFirstPass(std::string path, std::string srcRoot) { - const char* cargs[] = { "-x", "c++", "-I", srcRoot.c_str(), 0 }; - // THANK YOU SO MUCH THIS STACKOVERFLOW ANSWER IS SO HELPFUL - // https://stackoverflow.com/a/59206378/16255372 - CXIndex index = clang_createIndex(0, 0); - CXTranslationUnit unit = clang_parseTranslationUnit( - index, - path.c_str(), cargs, 4, - nullptr, 0, - CXTranslationUnit_None); - - if (!unit) { - fprintf(stderr, "Failed to parse file\n"); - return 1; - } - - // Print errors - int ndiags = clang_getNumDiagnostics(unit); - for (int i = 0; i < ndiags; i++) { - CXDiagnostic diag = clang_getDiagnostic(unit, i); - CXString str = clang_formatDiagnostic(diag, 0); - fprintf(stderr, "diag: %s\n", clang_getCString(str)); - - clang_disposeString(str); - clang_disposeDiagnostic(diag); - } - - CXCursor cursor = clang_getTranslationUnitCursor(unit); - - // Search for classes - x_clang_visitChildren(cursor, [&](CXCursor cur, CXCursor parent) { - CXCursorKind kind = clang_getCursorKind(cur); - if (kind != CXCursor_ClassDecl) return CXChildVisit_Continue; - - std::string className = x_clang_toString(clang_getCursorDisplayName(cur)); - - std::string baseClass = ""; - x_clang_visitChildren(cur, [&](CXCursor cur, CXCursor parent) { - CXCursorKind kind = clang_getCursorKind(cur); - if (kind != CXCursor_CXXBaseSpecifier) return CXChildVisit_Continue; - - baseClass = x_clang_toString(clang_getCursorDisplayName(cur)); - return CXChildVisit_Break; - }); - - if (baseClass != "") { - SUPERCLASS[className] = baseClass; - - std::vector subclasses = SUBCLASSES[baseClass]; - subclasses.push_back(className); - SUBCLASSES[baseClass] = subclasses; - } - - return CXChildVisit_Continue; - }); - - return true; } \ No newline at end of file diff --git a/core/src/objects/annotation.h b/core/src/objects/annotation.h new file mode 100644 index 0000000..d1aecb6 --- /dev/null +++ b/core/src/objects/annotation.h @@ -0,0 +1,7 @@ +#pragma once + +// Markers for the autogen engine to generate getters, setters, lua, etc. + +#define INSTANCE [[clang::annotate("OB::INSTANCE")]] + +#define def_prop(...) clang::annotate("OB::def_prop", #__VA_ARGS__) \ No newline at end of file diff --git a/core/src/objects/part.h b/core/src/objects/part.h index e4deddb..2406a7e 100644 --- a/core/src/objects/part.h +++ b/core/src/objects/part.h @@ -11,6 +11,7 @@ #include #include #include +#include "annotation.h" namespace rp = reactphysics3d; @@ -27,7 +28,7 @@ struct PartConstructParams { class Snap; -class Part : public Instance { +class INSTANCE Part : public Instance { protected: // Joints where this part is Part0 std::vector> primaryJoints; @@ -51,8 +52,11 @@ public: Vector3 velocity; CFrame cframe; + [[ def_prop(name="Size") ]] glm::vec3 size; + [[ def_prop(name="Color") ]] Color3 color; + [[ def_prop(name="Transparency") ]] float transparency = 0.f; bool selected = false;