diff --git a/CMakeLists.txt b/CMakeLists.txt index 72936465..45e41443 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -155,6 +155,7 @@ endif() # C++ transpiler if (LIBSCRATCHCPP_CODEGEN_BACKEND STREQUAL "cpp") target_compile_definitions(scratchcpp PRIVATE CODEGEN_BACKEND_CPP) + add_subdirectory(transpiler) endif() # Macros diff --git a/src/engine/internal/codegen/cpp/CMakeLists.txt b/src/engine/internal/codegen/cpp/CMakeLists.txt index bf3e3434..e1499830 100644 --- a/src/engine/internal/codegen/cpp/CMakeLists.txt +++ b/src/engine/internal/codegen/cpp/CMakeLists.txt @@ -12,4 +12,8 @@ target_sources(scratchcpp cppconstant.h cppcodebuilder.cpp cppcodebuilder.h + cppcoroutine.h + cppprelude.h + cppcodebasegenerator.cpp + cppcodebasegenerator.h ) diff --git a/src/engine/internal/codegen/cpp/cppcodebasegenerator.cpp b/src/engine/internal/codegen/cpp/cppcodebasegenerator.cpp new file mode 100644 index 00000000..45892fea --- /dev/null +++ b/src/engine/internal/codegen/cpp/cppcodebasegenerator.cpp @@ -0,0 +1,190 @@ +// SPDX-License-Identifier: Apache-2.0 + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "cppcodebasegenerator.h" +#include "cppcompilercontext.h" + +using namespace libscratchcpp; + +namespace +{ + +bool writeFile(const std::filesystem::path &path, const std::string &content) +{ + std::ofstream file(path, std::ios::binary); + + if (!file.is_open()) { + std::cerr << "error: failed to write " << path.string() << std::endl; + return false; + } + + file << content; + return true; +} + +} // namespace + +/*! + * Compiles the given project using the C++ backend and writes a CMake project to outputDir: + * - project.cpp - the generated code of all scripts + * - project_data.cpp - the embedded project file + * - main.cpp - loads and runs the embedded project + * - CMakeLists.txt + */ +bool CppCodebaseGenerator::generate(const std::string &projectFile, const std::string &outputDir, const std::string &projectName) +{ + std::ifstream input(projectFile, std::ios::binary); + + if (!input.is_open()) { + std::cerr << "error: failed to read " << projectFile << std::endl; + return false; + } + + std::stringstream data; + data << input.rdbuf(); + + // The code builder reports the source of every compiled script + std::map sources; + CppCompilerContext::setSourceCallback([&sources](uint64_t hash, const std::string &source) { sources[hash] = source; }); + + std::stringstream code; + bool ret; + + { + Project project(projectFile); + ret = project.load(); + } + + CppCompilerContext::setSourceCallback(nullptr); + + if (ret) { + code << CppCompilerContext::sourceHeader(); + + for (const auto &[hash, source] : sources) + code << source << "\n"; + } + + if (!ret) + return false; + + std::filesystem::path dir(outputDir); + std::error_code err; + std::filesystem::create_directories(dir, err); + + return writeFile(dir / "project.cpp", code.str()) && writeFile(dir / "project_data.cpp", dataSource(data.str())) && writeFile(dir / "main.cpp", mainSource()) && + writeFile(dir / "CMakeLists.txt", cmakeSource(projectName)); +} + +std::string CppCodebaseGenerator::mainSource() +{ + return R"(// Generated by libscratchcpp, do not edit + +#include +#include +#include +#include + +extern const unsigned char scratchcpp_project_data[]; +extern const unsigned long long scratchcpp_project_size; + +int main(int argc, char **argv) +{ + // libscratchcpp loads projects from files, so the embedded project is extracted first + std::filesystem::path path = std::filesystem::temp_directory_path() / ("scratchcpp_project_" + std::to_string(std::hash()(argv[0])) + ".sb3"); + + { + std::ofstream file(path, std::ios::binary); + file.write(reinterpret_cast(scratchcpp_project_data), scratchcpp_project_size); + } + + // The C++ backend finds the generated code of each script in project.cpp + libscratchcpp::Project project(path.string()); + bool loaded = project.load(); + std::error_code err; + std::filesystem::remove(path, err); + + if (!loaded) + return 1; + + project.run(); + return 0; +} +)"; +} + +std::string CppCodebaseGenerator::dataSource(const std::string &data) +{ + std::stringstream ss; + ss << "// Generated by libscratchcpp, do not edit\n\n"; + ss << "extern const unsigned long long scratchcpp_project_size = " << data.size() << "ULL;\n\n"; + ss << "extern const unsigned char scratchcpp_project_data[] = {"; + + for (size_t i = 0; i < data.size(); i++) { + if (i % 16 == 0) + ss << "\n "; + + ss << " 0x" << std::hex << std::setw(2) << std::setfill('0') << (int)(unsigned char)data[i] << ","; + } + + ss << std::dec << "\n 0x00\n};\n"; + return ss.str(); +} + +std::string CppCodebaseGenerator::cmakeSource(const std::string &projectName) +{ + std::string ret = R"(# Generated by libscratchcpp +cmake_minimum_required(VERSION 3.14) +project(@NAME@ LANGUAGES CXX) + +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +# Use either libscratchcpp sources (LIBSCRATCHCPP_SOURCE_DIR) or a prebuilt library (LIBSCRATCHCPP_INCLUDE_DIR and LIBSCRATCHCPP_LIBRARY) +set(LIBSCRATCHCPP_SOURCE_DIR "" CACHE PATH "libscratchcpp source directory") +set(LIBSCRATCHCPP_INCLUDE_DIR "" CACHE PATH "libscratchcpp include directory") +set(LIBSCRATCHCPP_LIBRARY "" CACHE FILEPATH "libscratchcpp library") + +add_executable(@NAME@ main.cpp project.cpp project_data.cpp) + +if(LIBSCRATCHCPP_SOURCE_DIR) + set(LIBSCRATCHCPP_BUILD_UNIT_TESTS OFF CACHE BOOL "" FORCE) + set(LIBSCRATCHCPP_CODEGEN_BACKEND "cpp" CACHE STRING "" FORCE) + add_subdirectory(${LIBSCRATCHCPP_SOURCE_DIR} libscratchcpp) + target_link_libraries(@NAME@ PRIVATE scratchcpp) +elseif(LIBSCRATCHCPP_INCLUDE_DIR AND LIBSCRATCHCPP_LIBRARY) + target_include_directories(@NAME@ PRIVATE ${LIBSCRATCHCPP_INCLUDE_DIR}) + target_link_libraries(@NAME@ PRIVATE ${LIBSCRATCHCPP_LIBRARY}) +else() + message(FATAL_ERROR "Set LIBSCRATCHCPP_SOURCE_DIR or LIBSCRATCHCPP_INCLUDE_DIR and LIBSCRATCHCPP_LIBRARY") +endif() +)"; + std::string name = projectName; + + for (char &c : name) { + if (!std::isalnum((unsigned char)c) && c != '_' && c != '-') + c = '_'; + } + + size_t pos; + + while ((pos = ret.find("@NAME@")) != std::string::npos) + ret.replace(pos, 6, name); + + return ret; +} + +extern "C" +{ + LIBSCRATCHCPP_EXPORT bool cpp_generate_codebase(const char *projectFile, const char *outputDir, const char *projectName) + { + return CppCodebaseGenerator::generate(projectFile, outputDir, projectName); + } +} diff --git a/src/engine/internal/codegen/cpp/cppcodebasegenerator.h b/src/engine/internal/codegen/cpp/cppcodebasegenerator.h new file mode 100644 index 00000000..06cef71a --- /dev/null +++ b/src/engine/internal/codegen/cpp/cppcodebasegenerator.h @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: Apache-2.0 + +#pragma once + +#include + +#include "test_export.h" + +namespace libscratchcpp +{ + +/*! Generates a standalone C++ codebase (CMake project) from a Scratch project. */ +class LIBSCRATCHCPP_TEST_EXPORT CppCodebaseGenerator +{ + public: + static bool generate(const std::string &projectFile, const std::string &outputDir, const std::string &projectName = "project"); + + static std::string mainSource(); + static std::string dataSource(const std::string &data); + static std::string cmakeSource(const std::string &projectName); +}; + +} // namespace libscratchcpp diff --git a/src/engine/internal/codegen/cpp/cppcodebuilder.cpp b/src/engine/internal/codegen/cpp/cppcodebuilder.cpp index 482e0e2a..4749f74e 100644 --- a/src/engine/internal/codegen/cpp/cppcodebuilder.cpp +++ b/src/engine/internal/codegen/cpp/cppcodebuilder.cpp @@ -1,354 +1,1184 @@ // SPDX-License-Identifier: Apache-2.0 +#include +#include +#include +#include #include +#include +#include +#include +#include +#include #include "cppcodebuilder.h" +#include "cppcompilercontext.h" #include "cppexecutablecode.h" #include "cppconstant.h" #include "../typeutils.h" using namespace libscratchcpp; +static const std::string RETURN_PLACEHOLDER = "$RETURN("; +static const std::string CLASS_PLACEHOLDER = "$CLASS"; + CppCodeBuilder::CppCodeBuilder(CppCompilerContext *ctx, BlockPrototype *procedurePrototype, Compiler::CodeType codeType) : m_ctx(ctx), m_procedurePrototype(procedurePrototype), m_codeType(codeType) { + assert(m_ctx); } std::shared_ptr CppCodeBuilder::build() { - return std::make_shared(m_ctx); + // Create the return value of reporters and hat predicates + switch (m_codeType) { + case Compiler::CodeType::Script: + emitReturn(false); + break; + + case Compiler::CodeType::Reporter: { + assert(m_lastValue); + std::string v = m_lastValue ? cast(m_lastValue, Compiler::StaticType::Unknown) : ""; + + if (m_lastValue) { + emit("ValueData ret;"); + emit("value_init(&ret);"); + emit("value_assign_copy(&ret, &" + v + ");"); + emit("return ret;"); + } else { + emit("ValueData ret;"); + emit("value_init(&ret);"); + emit("return ret;"); + } + + break; + } + + case Compiler::CodeType::HatPredicate: + assert(m_lastValue); + emit("return " + (m_lastValue ? cast(m_lastValue, Compiler::StaticType::Bool) : std::string("false")) + ";"); + break; + } + + // Name generated classes after the hash of their source, so that they can be found when the code is built again + uint64_t hash = CppCompilerContext::hash(generateSource(CLASS_PLACEHOLDER)); + std::string className = CppCompilerContext::className(hash); + std::stringstream source; + source << "namespace\n{\n\n"; + source << generateSource(className) << "\n"; + source << "const bool " << className << "_registered = (CppCompilerContext::registerCode(\n"; + source << " 0x" << std::hex << hash << std::dec << "ULL,\n"; + source << " [](CppCompilerContext *ctx, const std::vector &pointers) -> std::shared_ptr {\n"; + source << " return std::make_shared<" << className << ">(ctx, pointers);\n"; + source << " }),\n"; + source << " true);\n\n"; + source << "} // namespace\n"; + CppCompilerContext::reportSource(hash, m_externs.str() + "\n" + source.str()); + + CppCompilerContext::CodeFactory factory = CppCompilerContext::findCode(hash); + std::shared_ptr code; + + if (factory) + code = factory(m_ctx, m_pointers); + else + code = std::make_shared(m_ctx, m_pointers); + + if (m_procedurePrototype) + m_ctx->addProcedure(m_procedurePrototype->procCode(), code); + + return code; } CompilerValue *CppCodeBuilder::addFunctionCall(const std::string &functionName, Compiler::StaticType returnType, const Compiler::ArgTypes &argTypes, const Compiler::Args &args) { - return addConstValue(Value()); + return createFunctionCall(functionName, returnType, argTypes, args, "", ""); } CompilerValue *CppCodeBuilder::addTargetFunctionCall(const std::string &functionName, Compiler::StaticType returnType, const Compiler::ArgTypes &argTypes, const Compiler::Args &args) { - return addConstValue(Value()); + return createFunctionCall(functionName, returnType, argTypes, args, "Target *", "target"); } CompilerValue *CppCodeBuilder::addFunctionCallWithCtx(const std::string &functionName, Compiler::StaticType returnType, const Compiler::ArgTypes &argTypes, const Compiler::Args &args) { - return addConstValue(Value()); + return createFunctionCall(functionName, returnType, argTypes, args, "ExecutionContext *", "ctx"); } CompilerConstant *CppCodeBuilder::addConstValue(const Value &value) { - auto constValue = std::make_shared(TypeUtils::mapType(value.type()), value); - auto genericValue = std::static_pointer_cast(constValue); - return static_cast(static_cast(addValue(genericValue, nullptr))); + Compiler::StaticType type = TypeUtils::mapType(value.type()); + auto constant = std::make_shared(type, value); + + switch (value.type()) { + case ValueType::Number: + constant->expr = numberLiteral(value.data().numberValue); + break; + + case ValueType::Bool: + constant->expr = value.toBool() ? "true" : "false"; + break; + + case ValueType::String: + constant->expr = addLocal(Storage::String, "c", stringLiteral(value.toString())) + ".s"; + break; + + case ValueType::Pointer: + constant->expr = pointer(const_cast(value.toPointer())); + break; + + default: + assert(false); + constant->expr = "0.0"; + break; + } + + m_values.push_back(constant); + m_lastValue = constant.get(); + return constant.get(); } CompilerValue *CppCodeBuilder::addStringChar(CompilerValue *string, CompilerValue *index) { - return addConstValue(Value()); + std::string str = cast(string, Compiler::StaticType::String); + std::string i = cast(index, Compiler::StaticType::Number); + addInstruction(CppInstruction::Type::StringChar); + CppValue *reg = addRegister(Compiler::StaticType::String); + emit("cpp::stringChar(" + reg->expr + ", " + str + ", " + i + ");"); + return reg; } CompilerValue *CppCodeBuilder::addStringLength(CompilerValue *string) { - return addConstValue(Value()); + return assignNumber(CppInstruction::Type::StringLength, "(double)" + cast(string, Compiler::StaticType::String) + "->size"); } CompilerValue *CppCodeBuilder::addLoopIndex() { - return addConstValue(Value()); + for (auto it = m_loops.rbegin(); it != m_loops.rend(); it++) { + if (it->isRepeatLoop) + return assignNumber(CppInstruction::Type::LoopIndex, it->index); + } + + return addConstValue(0); } CompilerValue *CppCodeBuilder::addLocalVariableValue(CompilerLocalVariable *variable) { - return addConstValue(Value()); + addInstruction(CppInstruction::Type::ReadLocalVariable); + CppValue *reg = addRegister(variable->type()); + assign(reg, variable->ptr()); + return reg; } CompilerValue *CppCodeBuilder::addVariableValue(Variable *variable) { - return addConstValue(Value()); + addInstruction(CppInstruction::Type::ReadVariable)->targetVariable = variable; + CppValue *reg = addRegister(Compiler::StaticType::Unknown); + emit("value_assign_copy(&" + reg->expr + ", " + variablePtr(variable) + ");"); + return reg; } CompilerValue *CppCodeBuilder::addListContents(List *list) { - return addConstValue(Value()); + addInstruction(CppInstruction::Type::GetListContents)->targetList = list; + CppValue *reg = addRegister(Compiler::StaticType::String); + emit(listPtr(list) + "->toStringPtr(" + reg->expr + ");"); + return reg; } CompilerValue *CppCodeBuilder::addListItem(List *list, CompilerValue *index) { - return addConstValue(Value()); + std::string i = cast(index, Compiler::StaticType::Number); + addInstruction(CppInstruction::Type::GetListItem)->targetList = list; + CppValue *reg = addRegister(Compiler::StaticType::Unknown); + emit("cpp::listItem(&" + reg->expr + ", " + listPtr(list) + ", " + i + ");"); + return reg; } CompilerValue *CppCodeBuilder::addListItemIndex(List *list, CompilerValue *item) { - return addConstValue(Value()); + std::string v = cast(item, Compiler::StaticType::Unknown); + CppValue *ret = assignNumber(CppInstruction::Type::GetListItemIndex, "cpp::listIndexOf(" + listPtr(list) + ", " + v + ")"); + m_instructions.last()->targetList = list; + return ret; } CompilerValue *CppCodeBuilder::addListContains(List *list, CompilerValue *item) { - return addConstValue(Value()); + std::string v = cast(item, Compiler::StaticType::Unknown); + CppValue *ret = assignBool(CppInstruction::Type::ListContainsItem, listPtr(list) + "->contains(" + v + ")"); + m_instructions.last()->targetList = list; + return ret; } CompilerValue *CppCodeBuilder::addListSize(List *list) { - return addConstValue(Value()); + CppValue *ret = assignNumber(CppInstruction::Type::GetListSize, "(double)" + listPtr(list) + "->size()"); + m_instructions.last()->targetList = list; + return ret; } CompilerValue *CppCodeBuilder::addProcedureArgument(const std::string &name) { - return addConstValue(Value()); + if (!m_procedurePrototype) + return addConstValue(Value()); + + const auto &names = m_procedurePrototype->argumentNames(); + const auto &types = m_procedurePrototype->argumentTypes(); + auto it = std::find(names.begin(), names.end(), name); + + if (it == names.end()) { + std::cout << "warning: could not find argument '" << name << "' in custom block '" << m_procedurePrototype->procCode() << "'" << std::endl; + return addConstValue(Value()); + } + + size_t index = it - names.begin(); + std::string arg = "args[" + std::to_string(index) + "]"; + + if (index < types.size() && types[index] == BlockPrototype::ArgType::Bool) + return assignBool(CppInstruction::Type::ProcedureArg, "value_toBool(&" + arg + ")"); + + addInstruction(CppInstruction::Type::ProcedureArg)->procedureArgIndex = index; + return addRegister(Compiler::StaticType::Unknown, arg); } CompilerValue *CppCodeBuilder::createAdd(CompilerValue *operand1, CompilerValue *operand2) { - return addConstValue(Value()); + return assignNumber(CppInstruction::Type::Add, cast(operand1, Compiler::StaticType::Number) + " + " + cast(operand2, Compiler::StaticType::Number)); } CompilerValue *CppCodeBuilder::createSub(CompilerValue *operand1, CompilerValue *operand2) { - return addConstValue(Value()); + return assignNumber(CppInstruction::Type::Sub, cast(operand1, Compiler::StaticType::Number) + " - " + cast(operand2, Compiler::StaticType::Number)); } CompilerValue *CppCodeBuilder::createMul(CompilerValue *operand1, CompilerValue *operand2) { - return addConstValue(Value()); + return assignNumber(CppInstruction::Type::Mul, cast(operand1, Compiler::StaticType::Number) + " * " + cast(operand2, Compiler::StaticType::Number)); } CompilerValue *CppCodeBuilder::createDiv(CompilerValue *operand1, CompilerValue *operand2) { - return addConstValue(Value()); + return assignNumber(CppInstruction::Type::Div, cast(operand1, Compiler::StaticType::Number) + " / " + cast(operand2, Compiler::StaticType::Number)); } CompilerValue *CppCodeBuilder::createRandom(CompilerValue *from, CompilerValue *to) { - return addConstValue(Value()); + // Strings can contain decimal numbers which are integers ("1.0"), so pass them as values + auto isNumeric = [](CompilerValue *v) { return storage(v->type()) != Storage::Unknown && storage(v->type()) != Storage::String; }; + Compiler::StaticType type = isNumeric(from) && isNumeric(to) ? Compiler::StaticType::Number : Compiler::StaticType::Unknown; + return assignNumber(CppInstruction::Type::Random, "cpp::random(ctx, " + cast(from, type) + ", " + cast(to, type) + ")"); } CompilerValue *CppCodeBuilder::createRandomInt(CompilerValue *from, CompilerValue *to) { - return addConstValue(Value()); + return assignNumber(CppInstruction::Type::RandomInt, "cpp::randomInt(ctx, " + cast(from, Compiler::StaticType::Number) + ", " + cast(to, Compiler::StaticType::Number) + ")"); } CompilerValue *CppCodeBuilder::createCmpEQ(CompilerValue *operand1, CompilerValue *operand2) { - return addConstValue(Value()); + return assignBool(CppInstruction::Type::CmpEQ, "value_equals(&" + cast(operand1, Compiler::StaticType::Unknown) + ", &" + cast(operand2, Compiler::StaticType::Unknown) + ")"); } CompilerValue *CppCodeBuilder::createCmpGT(CompilerValue *operand1, CompilerValue *operand2) { - return addConstValue(Value()); + return assignBool(CppInstruction::Type::CmpGT, "value_greater(&" + cast(operand1, Compiler::StaticType::Unknown) + ", &" + cast(operand2, Compiler::StaticType::Unknown) + ")"); } CompilerValue *CppCodeBuilder::createCmpLT(CompilerValue *operand1, CompilerValue *operand2) { - return addConstValue(Value()); + return assignBool(CppInstruction::Type::CmpLT, "value_lower(&" + cast(operand1, Compiler::StaticType::Unknown) + ", &" + cast(operand2, Compiler::StaticType::Unknown) + ")"); } CompilerValue *CppCodeBuilder::createStrCmpEQ(CompilerValue *string1, CompilerValue *string2, bool caseSensitive) { - return addConstValue(Value()); + std::string func = caseSensitive ? "string_compare_case_sensitive" : "string_compare_case_insensitive"; + auto type = caseSensitive ? CppInstruction::Type::StrCmpEQCS : CppInstruction::Type::StrCmpEQCI; + return assignBool(type, func + "(" + cast(string1, Compiler::StaticType::String) + ", " + cast(string2, Compiler::StaticType::String) + ") == 0"); } CompilerValue *CppCodeBuilder::createAnd(CompilerValue *operand1, CompilerValue *operand2) { - return addConstValue(Value()); + return assignBool(CppInstruction::Type::And, cast(operand1, Compiler::StaticType::Bool) + " && " + cast(operand2, Compiler::StaticType::Bool)); } CompilerValue *CppCodeBuilder::createOr(CompilerValue *operand1, CompilerValue *operand2) { - return addConstValue(Value()); + return assignBool(CppInstruction::Type::Or, cast(operand1, Compiler::StaticType::Bool) + " || " + cast(operand2, Compiler::StaticType::Bool)); } CompilerValue *CppCodeBuilder::createNot(CompilerValue *operand) { - return addConstValue(Value()); + return assignBool(CppInstruction::Type::Not, "!" + cast(operand, Compiler::StaticType::Bool)); } CompilerValue *CppCodeBuilder::createMod(CompilerValue *num1, CompilerValue *num2) { - return addConstValue(Value()); + return assignNumber(CppInstruction::Type::Mod, "cpp::mod(" + cast(num1, Compiler::StaticType::Number) + ", " + cast(num2, Compiler::StaticType::Number) + ")"); } CompilerValue *CppCodeBuilder::createRound(CompilerValue *num) { - return addConstValue(Value()); + return mathFunction(CppInstruction::Type::Round, num, "cpp::round"); } CompilerValue *CppCodeBuilder::createAbs(CompilerValue *num) { - return addConstValue(Value()); + return mathFunction(CppInstruction::Type::Abs, num, "std::fabs"); } CompilerValue *CppCodeBuilder::createFloor(CompilerValue *num) { - return addConstValue(Value()); + return mathFunction(CppInstruction::Type::Floor, num, "std::floor"); } CompilerValue *CppCodeBuilder::createCeil(CompilerValue *num) { - return addConstValue(Value()); + return mathFunction(CppInstruction::Type::Ceil, num, "std::ceil"); } CompilerValue *CppCodeBuilder::createSqrt(CompilerValue *num) { - return addConstValue(Value()); + return mathFunction(CppInstruction::Type::Sqrt, num, "std::sqrt"); } CompilerValue *CppCodeBuilder::createSin(CompilerValue *num) { - return addConstValue(Value()); + return mathFunction(CppInstruction::Type::Sin, num, "cpp::sin"); } CompilerValue *CppCodeBuilder::createCos(CompilerValue *num) { - return addConstValue(Value()); + return mathFunction(CppInstruction::Type::Cos, num, "cpp::cos"); } CompilerValue *CppCodeBuilder::createTan(CompilerValue *num) { - return addConstValue(Value()); + return mathFunction(CppInstruction::Type::Tan, num, "cpp::tan"); } CompilerValue *CppCodeBuilder::createAsin(CompilerValue *num) { - return addConstValue(Value()); + return mathFunction(CppInstruction::Type::Asin, num, "std::asin", " * 180.0 / cpp::PI + 0.0"); } CompilerValue *CppCodeBuilder::createAcos(CompilerValue *num) { - return addConstValue(Value()); + return mathFunction(CppInstruction::Type::Acos, num, "std::acos", " * 180.0 / cpp::PI"); } CompilerValue *CppCodeBuilder::createAtan(CompilerValue *num) { - return addConstValue(Value()); + return mathFunction(CppInstruction::Type::Atan, num, "std::atan", " * 180.0 / cpp::PI"); } CompilerValue *CppCodeBuilder::createLn(CompilerValue *num) { - return addConstValue(Value()); + return mathFunction(CppInstruction::Type::Ln, num, "std::log"); } CompilerValue *CppCodeBuilder::createLog10(CompilerValue *num) { - return addConstValue(Value()); + return mathFunction(CppInstruction::Type::Log10, num, "std::log10"); } CompilerValue *CppCodeBuilder::createExp(CompilerValue *num) { - return addConstValue(Value()); + return mathFunction(CppInstruction::Type::Exp, num, "std::exp"); } CompilerValue *CppCodeBuilder::createExp10(CompilerValue *num) { - return addConstValue(Value()); + return assignNumber(CppInstruction::Type::Exp10, "std::pow(10.0, cpp::num(" + cast(num, Compiler::StaticType::Number) + "))"); } CompilerValue *CppCodeBuilder::createStringConcat(CompilerValue *string1, CompilerValue *string2) { - return addConstValue(Value()); + std::string str1 = cast(string1, Compiler::StaticType::String); + std::string str2 = cast(string2, Compiler::StaticType::String); + addInstruction(CppInstruction::Type::StringConcat); + CppValue *reg = addRegister(Compiler::StaticType::String); + emit("cpp::concat(" + reg->expr + ", " + str1 + ", " + str2 + ");"); + return reg; } CompilerValue *CppCodeBuilder::createSelect(CompilerValue *cond, CompilerValue *trueValue, CompilerValue *falseValue, Compiler::StaticType valueType) { - return addConstValue(Value()); + Storage s = storage(valueType); + Compiler::StaticType castType = s == Storage::Unknown ? Compiler::StaticType::Unknown : valueType; + std::string c = cast(cond, Compiler::StaticType::Bool); + std::string v1 = cast(trueValue, castType); + std::string v2 = cast(falseValue, castType); + addInstruction(CppInstruction::Type::Select); + CppValue *reg = addRegister(castType); + std::string select = "(" + c + " ? " + v1 + " : " + v2 + ")"; + + switch (s) { + case Storage::String: + emit("cpp::str(" + reg->expr + ", " + select + ");"); + break; + + case Storage::Unknown: + emit("cpp::val(&" + reg->expr + ", " + select + ");"); + break; + + default: + emit(reg->expr + " = " + select + ";"); + break; + } + + return reg; } CompilerLocalVariable *CppCodeBuilder::createLocalVariable(Compiler::StaticType type) { - // TODO: Complete this - CompilerValue *ptr = addConstValue(Value()); - auto var = std::make_shared(ptr); + addInstruction(CppInstruction::Type::CreateLocalVariable); + CppValue *reg = addRegister(type); + auto var = std::make_shared(reg); m_localVars.push_back(var); return var.get(); } void CppCodeBuilder::createLocalVariableWrite(CompilerLocalVariable *variable, CompilerValue *value) { + addInstruction(CppInstruction::Type::WriteLocalVariable); + assign(dynamic_cast(variable->ptr()), value); } void CppCodeBuilder::createVariableWrite(Variable *variable, CompilerValue *value) { + addInstruction(CppInstruction::Type::WriteVariable)->targetVariable = variable; + std::string v = dynamic_cast(value)->expr; + + if (storage(value->type()) == Storage::Pointer) + emit("value_assign_pointer(" + variablePtr(variable) + ", " + v + ");"); + else + emit("cpp::val(" + variablePtr(variable) + ", " + v + ");"); } void CppCodeBuilder::createListClear(List *list) { + addInstruction(CppInstruction::Type::ClearList)->targetList = list; + emit(listPtr(list) + "->clear();"); } void CppCodeBuilder::createListRemove(List *list, CompilerValue *index) { + std::string i = cast(index, Compiler::StaticType::Number); + addInstruction(CppInstruction::Type::RemoveListItem)->targetList = list; + const std::string &l = listPtr(list); + emit("if (cpp::listIndexInRange(" + l + ", " + i + "))"); + emit(" " + l + "->removeAt((size_t)" + i + ");"); } void CppCodeBuilder::createListAppend(List *list, CompilerValue *item) { + std::string v = cast(item, Compiler::StaticType::Unknown); + addInstruction(CppInstruction::Type::AppendToList)->targetList = list; + emit(listPtr(list) + "->append(" + v + ");"); } void CppCodeBuilder::createListInsert(List *list, CompilerValue *index, CompilerValue *item) { + std::string i = cast(index, Compiler::StaticType::Number); + std::string v = cast(item, Compiler::StaticType::Unknown); + addInstruction(CppInstruction::Type::InsertToList)->targetList = list; + const std::string &l = listPtr(list); + emit("if (cpp::listIndexInRange(" + l + ", " + i + ", true))"); + emit(" " + l + "->insert((size_t)" + i + ", " + v + ");"); } void CppCodeBuilder::createListReplace(List *list, CompilerValue *index, CompilerValue *item) { + std::string i = cast(index, Compiler::StaticType::Number); + std::string v = cast(item, Compiler::StaticType::Unknown); + addInstruction(CppInstruction::Type::ListReplace)->targetList = list; + const std::string &l = listPtr(list); + emit("if (cpp::listIndexInRange(" + l + ", " + i + "))"); + emit(" " + l + "->replace((size_t)" + i + ", " + v + ");"); } void CppCodeBuilder::beginIfStatement(CompilerValue *cond) { + std::string c = cast(cond, Compiler::StaticType::Bool); + addInstruction(CppInstruction::Type::BeginIf); + emit("if (" + c + ") {"); + indent(); } void CppCodeBuilder::beginElseBranch() { + addInstruction(CppInstruction::Type::BeginElse); + unindent(); + emit("} else {"); + indent(); } void CppCodeBuilder::endIf() { + addInstruction(CppInstruction::Type::EndIf); + unindent(); + emit("}"); } void CppCodeBuilder::beginRepeatLoop(CompilerValue *count) { + assert(!m_loopCondition); + std::string c = cast(count, Compiler::StaticType::Number); + addInstruction(CppInstruction::Type::BeginRepeatLoop); + std::string n = addLocal(Storage::Number, "n"); + std::string index = addLocal(Storage::Number, "i"); + + // Scratch rounds the count, a negative count skips the loop and infinity repeats forever + emit(n + " = " + c + ";"); + emit(n + " = " + n + " > 0 ? (std::isinf(" + n + ") ? " + n + " : std::round(" + n + ")) : 0;"); + emit("for (" + index + " = 0; std::isinf(" + n + ") || " + index + " < " + n + "; " + index + "++) {"); + indent(); + + Loop loop; + loop.isRepeatLoop = true; + loop.index = index; + m_loops.push_back(loop); } void CppCodeBuilder::beginWhileLoop(CompilerValue *cond) { + std::string c = cast(cond, Compiler::StaticType::Bool); + addInstruction(CppInstruction::Type::BeginWhileLoop); + + if (!m_loopCondition) { + emit("for (;;) {"); + indent(); + } + + emit("if (!" + c + ")"); + emit(" break;"); + m_loopCondition = false; + m_loops.push_back(Loop()); } void CppCodeBuilder::beginRepeatUntilLoop(CompilerValue *cond) { + std::string c = cast(cond, Compiler::StaticType::Bool); + addInstruction(CppInstruction::Type::BeginRepeatUntilLoop); + + if (!m_loopCondition) { + emit("for (;;) {"); + indent(); + } + + emit("if (" + c + ")"); + emit(" break;"); + m_loopCondition = false; + m_loops.push_back(Loop()); } void CppCodeBuilder::beginLoopCondition() { + assert(!m_loopCondition); + addInstruction(CppInstruction::Type::BeginLoopCondition); + + // The condition is evaluated at the beginning of every iteration + emit("for (;;) {"); + indent(); + m_loopCondition = true; } void CppCodeBuilder::endLoop() { + assert(!m_loops.empty()); + emitYield(); + addInstruction(CppInstruction::Type::EndLoop); + unindent(); + emit("}"); + m_loops.pop_back(); } void CppCodeBuilder::yield() { + emitYield(); } void CppCodeBuilder::createStop() { + addInstruction(CppInstruction::Type::Stop); + emitReturn(false); } void CppCodeBuilder::createThreadStop() { + addInstruction(CppInstruction::Type::ThreadStop); + emitReturn(m_codeType == Compiler::CodeType::Script); } void CppCodeBuilder::invalidateTarget() { + addInstruction(CppInstruction::Type::InvalidateTarget); + emit("resolve();"); } void CppCodeBuilder::createProcedureCall(BlockPrototype *prototype, const Compiler::Args &args) { + assert(prototype); + const auto &types = prototype->argumentTypes(); + assert(args.size() == types.size()); + std::vector values; + + for (size_t i = 0; i < args.size(); i++) + values.push_back(cast(args[i], types[i] == BlockPrototype::ArgType::Bool ? Compiler::StaticType::Bool : Compiler::StaticType::Unknown)); + + addInstruction(CppInstruction::Type::CallProcedure)->procedurePrototype = prototype; + std::string argArray = addLocal(Storage::Void, "pa", std::to_string(args.size())); + + for (size_t i = 0; i < args.size(); i++) { + std::string dst = "&" + argArray + ".v[" + std::to_string(i) + "]"; + + if (types[i] == BlockPrototype::ArgType::Bool) + emit("value_assign_bool(" + dst + ", " + values[i] + ");"); + else + emit("value_assign_copy(" + dst + ", &" + values[i] + ");"); + } + + std::string warp = isWarp() ? "true" : (m_procedurePrototype ? "warp" : "false"); + std::string coroutine = addLocal(Storage::Void, "pc"); + emit("if (CppExecutableCode *procedure = compilerContext()->procedure(" + stringLiteral(prototype->procCode()) + ")) {"); + indent(); + emit(coroutine + " = procedure->runProcedure(ctx, target, targetVariables, targetLists, " + warp + ", " + argArray + ".v);"); + + if (!isWarp() && !prototype->warp()) { + // Resume the procedure until it finishes + m_suspends = true; + emit("while (!" + coroutine + ".done()) {"); + emit(" co_await std::suspend_always{};"); + emit(" " + coroutine + ".resume();"); + emit("}"); + } + + emit("if (" + coroutine + ".endThread() || static_cast(ctx)->finished())"); + m_indent++; + emitReturn(true); + m_indent--; + emit(coroutine + " = CppCoroutine();"); + unindent(); + emit("}"); + emit("resolve();"); } void CppCodeBuilder::createPrint(CompilerValue *string) { + std::string str = cast(string, Compiler::StaticType::String); + addInstruction(CppInstruction::Type::Print); + emit("cpp::print(" + str + ");"); +} + +std::shared_ptr CppCodeBuilder::addInstruction(CppInstruction::Type type) +{ + auto ins = std::make_shared(type, m_loopCondition); + m_instructions.addInstruction(ins); + m_lastInstruction = ins; + return ins; } CppValue *CppCodeBuilder::addValue(std::shared_ptr value, std::shared_ptr ins) { value->instruction = ins; + + if (ins) + ins->functionReturnReg = value.get(); + m_values.push_back(value); + m_lastValue = value.get(); return value.get(); } + +CppValue *CppCodeBuilder::addRegister(Compiler::StaticType type) +{ + Storage s = storage(type); + std::string name = addLocal(s, "r"); + + if (s == Storage::String) + name += ".s"; + else if (s == Storage::Unknown) + name += ".v"; + + return addRegister(type, name); +} + +CppValue *CppCodeBuilder::addRegister(Compiler::StaticType type, const std::string &expr) +{ + auto reg = std::make_shared(type); + reg->expr = expr; + reg->isRawValue = TypeUtils::isSingleType(type); + return addValue(reg, m_lastInstruction); +} + +CppValue *CppCodeBuilder::assignNumber(CppInstruction::Type type, const std::string &expr) +{ + addInstruction(type); + CppValue *reg = addRegister(Compiler::StaticType::Number); + emit(reg->expr + " = " + expr + ";"); + return reg; +} + +CppValue *CppCodeBuilder::assignBool(CppInstruction::Type type, const std::string &expr) +{ + addInstruction(type); + CppValue *reg = addRegister(Compiler::StaticType::Bool); + emit(reg->expr + " = " + expr + ";"); + return reg; +} + +CppValue *CppCodeBuilder::mathFunction(CppInstruction::Type type, CompilerValue *num, const std::string &func, const std::string &suffix) +{ + return assignNumber(type, func + "(cpp::num(" + cast(num, Compiler::StaticType::Number) + "))" + suffix); +} + +CompilerValue *CppCodeBuilder::createFunctionCall( + const std::string &functionName, + Compiler::StaticType returnType, + const Compiler::ArgTypes &argTypes, + const Compiler::Args &args, + const std::string &firstArgType, + const std::string &firstArg) +{ + assert(argTypes.size() == args.size()); + // Functions are declared without parameters and cast to the actual type on call, + // so different signatures of the same function don't conflict in a translation unit + // Strings are returned through an output parameter + static const std::string STRING_RET_PLACEHOLDER = "$STRING_RET"; + const bool stringRet = storage(returnType) == Storage::String; + std::string type = (stringRet ? std::string("void") : cppType(returnType, false)) + " (*)("; + std::string call = "("; + bool first = true; + + if (stringRet) { + type += "StringPtr *"; + call += STRING_RET_PLACEHOLDER; + first = false; + } + + if (!firstArgType.empty()) { + if (!first) { + type += ", "; + call += ", "; + } + + type += firstArgType; + call += firstArg; + first = false; + } + + for (size_t i = 0; i < args.size(); i++) { + std::string v = cast(args[i], argTypes[i]); + + if (!first) { + type += ", "; + call += ", "; + } + + type += cppType(argTypes[i], true); + call += storage(argTypes[i]) == Storage::Unknown ? "&" + v : v; + first = false; + } + + type += ")"; + + // value_* and string_* functions are declared by libscratchcpp headers included in the prelude + if (functionName.rfind("value_", 0) == 0 || functionName.rfind("string_", 0) == 0) + call = functionName + call + ")"; + else { + call = "reinterpret_cast<" + type + ">(&" + functionName + ")" + call + ")"; + + if (m_declaredFunctions.insert(functionName).second) + m_externs << "extern \"C\" void " << functionName << "();\n"; + } + + auto ins = addInstruction(CppInstruction::Type::FunctionCall); + ins->functionName = functionName; + ins->functionTargetArg = (firstArg == "target"); + ins->functionCtxArg = (firstArg == "ctx"); + + switch (storage(returnType)) { + case Storage::Void: + emit(call + ";"); + return nullptr; + + case Storage::String: { + CppValue *reg = addRegister(returnType); + call.replace(call.find(STRING_RET_PLACEHOLDER), STRING_RET_PLACEHOLDER.size(), reg->expr); + emit(call + ";"); + return reg; + } + + case Storage::Unknown: { + // The returned value is owned by the caller + CppValue *reg = addRegister(returnType); + std::string local = reg->expr.substr(0, reg->expr.size() - 2); + emit(local + ".reset(" + call + ");"); + return reg; + } + + default: { + CppValue *reg = addRegister(returnType); + emit(reg->expr + " = " + call + ";"); + return reg; + } + } +} + +CppCodeBuilder::Storage CppCodeBuilder::storage(Compiler::StaticType type) +{ + switch (type) { + case Compiler::StaticType::Void: + return Storage::Void; + case Compiler::StaticType::Number: + return Storage::Number; + case Compiler::StaticType::Bool: + return Storage::Bool; + case Compiler::StaticType::String: + return Storage::String; + case Compiler::StaticType::Pointer: + return Storage::Pointer; + default: + return Storage::Unknown; + } +} + +std::string CppCodeBuilder::cppType(Compiler::StaticType type, bool arg) +{ + switch (storage(type)) { + case Storage::Void: + return "void"; + case Storage::Number: + return "double"; + case Storage::Bool: + return "bool"; + case Storage::String: + return arg ? "const StringPtr *" : "StringPtr *"; + case Storage::Pointer: + return "void *"; + case Storage::Unknown: + return arg ? "ValueData *" : "ValueData"; + } + + return "void"; +} + +std::string CppCodeBuilder::numberLiteral(double value) +{ + if (std::isnan(value)) + return "std::numeric_limits::quiet_NaN()"; + else if (std::isinf(value)) + return value > 0 ? "std::numeric_limits::infinity()" : "-std::numeric_limits::infinity()"; + + std::stringstream ss; + ss << std::setprecision(17) << value; + std::string ret = ss.str(); + + if (ret.find_first_of(".e") == std::string::npos) + ret += ".0"; + + return "(" + ret + ")"; +} + +std::string CppCodeBuilder::stringLiteral(const std::string &str) +{ + std::stringstream ss; + ss << '"'; + + for (unsigned char c : str) { + if (c == '"' || c == '\\' || c == '?' || c < 0x20 || c >= 0x7f) + ss << '\\' << std::oct << std::setw(3) << std::setfill('0') << (int)c << std::dec; + else + ss << c; + } + + ss << '"'; + return ss.str(); +} + +std::string CppCodeBuilder::addLocal(Storage storage, const std::string &prefix, const std::string &init) +{ + std::string name = prefix + std::to_string(m_localCount++); + const std::string indentation(12, ' '); + + switch (storage) { + case Storage::Number: + m_decls << indentation << "double " << name << " = 0;\n"; + break; + case Storage::Bool: + m_decls << indentation << "bool " << name << " = false;\n"; + break; + case Storage::String: + if (init.empty()) + m_decls << indentation << "cpp::Str " << name << ";\n"; + else + m_decls << indentation << "cpp::Str " << name << "{ " << init << " };\n"; + break; + case Storage::Pointer: + m_decls << indentation << "void *" << name << " = nullptr;\n"; + break; + case Storage::Unknown: + m_decls << indentation << "cpp::Val " << name << ";\n"; + break; + case Storage::Void: + // Procedure call (argument array or coroutine) + if (init.empty()) + m_decls << indentation << "CppCoroutine " << name << ";\n"; + else + m_decls << indentation << "cpp::Args<" << init << "> " << name << ";\n"; + break; + } + + return name; +} + +std::string CppCodeBuilder::cast(CompilerValue *value, Compiler::StaticType targetType) +{ + assert(value); + CppValue *cppValue = dynamic_cast(value); + assert(cppValue); + const std::string &expr = cppValue->expr; + Storage src = storage(value->type()); + Storage dst = storage(targetType); + + if (src == dst) + return expr; + + switch (dst) { + case Storage::Number: + return "cpp::num(" + expr + ")"; + + case Storage::Bool: + return "cpp::boolean(" + expr + ")"; + + case Storage::String: { + std::string tmp = addLocal(Storage::String, "t") + ".s"; + emit("cpp::str(" + tmp + ", " + expr + ");"); + return tmp; + } + + case Storage::Unknown: { + std::string tmp = addLocal(Storage::Unknown, "t") + ".v"; + + if (src == Storage::Pointer) + emit("value_assign_pointer(&" + tmp + ", " + expr + ");"); + else + emit("cpp::val(&" + tmp + ", " + expr + ");"); + + return tmp; + } + + default: + return expr; + } +} + +void CppCodeBuilder::assign(CppValue *dst, CompilerValue *src) +{ + std::string v = cast(src, dst->type()); + + switch (storage(dst->type())) { + case Storage::String: + emit("cpp::str(" + dst->expr + ", " + v + ");"); + break; + + case Storage::Unknown: + emit("cpp::val(&" + dst->expr + ", " + v + ");"); + break; + + default: + emit(dst->expr + " = " + v + ";"); + break; + } +} + +void CppCodeBuilder::emit(const std::string &line) +{ + m_body << std::string(m_indent * 4, ' ') << line << "\n"; +} + +void CppCodeBuilder::indent() +{ + m_indent++; +} + +void CppCodeBuilder::unindent() +{ + assert(m_indent > 3); + m_indent--; +} + +void CppCodeBuilder::emitYield(bool checkWarp) +{ + if (isWarp()) + return; + + addInstruction(CppInstruction::Type::Yield); + m_suspends = true; + + if (checkWarp && m_procedurePrototype) + emit("if (!warp)"); + else + emit("if (true)"); + + emit(" co_await std::suspend_always{};"); +} + +void CppCodeBuilder::emitReturn(bool endThread) +{ + switch (m_codeType) { + case Compiler::CodeType::Script: + emit(RETURN_PLACEHOLDER + (endThread ? "true" : "false") + ");"); + break; + + case Compiler::CodeType::Reporter: + emit("return ValueData{ { 0.0 }, ValueType::Number, 0 };"); + break; + + case Compiler::CodeType::HatPredicate: + emit("return false;"); + break; + } +} + +bool CppCodeBuilder::isWarp() const +{ + // Reporters and hat predicates can't be suspended + if (m_codeType != Compiler::CodeType::Script) + return true; + + return m_procedurePrototype && m_procedurePrototype->warp(); +} + +/*! Returns a pointer from the pointer table of the generated code (keeps the generated source independent of memory addresses). */ +std::string CppCodeBuilder::pointer(void *ptr) +{ + auto it = m_pointerIndexes.find(ptr); + size_t index; + + if (it == m_pointerIndexes.cend()) { + index = m_pointers.size(); + m_pointers.push_back(ptr); + m_pointerIndexes[ptr] = index; + } else + index = it->second; + + return "pointers()[" + std::to_string(index) + "]"; +} + +const std::string &CppCodeBuilder::variablePtr(Variable *variable) +{ + auto it = m_variables.find(variable); + + if (it != m_variables.cend()) + return it->second; + + Target *target = m_ctx->target(); + ValueData *data = &variable->valuePtr()->data(); + + // Variables of the target are read from the target's variable array, so that clones use their own variables + if (target && variable->target() == target) { + ValueData **variableData = target->variableData(); + const size_t count = target->variables().size(); + + for (size_t i = 0; i < count; i++) { + if (variableData[i] == data) { + std::string name = addLocal(Storage::Pointer, "v"); + m_resolve << " " << name << " = targetVariables[" << i << "];\n"; + return m_variables[variable] = "static_cast(" + name + ")"; + } + } + } + + return m_variables[variable] = "static_cast(" + pointer(data) + ")"; +} + +const std::string &CppCodeBuilder::listPtr(List *list) +{ + auto it = m_lists.find(list); + + if (it != m_lists.cend()) + return it->second; + + Target *target = m_ctx->target(); + + // Lists of the target are read from the target's list array, so that clones use their own lists + if (target && list->target() == target) { + List **listData = target->listData(); + const size_t count = target->lists().size(); + + for (size_t i = 0; i < count; i++) { + if (listData[i] == list) { + std::string name = addLocal(Storage::Pointer, "l"); + m_resolve << " " << name << " = targetLists[" << i << "];\n"; + return m_lists[list] = "static_cast(" + name + ")"; + } + } + } + + return m_lists[list] = "static_cast(" + pointer(list) + ")"; +} + +std::string CppCodeBuilder::generateSource(const std::string &className) const +{ + std::string signature; + bool coroutine = m_codeType == Compiler::CodeType::Script && m_suspends; + const std::string params = "ExecutionContext *ctx, Target *target, ValueData **targetVariables, List **targetLists"; + + switch (m_codeType) { + case Compiler::CodeType::Script: + if (m_procedurePrototype) + signature = "CppCoroutine runProcedure(" + params + ", bool warp, ValueData *args) override"; + else + signature = "CppCoroutine runScript(" + params + ") override"; + break; + + case Compiler::CodeType::Reporter: + signature = "ValueData runReporter(" + params + ") override"; + break; + + case Compiler::CodeType::HatPredicate: + signature = "bool runPredicate(" + params + ") override"; + break; + } + + std::string body = m_body.str(); + const std::string returnStr = coroutine ? "co_return (" : "return CppCoroutine("; + size_t pos = 0; + + while ((pos = body.find(RETURN_PLACEHOLDER, pos)) != std::string::npos) { + body.replace(pos, RETURN_PLACEHOLDER.size(), returnStr); + pos += returnStr.size(); + } + + std::stringstream ss; + ss << "class " << className << " : public CppExecutableCode\n"; + ss << "{\n"; + ss << " public:\n"; + ss << " using CppExecutableCode::CppExecutableCode;\n"; + ss << " using CppExecutableCode::runReporter;\n"; + ss << " using CppExecutableCode::runPredicate;\n\n"; + ss << " " << signature << "\n"; + ss << " {\n"; + ss << m_decls.str(); + ss << " auto resolve = [&]() {\n"; + ss << m_resolve.str(); + ss << " };\n"; + ss << " resolve();\n"; + ss << body; + ss << " }\n"; + + // Procedures can be also run directly as scripts + if (m_codeType == Compiler::CodeType::Script && m_procedurePrototype) { + ss << "\n CppCoroutine runScript(" << params << ") override\n"; + ss << " {\n"; + ss << " return runProcedure(ctx, target, targetVariables, targetLists, " << (m_procedurePrototype->warp() ? "true" : "false") << ", nullptr);\n"; + ss << " }\n"; + } + + ss << "};\n"; + return ss.str(); +} diff --git a/src/engine/internal/codegen/cpp/cppcodebuilder.h b/src/engine/internal/codegen/cpp/cppcodebuilder.h index ef08d824..4accc610 100644 --- a/src/engine/internal/codegen/cpp/cppcodebuilder.h +++ b/src/engine/internal/codegen/cpp/cppcodebuilder.h @@ -2,14 +2,18 @@ #pragma once +#include +#include +#include + #include "../icodebuilder.h" +#include "cppinstruction.h" +#include "cppinstructionlist.h" namespace libscratchcpp { class CppCompilerContext; -class CppValue; -class CppInstruction; class CppCodeBuilder : public ICodeBuilder { @@ -106,14 +110,82 @@ class CppCodeBuilder : public ICodeBuilder void createPrint(CompilerValue *string) override; private: + enum class Storage + { + Void, + Number, + Bool, + String, + Pointer, + Unknown + }; + + struct Loop + { + bool isRepeatLoop = false; + std::string index; + }; + + std::shared_ptr addInstruction(CppInstruction::Type type); CppValue *addValue(std::shared_ptr value, std::shared_ptr ins); + CppValue *addRegister(Compiler::StaticType type); + CppValue *addRegister(Compiler::StaticType type, const std::string &expr); + CppValue *assignNumber(CppInstruction::Type type, const std::string &expr); + CppValue *assignBool(CppInstruction::Type type, const std::string &expr); + CppValue *mathFunction(CppInstruction::Type type, CompilerValue *num, const std::string &func, const std::string &suffix = ""); + + CompilerValue *createFunctionCall( + const std::string &functionName, + Compiler::StaticType returnType, + const Compiler::ArgTypes &argTypes, + const Compiler::Args &args, + const std::string &firstArgType, + const std::string &firstArg); + + static Storage storage(Compiler::StaticType type); + static std::string cppType(Compiler::StaticType type, bool arg); + static std::string numberLiteral(double value); + static std::string stringLiteral(const std::string &str); + + std::string addLocal(Storage storage, const std::string &prefix, const std::string &init = ""); + std::string cast(CompilerValue *value, Compiler::StaticType targetType); + void assign(CppValue *dst, CompilerValue *src); + void emit(const std::string &line); + void indent(); + void unindent(); + void emitYield(bool checkWarp = true); + void emitReturn(bool endThread); + bool isWarp() const; + + std::string pointer(void *ptr); + const std::string &variablePtr(Variable *variable); + const std::string &listPtr(List *list); + std::string generateSource(const std::string &className) const; CppCompilerContext *m_ctx = nullptr; BlockPrototype *m_procedurePrototype = nullptr; Compiler::CodeType m_codeType = Compiler::CodeType::Script; + CppInstructionList m_instructions; std::vector> m_values; std::vector> m_localVars; + CppValue *m_lastValue = nullptr; + std::shared_ptr m_lastInstruction; + + std::stringstream m_decls; + std::stringstream m_body; + std::stringstream m_resolve; + std::stringstream m_externs; + std::unordered_set m_declaredFunctions; + std::unordered_map m_variables; + std::unordered_map m_lists; + std::unordered_map m_pointerIndexes; + std::vector m_pointers; + std::vector m_loops; + int m_localCount = 0; + int m_indent = 3; + bool m_loopCondition = false; + bool m_suspends = false; }; } // namespace libscratchcpp diff --git a/src/engine/internal/codegen/cpp/cppcompilercontext.cpp b/src/engine/internal/codegen/cpp/cppcompilercontext.cpp index 5837fa65..6ff9eacd 100644 --- a/src/engine/internal/codegen/cpp/cppcompilercontext.cpp +++ b/src/engine/internal/codegen/cpp/cppcompilercontext.cpp @@ -1,10 +1,92 @@ // SPDX-License-Identifier: Apache-2.0 +#include +#include + #include "cppcompilercontext.h" +#include "cppexecutablecode.h" using namespace libscratchcpp; +static std::unordered_map &codeRegistry() +{ + static std::unordered_map registry; + return registry; +} + +static CppCompilerContext::SourceCallback &sourceCallback() +{ + static CppCompilerContext::SourceCallback callback; + return callback; +} + CppCompilerContext::CppCompilerContext(IEngine *engine, Target *target, const std::unordered_set &unsafeConstants) : CompilerContext(engine, target, unsafeConstants) { } + +void CppCompilerContext::addProcedure(const std::string &procCode, std::shared_ptr code) +{ + m_procedures[procCode] = code; +} + +CppExecutableCode *CppCompilerContext::procedure(const std::string &procCode) const +{ + auto it = m_procedures.find(procCode); + return it == m_procedures.cend() ? nullptr : it->second.get(); +} + +/*! Returns the code which must be placed before generated sources. */ +std::string CppCompilerContext::sourceHeader() +{ + return "// Generated by libscratchcpp\n\n#include \n\nusing namespace libscratchcpp;\n\n"; +} + +/*! Returns the 64-bit FNV-1a hash of the given generated source. */ +uint64_t CppCompilerContext::hash(const std::string &source) +{ + uint64_t ret = 14695981039346656037ULL; + + for (unsigned char c : source) { + ret ^= c; + ret *= 1099511628211ULL; + } + + return ret; +} + +/*! Returns the name of the generated class with the given source hash. */ +std::string CppCompilerContext::className(uint64_t hash) +{ + std::stringstream ss; + ss << "Code_" << std::hex << std::setw(16) << std::setfill('0') << hash; + return ss.str(); +} + +/*! Registers generated code (called by generated code during static initialization). */ +void CppCompilerContext::registerCode(uint64_t hash, CodeFactory factory) +{ + codeRegistry()[hash] = factory; +} + +/*! Returns the factory of the generated code with the given source hash, or nullptr if it doesn't exist. */ +CppCompilerContext::CodeFactory CppCompilerContext::findCode(uint64_t hash) +{ + auto ®istry = codeRegistry(); + auto it = registry.find(hash); + return it == registry.cend() ? nullptr : it->second; +} + +/*! Sets a function which is called with the source of all built code (used to collect sources for codebase generation). */ +void CppCompilerContext::setSourceCallback(const SourceCallback &callback) +{ + sourceCallback() = callback; +} + +void CppCompilerContext::reportSource(uint64_t hash, const std::string &source) +{ + auto &callback = sourceCallback(); + + if (callback) + callback(hash, source); +} diff --git a/src/engine/internal/codegen/cpp/cppcompilercontext.h b/src/engine/internal/codegen/cpp/cppcompilercontext.h index 15b62cd8..015b6ada 100644 --- a/src/engine/internal/codegen/cpp/cppcompilercontext.h +++ b/src/engine/internal/codegen/cpp/cppcompilercontext.h @@ -3,16 +3,39 @@ #pragma once #include - -#include "test_export.h" +#include +#include +#include +#include namespace libscratchcpp { -class LIBSCRATCHCPP_TEST_EXPORT CppCompilerContext : public CompilerContext +class CppExecutableCode; + +class LIBSCRATCHCPP_EXPORT CppCompilerContext : public CompilerContext { public: + using CodeFactory = std::shared_ptr (*)(CppCompilerContext *ctx, const std::vector &pointers); + using SourceCallback = std::function; + CppCompilerContext(IEngine *engine, Target *target, const std::unordered_set &unsafeConstants = {}); + + void addProcedure(const std::string &procCode, std::shared_ptr code); + CppExecutableCode *procedure(const std::string &procCode) const; + + static std::string sourceHeader(); + static uint64_t hash(const std::string &source); + static std::string className(uint64_t hash); + + static void registerCode(uint64_t hash, CodeFactory factory); + static CodeFactory findCode(uint64_t hash); + + static void setSourceCallback(const SourceCallback &callback); + static void reportSource(uint64_t hash, const std::string &source); + + private: + std::unordered_map> m_procedures; }; } // namespace libscratchcpp diff --git a/src/engine/internal/codegen/cpp/cppcoroutine.h b/src/engine/internal/codegen/cpp/cppcoroutine.h new file mode 100644 index 00000000..6fea2f4c --- /dev/null +++ b/src/engine/internal/codegen/cpp/cppcoroutine.h @@ -0,0 +1,94 @@ +// SPDX-License-Identifier: Apache-2.0 + +#pragma once + +#include +#include +#include + +namespace libscratchcpp +{ + +/*! Return type of generated scripts and procedures. The returned value of co_return is the end thread flag. */ +class CppCoroutine +{ + public: + struct promise_type + { + CppCoroutine get_return_object() { return CppCoroutine(std::coroutine_handle::from_promise(*this)); } + std::suspend_never initial_suspend() noexcept { return {}; } + std::suspend_always final_suspend() noexcept { return {}; } + void return_value(bool endThread) { this->endThread = endThread; } + void unhandled_exception() { std::terminate(); } + + bool endThread = false; + }; + + using Handle = std::coroutine_handle; + + CppCoroutine() = default; + + /*! Creates a finished coroutine (used by code which doesn't need to be suspended). */ + explicit CppCoroutine(bool endThread) : + m_endThread(endThread) + { + } + + CppCoroutine(const CppCoroutine &) = delete; + + CppCoroutine(CppCoroutine &&other) noexcept : + m_handle(std::exchange(other.m_handle, nullptr)), + m_endThread(other.m_endThread) + { + } + + ~CppCoroutine() { destroy(); } + + CppCoroutine &operator=(const CppCoroutine &) = delete; + + CppCoroutine &operator=(CppCoroutine &&other) noexcept + { + if (this != &other) { + destroy(); + m_handle = std::exchange(other.m_handle, nullptr); + m_endThread = other.m_endThread; + } + + return *this; + } + + /*! Returns true if the coroutine is finished or there isn't any coroutine. */ + bool done() const { return !m_handle || m_handle.done(); } + + /*! Resumes the coroutine and returns true if it's finished. */ + bool resume() + { + if (!done()) + m_handle.resume(); + + return done(); + } + + /*! Returns true if the coroutine finished with a thread stop. */ + bool endThread() const { return m_handle ? (m_handle.done() && m_handle.promise().endThread) : m_endThread; } + + /*! Destroys the coroutine frame. */ + void destroy() + { + if (m_handle) { + m_handle.destroy(); + m_handle = nullptr; + } + } + + private: + explicit CppCoroutine(Handle handle) : + m_handle(handle) + { + } + + Handle m_handle = nullptr; + bool m_endThread = false; +}; + +} // namespace libscratchcpp diff --git a/src/engine/internal/codegen/cpp/cppexecutablecode.cpp b/src/engine/internal/codegen/cpp/cppexecutablecode.cpp index 2fc611f1..b1e6a6e2 100644 --- a/src/engine/internal/codegen/cpp/cppexecutablecode.cpp +++ b/src/engine/internal/codegen/cpp/cppexecutablecode.cpp @@ -1,6 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 #include +#include #include #include #include @@ -12,8 +13,9 @@ using namespace libscratchcpp; -CppExecutableCode::CppExecutableCode(CppCompilerContext *ctx) : - m_ctx(ctx) +CppExecutableCode::CppExecutableCode(CppCompilerContext *ctx, const std::vector &pointers) : + m_ctx(ctx), + m_pointers(pointers) { assert(m_ctx); } @@ -34,11 +36,23 @@ void CppExecutableCode::run(ExecutionContext *context) return; } - // TODO: Add support for coroutines - // TODO: Add support for end thread sentinel value - Target *target = ctx->thread()->target(); - runScript(context, target, target->variableData(), target->listData()); - ctx->setFinished(true); + CppCoroutine &coroutine = ctx->coroutine(); + + if (!coroutine.done()) { + if (coroutine.resume()) { + ctx->setCoroutine(CppCoroutine()); + ctx->setFinished(true); + } + } else { + Target *target = ctx->thread()->target(); + CppCoroutine ret = runScript(context, target, target->variableData(), target->listData()); + + // NOTE: The end thread flag only stops the code (procedures propagate it) + if (ret.done()) + ctx->setFinished(true); + else + ctx->setCoroutine(std::move(ret)); + } } ValueData CppExecutableCode::runReporter(ExecutionContext *context) @@ -56,7 +70,6 @@ bool CppExecutableCode::runPredicate(ExecutionContext *context) void CppExecutableCode::kill(ExecutionContext *context) { CppExecutionContext *ctx = getContext(context); - // TODO: Add support for coroutines // NOTE: Do not destroy coroutine, it will be destroyed later // (really bad things happen when a coroutine destroys itself...) // The code is finished and the coroutine won't be used again until reset() is called @@ -67,8 +80,7 @@ void CppExecutableCode::kill(ExecutionContext *context) void CppExecutableCode::reset(ExecutionContext *context) { CppExecutionContext *ctx = getContext(context); - - // TODO: Add support for coroutines + ctx->setCoroutine(CppCoroutine()); ctx->setFinished(false); ctx->setPromise(nullptr); } @@ -83,15 +95,24 @@ std::shared_ptr CppExecutableCode::createExecutionContext(Thre return std::make_shared(m_ctx, thread); } -void CppExecutableCode::runScript(ExecutionContext *ctx, Target *target, ValueData **targetVariables, List **targetLists) +CppCoroutine CppExecutableCode::runProcedure(ExecutionContext *ctx, Target *target, ValueData **targetVariables, List **targetLists, bool warp, ValueData *args) +{ + // Generated code overrides this + return CppCoroutine(false); +} + +CppCoroutine CppExecutableCode::runScript(ExecutionContext *ctx, Target *target, ValueData **targetVariables, List **targetLists) { // Generated code overrides this + return CppCoroutine(false); } ValueData CppExecutableCode::runReporter(ExecutionContext *ctx, Target *target, ValueData **targetVariables, List **targetLists) { // Generated code overrides this - return ValueData(); + ValueData ret; + value_init(&ret); + return ret; } bool CppExecutableCode::runPredicate(ExecutionContext *ctx, Target *target, ValueData **targetVariables, List **targetLists) @@ -100,6 +121,16 @@ bool CppExecutableCode::runPredicate(ExecutionContext *ctx, Target *target, Valu return false; } +CppCompilerContext *CppExecutableCode::compilerContext() const +{ + return m_ctx; +} + +const std::vector &CppExecutableCode::pointers() const +{ + return m_pointers; +} + CppExecutionContext *CppExecutableCode::getContext(ExecutionContext *context) { assert(dynamic_cast(context)); diff --git a/src/engine/internal/codegen/cpp/cppexecutablecode.h b/src/engine/internal/codegen/cpp/cppexecutablecode.h index 97ce1f0a..367fb6d3 100644 --- a/src/engine/internal/codegen/cpp/cppexecutablecode.h +++ b/src/engine/internal/codegen/cpp/cppexecutablecode.h @@ -3,8 +3,10 @@ #pragma once #include +#include +#include -#include "test_export.h" +#include "cppcoroutine.h" namespace libscratchcpp { @@ -14,10 +16,14 @@ class List; class CppCompilerContext; class CppExecutionContext; -class LIBSCRATCHCPP_TEST_EXPORT CppExecutableCode : public ExecutableCode +/*! + * Base class of generated C++ code. + * Generated classes override runScript(), runReporter(), runPredicate() and runProcedure(). + */ +class LIBSCRATCHCPP_EXPORT CppExecutableCode : public ExecutableCode { public: - CppExecutableCode(CppCompilerContext *ctx); + CppExecutableCode(CppCompilerContext *ctx, const std::vector &pointers = {}); virtual void run(ExecutionContext *context) override final; virtual ValueData runReporter(ExecutionContext *context) override final; @@ -29,15 +35,21 @@ class LIBSCRATCHCPP_TEST_EXPORT CppExecutableCode : public ExecutableCode virtual std::shared_ptr createExecutionContext(Thread *thread) const override final; + virtual CppCoroutine runProcedure(ExecutionContext *ctx, Target *target, ValueData **targetVariables, List **targetLists, bool warp, ValueData *args); + protected: - virtual void runScript(ExecutionContext *ctx, Target *target, ValueData **targetVariables, List **targetLists); + virtual CppCoroutine runScript(ExecutionContext *ctx, Target *target, ValueData **targetVariables, List **targetLists); virtual ValueData runReporter(ExecutionContext *ctx, Target *target, ValueData **targetVariables, List **targetLists); virtual bool runPredicate(ExecutionContext *ctx, Target *target, ValueData **targetVariables, List **targetLists); + CppCompilerContext *compilerContext() const; + const std::vector &pointers() const; + private: static CppExecutionContext *getContext(ExecutionContext *context); CppCompilerContext *m_ctx = nullptr; + std::vector m_pointers; }; } // namespace libscratchcpp diff --git a/src/engine/internal/codegen/cpp/cppexecutioncontext.cpp b/src/engine/internal/codegen/cpp/cppexecutioncontext.cpp index 3ed4b96d..c7e46405 100644 --- a/src/engine/internal/codegen/cpp/cppexecutioncontext.cpp +++ b/src/engine/internal/codegen/cpp/cppexecutioncontext.cpp @@ -19,3 +19,13 @@ void CppExecutionContext::setFinished(bool finished) { m_finished = finished; } + +CppCoroutine &CppExecutionContext::coroutine() +{ + return m_coroutine; +} + +void CppExecutionContext::setCoroutine(CppCoroutine &&coroutine) +{ + m_coroutine = std::move(coroutine); +} diff --git a/src/engine/internal/codegen/cpp/cppexecutioncontext.h b/src/engine/internal/codegen/cpp/cppexecutioncontext.h index 60886061..52f1fcdb 100644 --- a/src/engine/internal/codegen/cpp/cppexecutioncontext.h +++ b/src/engine/internal/codegen/cpp/cppexecutioncontext.h @@ -5,6 +5,7 @@ #include #include "test_export.h" +#include "cppcoroutine.h" namespace libscratchcpp { @@ -19,9 +20,13 @@ class LIBSCRATCHCPP_TEST_EXPORT CppExecutionContext : public ExecutionContext bool finished() const; void setFinished(bool finished); + CppCoroutine &coroutine(); + void setCoroutine(CppCoroutine &&coroutine); + private: CppCompilerContext *m_ctx = nullptr; bool m_finished = false; + CppCoroutine m_coroutine; }; } // namespace libscratchcpp diff --git a/src/engine/internal/codegen/cpp/cppprelude.h b/src/engine/internal/codegen/cpp/cppprelude.h new file mode 100644 index 00000000..c4e6b406 --- /dev/null +++ b/src/engine/internal/codegen/cpp/cppprelude.h @@ -0,0 +1,296 @@ +// SPDX-License-Identifier: Apache-2.0 + +// Runtime support for C++ code generated by CppCodeBuilder. + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "cppexecutablecode.h" +#include "cppcompilercontext.h" +#include "cppexecutioncontext.h" + +namespace libscratchcpp::cpp +{ + +static constexpr double PI = std::numbers::pi; + +struct Str +{ + Str() : + s(string_pool_new()) + { + // Pooled strings may contain old data + string_assign_cstring(s, ""); + } + + Str(const char *str) : + s(string_pool_new()) + { + string_assign_cstring(s, str); + } + + Str(const Str &) = delete; + ~Str() { string_pool_free(s); } + + void reset(StringPtr *str) + { + string_pool_free(s); + s = str; + } + + StringPtr *s; +}; + +struct Val +{ + Val() { value_init(&v); } + Val(const Val &) = delete; + ~Val() { value_free(&v); } + + void reset(const ValueData &data) + { + value_free(&v); + v = data; + } + + ValueData v; +}; + +// Procedure arguments +template +struct Args +{ + Args() + { + for (ValueData &value : v) + value_init(&value); + } + + Args(const Args &) = delete; + + ~Args() + { + for (ValueData &value : v) + value_free(&value); + } + + ValueData v[N > 0 ? N : 1]; +}; + +inline double num(double v) +{ + return std::isnan(v) ? 0 : v; +} + +inline double num(bool v) +{ + return v; +} + +inline double num(const StringPtr *v) +{ + return value_stringToDouble(v); +} + +inline double num(const ValueData &v) +{ + return value_toDouble(&v); +} + +inline bool boolean(double v) +{ + return v != 0 && !std::isnan(v); +} + +inline bool boolean(bool v) +{ + return v; +} + +inline bool boolean(const StringPtr *v) +{ + return value_stringToBool(v); +} + +inline bool boolean(const ValueData &v) +{ + return value_toBool(&v); +} + +inline void str(StringPtr *dst, double v) +{ + value_doubleToStringPtr(v, dst); +} + +inline void str(StringPtr *dst, bool v) +{ + string_assign_cstring(dst, v ? "true" : "false"); +} + +inline void str(StringPtr *dst, const StringPtr *v) +{ + string_assign(dst, v); +} + +inline void str(StringPtr *dst, const ValueData &v) +{ + value_toStringPtr(&v, dst); +} + +inline void val(ValueData *dst, double v) +{ + value_assign_double(dst, v); +} + +inline void val(ValueData *dst, bool v) +{ + value_assign_bool(dst, v); +} + +inline void val(ValueData *dst, const StringPtr *v) +{ + value_assign_stringPtr(dst, v); +} + +inline void val(ValueData *dst, const ValueData &v) +{ + value_assign_copy(dst, &v); +} + +inline double mod(double a, double b) +{ + double r = std::fmod(a, b); + return (r / b < 0.0) ? r + b : r; +} + +inline double round(double x) +{ + return x >= 0.0 ? std::round(x) : (x >= -0.5 ? -0.0 : std::floor(x + 0.5)); +} + +inline double sin(double x) +{ + return std::round(std::sin(x * PI / 180.0) * 1e10) / 1e10 + 0.0; +} + +inline double cos(double x) +{ + return std::round(std::cos(x * PI / 180.0) * 1e10) / 1e10; +} + +inline double tan(double x) +{ + double m = std::fmod(x, 360.0); + + if (m == -270.0 || m == 90.0) + return std::numeric_limits::infinity(); + else if (m == -90.0 || m == 270.0) + return -std::numeric_limits::infinity(); + + return std::round(std::tan(x * PI / 180.0) * 1e10) / 1e10 + 0.0; +} + +inline double randomValue(ExecutionContext *ctx, const ValueData *from, const ValueData *to) +{ + return value_isInt(from) && value_isInt(to) ? ctx->rng()->randint(value_toLong(from), value_toLong(to)) : ctx->rng()->randintDouble(value_toDouble(from), value_toDouble(to)); +} + +inline double randomInt(ExecutionContext *ctx, double from, double to) +{ + return ctx->rng()->randint((int64_t)from, (int64_t)to); +} + +inline void print(const StringPtr *string) +{ + std::cout << utf8::utf16to8(std::u16string(string->data, string->size)); +} + +inline double random(ExecutionContext *ctx, double from, double to) +{ + double sum = from + to; + + if (std::isinf(sum) || std::isnan(sum)) + return sum; + + ValueData a, b; + value_init(&a); + value_init(&b); + value_assign_double(&a, from); + value_assign_double(&b, to); + double ret = randomValue(ctx, &a, &b); + value_free(&a); + value_free(&b); + return ret; +} + +inline double random(ExecutionContext *ctx, const ValueData &from, const ValueData &to) +{ + double sum = value_toDouble(&from) + value_toDouble(&to); + + if (std::isinf(sum) || std::isnan(sum)) + return sum; + + return randomValue(ctx, &from, &to); +} + +inline void concat(StringPtr *dst, const StringPtr *a, const StringPtr *b) +{ + size_t size = a->size + b->size; + string_alloc(dst, size); + std::memmove(dst->data, a->data, a->size * sizeof(char16_t)); + std::memmove(dst->data + a->size, b->data, b->size * sizeof(char16_t)); + dst->data[size] = 0; + dst->size = size; +} + +inline void stringChar(StringPtr *dst, const StringPtr *s, double index) +{ + string_alloc(dst, 1); + bool inRange = index >= 0 && index < s->size; + dst->data[0] = inRange ? s->data[(size_t)index] : 0; + dst->data[1] = 0; + dst->size = inRange ? 1 : 0; +} + +inline bool listIndexInRange(List *list, double index, bool allowEnd = false) +{ + return index >= 0 && (allowEnd ? index <= list->size() : index < list->size()); +} + +inline void listItem(ValueData *dst, List *list, double index) +{ + if (listIndexInRange(list, index)) + value_assign_copy(dst, &(*list)[(size_t)index]); + else + value_assign_cstring(dst, ""); +} + +inline double listIndexOf(List *list, const ValueData &item) +{ + size_t index = list->indexOf(item); + return index == (size_t)-1 ? -1.0 : (double)index; +} + +inline Stage *stage(ExecutionContext *ctx) +{ + return ctx->engine()->stage(); +} + +} // namespace libscratchcpp::cpp diff --git a/src/engine/internal/codegen/cpp/cppvalue.h b/src/engine/internal/codegen/cpp/cppvalue.h index 77f6d782..1f6fc129 100644 --- a/src/engine/internal/codegen/cpp/cppvalue.h +++ b/src/engine/internal/codegen/cpp/cppvalue.h @@ -4,6 +4,7 @@ #include #include +#include namespace libscratchcpp { @@ -24,6 +25,7 @@ struct CppValue : public virtual CompilerValue } bool isRawValue = false; + std::string expr; // C++ expression of the value in generated code std::shared_ptr instruction; }; diff --git a/src/scratch/string_functions.cpp b/src/scratch/string_functions.cpp index c9767179..37b4bdfe 100644 --- a/src/scratch/string_functions.cpp +++ b/src/scratch/string_functions.cpp @@ -2,6 +2,7 @@ #include #include +#include #include #include #include diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 44b15bf6..97f62868 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -1,7 +1,7 @@ cmake_minimum_required(VERSION 3.14) project(libscratchcpp_test) -set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) add_compile_definitions(DATA_DIR="${CMAKE_CURRENT_SOURCE_DIR}") diff --git a/test/codegen/CMakeLists.txt b/test/codegen/CMakeLists.txt index 26ddb465..ad0298ac 100644 --- a/test/codegen/CMakeLists.txt +++ b/test/codegen/CMakeLists.txt @@ -24,9 +24,7 @@ add_executable( code_analyzer/mixed_type_analysis.cpp ) -if (NOT LIBSCRATCHCPP_CODEGEN_BACKEND STREQUAL "cpp") - # TODO: Move this to the executable sources - target_sources(codegen_test PUBLIC +set(CODEBUILDER_TEST_SOURCES codebuilder_test.cpp operators/equal_comparison_test.cpp operators/greater_than_test.cpp @@ -61,7 +59,49 @@ if (NOT LIBSCRATCHCPP_CODEGEN_BACKEND STREQUAL "cpp") operators/string/string_concat_test.cpp operators/string/string_char_test.cpp operators/string/string_length_test.cpp +) + +target_sources(codegen_test PRIVATE ${CODEBUILDER_TEST_SOURCES}) + +if (LIBSCRATCHCPP_CODEGEN_BACKEND STREQUAL "cpp") + # Run the tests once to collect the C++ code generated by them, then compile it into the test executable + add_executable( + codegen_test_generator + cpp/generator_main.cpp + codegentestutils.cpp + codegentestutils.h + ${CODEBUILDER_TEST_SOURCES} ) + + target_link_libraries( + codegen_test_generator + GTest::gtest + GTest::gmock + scratchcpp + scratchcpp_mocks + codegen_test_functions + ) + + if (MSVC) + target_compile_options(codegen_test_generator PRIVATE /FI${CMAKE_CURRENT_SOURCE_DIR}/cpp/generator_asserts.h) + else() + target_compile_options(codegen_test_generator PRIVATE -include ${CMAKE_CURRENT_SOURCE_DIR}/cpp/generator_asserts.h) + endif() + + set(CODEGEN_TEST_GENERATED_SOURCES "") + + foreach(i RANGE 15) + list(APPEND CODEGEN_TEST_GENERATED_SOURCES ${CMAKE_CURRENT_BINARY_DIR}/generated_code_${i}.cpp) + endforeach() + + add_custom_command( + OUTPUT ${CODEGEN_TEST_GENERATED_SOURCES} + COMMAND codegen_test_generator ${CODEGEN_TEST_GENERATED_SOURCES} + DEPENDS codegen_test_generator + COMMENT "Generating C++ code for codegen tests" + ) + + target_sources(codegen_test PRIVATE ${CODEGEN_TEST_GENERATED_SOURCES}) endif() target_link_libraries( diff --git a/test/codegen/cpp/cppexecutablecodemock.h b/test/codegen/cpp/cppexecutablecodemock.h index 1d2063f6..6ebf8680 100644 --- a/test/codegen/cpp/cppexecutablecodemock.h +++ b/test/codegen/cpp/cppexecutablecodemock.h @@ -15,7 +15,7 @@ class CppExecutableCodeMock : public CppExecutableCode { } - MOCK_METHOD(void, runScript, (ExecutionContext * ctx, Target *, ValueData **, List **)); + MOCK_METHOD(CppCoroutine, runScript, (ExecutionContext * ctx, Target *, ValueData **, List **)); MOCK_METHOD(ValueData, runReporter, (ExecutionContext * ctx, Target *, ValueData **, List **)); MOCK_METHOD(bool, runPredicate, (ExecutionContext * ctx, Target *, ValueData **, List **)); }; diff --git a/test/codegen/cpp/generator_asserts.h b/test/codegen/cpp/generator_asserts.h new file mode 100644 index 00000000..c0556585 --- /dev/null +++ b/test/codegen/cpp/generator_asserts.h @@ -0,0 +1,38 @@ +// Included in all codegen_test_generator sources. +// The generator only collects the code built by the tests, so fatal assertions are +// turned into non-fatal ones to make sure all code in the tests is built. + +#pragma once + +#include +#include + +#undef ASSERT_TRUE +#undef ASSERT_FALSE +#undef ASSERT_EQ +#undef ASSERT_NE +#undef ASSERT_LT +#undef ASSERT_LE +#undef ASSERT_GT +#undef ASSERT_GE +#undef ASSERT_STREQ +#undef ASSERT_STRNE +#undef ASSERT_FLOAT_EQ +#undef ASSERT_DOUBLE_EQ +#undef ASSERT_NEAR +#undef ASSERT_THAT + +#define ASSERT_TRUE EXPECT_TRUE +#define ASSERT_FALSE EXPECT_FALSE +#define ASSERT_EQ EXPECT_EQ +#define ASSERT_NE EXPECT_NE +#define ASSERT_LT EXPECT_LT +#define ASSERT_LE EXPECT_LE +#define ASSERT_GT EXPECT_GT +#define ASSERT_GE EXPECT_GE +#define ASSERT_STREQ EXPECT_STREQ +#define ASSERT_STRNE EXPECT_STRNE +#define ASSERT_FLOAT_EQ EXPECT_FLOAT_EQ +#define ASSERT_DOUBLE_EQ EXPECT_DOUBLE_EQ +#define ASSERT_NEAR EXPECT_NEAR +#define ASSERT_THAT EXPECT_THAT diff --git a/test/codegen/cpp/generator_main.cpp b/test/codegen/cpp/generator_main.cpp new file mode 100644 index 00000000..295d7d78 --- /dev/null +++ b/test/codegen/cpp/generator_main.cpp @@ -0,0 +1,164 @@ +// Runs the code builder tests and writes the C++ code generated by them to the given files. +// The test results are ignored because the generated code isn't available yet. +// Tests run in a child process which is restarted if a test crashes (code after a failed +// assertion can crash), so that the code of all remaining tests is collected. + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace libscratchcpp; + +namespace +{ + +class ProgressListener : public testing::EmptyTestEventListener +{ + public: + ProgressListener(const std::filesystem::path &file) : + m_file(file, std::ios::app) + { + } + + void OnTestStart(const testing::TestInfo &info) override { write("S", info); } + void OnTestEnd(const testing::TestInfo &info) override { write("E", info); } + + private: + void write(const std::string &prefix, const testing::TestInfo &info) { m_file << prefix << " " << info.test_suite_name() << "." << info.name() << std::endl; } + + std::ofstream m_file; +}; + +int runChild(const std::filesystem::path &dir) +{ + std::ifstream excluded(dir / "excluded.txt"); + std::string line, filter = "-"; + + while (std::getline(excluded, line)) { + if (!line.empty()) + filter += line + ":"; + } + + if (filter.size() > 1) + GTEST_FLAG_SET(filter, filter); + + GMOCK_FLAG_SET(verbose, "error"); + + std::ofstream sources(dir / "sources.txt", std::ios::app | std::ios::binary); + + CppCompilerContext::setSourceCallback([&sources](uint64_t hash, const std::string &source) { + sources << hash << " " << source.size() << "\n" << source; + sources.flush(); + }); + + testing::TestEventListeners &listeners = testing::UnitTest::GetInstance()->listeners(); + delete listeners.Release(listeners.default_result_printer()); + listeners.Append(new ProgressListener(dir / "progress.txt")); + (void)RUN_ALL_TESTS(); + + std::ofstream(dir / "progress.txt", std::ios::app) << "DONE" << std::endl; + + // Skip static destructors (failed tests can leave mocks in a bad state) + std::_Exit(0); +} + +} // namespace + +int main(int argc, char **argv) +{ + testing::InitGoogleTest(&argc, argv); + + if (argc == 3 && std::string(argv[1]) == "--child") + return runChild(argv[2]); + + if (argc < 2) { + std::cerr << "usage: " << argv[0] << " " << std::endl; + return 1; + } + + const std::filesystem::path dir = std::filesystem::path(argv[1]).parent_path() / "codegen_test_generator_work"; + std::filesystem::remove_all(dir); + std::filesystem::create_directories(dir); + + std::set excluded; + bool done = false; + + while (!done) { + const std::string cmd = "\"" + std::string(argv[0]) + "\" --child \"" + dir.string() + "\""; + (void)std::system(cmd.c_str()); + + std::ifstream progress(dir / "progress.txt"); + std::set started, crashed; + std::string line; + + while (std::getline(progress, line)) { + if (line == "DONE") + done = true; + else if (line.rfind("S ", 0) == 0) + started.insert(line.substr(2)); + else if (line.rfind("E ", 0) == 0) { + started.erase(line.substr(2)); + excluded.insert(line.substr(2)); + } + } + + if (done) + break; + + size_t oldCount = excluded.size(); + excluded.insert(started.begin(), started.end()); + + if (excluded.size() == oldCount) { + std::cerr << "error: the test process failed outside of a test" << std::endl; + return 1; + } + + std::ofstream excludedFile(dir / "excluded.txt"); + + for (const std::string &test : excluded) + excludedFile << test << "\n"; + } + + std::map sources; + std::ifstream sourcesFile(dir / "sources.txt", std::ios::binary); + uint64_t hash; + size_t size; + + while (sourcesFile >> hash >> size) { + sourcesFile.get(); + std::string source(size, '\0'); + sourcesFile.read(source.data(), size); + sources[hash] = source; + } + + // The code is split into multiple files to reduce compile time and memory usage + const size_t fileCount = argc - 1; + std::vector files; + + for (size_t i = 0; i < fileCount; i++) { + files.emplace_back(argv[i + 1]); + + if (!files.back().is_open()) { + std::cerr << "error: could not open " << argv[i + 1] << std::endl; + return 1; + } + + files.back() << CppCompilerContext::sourceHeader(); + } + + size_t i = 0; + + for (const auto &[hash, source] : sources) + files[i++ % fileCount] << source << "\n"; + + files.clear(); + std::cout << "Generated " << sources.size() << " code units" << std::endl; + std::_Exit(0); +} diff --git a/transpiler/CMakeLists.txt b/transpiler/CMakeLists.txt new file mode 100644 index 00000000..9aaf0440 --- /dev/null +++ b/transpiler/CMakeLists.txt @@ -0,0 +1,2 @@ +add_executable(scratchcpp-transpiler main.cpp) +target_link_libraries(scratchcpp-transpiler PRIVATE scratchcpp) diff --git a/transpiler/main.cpp b/transpiler/main.cpp new file mode 100644 index 00000000..4bf0331f --- /dev/null +++ b/transpiler/main.cpp @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: Apache-2.0 + +// Converts a Scratch project to a standalone C++ codebase + +#include +#include + +extern "C" bool cpp_generate_codebase(const char *projectFile, const char *outputDir, const char *projectName); + +int main(int argc, char **argv) +{ + if (argc < 3 || argc > 4) { + std::cerr << "usage: " << argv[0] << " [project name]" << std::endl; + return 1; + } + + std::string name = argc == 4 ? argv[3] : "project"; + return cpp_generate_codebase(argv[1], argv[2], name.c_str()) ? 0 : 1; +}