diff --git a/tools/transpiler/.gitignore b/tools/transpiler/.gitignore new file mode 100644 index 00000000..43add80b --- /dev/null +++ b/tools/transpiler/.gitignore @@ -0,0 +1,4 @@ +generated/ +build/ +.vscode +.cache \ No newline at end of file diff --git a/tools/transpiler/CMakeLists.txt b/tools/transpiler/CMakeLists.txt new file mode 100644 index 00000000..06faff69 --- /dev/null +++ b/tools/transpiler/CMakeLists.txt @@ -0,0 +1,63 @@ +# +# CMake file for building the transpiler +# + +cmake_minimum_required(VERSION 3.10) + +project(SplashKitTranspiler) # Define project name + +set(CMAKE_CXX_STANDARD 17) # Set C++ required version + +# set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR}) # Put the executable in the main directory +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) + +# Add source files to executable +add_executable(Transpiler + src/main.cpp + src/parser.cpp + src/builder.cpp + src/translator.cpp +) + +# Define CMake variables +set(SPLASHKIT_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/../..") +set(SPLASHKIT_SRC "${SPLASHKIT_ROOT}/coresdk/src") +set(SPLASHKIT_BACKEND "${SPLASHKIT_SRC}/backend") +set(SPLASHKIT_CORESDK "${SPLASHKIT_SRC}/coresdk") +set(SPLASHKIT_CATCH "${SPLASHKIT_ROOT}/coresdk/external/catch") +set(SPLASHKIT_EASYLOGGING "${SPLASHKIT_ROOT}/coresdk/external/easyloggingpp") +set(SPLASHKIT_TESTS "${SPLASHKIT_ROOT}/coresdk/src/test") + +# Pass as Macros to C++ +target_compile_definitions(Transpiler PRIVATE + SPLASHKIT_CORESDK="${SPLASHKIT_CORESDK}" + SPLASHKIT_BACKEND="${SPLASHKIT_BACKEND}" + SPLASHKIT_CATCH="${SPLASHKIT_CATCH}" + SPLASHKIT_EASYLOGGING="${SPLASHKIT_EASYLOGGING}" + SPLASHKIT_TESTS="${SPLASHKIT_TESTS}" +) + +# Output to /bin +set_target_properties(Transpiler PROPERTIES + RUNTIME_OUTPUT_DIRECTORY ${SPLASHKIT_ROOT}/bin +) + +find_package(LLVM REQUIRED CONFIG) # Find the LLVM installation and use its CMake config +find_package(Clang REQUIRED CONFIG) # Find the clang installation and use its CMake config +find_package(nlohmann_json REQUIRED) + +# Add the header paths from the config to the include dirs (PRIVATE so only this executable uses these paths) +target_include_directories(Transpiler PRIVATE + ${LLVM_INCLUDE_DIRS} + ${CLANG_INCLUDE_DIRS} + ${CMAKE_CURRENT_SOURCE_DIR}/external + ${CMAKE_CURRENT_BINARY_DIR} +) + +# Add the libraries +target_link_libraries(Transpiler PRIVATE + clangTooling + clangAST + clangFrontend + nlohmann_json::nlohmann_json +) diff --git a/tools/transpiler/clean.sh b/tools/transpiler/clean.sh new file mode 100644 index 00000000..104b8be2 --- /dev/null +++ b/tools/transpiler/clean.sh @@ -0,0 +1,3 @@ +#!/bin/bash +rm -rf build +cmake -S . -B build \ No newline at end of file diff --git a/tools/transpiler/src/ast.h b/tools/transpiler/src/ast.h new file mode 100644 index 00000000..72102bb9 --- /dev/null +++ b/tools/transpiler/src/ast.h @@ -0,0 +1,253 @@ +#pragma once + +#include + +using json = nlohmann::json; + +struct Type +{ + std::string name; + bool isConst = false; + bool isPointer = false; + std::vector templateArguments; // e.g. SomeType + + bool operator==(const Type& other) const + { + return name == other.name && + isConst == other.isConst && + isPointer == other.isPointer && + templateArguments == other.templateArguments; + } + + NLOHMANN_DEFINE_TYPE_INTRUSIVE_ONLY_SERIALIZE(Type, name, isConst, isPointer, templateArguments) +}; + +struct Expression +{ + Type type; + + virtual ~Expression() = default; // Explicit destructor for safety (pointers in derived classes) + virtual void serialise(json &j) const = 0; // Virtual function for base classes to define how they should be serialised +}; + +// Defines how nlohmann/json converts this type (Expression) to json +inline void to_json(json &j, const Expression &e) +{ + e.serialise(j); +} + +// Defines how nlohmann/json converts this type (Unique pointer to Expression) to json +inline void to_json(json &j, const std::unique_ptr &e) +{ + if (e) e->serialise(j); + else j = nullptr; // This converts to "null" with nlohmann/json +} + +struct LiteralExpression : Expression +{ + std::string value; + NLOHMANN_DEFINE_TYPE_INTRUSIVE_ONLY_SERIALIZE(LiteralExpression, value, type) + // Convert the current object (the derived type) to json, and assign a "kind" + void serialise(json &j) const override { j = *this; j["kind"] = "LiteralExpression"; } +}; + +struct Argument +{ + std::unique_ptr expression; + bool isRef = false; + + NLOHMANN_DEFINE_TYPE_INTRUSIVE_ONLY_SERIALIZE(Argument, expression, isRef) +}; + +enum class ReferenceKind +{ + Variable, + EnumConstant +}; + +struct ReferenceExpression : Expression +{ + std::string name; + ReferenceKind refKind; + std::string parentEnumType; // Used for enum constants + NLOHMANN_DEFINE_TYPE_INTRUSIVE_ONLY_SERIALIZE(ReferenceExpression, type, name, refKind, type, parentEnumType) + void serialise(json &j) const override { j = *this; j["kind"] = "ReferenceExpression"; } +}; + +struct ConstructorExpression : Expression +{ + std::vector> arguments; + NLOHMANN_DEFINE_TYPE_INTRUSIVE_ONLY_SERIALIZE(ConstructorExpression, type, arguments) + void serialise(json &j) const override { j = *this; j["kind"] = "ConstructorExpression"; } +}; + +struct InitListExpression : Expression +{ + std::vector> elements; + NLOHMANN_DEFINE_TYPE_INTRUSIVE_ONLY_SERIALIZE(InitListExpression, elements, type) + void serialise(json &j) const override { j = *this; j["kind"] = "InitListExpression"; } +}; + +struct CallExpression : Expression +{ + std::string functionName; + std::string qualifiedFunctionName; + std::vector arguments; + std::unique_ptr parentObject; // nullptr if not a member call + NLOHMANN_DEFINE_TYPE_INTRUSIVE_ONLY_SERIALIZE(CallExpression, type, functionName, qualifiedFunctionName, arguments, parentObject) + void serialise(json &j) const override { j = *this; j["kind"] = "CallExpression"; } +}; + +struct IndexExpression : Expression +{ + std::unique_ptr object; + std::unique_ptr index; + NLOHMANN_DEFINE_TYPE_INTRUSIVE_ONLY_SERIALIZE(IndexExpression, type, object, index) + void serialise(json &j) const override { j = *this; j["kind"] = "IndexExpression"; } +}; + +struct BinaryExpression : Expression +{ + std::string op; + std::unique_ptr left; + std::unique_ptr right; + NLOHMANN_DEFINE_TYPE_INTRUSIVE_ONLY_SERIALIZE(BinaryExpression, type, op, left, right) + void serialise(json &j) const override { j = *this; j["kind"] = "BinaryExpression"; } +}; + +struct UnaryExpression : Expression +{ + std::string op; + std::unique_ptr operand; + NLOHMANN_DEFINE_TYPE_INTRUSIVE_ONLY_SERIALIZE(UnaryExpression, type, op, operand) + void serialise(json &j) const override { j = *this; j["kind"] = "UnaryExpression"; } +}; + +struct MemberExpression : Expression +{ + std::unique_ptr parentObject; + std::string memberName; + + NLOHMANN_DEFINE_TYPE_INTRUSIVE_ONLY_SERIALIZE(MemberExpression, type, parentObject, memberName) + + void serialise(json &j) const override { j = *this; j["kind"] = "MemberExpression"; } +}; + +struct VariableDeclaration +{ + std::string name; + Type type; + std::string declarationId; + std::unique_ptr initializer; + NLOHMANN_DEFINE_TYPE_INTRUSIVE_ONLY_SERIALIZE(VariableDeclaration, name, type, declarationId, initializer) +}; + +struct Parameter +{ + std::string name; + Type type; + std::unique_ptr defaultValue; + NLOHMANN_DEFINE_TYPE_INTRUSIVE_ONLY_SERIALIZE(Parameter, name, type, defaultValue) +}; + +struct Statement +{ + virtual ~Statement() = default; // Explicit destructor for safety (pointers in derived classes) + virtual void serialise(json &j) const = 0; // Virtual function for base classes to define how they should be serialised +}; + +// Defines how nlohmann/json converts this type (Statement) to json +inline void to_json(json &j, const Statement &e) +{ + e.serialise(j); +} + +// Defines how nlohmann/json converts this type (Shared pointer to Statement) to json +inline void to_json(json &j, const std::shared_ptr &e) +{ + if (e) e->serialise(j); + else j = nullptr; // This converts to "null" with nlohmann/json +} + +struct FunctionDeclaration +{ + std::string name; + std::vector parameters; + std::vector> body; + Type returnType; + bool isGlobal; + NLOHMANN_DEFINE_TYPE_INTRUSIVE_ONLY_SERIALIZE(FunctionDeclaration, name, parameters, body, returnType, isGlobal) +}; + +struct ExpressionStatement : Statement +{ + std::unique_ptr expression; + NLOHMANN_DEFINE_TYPE_INTRUSIVE_ONLY_SERIALIZE(ExpressionStatement, expression) + void serialise(json &j) const override { j = *this; j["kind"] = "ExpressionStatement"; } +}; + +struct VariableDeclarationStatement : Statement +{ + VariableDeclaration variable; + bool isGlobal; + NLOHMANN_DEFINE_TYPE_INTRUSIVE_ONLY_SERIALIZE(VariableDeclarationStatement, variable, isGlobal) + void serialise(json &j) const override { j = *this; j["kind"] = "VariableDeclarationStatement"; } +}; + +struct ReturnStatement : Statement +{ + std::unique_ptr value; + NLOHMANN_DEFINE_TYPE_INTRUSIVE_ONLY_SERIALIZE(ReturnStatement, value) + void serialise(json &j) const override { j = *this; j["kind"] = "ReturnStatement"; } +}; + +struct ForStatement : Statement +{ + std::vector> initialiser; + std::unique_ptr condition; + std::unique_ptr increment; + std::vector> body; + NLOHMANN_DEFINE_TYPE_INTRUSIVE_ONLY_SERIALIZE(ForStatement, initialiser, condition, increment, body) + void serialise(json &j) const override { j = *this; j["kind"] = "ForStatement"; } +}; + +enum class AssertionType +{ + Require, + RequireFalse, + RequireThrows, + Check, + CheckFalse +}; + +struct AssertionStatement : Statement +{ + AssertionType type; + std::unique_ptr expression; + NLOHMANN_DEFINE_TYPE_INTRUSIVE_ONLY_SERIALIZE(AssertionStatement, type, expression) + void serialise(json &j) const override { j = *this; j["kind"] = "AssertionStatement"; } +}; + +struct Section +{ + std::string name; + std::vector> body; + NLOHMANN_DEFINE_TYPE_INTRUSIVE_ONLY_SERIALIZE(Section, name, body) +}; + +struct TestCase +{ + std::string name; + std::vector tags; + std::vector
sections; + NLOHMANN_DEFINE_TYPE_INTRUSIVE_ONLY_SERIALIZE(TestCase, name, tags, sections) +}; + +struct CustomAST +{ + std::string filename; + std::vector globals; + std::vector functions; + std::vector tests; + NLOHMANN_DEFINE_TYPE_INTRUSIVE_ONLY_SERIALIZE(CustomAST, filename, globals, functions, tests) +}; diff --git a/tools/transpiler/src/builder.cpp b/tools/transpiler/src/builder.cpp new file mode 100644 index 00000000..3d5d4e09 --- /dev/null +++ b/tools/transpiler/src/builder.cpp @@ -0,0 +1,863 @@ +#include +#include + +#include +#include +#include +#include + +#include "builder.h" +#include "ast.h" +#include "macro.h" +#include "clang/AST/Decl.h" +#include "clang/AST/Expr.h" +#include "clang/AST/ExprCXX.h" +#include "clang/AST/Stmt.h" +#include "clang/AST/Type.h" +#include "clang/AST/TypeLoc.h" +#include "clang/Basic/OperatorKinds.h" + +// Gets the line number of a declaration from the source file +// int ASTBuilder::getLineNumber(const clang::Decl *decl) +// { +// return sourceManager.getSpellingLineNumber(decl->getLocation()); +// } + +unsigned ASTBuilder::getLocationKey(clang::SourceLocation loc) +{ + return sourceManager.getExpansionLoc(loc).getRawEncoding(); +} + +// Gets the source text of a given expression (e.g. "x + 5") +std::string ASTBuilder::getSourceText(const clang::Expr &expr) +{ + // Token range uses the entire range of the expression + auto range = clang::CharSourceRange::getTokenRange(expr.getSourceRange()); + + clang::LangOptions options; // Default options (language rules) + + // Use the lexer to ask for the source text at the given location + return clang::Lexer::getSourceText(range, sourceManager, options).str(); +} + +// Build a literal with a string value +std::unique_ptr ASTBuilder::buildLiteral(const clang::Expr &expr) +{ + auto result = std::make_unique(); + + // String literal + if (auto *literal = llvm::dyn_cast(&expr)) + { + result->value = literal->getString().str(); + + Type stringType; + stringType.name = "string"; // This ensures the type isn't "const char[9]" - not useful for translation + result->type = stringType; + } + else + { + result->value = getSourceText(expr); + result->type = buildType(expr.getType()); + } + + return result; +} + +// Build a binary expression +std::unique_ptr ASTBuilder::buildBinaryExpression(const clang::BinaryOperator &binary) +{ + auto result = std::make_unique(); + + result->op = binary.getOpcodeStr().str(); + result->type = buildType(binary.getType()); + result->left = buildExpression(*binary.getLHS()); + result->right = buildExpression(*binary.getRHS()); + + return result; +} + +// Build a unary expression +std::unique_ptr ASTBuilder::buildUnaryExpression(const clang::UnaryOperator &unary) +{ + auto result = std::make_unique(); + + result->type = buildType(unary.getType()); + result->op = clang::UnaryOperator::getOpcodeStr(unary.getOpcode()).str(); + result->operand = buildExpression(*unary.getSubExpr()); + + return result; +} + +// Build initialiser list expression +std::unique_ptr ASTBuilder::buildInitListExpression(const clang::InitListExpr &initList) +{ + auto result = std::make_unique(); + + if (initList.getNumInits() > 0) + { + auto initListType = initList.getInit(0)->IgnoreImplicit()->getType(); + result->type = buildType(initListType); + } + + for (unsigned i = 0; i < initList.getNumInits(); i++) + { + auto *element = initList.getInit(i); + auto expr = buildExpression(*element); + result->elements.push_back(std::move(expr)); + } + + return result; +} + +// Build constructor expression +std::unique_ptr ASTBuilder::buildConstructorExpression(const clang::CXXConstructExpr &ctorExpr) +{ + auto result = std::make_unique(); + result->type = buildType(ctorExpr.getType()); + + for (unsigned i = 0; i < ctorExpr.getNumArgs(); i++) + { + auto *arg = ctorExpr.getArg(i); + + if (llvm::isa(arg)) continue; + + auto expr = buildExpression(*arg); + result->arguments.push_back(std::move(expr)); + } + + return result; +} + +// Build a function call +std::unique_ptr ASTBuilder::buildFunctionCall(const clang::CallExpr &call, const clang::FunctionDecl &function) +{ + // Catch2 compiler-generated wrapper + if (function.getNameAsString() == "__builtin_constant_p") + { + if (call.getNumArgs() == 1) + { + return buildExpression(*call.getArg(0)); + } + } + + auto result = std::make_unique(); + result->functionName = function.getNameAsString(); + result->qualifiedFunctionName = function.getQualifiedNameAsString(); + result->type = buildType(call.getType()); + + // If this is a member function call, store the object. + if (auto *memberCall = llvm::dyn_cast(&call)) + { + if (const clang::Expr *object = memberCall->getImplicitObjectArgument()) + { + result->parentObject = buildExpression(*object); + } + } + + unsigned numArgs = call.getNumArgs(); + + // Add all arguments + for (int i = 0; i < numArgs; i++) + { + auto *currentArg = call.getArg(i); + + Argument argument; + + argument.expression = buildExpression(*currentArg); + + if (i < function.getNumParams()) + { + // Check parameter + const clang::ParmVarDecl *parameter = function.getParamDecl(i); + + // Is it a reference? + if (parameter->getType()->isLValueReferenceType() && + !parameter->getType().getNonReferenceType().isConstQualified()) + { + argument.isRef = true; + } + } + + result->arguments.push_back(std::move(argument)); + } + + return result; +} + +// Build a reference to something already declared +std::unique_ptr ASTBuilder::buildReference(const clang::DeclRefExpr &ref) +{ + auto result = std::make_unique(); + result->name = ref.getDecl()->getNameAsString(); + result->type = buildType(ref.getType()); + + // Check enum type + if (auto *eNumConstant = llvm::dyn_cast(ref.getDecl())) + { + result->refKind = ReferenceKind::EnumConstant; + + // Get enum type name + if (auto *enumDecl = llvm::dyn_cast(eNumConstant->getDeclContext())) + { + result->parentEnumType = enumDecl->getNameAsString(); + } + } + else if (llvm::isa(ref.getDecl())) + { + result->refKind = ReferenceKind::Variable; + } + + return result; +} + +// Expression dispatcher +std::unique_ptr ASTBuilder::buildExpression(const clang::Expr &expr) +{ + // Literals + if (llvm::isa(expr) || + llvm::isa(expr) || + llvm::isa(expr) || + llvm::isa(expr) || + llvm::isa(expr) || + llvm::isa(expr)) + { + return buildLiteral(expr); + } + else if (auto *binary = llvm::dyn_cast(&expr)) + { + return buildBinaryExpression(*binary); + } + else if (auto *unary = llvm::dyn_cast(&expr)) + { + return buildUnaryExpression(*unary); + } + // Reference to variable, function, enum, etc. + else if (auto *ref = llvm::dyn_cast(&expr)) + { + return buildReference(*ref); + } + // Member expression + else if (auto *member = llvm::dyn_cast(&expr)) + { + auto memberExpr = std::make_unique(); + + memberExpr->memberName = member->getMemberDecl()->getNameAsString(); + memberExpr->parentObject = buildExpression(*member->getBase()); + + return memberExpr; + } + // Overloaded operators (e.g. someString == "testString") - store as binary expression + else if (auto *opCall = llvm::dyn_cast(&expr)) + { + // Indexed expression (like someVar[i]) + if (opCall->getOperator() == clang::OO_Subscript) + { + auto indexExpr = std::make_unique(); + + indexExpr->object = buildExpression(*opCall->getArg(0)); + indexExpr->index = buildExpression(*opCall->getArg(1)); + + return indexExpr; + } + + // Create a binary expression node and assign the correct operator + auto binary = std::make_unique(); + + binary->op = clang::getOperatorSpelling(opCall->getOperator()); + binary->left = buildExpression(*opCall->getArg(0)); + binary->right = buildExpression(*opCall->getArg(1)); + + return binary; + } + // Calls + else if (auto *call = llvm::dyn_cast(&expr)) + { + // Function call + if (auto *funcDecl = call->getDirectCallee()) + { + return buildFunctionCall(*call, *funcDecl); + } + } + // Initialiser list + else if (auto *initList = llvm::dyn_cast(&expr)) + { + return buildInitListExpression(*initList); + } + // Initialiser list wrapper + else if (auto *initList = llvm::dyn_cast(&expr)) + { + return buildExpression(*initList->getSubExpr()); + } + // Implicit casts (unwrap them) + else if (auto *cast = llvm::dyn_cast(&expr)) + { + return buildExpression(*cast->getSubExpr()); + } + // Parentheses (unwrap them) + else if (auto *paren = llvm::dyn_cast(&expr)) + { + return buildExpression(*paren->getSubExpr()); + } + // Expressions with cleanups (cleang lifetime management - unwrap) + else if (auto *cleanups = llvm::dyn_cast(&expr)) + { + return buildExpression(*cleanups->getSubExpr()); + } + else if (auto *compoundLiteral = llvm::dyn_cast(&expr)) + { + return buildExpression(*compoundLiteral->getInitializer()); + } + // Constructor expressions (or wrappers like C++ making a string() object for a string literal) + else if (auto *construct = llvm::dyn_cast(&expr)) + { + // Check the target type + Type targetType = buildType(construct->getType()); + + // If there's one argument and it's the same type as the target (e.g. color(someColor)), unwrap it + if (construct->getNumArgs() == 1) + { + const clang::Expr *arg = construct->getArg(0)->IgnoreImplicit(); + + // Clang has generated a copy/temporary constructor around + // an existing variable reference. Preserve the reference. + if (llvm::isa(arg)) + { + return buildExpression(*arg); + } + } + + // Constructor is a string - unwrap the literal + if ((targetType.name == "string" || targetType.name == "std::string" || targetType.name == "class std::basic_string") && construct->getNumArgs() > 0) + { + return buildExpression(*construct->getArg(0)); + } + + // Constructor uses list initialisation + if (construct->isListInitialization()) + { + return buildExpression(*construct->getArg(0)); + } + + return buildConstructorExpression(*construct); + } + // Another clang wrapper for memory management - unwrap + else if (auto *bind = llvm::dyn_cast(&expr)) + { + return buildExpression(*bind->getSubExpr()); + } + // Another clang wrapper for memory management - unwrap + else if (auto *mat = llvm::dyn_cast(&expr)) + { + return buildExpression(*mat->getSubExpr()); + } + + std::cout + << "Unsupported expression found at line " + << sourceManager.getSpellingLineNumber(expr.getExprLoc()) + << "\nExpression class: " + << expr.getStmtClassName() + << "\nType: " + << expr.getType().getAsString() + << "\nSource: " + << getSourceText(expr) + << std::endl; + throw std::runtime_error("Unsupported expression"); +} + +// Build a variable declaration +VariableDeclaration ASTBuilder::buildVariableDecl(const clang::VarDecl &var) +{ + VariableDeclaration result; + + result.name = var.getNameAsString(); + // result.line = getLineNumber(var); + // result.location = getLocationKey(var->getLocation()); + + result.type = buildType(var.getType()); + + // Is it initialised? + if (var.hasInit()) + { + result.initializer = buildExpression(*var.getInit()); + } + + return result; +} + +// Build a parameter +Parameter ASTBuilder::buildParameter(const clang::ParmVarDecl ¶m) +{ + Parameter result; + + result.name = param.getNameAsString(); + result.type = buildType(param.getType()); + + // Is there a default? + if (param.hasDefaultArg()) + { + result.defaultValue = buildExpression(*param.getDefaultArg()); + } + + return result; +} + +// Build a type +Type ASTBuilder::buildType(const clang::QualType &qualType) +{ + Type result; + + result.isConst = qualType.isConstQualified(); + + if (qualType->isPointerType()) + { + result.isPointer = true; + result.name = qualType.getAsString(); + } + else + { + result.name = qualType.getAsString(); + + if (result.isConst) + { + result.name = result.name.substr(6); + } + } + + // Look through ElaboratedType to find the underlying template. + clang::QualType underlyingType = qualType; + + if (const auto *elaborated = + llvm::dyn_cast( + underlyingType.getTypePtr())) + { + underlyingType = elaborated->getNamedType(); + } + + if (const auto *templateType = + underlyingType->getAs()) + { + result.name = templateType->getTemplateName() + .getAsTemplateDecl() + ->getNameAsString(); + + if (result.name == "basic_string") result.name = "string"; + else + { + for (const auto &arg : templateType->template_arguments()) + { + // Ensure the argument passed to the template type is a type itself + if (arg.getKind() != clang::TemplateArgument::Type) + continue; + + result.templateArguments.push_back(buildType(arg.getAsType()) + ); + } + }; + + } + + return result; +} + +// Build a function declaration +FunctionDeclaration ASTBuilder::buildFunctionDecl(const clang::FunctionDecl &fn, bool isGlobal) +{ + FunctionDeclaration result; + + // Use getQualifiedNameAsString(); to check for SplashKit functions "splashkit_lib::draw_bitmap" + + result.name = fn.getNameAsString(); + // result.line = getLineNumber(fn); + // result.location = getLocationKey(fn->getLocation()); + result.returnType = buildType(fn.getReturnType()); + result.isGlobal = isGlobal; + + // Get params + for (auto *param : fn.parameters()) + { + result.parameters.push_back(buildParameter(*param)); + } + + // Build the function body + auto *body = llvm::cast(fn.getBody()); + for (const auto *stmt : body->body()) + { + auto statements = buildStatements(*stmt); + + result.body.insert(result.body.end(), statements.begin(), statements.end()); + } + + return result; +} + +void ASTBuilder::checkMinAndMax( + clang::SourceLocation loc, + clang::SourceLocation &min, + clang::SourceLocation &max) +{ + if (loc.isInvalid()) return; + + clang::SourceLocation spelling = sourceManager.getSpellingLoc(loc); + + // Not defined in main file - ignore + if (sourceManager.getFileID(spelling) != sourceManager.getMainFileID()) + return; + + // If this the earliest location yet? + if (min.isInvalid() || + sourceManager.isBeforeInTranslationUnit(spelling, min)) + { + min = spelling; + } + + // Is this the latest location yet? + if (max.isInvalid() || + sourceManager.isBeforeInTranslationUnit(max, spelling)) + { + max = spelling; + } +} + +// Recurse through child statements until an expression is found +const clang::Expr* ASTBuilder::findExpressionInRange( + const clang::Stmt *stmt, + const clang::SourceRange targetRange, + clang::SourceLocation &min, + clang::SourceLocation &max) +{ + if (!stmt) return nullptr; + + clang::SourceLocation targetBegin = sourceManager.getSpellingLoc(targetRange.getBegin()); + + clang::SourceLocation targetEnd = sourceManager.getSpellingLoc(targetRange.getEnd()); + + // Include this node's own range + if (auto *expr = llvm::dyn_cast(stmt)) + { + checkMinAndMax(sourceManager.getSpellingLoc(expr->getBeginLoc()), min, max); + checkMinAndMax(sourceManager.getSpellingLoc(expr->getEndLoc()), min, max); + } + + // Include all children + for (const clang::Stmt *child : stmt->children()) + { + clang::SourceLocation childMin; + clang::SourceLocation childMax; + + if (auto *result = findExpressionInRange(child, targetRange, childMin, childMax)) + { + return result; + } + + if (childMin.isValid()) checkMinAndMax(childMin, min, max); + + if (childMax.isValid()) checkMinAndMax(childMax, min, max); + } + + // Check the entire subtree range + if (auto *expr = llvm::dyn_cast(stmt)) + { + if (min.isValid() && max.isValid() && min == targetBegin && max == targetEnd) + { + return expr; + } + } + + return nullptr; +} + +// Dispatcher for building macros +std::shared_ptr ASTBuilder::buildMacro( + const clang::Stmt &stmt, + const MacroInfo ¯oInfo) +{ + auto result = std::make_shared(); + + switch (macroInfo.kind) + { + case MacroKind::Require: + { + result->type = AssertionType::Require; + break; + } + case MacroKind::RequireFalse: + { + result->type = AssertionType::RequireFalse; + break; + } + case MacroKind::RequireThrows: + { + result->type = AssertionType::RequireThrows; + break; + } + case MacroKind::Check: + { + result->type = AssertionType::Check; + break; + } + case MacroKind::CheckFalse: + { + result->type = AssertionType::CheckFalse; + break; + } + default: + { + return result; + } + } + + clang::SourceLocation min; + clang::SourceLocation max; + + auto argument = findExpressionInRange(&stmt, macroInfo.argumentRange, min, max); + + if (!argument) + { + std::cout << "findExpressionInRange returned nullptr!" << std::endl; + return result; + } + + result->expression = buildExpression(*argument); + + return result; +} + +VariableDeclarationStatement ASTBuilder::buildVariableDeclStmt(const clang::VarDecl &var, bool isGlobal) +{ + VariableDeclarationStatement result; + result.variable = buildVariableDecl(var); + result.isGlobal = isGlobal; + return result; +} + +std::vector> ASTBuilder::buildStatements(const clang::Stmt &stmt) +{ + std::vector> result; + // Start by checking for macros + // Get the key for this location + unsigned key = getLocationKey(stmt.getBeginLoc()); + + // Check for any macros at this location + auto it = macros.find(key); + + // If there is one, build it + if (it != macros.end()) + { + auto macro = buildMacro(stmt, it->second); + result.push_back(macro); + } + // Declaration statement + else if (auto *declStmt = llvm::dyn_cast(&stmt)) + { + // Check declarations + for (clang::Decl *decl : declStmt->decls()) + { + // Variable declaration + if (auto *var = llvm::dyn_cast(decl)) + { + auto varDecl = std::make_shared(buildVariableDeclStmt(*var)); + result.push_back(varDecl); + } + } + } + else if (auto *retrn = llvm::dyn_cast(&stmt)) + { + auto returnStmt = std::make_shared(); + returnStmt->value = buildExpression(*retrn->getRetValue()); + result.push_back(returnStmt); + } + // Expression statement + else if (auto *expr = llvm::dyn_cast(&stmt)) + { + auto exprStmt = std::make_shared(); + exprStmt->expression = buildExpression(*expr); + result.push_back(exprStmt); + } + // For loop + else if (auto *clangForStmt = llvm::dyn_cast(&stmt)) + { + auto forStmt = std::make_shared(); + + // If there is an initialiser (or multiple), add it + if (const auto *init = clangForStmt->getInit()) + { + auto initStatements = buildStatements(*init); + + if (!initStatements.empty()) + forStmt->initialiser = initStatements; + } + + // If there's a condition, add it + if (const auto *cond = clangForStmt->getCond()) + { + forStmt->condition = buildExpression(*cond); + } + + // If there's an increment expression, add it + if (const auto *inc = clangForStmt->getInc()) + { + forStmt->increment = buildExpression(*inc); + } + + // If there's a body, add it + if (const auto *body = llvm::dyn_cast(clangForStmt->getBody())) + { + for (const auto *stmt : body->body()) + { + auto statements = buildStatements(*stmt); + + forStmt->body.insert( + forStmt->body.end(), + statements.begin(), + statements.end() + ); + } + } + + result.push_back(forStmt); + } + + return result; +} + +std::vector
ASTBuilder::buildSections( + std::vector> runningStmts, + const clang::CompoundStmt &srcStatements, + std::string sectionName) +{ + std::vector
foundSections; + + // For every statement + for (clang::Stmt *stmt : srcStatements.body()) + { + // Get the key for this location + unsigned key = getLocationKey(stmt->getBeginLoc()); + + // Check for any macros at this location + auto it = macros.find(key); + + // Section found + if (it != macros.end() && it->second.kind == MacroKind::Section) + { + MacroInfo macroInfo = it->second; + + // The SECTION macro becomes an if statement during compilation + auto *ifStmt = llvm::dyn_cast(stmt); + + if (!ifStmt) break; + + // The block of the if statement becomes the "body" of the section + auto *compound = + llvm::dyn_cast(ifStmt->getThen()); + + if (!compound) break; + + // Explore section body + std::vector
subsections = buildSections(runningStmts, *compound, macroInfo.name); + // Add any subsections to the running list + foundSections.insert(foundSections.end(), subsections.begin(), subsections.end()); + } + // Not a section - build statement(s) + else + { + // Build statements + auto stmts = buildStatements(*stmt); + + if (!stmts.empty()) + { + // Add the new statements to the end of all the sections found at this level + for (auto §ion : foundSections) + { + section.body.insert(section.body.end(), stmts.begin(), stmts.end()); + } + + runningStmts.insert(runningStmts.end(), stmts.begin(), stmts.end()); + } + } + } + + if (foundSections.empty() && !runningStmts.empty()) + { + // No sections found at this level (leaf node) + // Create the new section + Section newSection; + newSection.name = sectionName; + // Add all the setup statements so far + newSection.body.insert(newSection.body.end(), runningStmts.begin(), runningStmts.end()); + foundSections.push_back(newSection); + } + + return foundSections; +} + +// Build a test case +TestCase ASTBuilder::buildTestCase( + const clang::FunctionDecl &func, + const MacroInfo ¯oInfo) +{ + TestCase result; + + // Utilise macro info obtained by the preprocessor + result.name = macroInfo.name; + // testCase.line = getLineNumber(func); + // testCase.location = getLocationKey(macroInfo.location); + result.tags = macroInfo.tags; + + // Build sections + clang::Stmt *body = func.getBody(); + auto *compound = llvm::dyn_cast(body); + if (!compound) return result; + result.sections = buildSections({}, *compound, ""); + + return result; +} + +// Build a test file (top level) +void ASTBuilder::buildAST(CustomAST &AST) +{ + // Root node of the AST + clang::TranslationUnitDecl *translationUnit = context.getTranslationUnitDecl(); + + // Top-level declarations + for (clang::Decl *decl : translationUnit->decls()) + { + clang::SourceLocation loc = decl->getLocation(); + clang::SourceLocation expansionLoc = + sourceManager.getExpansionLoc(loc); + + // If the declaration isn't written in the main file, ignore it + if (!sourceManager.isWrittenInMainFile(expansionLoc)) + { + continue; + } + + // Try to cast as each type of declaration, and manage accordingly + // Variables + if (auto *var = llvm::dyn_cast(decl)) + { + AST.globals.push_back(buildVariableDeclStmt(*var, true)); + } + else if (auto *func = llvm::dyn_cast(decl)) + { + if (func != func->getCanonicalDecl()) + continue; + + // Get the key for this location + unsigned key = getLocationKey(func->getLocation()); + + // Check for any macros at this location + auto it = macros.find(key); + + // If there is one, and it's a test case, build it + if (it != macros.end() && it->second.kind == MacroKind::TestCase) + { + AST.tests.push_back(buildTestCase(*func, it->second)); + } + // Otherwise it's a top-level function + else + { + AST.functions.push_back(buildFunctionDecl(*func, true)); + } + } + } +} diff --git a/tools/transpiler/src/builder.h b/tools/transpiler/src/builder.h new file mode 100644 index 00000000..ca42a7b1 --- /dev/null +++ b/tools/transpiler/src/builder.h @@ -0,0 +1,91 @@ +#pragma once + +#include +#include "ast.h" +#include "macro.h" +#include "clang/AST/Stmt.h" + +// Builds a data structure for a custom AST +class ASTBuilder +{ +private: + // Need source manager for info about the source file (like line numbers) + clang::ASTContext &context; + clang::SourceManager &sourceManager; + std::unordered_map ¯os; + +public: + ASTBuilder(clang::ASTContext &context, std::unordered_map ¯os) + : context(context), + sourceManager(context.getSourceManager()), + macros(macros) + { + } + + // Gets the line number of a declaration from the source file + // int getLineNumber(const clang::Decl *decl); + + unsigned getLocationKey(clang::SourceLocation loc); + + // Gets the source text of a given expression (e.g. "x + 5") + std::string getSourceText(const clang::Expr &expr); + + // Build a literal with a string value + std::unique_ptr buildLiteral(const clang::Expr &expr); + + // Build a binary expression + std::unique_ptr buildBinaryExpression(const clang::BinaryOperator &binary); + + // Build a unary expression + std::unique_ptr buildUnaryExpression(const clang::UnaryOperator &unary); + + // Build an initialiser list expression + std::unique_ptr buildInitListExpression(const clang::InitListExpr &initList); + + // Build a constructor expression + std::unique_ptr buildConstructorExpression(const clang::CXXConstructExpr &ctorExpr); + + // Build a function call + std::unique_ptr buildFunctionCall(const clang::CallExpr &call, const clang::FunctionDecl &function); + + // Build a reference to something already declared + std::unique_ptr buildReference(const clang::DeclRefExpr &ref); + + // Expression dispatcher + std::unique_ptr buildExpression(const clang::Expr &expr); + + // Build a variable declaration + VariableDeclaration buildVariableDecl(const clang::VarDecl &var); + + VariableDeclarationStatement buildVariableDeclStmt(const clang::VarDecl &var, bool isGlobal = false); + + // Build a parameter + Parameter buildParameter(const clang::ParmVarDecl ¶m); + + // Build a type + Type buildType(const clang::QualType &qualType); + + // Build a function declaration + FunctionDeclaration buildFunctionDecl(const clang::FunctionDecl &fn, bool isGlobal = false); + + void checkMinAndMax(clang::SourceLocation loc, clang::SourceLocation &min, clang::SourceLocation &max); + + // Recurse through child statements until an expression is found + const clang::Expr* findExpressionInRange(const clang::Stmt *stmt, const clang::SourceRange targetRange, clang::SourceLocation &min, clang::SourceLocation &max); + + // Dispatcher for building macros + std::shared_ptr buildMacro(const clang::Stmt &stmt, const MacroInfo ¯oInfo); + + std::vector> buildStatements(const clang::Stmt &stmt); + + std::vector
buildSections(std::vector> cumulativeStatements, const clang::CompoundStmt &srcStatements, std::string sectionName); + + // Build a test case + TestCase buildTestCase(const clang::FunctionDecl &func, const MacroInfo ¯oInfo); + + // Build a custom AST + void buildAST(CustomAST &AST); + + const clang::Expr* findCatchAssertionExpression(const clang::Stmt* stmt); + const clang::Expr* findBooleanExpression(const clang::Stmt* stmt); +}; diff --git a/tools/transpiler/src/macro.h b/tools/transpiler/src/macro.h new file mode 100644 index 00000000..0662527f --- /dev/null +++ b/tools/transpiler/src/macro.h @@ -0,0 +1,23 @@ +#pragma once + +#include "clang/Basic/SourceLocation.h" + +enum class MacroKind +{ + TestCase, + Section, + Require, + Check, + RequireFalse, + CheckFalse, + RequireThrows +}; + +struct MacroInfo +{ + MacroKind kind; + clang::SourceLocation location; + clang::SourceRange argumentRange; + std::string name; + std::vector tags; +}; diff --git a/tools/transpiler/src/main.cpp b/tools/transpiler/src/main.cpp new file mode 100644 index 00000000..ae639bea --- /dev/null +++ b/tools/transpiler/src/main.cpp @@ -0,0 +1,77 @@ +#include +#include +#include +#include + +#include "ast.h" +#include "parser.h" +#include "translator.h" + +namespace fs = std::filesystem; + +int main(int argc, char** argv) { + + // Set target language (Default to C#) + std::string targetLanguage; + if (argc > 1) targetLanguage = argv[1]; + + // Set scan directory (Default to unit test directory) + fs::path scanDir = fs::path(SPLASHKIT_TESTS) / "unit_tests"; + if (argc > 2) scanDir = argv[2]; + + std::cout << "Scanning directory: " << scanDir << "\n\n"; + + // Check the chosen directory exists, and is actually a directory + std::error_code ec; + if (!fs::exists(scanDir, ec) || !fs::is_directory(scanDir, ec)) + { + std::cerr << "Error: Directory does not exist or is inaccessible: " << scanDir << "\n"; + return 1; + } + + // Add every .cpp file in the directory + std::vector cppFiles; + for (const auto& entry : fs::directory_iterator(scanDir)) + { + if (entry.path().extension() == ".cpp") + { + std::string stem = entry.path().stem().string(); // Filename without extension + if (stem == "unit_test_main" || stem == "logging_handling") continue; // Skip "main" file + cppFiles.push_back(entry.path()); // Add file to list + } + } + + // No valid files found + if (cppFiles.empty()) + { + std::cerr << "No .cpp files found in " << scanDir << "\n"; + return 1; + } + + std::cout << "Found " << cppFiles.size() << " .cpp files\n\n"; + + // Parse files + fs::path debugOutputDir = fs::path(SPLASHKIT_TESTS) / "unit_tests" / "generated" / "json"; + std::vector *allASTs = parseTestFiles(cppFiles, debugOutputDir); + + // Translate the files to the target language + std::unique_ptr translator; + + // C# + if (targetLanguage == "cs" || targetLanguage == "csharp" || targetLanguage == "C#") + { + translator = std::make_unique(); + } + // Default to C# + else + { + std::cout << "Target language defaulted to C#" << std::endl; + translator = std::make_unique(); + } + + fs::path outputDir = fs::path(SPLASHKIT_TESTS) / "unit_tests" / "generated" / "csharp"; + translator->translateASTSet(*allASTs, outputDir); + std::cout << "Translated " << cppFiles.size() << " C++ files\n\n"; + + return 0; +} diff --git a/tools/transpiler/src/parser.cpp b/tools/transpiler/src/parser.cpp new file mode 100644 index 00000000..85cce324 --- /dev/null +++ b/tools/transpiler/src/parser.cpp @@ -0,0 +1,206 @@ +#include "parser.h" + +#include +#include +#include +#include + +#include +#include +#include +#include + +#include "ast.h" +#include "builder.h" +#include "macro.h" + +namespace fs = std::filesystem; + +// Full set of ASTs generated +std::vector allASTs; + +std::string getMacroArgumentString( + const clang::MacroArgs *args, + unsigned tokenIndex, + clang::Preprocessor &pp) +{ + const clang::Token *argTokens = args->getUnexpArgument(0); // Get a pointer to the first token + std::string result = pp.getSpelling(argTokens[tokenIndex]); // Get the source text for the selected token + + // Strip out the quotes + if (result.size() >= 2 && + result.front() == '"' && + result.back() == '"') + { + result = result.substr(1, result.size() - 2); + } + + return result; +} + +void addMacroArgs(MacroInfo ¯o, const clang::MacroArgs *args) +{ + const clang::Token *tokens = args->getUnexpArgument(0); + unsigned numTokens = args->getArgLength(tokens); + + // Get the location of the first and last tokens in the unexpanded arguments + clang::SourceLocation begin = tokens[0].getLocation(); + clang::SourceLocation end = tokens[numTokens - 1].getLocation(); + macro.argumentRange = clang::SourceRange(begin, end); +} + +// Takes a set of tags in the form "[tag1][tag2][tag3]" +// Returns each tag as a string in a vector +std::vector parseTags(std::string tagString) +{ + std::vector result; + + int start = 0; + + // Extract substrings between "[]" + while ((start = tagString.find('[', start)) != std::string::npos) + { + int end = tagString.find(']', start); + + if (end == std::string::npos) break; + + result.push_back(tagString.substr(start + 1, end - start - 1)); + start = end + 1; + } + + return result; +} + +// PPCallbacks is a pre-processor interface. MacroExpands() is called when the pre-processor encounters a Macro +void TestFinder::MacroExpands( + const clang::Token ¯oName, + const clang::MacroDefinition ¯oDefinition, + clang::SourceRange range, + const clang::MacroArgs *args) +{ + std::string name = macroName.getIdentifierInfo()->getName().str(); + + // Record macro info + MacroInfo macro; + macro.location = sourceManager.getExpansionLoc(range.getBegin()); + // Get the raw version of location as a key + unsigned key = macro.location.getRawEncoding(); + + if (name == "TEST_CASE") + { + macro.kind = MacroKind::TestCase; + macro.name = getMacroArgumentString(args, 0, pp); + + // Add tags + std::string allTagsString = getMacroArgumentString(args, 2, pp); + macro.tags = parseTags(allTagsString); + std::cout << "- Test case found at " << macro.location.getRawEncoding() << std::endl; + currentTestKey = key; + } + else if (name == "SECTION") + { + macro.kind = MacroKind::Section; + macro.name = getMacroArgumentString(args, 0, pp); + std::cout << "- Section found at " << macro.location.getRawEncoding() << std::endl; + } + else if (name == "REQUIRE") + { + macro.kind = MacroKind::Require; + addMacroArgs(macro, args); + } + else if (name == "REQUIRE_FALSE") + { + macro.kind = MacroKind::RequireFalse; + addMacroArgs(macro, args); + } + else if (name == "REQUIRE_THROWS") + { + macro.kind = MacroKind::RequireThrows; + addMacroArgs(macro, args); + } + else if (name == "CHECK") + { + macro.kind = MacroKind::Check; + addMacroArgs(macro, args); + } + else if (name == "CHECK_FALSE") + { + macro.kind = MacroKind::CheckFalse; + addMacroArgs(macro, args); + } + else + { + return; + } + + macros[key] = macro; +} + +void TopLevelConsumer::HandleTranslationUnit(clang::ASTContext &context) +{ + ASTBuilder builder(context, macros); + builder.buildAST(AST); + allASTs.push_back(std::move(AST)); +} + +// FrontEndAction is something you want Clang to do after it creates the AST after processing a source file +// It has default ones, this is a custom one +std::unique_ptr TopLevelAction::CreateASTConsumer( + clang::CompilerInstance &compiler, // Reference to Clang's internal compiler state + llvm::StringRef filename) // Filename (full path) being processed +{ + // Set filename + AST.filename = fs::path(filename.str()).stem().string(); + + // Create a new "TestFinder" and point the pre-processor at it + // The pre-processor will populate the source file with all the tests it finds + compiler.getPreprocessor().addPPCallbacks( + std::make_unique( + AST, + compiler.getSourceManager(), + compiler.getPreprocessor(), + macros + ) + ); + + // Create a new consumer to send the AST to, and return a unique pointer to it + // This consumer will build out the representation of the test file, and add it to the global list + return std::make_unique(AST, macros); +} + +// Parses all files with the ClangTool +// Gives clang the compilation instructions, files, and action it needs to build the AST +std::vector* parseTestFiles(const std::vector &filepaths, const fs::path &debugOutputDir) +{ + // Use these arguments when compiling ("Fixed" because it doesn't change per file) + clang::tooling::FixedCompilationDatabase compilations( + fs::current_path().string(), + { + std::string("-I") + SPLASHKIT_BACKEND, + std::string("-I") + SPLASHKIT_CATCH, + std::string("-I") + SPLASHKIT_EASYLOGGING, + std::string("-I") + SPLASHKIT_CORESDK + }); + + // Create a tool and assign the filepaths + clang::tooling::ClangTool tool(compilations, filepaths); + std::cout << "Catch: " << SPLASHKIT_CATCH << '\n'; + // For every source file, build a clang AST, create a TopLevelAction, and execute it. + tool.run(clang::tooling::newFrontendActionFactory().get()); + + // Ensure output directory exists + fs::create_directories(debugOutputDir); + + //Convert and save as JSON + for (const auto &test : allASTs) + { + json j = test; + fs::path outputFile = debugOutputDir / (test.filename + ".json"); // Set path + std::ofstream file(outputFile); + file << j.dump(4); + } + + std::cout << "\nSaved " << filepaths.size() << " tests to " << debugOutputDir << "\n"; + + return &allASTs; +} diff --git a/tools/transpiler/src/parser.h b/tools/transpiler/src/parser.h new file mode 100644 index 00000000..4468608c --- /dev/null +++ b/tools/transpiler/src/parser.h @@ -0,0 +1,73 @@ +#pragma once + +#include +#include +#include "ast.h" +#include "macro.h" + +namespace fs = std::filesystem; + +std::string getMacroArgumentString( + const clang::MacroArgs *args, + unsigned tokenIndex, + clang::Preprocessor &pp); + +// Takes a set of tags in the form "[tag1][tag2][tag3]" +// Returns each tag as a string in a vector +std::vector parseTags(std::string tagString); + +// PPCallbacks is a pre-processor interface. MacroExpands() is called when the pre-processor encounters a Macro +class TestFinder : public clang::PPCallbacks +{ +private: + CustomAST &AST; + clang::SourceManager &sourceManager; + clang::Preprocessor &pp; + std::unordered_map ¯os; + unsigned currentTestKey = 0; + +public: + TestFinder(CustomAST &AST, clang::SourceManager &sm, clang::Preprocessor &pp, std::unordered_map ¯os) + : AST(AST), sourceManager(sm), pp(pp), macros(macros) + { + } + + void MacroExpands( + const clang::Token ¯oName, + const clang::MacroDefinition ¯oDefinition, + clang::SourceRange range, + const clang::MacroArgs *args + ) override; +}; + +// ASTConsumer is an interface used to write generic actions on an AST, regardless of how the AST was produced +class TopLevelConsumer : public clang::ASTConsumer +{ +private: + CustomAST &AST; + std::unordered_map ¯os; + +public: + TopLevelConsumer(CustomAST &AST, std::unordered_map ¯os) : AST(AST), macros(macros) { } + + void HandleTranslationUnit(clang::ASTContext &context) override; +}; + +// FrontEndAction is something you want Clang to do after it creates the AST after processing a source file +// It has default ones, this is a custom one +class TopLevelAction : public clang::ASTFrontendAction +{ +private: + CustomAST AST; + std::unordered_map macros; + +public: + std::unique_ptr CreateASTConsumer( + clang::CompilerInstance &compiler, // Reference to Clang's internal compiler state + llvm::StringRef filename // Filename (full path) being processed + ) override; +}; + +// Parses all files with the ClangTool +// Gives clang the compilation instructions, files, and action it needs to build the AST +std::vector* parseTestFiles(const std::vector& filepaths, const fs::path &debugOutputDir); diff --git a/tools/transpiler/src/translator.cpp b/tools/transpiler/src/translator.cpp new file mode 100644 index 00000000..72e4608e --- /dev/null +++ b/tools/transpiler/src/translator.cpp @@ -0,0 +1,927 @@ +#include "translator.h" + +#include +#include +#include +#include +#include + +#include "ast.h" + +namespace fs = std::filesystem; + +void CSharpTranslator::translateASTSet( + const std::vector &ASTs, + const fs::path &outputDir) +{ + // Ensure output directory exists + fs::create_directories(outputDir); + std::string projectName = "SplashKit.CSharp.UnitTests"; + + writeProjectFiles(outputDir, projectName); + + // Translate each AST in the list + for (auto &AST : ASTs) + { + std::string filename = AST.filename + ".cs"; + fs::path outputFilepath = outputDir / filename; + writeAST(AST, projectName, outputFilepath); + } +} + +void CSharpTranslator::writeAST( + const CustomAST &AST, + const std::string &projectName, + const fs::path &outputFilepath) +{ + std::ofstream file(outputFilepath); + + // Get category + std::string category = AST.filename; + std::string prefix = "unit_test_"; + if (category.find(prefix) == 0) + { + category.erase(0, prefix.length()); + } + category = toPascalCase(category); + + // Top of file + file << "// Generated by translator" << "\n"; + file << "namespace " + projectName + ";\n\n"; + + // Declare class + file << "public class UnitTest" + category + "\n{\n"; + increaseIndent(); + + // Globals + if (!AST.globals.empty() || !AST.functions.empty()) + { + writeGlobals(AST, file); + } + + for (auto &test : AST.tests) + { + writeTestCase(test, file, category); + } + + // Clear globals for the next file + globals.clear(); + // End of file + decreaseIndent(); + file << "}\n"; + file.close(); +} + +void CSharpTranslator::writeGlobals(const CustomAST &AST, std::ofstream &file) +{ + // Declare GeneratedGlobals class + file << indt() << "private static class " + globalClassName + "\n" + indt() + "{\n"; + increaseIndent(); + + // Global variables + for (const auto &global : AST.globals) + { + writeStatement(global, file); + // Add to list of globals for refence when calling + globals.insert(global.variable.name); + } + + // Top level functions + for (const auto &func : AST.functions) + { + writeFunctionDecl(func, file); + // Add to list of globals for refence when calling + globals.insert(func.name); + } + + decreaseIndent(); + file << indt() << "}\n\n"; +} + +void CSharpTranslator::writeTestCase( + const TestCase &testCase, + std::ofstream &file, + std::string category) +{ + // Set category ("unit_test_color" == "Color") + std::string categoryTrait = "[Trait(\"Category\", \"" + category + "\")]\n"; + + // Set tags as xUnit "functions" + std::string functionTrait = "[Trait(\"Function\", \""; + int tagCount = testCase.tags.size(); + + for (int i = 0; i < tagCount; ++i) + { + if (i > 0) + functionTrait += ", "; + + functionTrait += testCase.tags[i]; + } + + functionTrait += "\")]\n"; + + // Set test name as the class name + std::string className = toPascalCase(testCase.name); + std::string classDecl = "public class " + className + "\n"; + + // OPEN TEST + file << indt() << categoryTrait; + file << indt() << functionTrait; + file << indt() << classDecl; + file << indt() << "{\n"; + increaseIndent(); + + // Body + for (int i = 0; i < testCase.sections.size(); i++) + { + writeSection(testCase.sections[i], file, className + std::to_string(i)); + } + + // CLOSE TEST + decreaseIndent(); + file << indt() << "}\n\n"; +} + +void CSharpTranslator::writeBody(const std::vector> &body, std::ofstream &file) +{ + // Body + for (const auto &statement : body) + { + writeStatement(*statement, file); + } +} + +std::string CSharpTranslator::translateStatement(const Statement &statement) +{ + if (auto *assertion = dynamic_cast(&statement)) + { + return translateAssertion(*assertion); + } + else if (auto *varDeclStmt = dynamic_cast(&statement)) + { + return translateVarDeclStmt(*varDeclStmt); + } + else if (auto *exprStmt = dynamic_cast(&statement)) + { + return translateExprStmt(*exprStmt); + } + else if (auto *returnStmt = dynamic_cast(&statement)) + { + return translateReturnStmt(*returnStmt); + } + + throw std::runtime_error( + "Unsupported Statement type in Translator: " + + std::string(typeid(statement).name())); +} + +void CSharpTranslator::writeStatement(const Statement &statement, std::ofstream &file) +{ + // For loop + if (auto *forStmt = dynamic_cast(&statement)) + { + writeForStmt(*forStmt, file); + } + // Regular statement + else + { + std::string result = translateStatement(statement); + + if (result.empty()) return; + + file << indt() << result << ";\n"; + } +} + +void CSharpTranslator::writeSection(const Section §ion, std::ofstream &file, std::string defaultName) +{ + // Use default name if no section name included + // This is useful for when sections aren't utilised at all + std::string sectionName = section.name.empty() ? defaultName : section.name; + + // Set display name + file << indt() << "[Fact(DisplayName = \"" + sectionName + "\")]\n"; + // Set class name + file << indt() << "public void " + toPascalCase(sectionName) + "()\n"; + file << indt() << "{\n"; + + // Section body + increaseIndent(); + + writeBody(section.body, file); + + // End section + decreaseIndent(); + file << indt() << "}\n\n"; +} + +std::string CSharpTranslator::translateAssertion(const AssertionStatement &assertion) +{ + if (!assertion.expression) return ""; + + const Expression *expr = assertion.expression.get(); + + // Skip if it requires throwing + if (assertion.type == AssertionType::RequireThrows) + { + return ""; + } + + // Check if it's an inverted assertion (e.g. "False") + bool inverted = + assertion.type == AssertionType::RequireFalse || + assertion.type == AssertionType::CheckFalse; + + // Get the type + if (auto *bin = dynamic_cast(expr)) + { + + const Expression *leftExpr = bin->left.get(); + const Expression *rightExpr = bin->right.get(); + + // If there's a literal or unary on the right, swap places + if (dynamic_cast(rightExpr) || + dynamic_cast(rightExpr)) + { + std::swap(leftExpr, rightExpr); + } + + // String conversions of each side + std::string left = translateExpression(*leftExpr); + std::string right = translateExpression(*rightExpr); + + // If there's a literal in this assertion + if (auto *literal = dynamic_cast(leftExpr)) + { + std::string leftType = translateType(leftExpr->type); + std::string rightType = translateType(rightExpr->type); + + // If the types don't match, cast the literal to match the other + if (leftType != rightType && + isNumericType(leftType) && + isNumericType(rightType)) + { + left = "(" + rightType + ")" + left; + } + } + + // Equality + if (bin->op == "==") + { + // Select correct assertion type based on expression + std::string assertion; + + if (left == "null" || left == "nullptr") + { + assertion = inverted ? "NotNull(" + right + ")" : "Null(" + right + ")"; + } + else if (left == "true") + { + assertion = inverted ? "False(" + right + ")" : "True(" + right + ")"; + } + else if (left == "false") + { + assertion = inverted ? "True(" + right + ")" : "False(" + right + ")"; + } + else + assertion = inverted ? "NotEqual(" + left + ", " + right + ")" : "Equal(" + left + ", " + right + ")"; + + return "Assert." + assertion; + } + // Inequality + else if (bin->op == "!=") + { + // Select correct assertion type based on expression + std::string assertion; + + if (left == "null" || left == "nullptr") + { + assertion = inverted ? "Null(" + right + ")" : "NotNull(" + right + ")"; + } + else if (left == "true") + { + assertion = inverted ? "True(" + right + ")" : "False(" + right + ")"; + } + else if (left == "false") + { + assertion = inverted ? "False(" + right + ")" : "True(" + right + ")"; + } + else + assertion = inverted ? "Equal(" + left + ", " + right + ")" : "NotEqual(" + left + ", " + right + ")"; + + return "Assert." + assertion; + } + // Logical AND + else if (bin->op == "&&") + { + return inverted ? "Assert.False(" + translateExpression(*bin) + ")" : "Assert.True(" + translateExpression(*bin) + ")"; + } + } + // Other + return inverted ? "Assert.False(" + translateExpression(*expr) + ")" : "Assert.True(" + translateExpression(*expr) + ")"; +} + +std::string CSharpTranslator::translateVarDeclStmt(const VariableDeclarationStatement &varDecl) +{ + std::string result; + std::string csType = translateType(varDecl.variable.type); + std::string varName = toCamelCase(varDecl.variable.name); + + std::string declaration = varDecl.isGlobal ? "public static " : ""; + declaration += csType + " " + varName; + + result = declaration; + + // If it's initialised + if (varDecl.variable.initializer) + { + result += " = " + translateExpression(*varDecl.variable.initializer); + } + + return result; +} + +std::string CSharpTranslator::translateExprStmt(const ExpressionStatement &exprStmt) +{ + std::string expr = translateExpression(*exprStmt.expression); + + // If it's a call to the logger, skip it + if (expr.find("EnableLogging") != std::string::npos || + expr.find("DisableLogging") != std::string::npos) + return ""; + + return expr; +} + +std::string CSharpTranslator::translateReturnStmt(const ReturnStatement &returnStmt) +{ + return "return " + translateExpression(*returnStmt.value); +} + +void CSharpTranslator::writeForStmt(const ForStatement &forStmt, std::ofstream &file) +{ + file << indt() << "for ("; + + // Initialiser statement(s) + if (!forStmt.initialiser.empty()) + { + // Count statements in initialiser + int initCount = forStmt.initialiser.size(); + + // Write initialiser statements + for (int i = 0; i < initCount; i++) + { + file << translateStatement(*forStmt.initialiser[i]); + + // Add comma depending on statement index + if (i < initCount - 1) file << ", "; + } + } + + file << "; "; + + // Write condition + if (forStmt.condition != nullptr) + file << translateExpression(*forStmt.condition); + + file << "; "; + + // Write incrementor + if (forStmt.increment != nullptr) + file << translateExpression(*forStmt.increment); + + file << ")\n" << indt() << "{\n"; + increaseIndent(); + writeBody(forStmt.body, file); + decreaseIndent(); + file << indt() << "}\n"; +} + +std::string CSharpTranslator::translateExpression(const Expression &expr) +{ + if (auto *binary = dynamic_cast(&expr)) + { + return translateBinaryExpr(*binary); + } + else if (auto *unary = dynamic_cast(&expr)) + { + return translateUnaryExpr(*unary); + } + else if (auto *reference = dynamic_cast(&expr)) + { + return translateRefExpr(*reference); + } + else if (auto *literal = dynamic_cast(&expr)) + { + return translateLiteralExpr(*literal); + } + else if (auto *call = dynamic_cast(&expr)) + { + return translateCallExpr(*call); + } + else if (auto *ctor = dynamic_cast(&expr)) + { + return translateConstructorExpr(*ctor); + } + else if (auto *initList = dynamic_cast(&expr)) + { + return translateInitListExpr(*initList); + } + else if (auto *indexed = dynamic_cast(&expr)) + { + return translateIndexExpr(*indexed); + } + else if (auto *member = dynamic_cast(&expr)) + { + return translateMemberExpr(*member); + } + + throw std::runtime_error("Unsupported Expression type in Translator: translateExpression()" + std::string(typeid(expr).name())); +} + +std::string CSharpTranslator::translateBinaryExpr(const BinaryExpression &expr) +{ + std::string result; + result += translateExpression(*expr.left); + result += " " + expr.op + " "; + result += translateExpression(*expr.right); + return result; +} + +std::string CSharpTranslator::translateUnaryExpr(const UnaryExpression &expr) +{ + return expr.op + translateExpression(*expr.operand); +} + +std::string CSharpTranslator::translateMemberExpr(const MemberExpression &expr) +{ + return translateExpression(*expr.parentObject) + "." + toPascalCase(expr.memberName); +} + +std::string CSharpTranslator::translateRefExpr(const ReferenceExpression &expr) +{ + std::string result; + + // Check if it's a global variable + if (globals.find(expr.name) != globals.end()) + { + result += globalClassName + "." + toCamelCase(expr.name); + } + // Check if it's an enum + else if (expr.refKind == ReferenceKind::EnumConstant) + { + result += toPascalCase(expr.parentEnumType) + "."; + result += toPascalCase(toLowerCase(expr.name)); + } + else + { + result += toCamelCase(expr.name); + } + + return result; +} + +std::string CSharpTranslator::translateIndexExpr(const IndexExpression &expr) +{ + std::string result = translateExpression(*expr.object); + result += "[" + translateExpression(*expr.index) + "]"; + return result; +} + +std::string CSharpTranslator::translateVectorMember(const CallExpression &expr) +{ + std::string object = translateExpression(*expr.parentObject); + + if (expr.functionName == "size") + return object + ".Count"; + + if (expr.functionName == "empty") + return object + ".Count == 0"; + + if (expr.functionName == "push_back") + return object + ".Add(" + translateCallArguments(expr) + ")"; + + if (expr.functionName == "pop_back") + return object + ".RemoveAt(" + object + ".Count - 1)"; + + if (expr.functionName == "front") + return object + "[0]"; + + if (expr.functionName == "back") + return object + "[" + object + ".Count - 1]"; + + if (expr.functionName == "clear") + return object + ".Clear()"; + + return object + "." + + expr.functionName + "(" + translateCallArguments(expr) + ")"; +} + +std::string CSharpTranslator::translateMemberCall(const CallExpression &expr) +{ + if (auto *reference = dynamic_cast(expr.parentObject.get())) + { + // If it's a vector + if (reference->type.name == "vector") + { + return translateVectorMember(expr); + } + } + else if (auto *call = dynamic_cast(expr.parentObject.get())) + { + // If it's a duration count, just return the parent + if (call->qualifiedFunctionName == "std::chrono::duration_cast" && expr.functionName == "count") + { + return "(long)" + translateExpression(*expr.parentObject); + } + } + return ""; +} + +std::string CSharpTranslator::translateCallArguments(const CallExpression &expr) +{ + std::string result; + + int argCount = expr.arguments.size(); + + // Add arguments + for (int i = 0; i < argCount; ++i) + { + if (i > 0) + result += ", "; + + if (expr.arguments[i].isRef) + result += "ref "; + + result += translateExpression(*expr.arguments[i].expression); + } + + return result; +} + +std::string CSharpTranslator::translateCallExpr(const CallExpression &expr) +{ + std::string result; + + // Edge cases + if (expr.qualifiedFunctionName == "std::chrono::duration_cast") + { + std::string duration = translateExpression(*expr.arguments[0].expression); + + return "((" + duration + ") * 1000.0 / Stopwatch.Frequency)"; + } + + // std::chrono::steady_clock::now() + if (expr.qualifiedFunctionName == "std::chrono::steady_clock::now") + { + return "Stopwatch.GetTimestamp()"; + } + + // Check if it's a global function + if (globals.find(expr.functionName) != globals.end()) + { + result += globalClassName + "."; + result += toPascalCase(expr.functionName) + "("; + result += translateCallArguments(expr) + ")"; + } + // Check if it's a member call + else if (expr.parentObject) + { + result += translateMemberCall(expr); + } + else + { + // Convert cmath functions to Math + if (expr.functionName == "pow" || expr.functionName == "sqrt") + { + result += "Math."; + } + result += toPascalCase(expr.functionName) + "("; + result += translateCallArguments(expr) + ")"; + } + + return result; +} + +std::string CSharpTranslator::translateConstructorExpr(const ConstructorExpression &expr) +{ + std::string result = "new " + translateType(expr.type) + "("; + int argCount = expr.arguments.size(); + + // Add arguments + for (int i = 0; i < argCount; ++i) + { + if (i > 0) + result += ", "; + + result += translateExpression(*expr.arguments[i]); + } + + result += ")"; + return result; +} + +std::string CSharpTranslator::translateInitListExpr(const InitListExpression &expr) +{ + std::string result; + + result += "new() {"; + + // Add arguments + for (int i = 0; i < expr.elements.size(); ++i) + { + if (i > 0) + result += ", "; + + result += translateExpression(*expr.elements[i]); + } + + result += "}"; + + return result; +} + +// Convert (originally escaped) strings back to escaped values +std::string CSharpTranslator::escapeCSharpString(const std::string &value) +{ + std::string result; + + for (char c : value) + { + switch (c) + { + case '\\': result += "\\\\"; break; + case '"': result += "\\\""; break; + case '\n': result += "\\n"; break; + case '\r': result += "\\r"; break; + case '\t': result += "\\t"; break; + case '\0': result += "\\0"; break; + default: result += c; break; + } + } + + return result; +} + +std::string CSharpTranslator::translateLiteralExpr(const LiteralExpression &expr) +{ + std::string result = expr.value; + + std::string typeStr = translateType(expr.type); + + if (typeStr == "String") result = "\"" + escapeCSharpString(result) + "\""; + return result; +} + +void CSharpTranslator::writeFunctionDecl(const FunctionDeclaration &funcDecl, std::ofstream &file) +{ + std::string declaration = funcDecl.isGlobal ? "public static " : ""; + declaration += translateType(funcDecl.returnType) + " " + toPascalCase(funcDecl.name); + + file << indt() << declaration << "("; + + int paramCount = funcDecl.parameters.size(); + + // Add arguments + for (int i = 0; i < paramCount; ++i) + { + if (i > 0) + file << ", "; + + file << translateParameter(funcDecl.parameters[i]); + } + + file << ")\n"; + file << indt() << "{\n"; + + increaseIndent(); + + writeBody(funcDecl.body, file); + + decreaseIndent(); + file << indt() << "}\n\n"; +} + +std::string CSharpTranslator::translateParameter(const Parameter ¶meter) +{ + std::string result = translateType(parameter.type) + " " + parameter.name; + if (parameter.defaultValue) + { + result += " = " + translateExpression(*parameter.defaultValue); + } + return result; +} + +void CSharpTranslator::writeProjectFiles(const fs::path &outputDir, const std::string &projectName) +{ + fs::path filepath = outputDir / (projectName + ".csproj"); + + // Project file + std::ofstream projFile(filepath); + + projFile << R"( + + + + net6.0 + enable + enable + false + + + + + + + + + + + + + + + + + + %(RecursiveDir)%(Filename)%(Extension) + PreserveNewest + + + + +)"; + projFile.close(); + + // Global usings file + std::ofstream usingsFile(outputDir / "GlobalUsings.cs"); + + usingsFile << R"(// Generated by translator +global using System.Diagnostics; +global using Xunit; +global using SplashKitSDK; +global using static SplashKitSDK.SplashKit;)"; + + usingsFile.close(); +} + +std::string CSharpTranslator::toLowerCase(const std::string &name) +{ + std::string result; + + for (const char &c : name) + { + result += std::tolower(c); + } + + return result; +} + +std::string CSharpTranslator::toCamelCase(const std::string &name) +{ + // Convert to PascalCase + std::string result = toPascalCase(name); + + // Lower case first letter + if (!result.empty()) + { + result[0] = static_cast(std::tolower(result[0])); + } + return result; +} + +std::string CSharpTranslator::toPascalCase(const std::string &name) +{ + // Special cases taken from SplashKit Translator: lib/core_ext/string.rb + const std::unordered_map specialCases = { + {"rgb", "RGB"}, + {"hsb", "HSB"}, + {"css", "CSS"}, + {"ip", "IP"}, + {"tcp", "TCP"}, + {"udp", "UDP"}, + {"uri", "URI"}, + {"rgba", "RGBA"}, + {"2d", "2D"}, + {"3d", "3D"} + }; + + // Split into words and capitalise + std::string s = name; + std::string result; + std::string delimiters = + "abcdefghijklmnopqrstuvwxyz" + "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + "0123456789"; + + while (true) + { + size_t pos = s.find_first_not_of(delimiters); + std::string word = s.substr(0, pos); + + if (!word.empty()) + { + // Replace special cases, or capitalise first letter + auto it = specialCases.find(word); + + if (it != specialCases.end()) + { + word = it->second; + } + else + { + // Convert whole word to lowercase + for (char &c : word) + { + c = static_cast( + std::tolower(static_cast(c)) + ); + } + // Capitalise first letter + word[0] = static_cast(std::toupper(word[0])); + } + + result += word; + } + + if (pos == std::string::npos) break; + s.erase(0, pos + 1); + } + + return result; +} + +bool CSharpTranslator::isNumericType(const std::string &type) +{ + static const std::unordered_set numericTypes = { + "byte", + "sbyte", + "short", + "ushort", + "int", + "uint", + "long", + "ulong", + "float", + "double", + "decimal" + }; + + return numericTypes.find(type) != numericTypes.end(); +} + +std::string CSharpTranslator::translateType(const Type &cppType) +{ + // Type map for standard C++ types + // Mappings from SplashKit Translator: src/translators/csharp.rb + const std::unordered_map typeMap = { + {"int", "int"}, + {"float", "float"}, + {"double", "double"}, + {"_Bool", "bool"}, + {"char", "char"}, + {"std::string", "string"}, + {"int8_t", "byte"}, + {"short", "short"}, + {"int64_t", "long"}, + {"byte", "byte"}, + {"unsigned char", "byte"}, + {"unsigned int", "uint"}, + {"unsigned short", "ushort"}, + {"nullptr_t", "null"}, + {"vector", "List"}, + {"time_point", "long"}, + {"long long", "long"}, + {"rep", "long"} + }; + + std::string result; + + auto it = typeMap.find(cppType.name); + + // If it's a known type, convert it + if (it != typeMap.end()) + result += it->second; + // If it's void, leave it + else if (cppType.name == "void") + result += cppType.name; + // Otherwise convert to pascal case + else + result += toPascalCase(cppType.name); + + // If there are template arguments, add them + const auto &args = cppType.templateArguments; + if (!args.empty() && cppType.name != "time_point") + { + result += "<"; + for (unsigned i = 0; i < args.size(); ++i) + { + if (i > 0) result += ", "; + result += translateType(args[i]); + } + result += ">"; + } + + return result; +} + diff --git a/tools/transpiler/src/translator.h b/tools/transpiler/src/translator.h new file mode 100644 index 00000000..3c8dd988 --- /dev/null +++ b/tools/transpiler/src/translator.h @@ -0,0 +1,83 @@ +#pragma once + +#include +#include +#include + +#include "ast.h" + +namespace fs = std::filesystem; + +class Translator +{ +public: + virtual void translateASTSet( + const std::vector &ASTs, + const fs::path &outputDir) = 0; +}; + +class CSharpTranslator : public Translator +{ +private: + // Global declaration management + const std::string globalClassName = "GeneratedGlobals"; + std::unordered_set globals; + + bool isNumericType(const std::string &type); + + // Indentation helpers + int indentLevel = 0; + std::string indt() { + std::string result; + for (int i = 0; i < indentLevel; i++) + result += " "; + return result; + } + void increaseIndent() { indentLevel++; } + void decreaseIndent() { indentLevel--; } + + // Write functions (write the AST elements to an external file) + void writeProjectFiles(const fs::path &outputDir, const std::string &projectName); + void writeAST(const CustomAST &AST, const std::string &projectName, const fs::path &outputFilepath); + void writeGlobals(const CustomAST &AST, std::ofstream &file); + void writeTestCase(const TestCase &testCase, std::ofstream &file, std::string category); + void writeSection(const Section §ion, std::ofstream &file, std::string defaultName); + void writeStatement(const Statement &statement, std::ofstream &file); + void writeForStmt(const ForStatement &forStmt, std::ofstream &file); + void writeBody(const std::vector> &body, std::ofstream &file); + void writeFunctionDecl(const FunctionDeclaration &funcDecl, std::ofstream &file); + + // Translation functions (return a string to be written inline) + std::string translateParameter(const Parameter ¶meter); + std::string translateVarDecl(const VariableDeclaration &varDecl, std::ofstream &file); + std::string translateExpression(const Expression &expr); + std::string translateBinaryExpr(const BinaryExpression &expr); + std::string translateUnaryExpr(const UnaryExpression &expr); + std::string translateCallExpr(const CallExpression &expr); + std::string translateRefExpr(const ReferenceExpression &expr); + std::string translateLiteralExpr(const LiteralExpression &expr); + std::string translateConstructorExpr(const ConstructorExpression &expr); + std::string translateIndexExpr(const IndexExpression &expr); + std::string translateInitListExpr(const InitListExpression &expr); + std::string translateMemberExpr(const MemberExpression &expr); + std::string translateAssertion(const AssertionStatement &assertion); + std::string translateStatement(const Statement &statement); + std::string translateVarDeclStmt(const VariableDeclarationStatement &varDecl); + std::string translateReturnStmt(const ReturnStatement &returnStmt); + std::string translateExprStmt(const ExpressionStatement &exprStmt); + std::string translateCallArguments(const CallExpression &expr); + std::string translateMemberCall(const CallExpression &reference); + std::string translateVectorMember(const CallExpression &expr); + + // String manipulation + std::string toLowerCase(const std::string &name); + std::string toCamelCase(const std::string &name); + std::string toPascalCase(const std::string &name); + std::string translateType(const Type &cppType); + std::string escapeCSharpString(const std::string &value); + +public: + void translateASTSet( + const std::vector &ASTs, + const fs::path &outputDir) override; +};