From 60d5272207080f207246c6c7fb138853e08ceb40 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sat, 17 Oct 2026 11:04:27 +0000 Subject: [PATCH 1/3] Add C++ transpiler backend with build-time generated tests Co-Authored-By: zemanadam893@gmail.com --- src/engine/CMakeLists.txt | 1 + src/engine/internal/cpp/CMakeLists.txt | 25 + src/engine/internal/cpp/cppcodebuilder.cpp | 1033 +++++++++++++++++ src/engine/internal/cpp/cppcodebuilder.h | 190 +++ .../internal/cpp/cppcodebuilderfactory.cpp | 27 + .../internal/cpp/cppcodebuilderfactory.h | 24 + .../internal/cpp/cppcompilercontext.cpp | 132 +++ src/engine/internal/cpp/cppcompilercontext.h | 56 + src/engine/internal/cpp/cppexecutablecode.cpp | 144 +++ src/engine/internal/cpp/cppexecutablecode.h | 58 + .../internal/cpp/cppexecutioncontext.cpp | 38 + src/engine/internal/cpp/cppexecutioncontext.h | 29 + src/engine/internal/cpp/cppprelude.h | 329 ++++++ src/engine/internal/cpp/cppregister.h | 42 + src/engine/internal/cpp/cppruntime.cpp | 48 + src/engine/internal/cpp/cppruntime.h | 23 + test/CMakeLists.txt | 1 + test/cpp/CMakeLists.txt | 41 + test/cpp/cases.cpp | 347 ++++++ test/cpp/cases.h | 49 + test/cpp/cpp_test.cpp | 145 +++ test/cpp/generator.cpp | 36 + test/cpp/testfunctions.cpp | 54 + 23 files changed, 2872 insertions(+) create mode 100644 src/engine/internal/cpp/CMakeLists.txt create mode 100644 src/engine/internal/cpp/cppcodebuilder.cpp create mode 100644 src/engine/internal/cpp/cppcodebuilder.h create mode 100644 src/engine/internal/cpp/cppcodebuilderfactory.cpp create mode 100644 src/engine/internal/cpp/cppcodebuilderfactory.h create mode 100644 src/engine/internal/cpp/cppcompilercontext.cpp create mode 100644 src/engine/internal/cpp/cppcompilercontext.h create mode 100644 src/engine/internal/cpp/cppexecutablecode.cpp create mode 100644 src/engine/internal/cpp/cppexecutablecode.h create mode 100644 src/engine/internal/cpp/cppexecutioncontext.cpp create mode 100644 src/engine/internal/cpp/cppexecutioncontext.h create mode 100644 src/engine/internal/cpp/cppprelude.h create mode 100644 src/engine/internal/cpp/cppregister.h create mode 100644 src/engine/internal/cpp/cppruntime.cpp create mode 100644 src/engine/internal/cpp/cppruntime.h create mode 100644 test/cpp/CMakeLists.txt create mode 100644 test/cpp/cases.cpp create mode 100644 test/cpp/cases.h create mode 100644 test/cpp/cpp_test.cpp create mode 100644 test/cpp/generator.cpp create mode 100644 test/cpp/testfunctions.cpp diff --git a/src/engine/CMakeLists.txt b/src/engine/CMakeLists.txt index d4aa8ed52..873a8bc5a 100644 --- a/src/engine/CMakeLists.txt +++ b/src/engine/CMakeLists.txt @@ -45,3 +45,4 @@ target_sources(scratchcpp ) add_subdirectory(internal/llvm) +add_subdirectory(internal/cpp) diff --git a/src/engine/internal/cpp/CMakeLists.txt b/src/engine/internal/cpp/CMakeLists.txt new file mode 100644 index 000000000..212da5246 --- /dev/null +++ b/src/engine/internal/cpp/CMakeLists.txt @@ -0,0 +1,25 @@ +target_sources(scratchcpp + PRIVATE + cppprelude.h + cppruntime.cpp + cppruntime.h + cppcodebuilder.cpp + cppcodebuilder.h + cppcodebuilderfactory.cpp + cppcodebuilderfactory.h + cppregister.h + cppcompilercontext.cpp + cppcompilercontext.h + cppexecutablecode.cpp + cppexecutablecode.h + cppexecutioncontext.cpp + cppexecutioncontext.h +) + +# Embed the runtime prelude into the library, it's a part of the generated code +set(CPP_PRELUDE_HEADER ${CMAKE_CURRENT_BINARY_DIR}/cppprelude_source.h) +file(READ ${CMAKE_CURRENT_SOURCE_DIR}/cppprelude.h CPP_PRELUDE_CONTENT) +file(WRITE ${CPP_PRELUDE_HEADER} "// Generated from cppprelude.h\n#pragma once\n\nstatic const char *CPP_PRELUDE_SOURCE = R\"scpp_raw(${CPP_PRELUDE_CONTENT})scpp_raw\";\n") +set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/cppprelude.h) + +target_include_directories(scratchcpp PRIVATE ${CMAKE_CURRENT_BINARY_DIR}) diff --git a/src/engine/internal/cpp/cppcodebuilder.cpp b/src/engine/internal/cpp/cppcodebuilder.cpp new file mode 100644 index 000000000..80c65bd25 --- /dev/null +++ b/src/engine/internal/cpp/cppcodebuilder.cpp @@ -0,0 +1,1033 @@ +// SPDX-License-Identifier: Apache-2.0 + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "cppcodebuilder.h" +#include "cppcompilercontext.h" +#include "cppexecutablecode.h" +#include "cppregister.h" + +using namespace libscratchcpp; + +static const std::string FRAME_ARGS = "ExecutionContext *ctx, Target *target, ValueData **varData, List **listData"; +static const std::string FRAME_ARG_NAMES = "ctx, target, varData, listData"; + +CppCodeBuilder::CppCodeBuilder(CppCompilerContext *ctx, BlockPrototype *procedurePrototype, Compiler::CodeType codeType) : + m_ctx(ctx), + m_procedurePrototype(procedurePrototype), + m_codeType(codeType) +{ + assert(m_ctx); + m_functionName = m_ctx->createFunctionName(m_procedurePrototype); +} + +CppCodeBuilder::~CppCodeBuilder() +{ +} + +std::shared_ptr CppCodeBuilder::build() +{ + assert(m_loops.empty()); + const std::string frame = m_functionName + "_frame"; + std::stringstream ss; + + // Procedure arguments + std::stringstream argMembers, argParams, argNames, argInit, argTypes; + + if (m_procedurePrototype) { + const auto &types = m_procedurePrototype->argumentTypes(); + + for (size_t i = 0; i < types.size(); i++) { + std::string name = "a" + std::to_string(i); + std::string param = "p" + std::to_string(i); + + if (types[i] == BlockPrototype::ArgType::Bool) { + argMembers << " bool " << name << " = false;\n"; + argParams << ", bool " << param; + argTypes << ", bool"; + argInit << " " << name << " = " << param << ";\n"; + } else { + argMembers << " cpp::Val " << name << ";\n"; + argParams << ", const ValueData *" << param; + argTypes << ", const ValueData *"; + argInit << " cpp::val(&" << name << ".v, *" << param << ");\n"; + } + + argNames << ", " << param; + } + } + + // Frame + ss << "// " << (m_procedurePrototype ? "procedure " + m_procedurePrototype->procCode() : m_functionName) << "\n"; + ss << "struct " << frame << " final : public cpp::Frame\n{\n"; + ss << " " << frame << "(" << FRAME_ARGS << ", bool warp" << argParams.str() << ") :\n"; + ss << " cpp::Frame(ctx),\n target(target),\n varData(varData),\n listData(listData),\n warp(warp)\n {\n"; + ss << argInit.str(); + ss << " resolve();\n }\n\n"; + ss << " ~" << frame << "()\n {\n" << m_destroy.str() << " }\n\n"; + ss << " void resolve()\n {\n" << m_resolve.str() << " }\n\n"; + ss << " int step() override\n {\n"; + ss << " switch (state) {\n case 0:;\n" << m_body.str() << " }\n\n"; + + if (m_lastValue) { + if (m_codeType == Compiler::CodeType::Reporter) + ss << " cpp::val(&ret.v, " << dynamic_cast(m_lastValue)->expr << ");\n"; + else if (m_codeType == Compiler::CodeType::HatPredicate) + ss << " ret = " << cast(m_lastValue, Compiler::StaticType::Bool) << ";\n"; + } + + ss << " return cpp::CPP_DONE;\n }\n\n"; + ss << " Target *target;\n ValueData **varData;\n List **listData;\n bool warp;\n"; + + if (m_codeType == Compiler::CodeType::Reporter) + ss << " cpp::Val ret;\n"; + else if (m_codeType == Compiler::CodeType::HatPredicate) + ss << " bool ret = false;\n"; + + ss << argMembers.str() << m_members.str() << "};\n\n"; + + // Entry function + switch (m_codeType) { + case Compiler::CodeType::Script: + if (m_procedurePrototype) { + std::string warp = m_procedurePrototype->warp() ? "true" : "warp"; + m_ctx->declareProcedure(m_functionName, "static void *" + m_functionName + "(" + FRAME_ARGS + ", bool warp" + argTypes.str() + ");"); + ss << "static void *" << m_functionName << "(" << FRAME_ARGS << ", bool warp" << argParams.str() << ")\n{\n"; + ss << " return cpp::start(new " << frame << "(" << FRAME_ARG_NAMES << ", " << warp << argNames.str() << "));\n}\n"; + } else { + ss << "static void *" << m_functionName << "(" << FRAME_ARGS << ")\n{\n"; + ss << " return cpp::start(new " << frame << "(" << FRAME_ARG_NAMES << ", false));\n}\n"; + } + break; + + case Compiler::CodeType::Reporter: + ss << "static ValueData " << m_functionName << "(" << FRAME_ARGS << ")\n{\n"; + ss << " " << frame << " f(" << FRAME_ARG_NAMES << ", true);\n f.step();\n"; + ss << " ValueData ret;\n value_init(&ret);\n value_assign_copy(&ret, &f.ret.v);\n return ret;\n}\n"; + break; + + case Compiler::CodeType::HatPredicate: + ss << "static bool " << m_functionName << "(" << FRAME_ARGS << ")\n{\n"; + ss << " " << frame << " f(" << FRAME_ARG_NAMES << ", true);\n f.step();\n return f.ret;\n}\n"; + break; + } + + m_ctx->addFunction(m_functionName, ss.str(), m_functionName, !m_procedurePrototype); + return std::make_shared(m_ctx, m_functionName, m_codeType); +} + +CompilerValue *CppCodeBuilder::addFunctionCall(const std::string &functionName, Compiler::StaticType returnType, const Compiler::ArgTypes &argTypes, const Compiler::Args &args) +{ + 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 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 createFunctionCall(functionName, returnType, argTypes, args, "ExecutionContext *", "ctx"); +} + +CompilerConstant *CppCodeBuilder::addConstValue(const Value &value) +{ + Compiler::StaticType type; + std::string expr; + + switch (value.type()) { + case ValueType::Number: + type = Compiler::StaticType::Number; + expr = numberLiteral(value.data().numberValue); + break; + + case ValueType::Bool: + type = Compiler::StaticType::Bool; + expr = value.toBool() ? "true" : "false"; + break; + + case ValueType::String: + type = Compiler::StaticType::String; + expr = addMember(Storage::String, "c", stringLiteral(value.toString())) + ".s"; + break; + + case ValueType::Pointer: { + // NOTE: Pointers are only valid in the current process + type = Compiler::StaticType::Pointer; + std::stringstream ss; + ss << "reinterpret_cast(0x" << std::hex << reinterpret_cast(value.toPointer()) << ")"; + expr = ss.str(); + break; + } + + default: + assert(false); + type = Compiler::StaticType::Number; + expr = "0.0"; + break; + } + + auto reg = std::make_unique(type, value, expr); + CppConstantRegister *ret = reg.get(); + m_values.push_back(std::move(reg)); + m_lastValue = ret; + return ret; +} + +CompilerValue *CppCodeBuilder::addStringChar(CompilerValue *string, CompilerValue *index) +{ + std::string str = cast(string, Compiler::StaticType::String); + std::string i = cast(index, Compiler::StaticType::Number); + CppRegister *reg = addRegister(Compiler::StaticType::String); + emit("cpp::stringChar(" + reg->expr + ", " + str + ", " + i + ");"); + return reg; +} + +CompilerValue *CppCodeBuilder::addStringLength(CompilerValue *string) +{ + return assignNumber("(double)" + cast(string, Compiler::StaticType::String) + "->size"); +} + +CompilerValue *CppCodeBuilder::addLoopIndex() +{ + for (auto it = m_loops.rbegin(); it != m_loops.rend(); it++) { + if (it->isRepeatLoop) + return assignNumber(it->index); + } + + return addConstValue(0); +} + +CompilerValue *CppCodeBuilder::addLocalVariableValue(CompilerLocalVariable *variable) +{ + CppRegister *reg = addRegister(variable->type()); + assign(reg, variable->ptr()); + return reg; +} + +CompilerValue *CppCodeBuilder::addVariableValue(Variable *variable) +{ + CppRegister *reg = addRegister(Compiler::StaticType::Unknown); + emit("value_assign_copy(&" + reg->expr + ", " + variablePtr(variable) + ");"); + return reg; +} + +CompilerValue *CppCodeBuilder::addListContents(List *list) +{ + CppRegister *reg = addRegister(Compiler::StaticType::String); + emit(listPtr(list) + "->toStringPtr(" + reg->expr + ");"); + return reg; +} + +CompilerValue *CppCodeBuilder::addListItem(List *list, CompilerValue *index) +{ + std::string i = cast(index, Compiler::StaticType::Number); + CppRegister *reg = addRegister(Compiler::StaticType::Unknown); + emit("cpp::listItem(&" + reg->expr + ", " + listPtr(list) + ", " + i + ");"); + return reg; +} + +CompilerValue *CppCodeBuilder::addListItemIndex(List *list, CompilerValue *item) +{ + std::string v = cast(item, Compiler::StaticType::Unknown); + return assignNumber("cpp::listIndexOf(" + listPtr(list) + ", " + v + ")"); +} + +CompilerValue *CppCodeBuilder::addListContains(List *list, CompilerValue *item) +{ + std::string v = cast(item, Compiler::StaticType::Unknown); + return assignBool(listPtr(list) + "->contains(" + v + ")"); +} + +CompilerValue *CppCodeBuilder::addListSize(List *list) +{ + return assignNumber("(double)" + listPtr(list) + "->size()"); +} + +CompilerValue *CppCodeBuilder::addProcedureArgument(const std::string &name) +{ + 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()) + return addConstValue(Value()); + + size_t index = it - names.begin(); + std::string member = "a" + std::to_string(index); + + if (index < types.size() && types[index] == BlockPrototype::ArgType::Bool) + return addRegister(Compiler::StaticType::Bool, member); + else + return addRegister(Compiler::StaticType::Unknown, member + ".v"); +} + +CompilerValue *CppCodeBuilder::createAdd(CompilerValue *operand1, CompilerValue *operand2) +{ + return assignNumber(cast(operand1, Compiler::StaticType::Number) + " + " + cast(operand2, Compiler::StaticType::Number)); +} + +CompilerValue *CppCodeBuilder::createSub(CompilerValue *operand1, CompilerValue *operand2) +{ + return assignNumber(cast(operand1, Compiler::StaticType::Number) + " - " + cast(operand2, Compiler::StaticType::Number)); +} + +CompilerValue *CppCodeBuilder::createMul(CompilerValue *operand1, CompilerValue *operand2) +{ + return assignNumber(cast(operand1, Compiler::StaticType::Number) + " * " + cast(operand2, Compiler::StaticType::Number)); +} + +CompilerValue *CppCodeBuilder::createDiv(CompilerValue *operand1, CompilerValue *operand2) +{ + return assignNumber(cast(operand1, Compiler::StaticType::Number) + " / " + cast(operand2, Compiler::StaticType::Number)); +} + +CompilerValue *CppCodeBuilder::createRandom(CompilerValue *from, CompilerValue *to) +{ + // Strings can contain decimal numbers which are integers ("1.0"), so pass them as values + bool numbers = (storage(from->type()) != Storage::Unknown && storage(from->type()) != Storage::String) && (storage(to->type()) != Storage::Unknown && storage(to->type()) != Storage::String); + Compiler::StaticType type = numbers ? Compiler::StaticType::Number : Compiler::StaticType::Unknown; + return assignNumber("cpp::random(ctx, " + cast(from, type) + ", " + cast(to, type) + ")"); +} + +CompilerValue *CppCodeBuilder::createRandomInt(CompilerValue *from, CompilerValue *to) +{ + return assignNumber("(double)llvm_random_int64(ctx, (int64_t)" + cast(from, Compiler::StaticType::Number) + ", (int64_t)" + cast(to, Compiler::StaticType::Number) + ")"); +} + +CompilerValue *CppCodeBuilder::createCmpEQ(CompilerValue *operand1, CompilerValue *operand2) +{ + return assignBool("value_equals(&" + cast(operand1, Compiler::StaticType::Unknown) + ", &" + cast(operand2, Compiler::StaticType::Unknown) + ")"); +} + +CompilerValue *CppCodeBuilder::createCmpGT(CompilerValue *operand1, CompilerValue *operand2) +{ + return assignBool("value_greater(&" + cast(operand1, Compiler::StaticType::Unknown) + ", &" + cast(operand2, Compiler::StaticType::Unknown) + ")"); +} + +CompilerValue *CppCodeBuilder::createCmpLT(CompilerValue *operand1, CompilerValue *operand2) +{ + return assignBool("value_lower(&" + cast(operand1, Compiler::StaticType::Unknown) + ", &" + cast(operand2, Compiler::StaticType::Unknown) + ")"); +} + +CompilerValue *CppCodeBuilder::createStrCmpEQ(CompilerValue *string1, CompilerValue *string2, bool caseSensitive) +{ + std::string func = caseSensitive ? "string_compare_case_sensitive" : "string_compare_case_insensitive"; + return assignBool(func + "(" + cast(string1, Compiler::StaticType::String) + ", " + cast(string2, Compiler::StaticType::String) + ") == 0"); +} + +CompilerValue *CppCodeBuilder::createAnd(CompilerValue *operand1, CompilerValue *operand2) +{ + return assignBool(cast(operand1, Compiler::StaticType::Bool) + " && " + cast(operand2, Compiler::StaticType::Bool)); +} + +CompilerValue *CppCodeBuilder::createOr(CompilerValue *operand1, CompilerValue *operand2) +{ + return assignBool(cast(operand1, Compiler::StaticType::Bool) + " || " + cast(operand2, Compiler::StaticType::Bool)); +} + +CompilerValue *CppCodeBuilder::createNot(CompilerValue *operand) +{ + return assignBool("!" + cast(operand, Compiler::StaticType::Bool)); +} + +CompilerValue *CppCodeBuilder::createMod(CompilerValue *num1, CompilerValue *num2) +{ + return assignNumber("cpp::mod(" + cast(num1, Compiler::StaticType::Number) + ", " + cast(num2, Compiler::StaticType::Number) + ")"); +} + +CompilerValue *CppCodeBuilder::createRound(CompilerValue *num) +{ + return mathFunction(num, "cpp::round"); +} + +CompilerValue *CppCodeBuilder::createAbs(CompilerValue *num) +{ + return mathFunction(num, "std::fabs"); +} + +CompilerValue *CppCodeBuilder::createFloor(CompilerValue *num) +{ + return mathFunction(num, "std::floor"); +} + +CompilerValue *CppCodeBuilder::createCeil(CompilerValue *num) +{ + return mathFunction(num, "std::ceil"); +} + +CompilerValue *CppCodeBuilder::createSqrt(CompilerValue *num) +{ + // + 0.0 to avoid negative zero + return mathFunction(num, "std::sqrt", " + 0.0"); +} + +CompilerValue *CppCodeBuilder::createSin(CompilerValue *num) +{ + return mathFunction(num, "cpp::sin"); +} + +CompilerValue *CppCodeBuilder::createCos(CompilerValue *num) +{ + return mathFunction(num, "cpp::cos"); +} + +CompilerValue *CppCodeBuilder::createTan(CompilerValue *num) +{ + return mathFunction(num, "cpp::tan"); +} + +CompilerValue *CppCodeBuilder::createAsin(CompilerValue *num) +{ + return mathFunction(num, "std::asin", " * 180.0 / M_PI + 0.0"); +} + +CompilerValue *CppCodeBuilder::createAcos(CompilerValue *num) +{ + return mathFunction(num, "std::acos", " * 180.0 / M_PI"); +} + +CompilerValue *CppCodeBuilder::createAtan(CompilerValue *num) +{ + return mathFunction(num, "std::atan", " * 180.0 / M_PI"); +} + +CompilerValue *CppCodeBuilder::createLn(CompilerValue *num) +{ + return mathFunction(num, "std::log"); +} + +CompilerValue *CppCodeBuilder::createLog10(CompilerValue *num) +{ + return mathFunction(num, "std::log10"); +} + +CompilerValue *CppCodeBuilder::createExp(CompilerValue *num) +{ + return mathFunction(num, "std::exp"); +} + +CompilerValue *CppCodeBuilder::createExp10(CompilerValue *num) +{ + return assignNumber("std::pow(10.0, " + cast(num, Compiler::StaticType::Number) + ")"); +} + +CompilerValue *CppCodeBuilder::createStringConcat(CompilerValue *string1, CompilerValue *string2) +{ + std::string str1 = cast(string1, Compiler::StaticType::String); + std::string str2 = cast(string2, Compiler::StaticType::String); + CppRegister *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) +{ + 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); + CppRegister *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) +{ + CppRegister *reg = addRegister(type); + auto var = std::make_unique(reg); + CompilerLocalVariable *ret = var.get(); + m_localVariables.push_back(std::move(var)); + return ret; +} + +void CppCodeBuilder::createLocalVariableWrite(CompilerLocalVariable *variable, CompilerValue *value) +{ + assign(dynamic_cast(variable->ptr()), value); +} + +void CppCodeBuilder::createVariableWrite(Variable *variable, CompilerValue *value) +{ + 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) +{ + emit(listPtr(list) + "->clear();"); +} + +void CppCodeBuilder::createListRemove(List *list, CompilerValue *index) +{ + std::string i = cast(index, Compiler::StaticType::Number); + 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); + 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); + 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); + const std::string &l = listPtr(list); + emit("if (cpp::listIndexInRange(" + l + ", " + i + "))"); + emit(" " + l + "->replace((size_t)" + i + ", " + v + ");"); +} + +void CppCodeBuilder::beginIfStatement(CompilerValue *cond) +{ + emit("if (" + cast(cond, Compiler::StaticType::Bool) + ") {"); + indent(); +} + +void CppCodeBuilder::beginElseBranch() +{ + unindent(); + emit("} else {"); + indent(); +} + +void CppCodeBuilder::endIf() +{ + unindent(); + emit("}"); +} + +void CppCodeBuilder::beginRepeatLoop(CompilerValue *count) +{ + assert(!m_loopCondition); + std::string c = cast(count, Compiler::StaticType::Number); + std::string countMember = addMember(Storage::Number, "n"); + std::string index = addMember(Storage::Number, "i"); + + // Scratch rounds the count, a negative count skips the loop and infinity repeats forever + emit(countMember + " = " + c + ";"); + emit(countMember + " = " + countMember + " > 0 ? (std::isinf(" + countMember + ") ? " + countMember + " : std::round(" + countMember + ")) : 0;"); + emit("for (" + index + " = 0; std::isinf(" + countMember + ") || " + index + " < " + countMember + "; " + index + "++) {"); + indent(); + + Loop loop; + loop.isRepeatLoop = true; + loop.index = index; + m_loops.push_back(loop); +} + +void CppCodeBuilder::beginWhileLoop(CompilerValue *cond) +{ + if (!m_loopCondition) { + emit("for (;;) {"); + indent(); + } + + emit("if (!" + cast(cond, Compiler::StaticType::Bool) + ")"); + emit(" break;"); + m_loopCondition = false; + m_loops.push_back(Loop()); +} + +void CppCodeBuilder::beginRepeatUntilLoop(CompilerValue *cond) +{ + if (!m_loopCondition) { + emit("for (;;) {"); + indent(); + } + + emit("if (" + cast(cond, Compiler::StaticType::Bool) + ")"); + emit(" break;"); + m_loopCondition = false; + m_loops.push_back(Loop()); +} + +void CppCodeBuilder::beginLoopCondition() +{ + // The condition is evaluated at the beginning of every iteration + emit("for (;;) {"); + indent(); + m_loopCondition = true; +} + +void CppCodeBuilder::endLoop() +{ + assert(!m_loops.empty()); + + if (!isWarp()) + createSuspend(); + + unindent(); + emit("}"); + m_loops.pop_back(); +} + +void CppCodeBuilder::yield() +{ + if (!isWarp()) + createSuspend(); +} + +void CppCodeBuilder::createStop() +{ + emit("return cpp::CPP_DONE;"); +} + +void CppCodeBuilder::createThreadStop() +{ + if (m_codeType == Compiler::CodeType::Script) + emit("return cpp::CPP_END_THREAD;"); + else + emit("return cpp::CPP_DONE;"); +} + +void CppCodeBuilder::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::string name = CppCompilerContext::procedureFunctionName(prototype); + std::string call = name + "(" + FRAME_ARG_NAMES + ", " + (m_procedurePrototype ? "warp" : (isWarp() ? "true" : "false")); + + for (size_t i = 0; i < args.size(); i++) { + if (types[i] == BlockPrototype::ArgType::Bool) + call += ", " + cast(args[i], Compiler::StaticType::Bool); + else + call += ", &" + cast(args[i], Compiler::StaticType::Unknown); + } + + call += ")"; + + // The procedure handle is stored in the frame because the call can be suspended + std::string handle = addMember(Storage::Pointer, "h"); + m_destroy << " if (" << handle << ")\n cpp::destroy(" << handle << ");\n"; + + emit(handle + " = " + call + ";"); + emit("if (" + handle + " == cpp::END_THREAD_SENTINEL) {"); + emit(" " + handle + " = nullptr;"); + emit(m_codeType == Compiler::CodeType::Script ? " return cpp::CPP_END_THREAD;" : " return cpp::CPP_DONE;"); + emit("}"); + + if (!isWarp() && !prototype->warp()) { + emit("while (" + handle + ") {"); + indent(); + createSuspend(false); + emit("if (cpp::resume(" + handle + "))"); + emit(" " + handle + " = nullptr;"); + unindent(); + emit("}"); + + // The thread could be stopped from the procedure + emit("if (cpp_is_thread_finished(ctx))"); + emit(" return cpp::CPP_END_THREAD;"); + } else + emit(handle + " = nullptr;"); + + emit("resolve();"); +} + +void CppCodeBuilder::createPrint(CompilerValue *string) +{ + emit("llvm_print(" + cast(string, Compiler::StaticType::String) + ");"); +} + +/*! Returns the name of the generated function. */ +const std::string &CppCodeBuilder::functionName() const +{ + return m_functionName; +} + +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::addMember(Storage storage, const std::string &prefix, const std::string &init) +{ + std::string name = prefix + std::to_string(m_memberCount++); + + switch (storage) { + case Storage::Number: + m_members << " double " << name << " = 0;\n"; + break; + case Storage::Bool: + m_members << " bool " << name << " = false;\n"; + break; + case Storage::String: + if (init.empty()) + m_members << " cpp::Str " << name << ";\n"; + else + m_members << " cpp::Str " << name << "{ " << init << " };\n"; + break; + case Storage::Pointer: + m_members << " void *" << name << " = nullptr;\n"; + break; + case Storage::Unknown: + m_members << " cpp::Val " << name << ";\n"; + break; + case Storage::Void: + assert(false); + break; + } + + return name; +} + +CppRegister *CppCodeBuilder::addRegister(Compiler::StaticType type) +{ + Storage s = storage(type); + std::string name = addMember(s, "r"); + + if (s == Storage::String) + name += ".s"; + else if (s == Storage::Unknown) + name += ".v"; + + return addRegister(type, name); +} + +CppRegister *CppCodeBuilder::addRegister(Compiler::StaticType type, const std::string &expr) +{ + auto reg = std::make_unique(type, expr); + CppRegister *ret = reg.get(); + m_values.push_back(std::move(reg)); + m_lastValue = ret; + return ret; +} + +CppRegister *CppCodeBuilder::assignNumber(const std::string &expr) +{ + CppRegister *reg = addRegister(Compiler::StaticType::Number); + emit(reg->expr + " = " + expr + ";"); + return reg; +} + +CppRegister *CppCodeBuilder::assignBool(const std::string &expr) +{ + CppRegister *reg = addRegister(Compiler::StaticType::Bool); + emit(reg->expr + " = " + expr + ";"); + return reg; +} + +CppRegister *CppCodeBuilder::mathFunction(CompilerValue *num, const std::string &func, const std::string &suffix) +{ + return assignNumber(func + "(" + cast(num, Compiler::StaticType::Number) + ")" + suffix); +} + +std::string CppCodeBuilder::cast(CompilerValue *value, Compiler::StaticType targetType) +{ + assert(value); + const std::string &expr = dynamic_cast(value)->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 = addMember(Storage::String, "t") + ".s"; + emit("cpp::str(" + tmp + ", " + expr + ");"); + return tmp; + } + + case Storage::Unknown: { + std::string tmp = addMember(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(CppRegister *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::createSuspend(bool checkWarp) +{ + int state = ++m_stateCount; + std::string ret = "state = " + std::to_string(state) + "; return cpp::CPP_SUSPEND; case " + std::to_string(state) + ":;"; + + if (checkWarp && m_procedurePrototype) + emit("if (!warp) { " + ret + " }"); + else + emit(ret); +} + +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(); +} + +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()); + std::string decl = "extern \"C\" " + cppType(returnType, false) + " " + functionName + "("; + std::string call = functionName + "("; + bool first = true; + + if (!firstArgType.empty()) { + decl += firstArgType; + call += firstArg; + first = false; + } + + for (size_t i = 0; i < args.size(); i++) { + std::string v = cast(args[i], argTypes[i]); + + if (!first) { + decl += ", "; + call += ", "; + } + + decl += cppType(argTypes[i], true); + call += storage(argTypes[i]) == Storage::Unknown ? "&" + v : v; + first = false; + } + + decl += ");"; + call += ")"; + m_ctx->declareExternFunction(functionName, decl); + + switch (storage(returnType)) { + case Storage::Void: + emit(call + ";"); + return nullptr; + + case Storage::String: { + // The returned string is owned by the caller + CppRegister *reg = addRegister(returnType); + std::string member = reg->expr.substr(0, reg->expr.size() - 2); + emit(member + ".reset(" + call + ");"); + return reg; + } + + case Storage::Unknown: { + CppRegister *reg = addRegister(returnType); + std::string member = reg->expr.substr(0, reg->expr.size() - 2); + emit(member + ".reset(" + call + ");"); + return reg; + } + + default: { + CppRegister *reg = addRegister(returnType); + emit(reg->expr + " = " + call + ";"); + return reg; + } + } +} + +const std::string &CppCodeBuilder::variablePtr(Variable *variable) +{ + auto it = m_variables.find(variable); + + if (it != m_variables.cend()) + return it->second; + + std::string name = "v" + std::to_string(m_memberCount++); + Target *target = m_ctx->target(); + Target *owner = variable->target(); + assert(owner); + int index = owner ? owner->findVariableById(variable->id()) : -1; + assert(index != -1); + m_members << " ValueData *" << name << " = nullptr;\n"; + + if (!target || target->isStage() || owner == target) + m_resolve << " " << name << " = varData[" << index << "];\n"; + else + m_resolve << " " << name << " = cpp::stage(ctx)->variableData()[" << index << "];\n"; + + return m_variables[variable] = name; +} + +const std::string &CppCodeBuilder::listPtr(List *list) +{ + auto it = m_lists.find(list); + + if (it != m_lists.cend()) + return it->second; + + std::string name = "l" + std::to_string(m_memberCount++); + Target *target = m_ctx->target(); + Target *owner = list->target(); + assert(owner); + int index = owner ? owner->findListById(list->id()) : -1; + assert(index != -1); + m_members << " List *" << name << " = nullptr;\n"; + + if (!target || target->isStage() || owner == target) + m_resolve << " " << name << " = listData[" << index << "];\n"; + else + m_resolve << " " << name << " = cpp::stage(ctx)->listData()[" << index << "];\n"; + + return m_lists[list] = name; +} diff --git a/src/engine/internal/cpp/cppcodebuilder.h b/src/engine/internal/cpp/cppcodebuilder.h new file mode 100644 index 000000000..943b1bf4b --- /dev/null +++ b/src/engine/internal/cpp/cppcodebuilder.h @@ -0,0 +1,190 @@ +// SPDX-License-Identifier: Apache-2.0 + +#pragma once + +#include +#include +#include +#include + +#include "../icodebuilder.h" +#include "../../../test_export.h" + +namespace libscratchcpp +{ + +class CppCompilerContext; +struct CppRegister; + +/*! + * \brief The CppCodeBuilder class generates C++ code (one function) and adds it to a CppCompilerContext. + * + * Every value is stored in a function frame, so the function can be suspended and resumed later. + */ +class LIBSCRATCHCPP_TEST_EXPORT CppCodeBuilder : public ICodeBuilder +{ + public: + CppCodeBuilder(CppCompilerContext *ctx, BlockPrototype *procedurePrototype = nullptr, Compiler::CodeType codeType = Compiler::CodeType::Script); + ~CppCodeBuilder(); + + std::shared_ptr build() override; + + CompilerValue *addFunctionCall(const std::string &functionName, Compiler::StaticType returnType, const Compiler::ArgTypes &argTypes, const Compiler::Args &args) override; + CompilerValue *addTargetFunctionCall(const std::string &functionName, Compiler::StaticType returnType, const Compiler::ArgTypes &argTypes, const Compiler::Args &args) override; + CompilerValue *addFunctionCallWithCtx(const std::string &functionName, Compiler::StaticType returnType, const Compiler::ArgTypes &argTypes, const Compiler::Args &args) override; + CompilerConstant *addConstValue(const Value &value) override; + CompilerValue *addStringChar(CompilerValue *string, CompilerValue *index) override; + CompilerValue *addStringLength(CompilerValue *string) override; + CompilerValue *addLoopIndex() override; + CompilerValue *addLocalVariableValue(CompilerLocalVariable *variable) override; + CompilerValue *addVariableValue(Variable *variable) override; + CompilerValue *addListContents(List *list) override; + CompilerValue *addListItem(List *list, CompilerValue *index) override; + CompilerValue *addListItemIndex(List *list, CompilerValue *item) override; + CompilerValue *addListContains(List *list, CompilerValue *item) override; + CompilerValue *addListSize(List *list) override; + CompilerValue *addProcedureArgument(const std::string &name) override; + + CompilerValue *createAdd(CompilerValue *operand1, CompilerValue *operand2) override; + CompilerValue *createSub(CompilerValue *operand1, CompilerValue *operand2) override; + CompilerValue *createMul(CompilerValue *operand1, CompilerValue *operand2) override; + CompilerValue *createDiv(CompilerValue *operand1, CompilerValue *operand2) override; + + CompilerValue *createRandom(CompilerValue *from, CompilerValue *to) override; + CompilerValue *createRandomInt(CompilerValue *from, CompilerValue *to) override; + + CompilerValue *createCmpEQ(CompilerValue *operand1, CompilerValue *operand2) override; + CompilerValue *createCmpGT(CompilerValue *operand1, CompilerValue *operand2) override; + CompilerValue *createCmpLT(CompilerValue *operand1, CompilerValue *operand2) override; + + CompilerValue *createStrCmpEQ(CompilerValue *string1, CompilerValue *string2, bool caseSensitive = false) override; + + CompilerValue *createAnd(CompilerValue *operand1, CompilerValue *operand2) override; + CompilerValue *createOr(CompilerValue *operand1, CompilerValue *operand2) override; + CompilerValue *createNot(CompilerValue *operand) override; + + CompilerValue *createMod(CompilerValue *num1, CompilerValue *num2) override; + CompilerValue *createRound(CompilerValue *num) override; + CompilerValue *createAbs(CompilerValue *num) override; + CompilerValue *createFloor(CompilerValue *num) override; + CompilerValue *createCeil(CompilerValue *num) override; + CompilerValue *createSqrt(CompilerValue *num) override; + CompilerValue *createSin(CompilerValue *num) override; + CompilerValue *createCos(CompilerValue *num) override; + CompilerValue *createTan(CompilerValue *num) override; + CompilerValue *createAsin(CompilerValue *num) override; + CompilerValue *createAcos(CompilerValue *num) override; + CompilerValue *createAtan(CompilerValue *num) override; + CompilerValue *createLn(CompilerValue *num) override; + CompilerValue *createLog10(CompilerValue *num) override; + CompilerValue *createExp(CompilerValue *num) override; + CompilerValue *createExp10(CompilerValue *num) override; + + CompilerValue *createStringConcat(CompilerValue *string1, CompilerValue *string2) override; + + CompilerValue *createSelect(CompilerValue *cond, CompilerValue *trueValue, CompilerValue *falseValue, Compiler::StaticType valueType) override; + + CompilerLocalVariable *createLocalVariable(Compiler::StaticType type) override; + void createLocalVariableWrite(CompilerLocalVariable *variable, CompilerValue *value) override; + + void createVariableWrite(Variable *variable, CompilerValue *value) override; + + void createListClear(List *list) override; + void createListRemove(List *list, CompilerValue *index) override; + void createListAppend(List *list, CompilerValue *item) override; + void createListInsert(List *list, CompilerValue *index, CompilerValue *item) override; + void createListReplace(List *list, CompilerValue *index, CompilerValue *item) override; + + void beginIfStatement(CompilerValue *cond) override; + void beginElseBranch() override; + void endIf() override; + + void beginRepeatLoop(CompilerValue *count) override; + void beginWhileLoop(CompilerValue *cond) override; + void beginRepeatUntilLoop(CompilerValue *cond) override; + void beginLoopCondition() override; + void endLoop() override; + + void yield() override; + + void createStop() override; + void createThreadStop() override; + + void invalidateTarget() override; + + void createProcedureCall(BlockPrototype *prototype, const Compiler::Args &args) override; + + void createPrint(CompilerValue *string) override; + + const std::string &functionName() const; + + private: + enum class Storage + { + Void, + Number, + Bool, + String, + Pointer, + Unknown + }; + + struct Loop + { + bool isRepeatLoop = false; + std::string index; + }; + + 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 addMember(Storage storage, const std::string &prefix, const std::string &init = ""); + CppRegister *addRegister(Compiler::StaticType type); + CppRegister *addRegister(Compiler::StaticType type, const std::string &expr); + CppRegister *assignNumber(const std::string &expr); + CppRegister *assignBool(const std::string &expr); + CppRegister *mathFunction(CompilerValue *num, const std::string &func, const std::string &suffix = ""); + std::string cast(CompilerValue *value, Compiler::StaticType targetType); + void assign(CppRegister *dst, CompilerValue *src); + void emit(const std::string &line); + void indent(); + void unindent(); + void createSuspend(bool checkWarp = true); + bool isWarp() const; + + 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 = ""); + + const std::string &variablePtr(Variable *variable); + const std::string &listPtr(List *list); + + CppCompilerContext *m_ctx = nullptr; + BlockPrototype *m_procedurePrototype = nullptr; + Compiler::CodeType m_codeType = Compiler::CodeType::Script; + std::string m_functionName; + + std::stringstream m_members; + std::stringstream m_resolve; + std::stringstream m_destroy; + std::stringstream m_body; + int m_indent = 3; + int m_memberCount = 0; + int m_stateCount = 0; + + std::vector> m_values; + std::vector> m_localVariables; + std::unordered_map m_variables; + std::unordered_map m_lists; + std::vector m_loops; + bool m_loopCondition = false; + CompilerValue *m_lastValue = nullptr; +}; + +} // namespace libscratchcpp diff --git a/src/engine/internal/cpp/cppcodebuilderfactory.cpp b/src/engine/internal/cpp/cppcodebuilderfactory.cpp new file mode 100644 index 000000000..8d27bf79a --- /dev/null +++ b/src/engine/internal/cpp/cppcodebuilderfactory.cpp @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: Apache-2.0 + +#include + +#include "cppcodebuilderfactory.h" +#include "cppcodebuilder.h" +#include "cppcompilercontext.h" + +using namespace libscratchcpp; + +std::shared_ptr CppCodeBuilderFactory::m_instance = std::make_shared(); + +std::shared_ptr CppCodeBuilderFactory::instance() +{ + return m_instance; +} + +std::shared_ptr CppCodeBuilderFactory::create(CompilerContext *ctx, BlockPrototype *procedurePrototype, Compiler::CodeType codeType) const +{ + assert(dynamic_cast(ctx)); + return std::make_shared(static_cast(ctx), procedurePrototype, codeType); +} + +std::shared_ptr CppCodeBuilderFactory::createCtx(IEngine *engine, Target *target) const +{ + return std::make_shared(engine, target); +} diff --git a/src/engine/internal/cpp/cppcodebuilderfactory.h b/src/engine/internal/cpp/cppcodebuilderfactory.h new file mode 100644 index 000000000..cfc905369 --- /dev/null +++ b/src/engine/internal/cpp/cppcodebuilderfactory.h @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: Apache-2.0 + +#pragma once + +#include "../icodebuilderfactory.h" +#include "../../../test_export.h" + +namespace libscratchcpp +{ + +/*! \brief The CppCodeBuilderFactory class creates code builders which generate C++ code. */ +class LIBSCRATCHCPP_TEST_EXPORT CppCodeBuilderFactory : public ICodeBuilderFactory +{ + public: + static std::shared_ptr instance(); + + std::shared_ptr create(CompilerContext *ctx, BlockPrototype *procedurePrototype, Compiler::CodeType codeType) const override; + std::shared_ptr createCtx(IEngine *engine, Target *target) const override; + + private: + static std::shared_ptr m_instance; +}; + +} // namespace libscratchcpp diff --git a/src/engine/internal/cpp/cppcompilercontext.cpp b/src/engine/internal/cpp/cppcompilercontext.cpp new file mode 100644 index 000000000..af61a1223 --- /dev/null +++ b/src/engine/internal/cpp/cppcompilercontext.cpp @@ -0,0 +1,132 @@ +// SPDX-License-Identifier: Apache-2.0 + +#include +#include +#include + +#include "cppcompilercontext.h" +#include "cppprelude_source.h" + +using namespace libscratchcpp; + +CppCompilerContext::CppCompilerContext(IEngine *engine, Target *target) : + CompilerContext(engine, target) +{ +} + +/*! Returns a unique name for a new function (or the name of the procedure function). */ +std::string CppCompilerContext::createFunctionName(BlockPrototype *procedurePrototype) +{ + if (procedurePrototype) + return procedureFunctionName(procedurePrototype); + + return "script" + std::to_string(m_functionCount++); +} + +/*! Declares an external function (e.g. a block function implemented in an extension). */ +void CppCompilerContext::declareExternFunction(const std::string &name, const std::string &declaration) +{ + m_externDeclarations[name] = declaration; +} + +/*! Adds a forward declaration of a procedure function. */ +void CppCompilerContext::declareProcedure(const std::string &name, const std::string &declaration) +{ + m_procedureDeclarations[name] = declaration; +} + +/*! + * Adds a function. + * \param[in] code The frame and the entry function definition. + * \param[in] entry The name of the entry function. + * \param[in] registered Whether to register the function so that CppExecutableCode can find it. + */ +void CppCompilerContext::addFunction(const std::string &name, const std::string &code, const std::string &entry, bool registered) +{ + m_functions.push_back({ name, code, entry, registered }); +} + +/*! Returns the C++ code of all functions (without the prelude and registration code). */ +std::string CppCompilerContext::sourceCode() const +{ + std::stringstream ss; + + for (const auto &[name, decl] : m_externDeclarations) + ss << decl << "\n"; + + ss << "\n"; + + for (const auto &[name, decl] : m_procedureDeclarations) + ss << decl << "\n"; + + for (const Function &f : m_functions) + ss << "\n" << f.code; + + return ss.str(); +} + +/*! Returns the C++ code of the module with the registration code. The module source can be compiled after prelude(). */ +std::string CppCompilerContext::moduleSource() const +{ + std::stringstream ss; + uint64_t key = moduleKey(); + std::stringstream keyStr; + keyStr << "0x" << std::hex << std::setw(16) << std::setfill('0') << key; + + // Extern declarations must be outside of the module namespace + ss << "using namespace libscratchcpp;\n\n"; + + for (const auto &[name, decl] : m_externDeclarations) + ss << decl << "\n"; + + ss << "\nnamespace module_" << keyStr.str().substr(2) << "\n{\n\n"; + ss << "using namespace libscratchcpp;\n\n"; + + for (const auto &[name, decl] : m_procedureDeclarations) + ss << decl << "\n"; + + for (const Function &f : m_functions) + ss << "\n" << f.code; + + ss << "\nstruct Registrar\n{\n Registrar()\n {\n"; + + for (const Function &f : m_functions) { + if (f.registered) + ss << " cpp_register_function(" << keyStr.str() << "ULL, \"" << f.name << "\", reinterpret_cast(&" << f.entry << "));\n"; + } + + ss << " }\n};\n\nstatic Registrar registrar;\n\n} // namespace module_" << keyStr.str().substr(2) << "\n"; + return ss.str(); +} + +/*! Returns the unique key of this module (computed from the source code). */ +uint64_t CppCompilerContext::moduleKey() const +{ + return hash(sourceCode()); +} + +/*! Returns the C++ code of the runtime prelude. */ +std::string CppCompilerContext::prelude() +{ + return CPP_PRELUDE_SOURCE; +} + +std::string CppCompilerContext::procedureFunctionName(BlockPrototype *procedurePrototype) +{ + std::stringstream ss; + ss << "proc_" << std::hex << std::setw(16) << std::setfill('0') << hash(procedurePrototype->procCode()); + return ss.str(); +} + +/*! 64-bit FNV-1a hash. */ +uint64_t CppCompilerContext::hash(const std::string &str) +{ + uint64_t ret = 0xcbf29ce484222325ULL; + + for (unsigned char c : str) { + ret ^= c; + ret *= 0x100000001b3ULL; + } + + return ret; +} diff --git a/src/engine/internal/cpp/cppcompilercontext.h b/src/engine/internal/cpp/cppcompilercontext.h new file mode 100644 index 000000000..e9dc7cef8 --- /dev/null +++ b/src/engine/internal/cpp/cppcompilercontext.h @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: Apache-2.0 + +#pragma once + +#include +#include +#include +#include + +#include "../../../test_export.h" + +namespace libscratchcpp +{ + +class BlockPrototype; + +/*! + * \brief The CppCompilerContext class collects the C++ code of all functions compiled for a target. + * + * The C++ code can't be executed until it's compiled and linked into the application. + * Compiled modules register their functions on startup, see moduleKey(). + */ +class LIBSCRATCHCPP_TEST_EXPORT CppCompilerContext : public CompilerContext +{ + public: + CppCompilerContext(IEngine *engine, Target *target); + + std::string createFunctionName(BlockPrototype *procedurePrototype); + void declareExternFunction(const std::string &name, const std::string &declaration); + void declareProcedure(const std::string &name, const std::string &declaration); + void addFunction(const std::string &name, const std::string &code, const std::string &entry, bool registered); + + std::string sourceCode() const; + std::string moduleSource() const; + uint64_t moduleKey() const; + + static std::string prelude(); + static std::string procedureFunctionName(BlockPrototype *procedurePrototype); + static uint64_t hash(const std::string &str); + + private: + struct Function + { + std::string name; + std::string code; + std::string entry; + bool registered = false; + }; + + int m_functionCount = 0; + std::map m_externDeclarations; + std::map m_procedureDeclarations; + std::vector m_functions; +}; + +} // namespace libscratchcpp diff --git a/src/engine/internal/cpp/cppexecutablecode.cpp b/src/engine/internal/cpp/cppexecutablecode.cpp new file mode 100644 index 000000000..98e1876fc --- /dev/null +++ b/src/engine/internal/cpp/cppexecutablecode.cpp @@ -0,0 +1,144 @@ +// SPDX-License-Identifier: Apache-2.0 + +#include +#include +#include +#include +#include + +#include "cppexecutablecode.h" +#include "cppexecutioncontext.h" +#include "cppcompilercontext.h" +#include "cppruntime.h" +#include "cppprelude.h" + +using namespace libscratchcpp; + +CppExecutableCode::CppExecutableCode(CppCompilerContext *ctx, const std::string &functionName, Compiler::CodeType codeType) : + m_ctx(ctx), + m_functionName(functionName), + m_codeType(codeType) +{ + assert(m_ctx); +} + +void CppExecutableCode::run(ExecutionContext *context) +{ + assert(m_codeType == Compiler::CodeType::Script); + CppExecutionContext *ctx = getContext(context); + + if (ctx->finished()) + return; + + auto promise = ctx->promise(); + + if (promise) { + if (promise->isResolved()) + ctx->setPromise(nullptr); + else + return; + } + + if (ctx->frame()) { + bool done = cpp::resume(ctx->frame()); + + if (done) + ctx->setFrame(nullptr); + + ctx->setFinished(done); + } else { + resolve(); + Target *target = ctx->thread()->target(); + MainFunctionType f = reinterpret_cast(m_function); + void *handle = f(context, target, target->variableData(), target->listData()); + + if (!handle || handle == cpp::END_THREAD_SENTINEL) { + ctx->setFinished(true); + ctx->setFrame(nullptr); + } else + ctx->setFrame(handle); + } +} + +ValueData CppExecutableCode::runReporter(ExecutionContext *context) +{ + assert(m_codeType == Compiler::CodeType::Reporter); + resolve(); + Target *target = context->thread()->target(); + ReporterFunctionType f = reinterpret_cast(m_function); + return f(context, target, target->variableData(), target->listData()); +} + +bool CppExecutableCode::runPredicate(ExecutionContext *context) +{ + assert(m_codeType == Compiler::CodeType::HatPredicate); + resolve(); + Target *target = context->thread()->target(); + PredicateFunctionType f = reinterpret_cast(m_function); + return f(context, target, target->variableData(), target->listData()); +} + +void CppExecutableCode::kill(ExecutionContext *context) +{ + CppExecutionContext *ctx = getContext(context); + ctx->setFinished(true); + ctx->setPromise(nullptr); +} + +void CppExecutableCode::reset(ExecutionContext *context) +{ + CppExecutionContext *ctx = getContext(context); + + if (ctx->frame()) { + cpp::destroy(ctx->frame()); + ctx->setFrame(nullptr); + } + + ctx->setFinished(false); + ctx->setPromise(nullptr); +} + +bool CppExecutableCode::isFinished(ExecutionContext *context) const +{ + return getContext(context)->finished(); +} + +std::shared_ptr CppExecutableCode::createExecutionContext(Thread *thread) const +{ + resolve(); + return std::make_shared(thread); +} + +/*! Returns the name of the generated function. */ +const std::string &CppExecutableCode::functionName() const +{ + return m_functionName; +} + +Compiler::CodeType CppExecutableCode::codeType() const +{ + return m_codeType; +} + +/*! Returns true if the compiled function has been found. */ +bool CppExecutableCode::isResolved() const +{ + if (!m_function) + m_function = cppLookupFunction(m_ctx->moduleKey(), m_functionName); + + return m_function; +} + +void CppExecutableCode::resolve() const +{ + if (!isResolved()) { + std::cerr << "error: C++ function " << m_functionName << " (module " << std::hex << m_ctx->moduleKey() << std::dec << ") is not compiled into the application" << std::endl; + std::abort(); + } +} + +CppExecutionContext *CppExecutableCode::getContext(ExecutionContext *context) +{ + assert(dynamic_cast(context)); + return static_cast(context); +} diff --git a/src/engine/internal/cpp/cppexecutablecode.h b/src/engine/internal/cpp/cppexecutablecode.h new file mode 100644 index 000000000..faca3482f --- /dev/null +++ b/src/engine/internal/cpp/cppexecutablecode.h @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: Apache-2.0 + +#pragma once + +#include +#include +#include +#include + +#include "../../../test_export.h" + +namespace libscratchcpp +{ + +class List; +class CppCompilerContext; +class CppExecutionContext; + +/*! + * \brief The CppExecutableCode class runs a function generated by CppCodeBuilder. + * + * The generated C++ code must be compiled and linked into the application, + * the function is looked up when the first execution context is created. + */ +class LIBSCRATCHCPP_TEST_EXPORT CppExecutableCode : public ExecutableCode +{ + public: + using MainFunctionType = void *(*)(ExecutionContext *, Target *, ValueData **, List **); + using ReporterFunctionType = ValueData (*)(ExecutionContext *, Target *, ValueData **, List **); + using PredicateFunctionType = bool (*)(ExecutionContext *, Target *, ValueData **, List **); + + CppExecutableCode(CppCompilerContext *ctx, const std::string &functionName, Compiler::CodeType codeType); + + void run(ExecutionContext *context) override; + ValueData runReporter(ExecutionContext *context) override; + bool runPredicate(ExecutionContext *context) override; + void kill(ExecutionContext *context) override; + void reset(ExecutionContext *context) override; + + bool isFinished(ExecutionContext *context) const override; + + std::shared_ptr createExecutionContext(Thread *thread) const override; + + const std::string &functionName() const; + Compiler::CodeType codeType() const; + bool isResolved() const; + + private: + void resolve() const; + static CppExecutionContext *getContext(ExecutionContext *context); + + CppCompilerContext *m_ctx = nullptr; + std::string m_functionName; + Compiler::CodeType m_codeType; + mutable void *m_function = nullptr; +}; + +} // namespace libscratchcpp diff --git a/src/engine/internal/cpp/cppexecutioncontext.cpp b/src/engine/internal/cpp/cppexecutioncontext.cpp new file mode 100644 index 000000000..0f095621c --- /dev/null +++ b/src/engine/internal/cpp/cppexecutioncontext.cpp @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: Apache-2.0 + +#include "cppexecutioncontext.h" +#include "cppprelude.h" + +using namespace libscratchcpp; + +CppExecutionContext::CppExecutionContext(Thread *thread) : + ExecutionContext(thread) +{ +} + +CppExecutionContext::~CppExecutionContext() +{ + if (m_frame) + cpp::destroy(m_frame); +} + +/*! Returns the frame of the suspended script (or nullptr). */ +void *CppExecutionContext::frame() const +{ + return m_frame; +} + +void CppExecutionContext::setFrame(void *newFrame) +{ + m_frame = newFrame; +} + +bool CppExecutionContext::finished() const +{ + return m_finished; +} + +void CppExecutionContext::setFinished(bool newFinished) +{ + m_finished = newFinished; +} diff --git a/src/engine/internal/cpp/cppexecutioncontext.h b/src/engine/internal/cpp/cppexecutioncontext.h new file mode 100644 index 000000000..9e66e35ae --- /dev/null +++ b/src/engine/internal/cpp/cppexecutioncontext.h @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: Apache-2.0 + +#pragma once + +#include + +#include "../../../test_export.h" + +namespace libscratchcpp +{ + +class LIBSCRATCHCPP_TEST_EXPORT CppExecutionContext : public ExecutionContext +{ + public: + CppExecutionContext(Thread *thread); + ~CppExecutionContext(); + + void *frame() const; + void setFrame(void *newFrame); + + bool finished() const; + void setFinished(bool newFinished); + + private: + void *m_frame = nullptr; + bool m_finished = false; +}; + +} // namespace libscratchcpp diff --git a/src/engine/internal/cpp/cppprelude.h b/src/engine/internal/cpp/cppprelude.h new file mode 100644 index 000000000..8dfd51103 --- /dev/null +++ b/src/engine/internal/cpp/cppprelude.h @@ -0,0 +1,329 @@ +// SPDX-License-Identifier: Apache-2.0 + +// Runtime support for C++ code generated by CppCodeBuilder. +// This header is embedded into every generated source file, so it may only depend on public libscratchcpp headers. + +#ifndef LIBSCRATCHCPP_CPP_PRELUDE_H +#define LIBSCRATCHCPP_CPP_PRELUDE_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace libscratchcpp +{ + +extern "C" +{ + void cpp_register_function(uint64_t moduleKey, const char *name, void *function); + void cpp_mark_thread_as_finished(ExecutionContext *ctx); + bool cpp_is_thread_finished(ExecutionContext *ctx); + double llvm_random(ExecutionContext *ctx, ValueData *from, ValueData *to); + int64_t llvm_random_int64(ExecutionContext *ctx, int64_t from, int64_t to); + void llvm_print(const StringPtr *string); +} + +namespace cpp +{ + +enum StepResult +{ + CPP_SUSPEND = 0, + CPP_DONE = 1, + CPP_END_THREAD = 2 +}; + +static void *const END_THREAD_SENTINEL = (void *)0x1; + +// A resumable function frame. All values of a generated function are stored in the frame, +// so the function can be suspended (yield) and resumed later (switch-based coroutine). +struct Frame +{ + Frame(ExecutionContext *ctx) : + ctx(ctx) + { + } + + Frame(const Frame &) = delete; + virtual ~Frame() { } + + virtual int step() = 0; + + ExecutionContext *ctx = nullptr; + int state = 0; +}; + +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; +}; + +// Starts a frame, returns the frame handle if it's suspended +inline void *start(Frame *frame) +{ + int ret = frame->step(); + + if (ret == CPP_SUSPEND) + return frame; + + delete frame; + return ret == CPP_END_THREAD ? END_THREAD_SENTINEL : nullptr; +} + +// Resumes a frame, returns true if it's finished (the frame is deleted then) +inline bool resume(void *handle) +{ + Frame *frame = static_cast(handle); + int ret = frame->step(); + + if (ret == CPP_SUSPEND) + return false; + + if (ret == CPP_END_THREAD) + cpp_mark_thread_as_finished(frame->ctx); + + delete frame; + return true; +} + +inline void destroy(void *handle) +{ + delete static_cast(handle); +} + +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 * M_PI / 180.0) * 1e10) / 1e10 + 0.0; +} + +inline double cos(double x) +{ + return std::round(std::cos(x * M_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 * M_PI / 180.0) * 1e10) / 1e10 + 0.0; +} + +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 = llvm_random(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 llvm_random(ctx, const_cast(&from), const_cast(&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 cpp + +} // namespace libscratchcpp + +#endif // LIBSCRATCHCPP_CPP_PRELUDE_H diff --git a/src/engine/internal/cpp/cppregister.h b/src/engine/internal/cpp/cppregister.h new file mode 100644 index 000000000..3d79cf267 --- /dev/null +++ b/src/engine/internal/cpp/cppregister.h @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: Apache-2.0 + +#pragma once + +#include + +namespace libscratchcpp +{ + +/*! + * \brief The CppRegister struct represents a value in generated C++ code. + * The C++ type of the expression depends on the static type: + * - Number: double + * - Bool: bool + * - String: StringPtr * + * - Pointer: void * + * - Unknown (or any combination): ValueData (lvalue) + */ +struct CppRegister : public virtual CompilerValue +{ + CppRegister(Compiler::StaticType type, const std::string &expr) : + CompilerValue(type), + expr(expr) + { + } + + std::string expr; +}; + +struct CppConstantRegister + : public CompilerConstant + , public CppRegister +{ + CppConstantRegister(Compiler::StaticType type, const Value &value, const std::string &expr) : + CompilerValue(type), + CompilerConstant(type, value), + CppRegister(type, expr) + { + } +}; + +} // namespace libscratchcpp diff --git a/src/engine/internal/cpp/cppruntime.cpp b/src/engine/internal/cpp/cppruntime.cpp new file mode 100644 index 000000000..007edddc1 --- /dev/null +++ b/src/engine/internal/cpp/cppruntime.cpp @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: Apache-2.0 + +#include +#include + +#include "cppruntime.h" +#include "cppexecutioncontext.h" + +using namespace libscratchcpp; + +static std::unordered_map &functionRegistry() +{ + static std::unordered_map registry; + return registry; +} + +static std::string registryKey(uint64_t moduleKey, const std::string &name) +{ + return std::to_string(moduleKey) + ":" + name; +} + +/*! Returns the function registered by a compiled C++ module, or nullptr if it doesn't exist. */ +void *libscratchcpp::cppLookupFunction(uint64_t moduleKey, const std::string &name) +{ + auto ®istry = functionRegistry(); + auto it = registry.find(registryKey(moduleKey, name)); + return it == registry.cend() ? nullptr : it->second; +} + +extern "C" +{ + LIBSCRATCHCPP_EXPORT void cpp_register_function(uint64_t moduleKey, const char *name, void *function) + { + functionRegistry()[registryKey(moduleKey, name)] = function; + } + + LIBSCRATCHCPP_EXPORT void cpp_mark_thread_as_finished(ExecutionContext *ctx) + { + assert(dynamic_cast(ctx)); + static_cast(ctx)->setFinished(true); + } + + LIBSCRATCHCPP_EXPORT bool cpp_is_thread_finished(ExecutionContext *ctx) + { + assert(dynamic_cast(ctx)); + return static_cast(ctx)->finished(); + } +} diff --git a/src/engine/internal/cpp/cppruntime.h b/src/engine/internal/cpp/cppruntime.h new file mode 100644 index 000000000..7cc68bba2 --- /dev/null +++ b/src/engine/internal/cpp/cppruntime.h @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: Apache-2.0 + +#pragma once + +#include +#include +#include + +namespace libscratchcpp +{ + +class ExecutionContext; + +void *cppLookupFunction(uint64_t moduleKey, const std::string &name); + +extern "C" +{ + LIBSCRATCHCPP_EXPORT void cpp_register_function(uint64_t moduleKey, const char *name, void *function); + LIBSCRATCHCPP_EXPORT void cpp_mark_thread_as_finished(ExecutionContext *ctx); + LIBSCRATCHCPP_EXPORT bool cpp_is_thread_finished(ExecutionContext *ctx); +} + +} // namespace libscratchcpp diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index b6f51dace..d4bd5201a 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -37,5 +37,6 @@ add_subdirectory(compiler) add_subdirectory(blocks) add_subdirectory(executioncontext) add_subdirectory(llvm) +add_subdirectory(cpp) add_subdirectory(promise) add_subdirectory(test_api) diff --git a/test/cpp/CMakeLists.txt b/test/cpp/CMakeLists.txt new file mode 100644 index 000000000..12a8c8c00 --- /dev/null +++ b/test/cpp/CMakeLists.txt @@ -0,0 +1,41 @@ +# Test cases are shared by the generator and the test +add_library( + cpp_test_cases STATIC + cases.cpp + cases.h +) + +target_link_libraries(cpp_test_cases scratchcpp) + +# The generator writes the C++ code of the test cases at build time +add_executable( + cpp_test_generator + generator.cpp +) + +target_link_libraries(cpp_test_generator cpp_test_cases scratchcpp) + +set(CPP_TEST_GENERATED ${CMAKE_CURRENT_BINARY_DIR}/generated_cases.cpp) + +add_custom_command( + OUTPUT ${CPP_TEST_GENERATED} + COMMAND cpp_test_generator ${CPP_TEST_GENERATED} + DEPENDS cpp_test_generator + COMMENT "Generating C++ code of the C++ code builder test cases" +) + +add_executable( + cpp_test + cpp_test.cpp + testfunctions.cpp + ${CPP_TEST_GENERATED} +) + +target_link_libraries( + cpp_test + GTest::gtest_main + cpp_test_cases + scratchcpp +) + +gtest_discover_tests(cpp_test) diff --git a/test/cpp/cases.cpp b/test/cpp/cases.cpp new file mode 100644 index 000000000..99f94d7a9 --- /dev/null +++ b/test/cpp/cases.cpp @@ -0,0 +1,347 @@ +// SPDX-License-Identifier: Apache-2.0 + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "cases.h" + +using namespace libscratchcpp; + +using Type = Compiler::StaticType; +using CodeType = Compiler::CodeType; + +CppTestEnv::CppTestEnv() : + ctx(std::make_shared(nullptr, &sprite)) +{ +} + +CppTestEnv::~CppTestEnv() +{ +} + +CppCodeBuilder *CppTestEnv::createBuilder(Compiler::CodeType codeType, BlockPrototype *procedurePrototype) +{ + builders.push_back(std::make_shared(ctx.get(), procedurePrototype, codeType)); + return builders.back().get(); +} + +BlockPrototype *CppTestEnv::createProcedure(const std::string &procCode, const std::vector &argNames, bool warp) +{ + auto proc = std::make_shared(procCode); + proc->setArgumentNames(argNames); + proc->setWarp(warp); + procedures.push_back(proc); + return proc.get(); +} + +Variable *CppTestEnv::createVariable(const std::string &name, const Value &value) +{ + auto var = std::make_shared(name, name, value); + sprite.addVariable(var); + return var.get(); +} + +List *CppTestEnv::createList(const std::string &name, const std::vector &items) +{ + auto list = std::make_shared(name, name); + + for (const Value &item : items) + list->append(item); + + sprite.addList(list); + return list.get(); +} + +static void print(CppCodeBuilder *b, CompilerValue *v) +{ + b->createPrint(v); + b->createPrint(b->addConstValue("\n")); +} + +static void printConst(CppCodeBuilder *b, const Value &v) +{ + print(b, b->addConstValue(v)); +} + +// clang-format off +static const std::vector CASES = { + { "constants", [](CppTestEnv &env) { + auto b = env.createBuilder(); + printConst(b, 5.25); + printConst(b, -12); + printConst(b, true); + printConst(b, "Hello \"world\" \\ ?"); + printConst(b, "Unicode: ฤ›ลกฤล™ ๐Ÿ˜€"); + printConst(b, std::numeric_limits::infinity()); + printConst(b, -std::numeric_limits::infinity()); + printConst(b, std::numeric_limits::quiet_NaN()); + return b->build(); + }, "5.25\n-12\ntrue\nHello \"world\" \\ ?\nUnicode: ฤ›ลกฤล™ ๐Ÿ˜€\nInfinity\n-Infinity\nNaN\n" }, + + { "function_calls", [](CppTestEnv &env) { + auto b = env.createBuilder(); + b->addFunctionCall("cpp_test_empty", Type::Void, {}, {}); + auto v = b->addFunctionCall("cpp_test_add", Type::Number, { Type::Number, Type::Number }, { b->addConstValue(2), b->addConstValue("3.5") }); + print(b, v); + auto s = b->addFunctionCall("cpp_test_string", Type::String, { Type::String }, { v }); + print(b, s); + auto u = b->addFunctionCall("cpp_test_value", Type::Unknown, { Type::Unknown, Type::Bool }, { s, b->addConstValue(1) }); + print(b, u); + b->addTargetFunctionCall("cpp_test_target", Type::Void, {}, {}); + auto r = b->addFunctionCallWithCtx("cpp_test_ctx", Type::Bool, {}, {}); + print(b, r); + return b->build(); + }, "empty\n5.5\nstring: 5.5\nvalue: string: 5.5 true\ntarget: 1\ntrue\n" }, + + { "strings", [](CppTestEnv &env) { + auto b = env.createBuilder(); + auto str = b->addConstValue("abc"); + print(b, b->createStringConcat(str, b->addConstValue(12.5))); + print(b, b->addStringChar(str, b->addConstValue(1))); + print(b, b->addStringChar(str, b->addConstValue(3))); + print(b, b->addStringLength(b->addConstValue("Hello"))); + print(b, b->createStrCmpEQ(b->addConstValue("ABC"), str, false)); + print(b, b->createStrCmpEQ(b->addConstValue("ABC"), str, true)); + return b->build(); + }, "abc12.5\nb\n\n5\ntrue\nfalse\n" }, + + { "arithmetic", [](CppTestEnv &env) { + auto b = env.createBuilder(); + auto a = b->addConstValue(7); + auto c = b->addConstValue("-3"); + print(b, b->createAdd(a, c)); + print(b, b->createSub(a, c)); + print(b, b->createMul(a, c)); + print(b, b->createDiv(a, c)); + print(b, b->createDiv(b->addConstValue(1), b->addConstValue(0))); + print(b, b->createMod(a, c)); + print(b, b->createMod(b->addConstValue(-7), b->addConstValue(3))); + print(b, b->createAdd(b->addConstValue("abc"), b->addConstValue(true))); + print(b, b->createRandom(b->addConstValue(4), b->addConstValue(4))); + print(b, b->createRandomInt(b->addConstValue(2), b->addConstValue(2))); + return b->build(); + }, "4\n10\n-21\n-2.333333333333333\nInfinity\n-2\n2\n1\n4\n2\n" }, + + { "math_functions", [](CppTestEnv &env) { + auto b = env.createBuilder(); + print(b, b->createRound(b->addConstValue(2.5))); + print(b, b->createRound(b->addConstValue(-2.5))); + print(b, b->createAbs(b->addConstValue(-4.5))); + print(b, b->createFloor(b->addConstValue(-4.5))); + print(b, b->createCeil(b->addConstValue(4.2))); + print(b, b->createSqrt(b->addConstValue(16))); + print(b, b->createSin(b->addConstValue(30))); + print(b, b->createCos(b->addConstValue(60))); + print(b, b->createTan(b->addConstValue(45))); + print(b, b->createTan(b->addConstValue(90))); + print(b, b->createAsin(b->addConstValue(1))); + print(b, b->createAcos(b->addConstValue(0))); + print(b, b->createAtan(b->addConstValue(1))); + print(b, b->createLn(b->addConstValue(1))); + print(b, b->createLog10(b->addConstValue(1000))); + print(b, b->createExp(b->addConstValue(0))); + print(b, b->createExp10(b->addConstValue(2))); + return b->build(); + }, "3\n-2\n4.5\n-5\n5\n4\n0.5\n0.5\n1\nInfinity\n90\n90\n45\n0\n3\n1\n100\n" }, + + { "logic_and_comparison", [](CppTestEnv &env) { + auto b = env.createBuilder(); + auto t = b->addConstValue(true); + auto f = b->addConstValue("false"); + print(b, b->createAnd(t, f)); + print(b, b->createOr(t, f)); + print(b, b->createNot(f)); + print(b, b->createCmpEQ(b->addConstValue("10"), b->addConstValue(10))); + print(b, b->createCmpGT(b->addConstValue(10), b->addConstValue("9"))); + print(b, b->createCmpLT(b->addConstValue("abc"), b->addConstValue("abd"))); + print(b, b->createSelect(t, b->addConstValue("yes"), b->addConstValue(5), Type::Unknown)); + print(b, b->createSelect(f, b->addConstValue(1), b->addConstValue(2), Type::Number)); + return b->build(); + }, "false\ntrue\ntrue\ntrue\ntrue\ntrue\nyes\n2\n" }, + + { "if_statement", [](CppTestEnv &env) { + auto b = env.createBuilder(); + b->beginIfStatement(b->addConstValue(true)); + printConst(b, "if 1"); + b->beginElseBranch(); + printConst(b, "else 1"); + b->endIf(); + b->beginIfStatement(b->addConstValue(0)); + printConst(b, "if 2"); + b->beginElseBranch(); + b->beginIfStatement(b->addConstValue("true")); + printConst(b, "nested"); + b->endIf(); + b->endIf(); + return b->build(); + }, "if 1\nnested\n" }, + + { "repeat_loop", [](CppTestEnv &env) { + auto b = env.createBuilder(); + b->beginRepeatLoop(b->addConstValue(2.6)); + print(b, b->addLoopIndex()); + b->endLoop(); + b->beginRepeatLoop(b->addConstValue(-1)); + printConst(b, "never"); + b->endLoop(); + return b->build(); + }, "0\n1\n2\n", 4 }, + + { "while_loops", [](CppTestEnv &env) { + auto b = env.createBuilder(); + auto var = b->createLocalVariable(Type::Number); + b->createLocalVariableWrite(var, b->addConstValue(0)); + b->beginLoopCondition(); + b->beginWhileLoop(b->createCmpLT(b->addLocalVariableValue(var), b->addConstValue(3))); + b->createLocalVariableWrite(var, b->createAdd(b->addLocalVariableValue(var), b->addConstValue(1))); + print(b, b->addLocalVariableValue(var)); + b->endLoop(); + b->beginLoopCondition(); + b->beginRepeatUntilLoop(b->createCmpEQ(b->addLocalVariableValue(var), b->addConstValue(0))); + b->createLocalVariableWrite(var, b->createSub(b->addLocalVariableValue(var), b->addConstValue(1))); + b->endLoop(); + print(b, b->addLocalVariableValue(var)); + return b->build(); + }, "1\n2\n3\n0\n", 7 }, + + { "local_variables", [](CppTestEnv &env) { + auto b = env.createBuilder(); + auto s = b->createLocalVariable(Type::String); + auto u = b->createLocalVariable(Type::Unknown); + print(b, b->addLocalVariableValue(s)); + b->createLocalVariableWrite(s, b->addConstValue(12)); + b->createLocalVariableWrite(u, b->addConstValue(true)); + print(b, b->addLocalVariableValue(s)); + print(b, b->addLocalVariableValue(u)); + return b->build(); + }, "\n12\ntrue\n" }, + + { "variables", [](CppTestEnv &env) { + auto var1 = env.createVariable("a", 5); + auto var2 = env.createVariable("b", "test"); + auto b = env.createBuilder(); + print(b, b->addVariableValue(var1)); + print(b, b->addVariableValue(var2)); + auto v = b->addVariableValue(var1); + b->createVariableWrite(var1, b->createAdd(b->addVariableValue(var1), b->addConstValue(1))); + print(b, v); + print(b, b->addVariableValue(var1)); + b->createVariableWrite(var2, b->addConstValue(false)); + print(b, b->addVariableValue(var2)); + return b->build(); + }, "5\ntest\n5\n6\nfalse\n" }, + + { "lists", [](CppTestEnv &env) { + auto list = env.createList("l", { 1, "a", true }); + auto b = env.createBuilder(); + print(b, b->addListContents(list)); + print(b, b->addListSize(list)); + print(b, b->addListItem(list, b->addConstValue(1))); + print(b, b->addListItem(list, b->addConstValue(5))); + print(b, b->addListItemIndex(list, b->addConstValue(true))); + print(b, b->addListItemIndex(list, b->addConstValue("b"))); + print(b, b->addListContains(list, b->addConstValue("A"))); + b->createListAppend(list, b->addConstValue("x")); + b->createListInsert(list, b->addConstValue(0), b->addConstValue(0)); + b->createListInsert(list, b->addConstValue(10), b->addConstValue(10)); + b->createListReplace(list, b->addConstValue(2), b->addConstValue("y")); + b->createListRemove(list, b->addConstValue(1)); + b->createListRemove(list, b->addConstValue(-1)); + print(b, b->addListContents(list)); + b->createListClear(list); + print(b, b->addListSize(list)); + return b->build(); + }, "1 a true\n3\na\n\n0\n-1\ntrue\n0 y true x\n0\n" }, + + { "yield", [](CppTestEnv &env) { + auto b = env.createBuilder(); + printConst(b, "1"); + b->yield(); + printConst(b, "2"); + b->yield(); + printConst(b, "3"); + return b->build(); + }, "1\n2\n3\n", 3 }, + + { "stop", [](CppTestEnv &env) { + auto b = env.createBuilder(); + printConst(b, "before"); + b->beginIfStatement(b->addConstValue(true)); + b->createStop(); + b->endIf(); + printConst(b, "after"); + return b->build(); + }, "before\n", 1 }, + + { "thread_stop", [](CppTestEnv &env) { + auto b = env.createBuilder(); + b->yield(); + printConst(b, "before"); + b->createThreadStop(); + printConst(b, "after"); + return b->build(); + }, "before\n", 2 }, + + { "reporter", [](CppTestEnv &env) { + auto b = env.createBuilder(CodeType::Reporter); + b->createAdd(b->addConstValue(2), b->addConstValue(3)); + return b->build(); + }, "", -1, "5", CodeType::Reporter }, + + { "reporter_const", [](CppTestEnv &env) { + auto b = env.createBuilder(CodeType::Reporter); + b->addConstValue("hello"); + return b->build(); + }, "", -1, "hello", CodeType::Reporter }, + + { "hat_predicate", [](CppTestEnv &env) { + auto b = env.createBuilder(CodeType::HatPredicate); + b->createCmpGT(b->addConstValue(5), b->addConstValue("4")); + return b->build(); + }, "", -1, "true", CodeType::HatPredicate }, + + { "procedures", [](CppTestEnv &env) { + // Non-warp procedure with arguments and a yield + auto proc1 = env.createProcedure("proc1 %s %b", { "text", "flag" }, false); + auto p = env.createBuilder(CodeType::Script, proc1); + print(p, p->addProcedureArgument("text")); + p->yield(); + print(p, p->addProcedureArgument("flag")); + print(p, p->addProcedureArgument("invalid")); + p->build(); + + // Warp procedure: yields are ignored, the non-warp procedure call isn't suspended + auto proc2 = env.createProcedure("proc2", {}, true); + p = env.createBuilder(CodeType::Script, proc2); + p->createProcedureCall(proc1, { p->addConstValue("warp"), p->addConstValue(false) }); + p->yield(); + printConst(p, "proc2 end"); + p->build(); + + // Procedure which stops the thread + auto proc3 = env.createProcedure("proc3", {}, false); + p = env.createBuilder(CodeType::Script, proc3); + p->yield(); + p->createThreadStop(); + p->build(); + + auto b = env.createBuilder(); + b->createProcedureCall(proc1, { b->addConstValue(12), b->addConstValue(true) }); + b->createProcedureCall(proc2, {}); + b->createProcedureCall(proc3, {}); + printConst(b, "never"); + return b->build(); + }, "12\ntrue\n0\nwarp\nfalse\n0\nproc2 end\n", 3 }, +}; +// clang-format on + +const std::vector &libscratchcpp::cppTestCases() +{ + return CASES; +} diff --git a/test/cpp/cases.h b/test/cpp/cases.h new file mode 100644 index 000000000..403942fab --- /dev/null +++ b/test/cpp/cases.h @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: Apache-2.0 + +#pragma once + +#include +#include +#include +#include +#include +#include + +namespace libscratchcpp +{ + +class CppCompilerContext; +class CppCodeBuilder; +class BlockPrototype; +class ExecutableCode; + +// The environment of a test case, the same environment is used by the generator and by the test +struct CppTestEnv +{ + CppTestEnv(); + ~CppTestEnv(); + + CppCodeBuilder *createBuilder(Compiler::CodeType codeType = Compiler::CodeType::Script, BlockPrototype *procedurePrototype = nullptr); + BlockPrototype *createProcedure(const std::string &procCode, const std::vector &argNames, bool warp); + Variable *createVariable(const std::string &name, const Value &value); + List *createList(const std::string &name, const std::vector &items); + + Sprite sprite; + std::shared_ptr ctx; + std::vector> builders; + std::vector> procedures; +}; + +struct CppTestCase +{ + std::string name; + std::function(CppTestEnv &)> build; + std::string expectedOutput; // standard output after running the code + int expectedRuns = -1; // the number of run() calls until the script finishes (-1 to skip) + std::string expectedValue; // the return value of reporters and hat predicates + Compiler::CodeType codeType = Compiler::CodeType::Script; +}; + +const std::vector &cppTestCases(); + +} // namespace libscratchcpp diff --git a/test/cpp/cpp_test.cpp b/test/cpp/cpp_test.cpp new file mode 100644 index 000000000..95fb49d54 --- /dev/null +++ b/test/cpp/cpp_test.cpp @@ -0,0 +1,145 @@ +// SPDX-License-Identifier: Apache-2.0 + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "cases.h" + +using namespace libscratchcpp; + +class CppCodeBuilderTest : public testing::TestWithParam +{ +}; + +static std::string caseName(const testing::TestParamInfo &info) +{ + return info.param.name; +} + +TEST_P(CppCodeBuilderTest, Run) +{ + const CppTestCase &testCase = GetParam(); + CppTestEnv env; + auto code = testCase.build(env); + ASSERT_TRUE(code); + ASSERT_TRUE(std::static_pointer_cast(code)->isResolved()) << "the generated code is out of date"; + + Thread thread(&env.sprite, nullptr, nullptr); + auto ctx = code->createExecutionContext(&thread); + testing::internal::CaptureStdout(); + + switch (testCase.codeType) { + case Compiler::CodeType::Script: { + int runs = 0; + + while (!code->isFinished(ctx.get()) && runs < 1000) { + code->run(ctx.get()); + runs++; + } + + ASSERT_TRUE(code->isFinished(ctx.get())); + + if (testCase.expectedRuns != -1) + ASSERT_EQ(runs, testCase.expectedRuns); + + break; + } + + case Compiler::CodeType::Reporter: { + ValueData ret = code->runReporter(ctx.get()); + Value v(ret); + value_free(&ret); + ASSERT_EQ(v.toString(), testCase.expectedValue); + break; + } + + case Compiler::CodeType::HatPredicate: + ASSERT_EQ(Value(code->runPredicate(ctx.get())).toString(), testCase.expectedValue); + break; + } + + ASSERT_EQ(testing::internal::GetCapturedStdout(), testCase.expectedOutput); +} + +INSTANTIATE_TEST_SUITE_P(Generated, CppCodeBuilderTest, testing::ValuesIn(cppTestCases()), caseName); + +static const CppTestCase &findCase(const std::string &name) +{ + for (const CppTestCase &testCase : cppTestCases()) { + if (testCase.name == name) + return testCase; + } + + assert(false); + return cppTestCases()[0]; +} + +TEST(CppExecutableCodeTest, PromiseKillReset) +{ + CppTestEnv env; + auto code = findCase("yield").build(env); + Thread thread(&env.sprite, nullptr, nullptr); + auto ctx = code->createExecutionContext(&thread); + testing::internal::CaptureStdout(); + + code->run(ctx.get()); + ASSERT_FALSE(code->isFinished(ctx.get())); + + // Unresolved promise blocks the script + auto promise = std::make_shared(); + ctx->setPromise(promise); + code->run(ctx.get()); + code->run(ctx.get()); + ASSERT_EQ(testing::internal::GetCapturedStdout(), "1\n"); + + testing::internal::CaptureStdout(); + promise->resolve(); + code->run(ctx.get()); + ASSERT_EQ(ctx->promise(), nullptr); + ASSERT_EQ(testing::internal::GetCapturedStdout(), "2\n"); + + // Reset destroys the suspended frame + code->reset(ctx.get()); + ASSERT_FALSE(code->isFinished(ctx.get())); + testing::internal::CaptureStdout(); + code->run(ctx.get()); + ASSERT_EQ(testing::internal::GetCapturedStdout(), "1\n"); + + // Kill finishes the script + code->kill(ctx.get()); + ASSERT_TRUE(code->isFinished(ctx.get())); + testing::internal::CaptureStdout(); + code->run(ctx.get()); + ASSERT_EQ(testing::internal::GetCapturedStdout(), ""); +} + +TEST(CppCompilerContextTest, ModuleSource) +{ + CppTestEnv env; + auto b = env.createBuilder(); + b->createPrint(b->addConstValue("test")); + b->build(); + + std::string source = env.ctx->moduleSource(); + ASSERT_NE(source.find(env.ctx->sourceCode().substr(0, 20)), std::string::npos); + ASSERT_NE(source.find("cpp_register_function"), std::string::npos); + ASSERT_EQ(env.ctx->moduleKey(), CppCompilerContext::hash(env.ctx->sourceCode())); + ASSERT_NE(CppCompilerContext::prelude().find("struct Frame"), std::string::npos); +} + +TEST(CppExecutableCodeTest, Unresolved) +{ + CppTestEnv env; + auto b = env.createBuilder(); + b->createPrint(b->addConstValue("this code is not compiled")); + auto code = std::static_pointer_cast(b->build()); + ASSERT_FALSE(code->isResolved()); + ASSERT_EQ(code->functionName(), "script0"); +} diff --git a/test/cpp/generator.cpp b/test/cpp/generator.cpp new file mode 100644 index 000000000..36aa0299e --- /dev/null +++ b/test/cpp/generator.cpp @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: Apache-2.0 + +// Generates C++ code of the test cases, the generated code is compiled into cpp_test + +#include +#include +#include + +#include "cases.h" + +using namespace libscratchcpp; + +int main(int argc, char **argv) +{ + if (argc != 2) { + std::cerr << "usage: " << argv[0] << " " << std::endl; + return 1; + } + + std::ofstream out(argv[1]); + + if (!out.is_open()) { + std::cerr << "error: failed to open " << argv[1] << std::endl; + return 1; + } + + out << "// Generated by cpp_test_generator, do not edit\n\n" << CppCompilerContext::prelude() << "\n"; + + for (const CppTestCase &testCase : cppTestCases()) { + CppTestEnv env; + testCase.build(env); + out << "\n// Test case: " << testCase.name << "\n" << env.ctx->moduleSource(); + } + + return 0; +} diff --git a/test/cpp/testfunctions.cpp b/test/cpp/testfunctions.cpp new file mode 100644 index 000000000..a45cd8787 --- /dev/null +++ b/test/cpp/testfunctions.cpp @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: Apache-2.0 + +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace libscratchcpp; + +// Functions called by the generated test code +extern "C" +{ + void cpp_test_empty() + { + std::cout << "empty" << std::endl; + } + + double cpp_test_add(double a, double b) + { + return a + b; + } + + StringPtr *cpp_test_string(const StringPtr *str) + { + StringPtr *ret = string_pool_new(); + std::string s = Value(str->data ? utf8::utf16to8(std::u16string(str->data)) : "").toString(); + std::string result = "string: " + s; + string_assign_cstring(ret, result.c_str()); + return ret; + } + + ValueData cpp_test_value(ValueData *v, bool flag) + { + ValueData ret; + value_init(&ret); + std::string str = "value: " + Value(*v).toString() + (flag ? " true" : " false"); + value_assign_cstring(&ret, str.c_str()); + return ret; + } + + void cpp_test_target(Target *target) + { + std::cout << "target: " << (target != nullptr) << std::endl; + } + + bool cpp_test_ctx(ExecutionContext *ctx) + { + return ctx != nullptr; + } +} From bc7d499124f3f966c1e07943c44e03c256cf62c7 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sat, 17 Oct 2026 11:07:19 +0000 Subject: [PATCH 2/3] Add C++ codebase generator and transpiler tool Co-Authored-By: zemanadam893@gmail.com --- CMakeLists.txt | 6 + src/engine/internal/cpp/CMakeLists.txt | 4 +- .../internal/cpp/cppcodebasegenerator.cpp | 205 ++++++++++++++++++ .../internal/cpp/cppcodebasegenerator.h | 23 ++ src/engine/internal/cpp/cppcodebuilder.cpp | 35 ++- src/engine/internal/cpp/cppcodebuilder.h | 1 + src/engine/internal/cpp/cppruntime.cpp | 16 ++ src/engine/internal/cpp/cppruntime.h | 6 + test/cpp/cases.h | 6 +- test/cpp/cpp_test.cpp | 39 +++- transpiler/CMakeLists.txt | 2 + transpiler/main.cpp | 19 ++ 12 files changed, 355 insertions(+), 7 deletions(-) create mode 100644 src/engine/internal/cpp/cppcodebasegenerator.cpp create mode 100644 src/engine/internal/cpp/cppcodebasegenerator.h create mode 100644 transpiler/CMakeLists.txt create mode 100644 transpiler/main.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index c8b9c0bc6..cc0e74e72 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -11,6 +11,7 @@ option(LIBSCRATCHCPP_NETWORK_SUPPORT "Support for downloading projects" ON) option(LIBSCRATCHCPP_PRINT_LLVM_IR "Print LLVM IR of compiled Scratch scripts (for debugging)" OFF) option(LIBSCRATCHCPP_ENABLE_CODE_ANALYZER "Analyze Scratch scripts to enable various optimizations" ON) option(LIBSCRATCHCPP_LLVM_INTEGER_SUPPORT "Use integers when possible to enable various optimizations" ON) +option(LIBSCRATCHCPP_BUILD_TRANSPILER "Build the Scratch to C++ transpiler tool" ON) option(LIBSCRATCHCPP_ENABLE_SANITIZER "Enable sanitizer to detect memory issues" OFF) if (LIBSCRATCHCPP_ENABLE_SANITIZER) @@ -153,6 +154,11 @@ target_compile_definitions(scratchcpp PUBLIC LIBSCRATCHCPP_VERSION_MAJOR=${PROJE target_compile_definitions(scratchcpp PUBLIC LIBSCRATCHCPP_VERSION_MINOR=${PROJECT_VERSION_MINOR}) target_compile_definitions(scratchcpp PUBLIC LIBSCRATCHCPP_VERSION_PATCH=${PROJECT_VERSION_PATCH}) +# Scratch to C++ transpiler +if (LIBSCRATCHCPP_BUILD_TRANSPILER) + add_subdirectory(transpiler) +endif() + # Unit tests target_link_libraries(scratchcpp PRIVATE scratchcpp-test-export) diff --git a/src/engine/internal/cpp/CMakeLists.txt b/src/engine/internal/cpp/CMakeLists.txt index 212da5246..4d0af3148 100644 --- a/src/engine/internal/cpp/CMakeLists.txt +++ b/src/engine/internal/cpp/CMakeLists.txt @@ -8,7 +8,9 @@ target_sources(scratchcpp cppcodebuilderfactory.cpp cppcodebuilderfactory.h cppregister.h - cppcompilercontext.cpp + cppcodebasegenerator.cpp + cppcodebasegenerator.h + cppcompilercontext.cpp cppcompilercontext.h cppexecutablecode.cpp cppexecutablecode.h diff --git a/src/engine/internal/cpp/cppcodebasegenerator.cpp b/src/engine/internal/cpp/cppcodebasegenerator.cpp new file mode 100644 index 000000000..a430e8b59 --- /dev/null +++ b/src/engine/internal/cpp/cppcodebasegenerator.cpp @@ -0,0 +1,205 @@ +// SPDX-License-Identifier: Apache-2.0 + +#include +#include +#include +#include +#include +#include +#include + +#include "cppcodebasegenerator.h" +#include "cppcodebuilderfactory.h" +#include "cppcompilercontext.h" +#include "../../compiler_p.h" + +using namespace libscratchcpp; + +namespace +{ + +// Creates C++ code builders and keeps the compiler contexts to collect the generated code +class RecordingFactory : public ICodeBuilderFactory +{ + public: + std::shared_ptr create(CompilerContext *ctx, BlockPrototype *procedurePrototype, Compiler::CodeType codeType) const override + { + return CppCodeBuilderFactory::instance()->create(ctx, procedurePrototype, codeType); + } + + std::shared_ptr createCtx(IEngine *engine, Target *target) const override + { + auto ctx = CppCodeBuilderFactory::instance()->createCtx(engine, target); + contexts.push_back(ctx); + return ctx; + } + + mutable std::vector> contexts; +}; + +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(); + + RecordingFactory factory; + ICodeBuilderFactory *oldFactory = CompilerPrivate::builderFactory(); + CompilerPrivate::setBuilderFactory(&factory); + + std::stringstream code; + bool ret; + + { + Project project(projectFile); + ret = project.load(); + + if (ret) { + code << "// Generated by libscratchcpp, do not edit\n\n" << CppCompilerContext::prelude() << "\n"; + + for (auto ctx : factory.contexts) + code << "\n" << static_cast(ctx.get())->moduleSource(); + } + } + + CompilerPrivate::setBuilderFactory(oldFactory); + + 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; + +extern "C" void cpp_use_cpp_backend(); + +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); + } + + // Scripts are compiled by the C++ backend which uses the code in project.cpp + cpp_use_cpp_backend(); + + 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 17) +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) + 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; +} diff --git a/src/engine/internal/cpp/cppcodebasegenerator.h b/src/engine/internal/cpp/cppcodebasegenerator.h new file mode 100644 index 000000000..3d6038e4b --- /dev/null +++ b/src/engine/internal/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/cpp/cppcodebuilder.cpp b/src/engine/internal/cpp/cppcodebuilder.cpp index 80c65bd25..e696b22fe 100644 --- a/src/engine/internal/cpp/cppcodebuilder.cpp +++ b/src/engine/internal/cpp/cppcodebuilder.cpp @@ -6,6 +6,8 @@ #include #include #include +#include +#include #include #include #include @@ -161,8 +163,13 @@ CompilerConstant *CppCodeBuilder::addConstValue(const Value &value) break; case ValueType::Pointer: { - // NOTE: Pointers are only valid in the current process type = Compiler::StaticType::Pointer; + expr = entityPtr(value.toPointer()); + + if (!expr.empty()) + break; + + // NOTE: Other pointers are only valid in the current process std::stringstream ss; ss << "reinterpret_cast(0x" << std::hex << reinterpret_cast(value.toPointer()) << ")"; expr = ss.str(); @@ -1009,6 +1016,32 @@ const std::string &CppCodeBuilder::variablePtr(Variable *variable) return m_variables[variable] = name; } +std::string CppCodeBuilder::entityPtr(const void *ptr) +{ + // Lists and variables are resolved at runtime, so generated code doesn't depend on their addresses + std::vector targets; + + if (m_ctx->target()) + targets.push_back(m_ctx->target()); + + if (m_ctx->engine() && m_ctx->engine()->stage()) + targets.push_back(m_ctx->engine()->stage()); + + for (Target *target : targets) { + for (auto list : target->lists()) { + if (list.get() == ptr) + return "static_cast(" + listPtr(list.get()) + ")"; + } + + for (auto var : target->variables()) { + if (var.get() == ptr) + return "static_cast(" + variablePtr(var.get()) + ")"; + } + } + + return ""; +} + const std::string &CppCodeBuilder::listPtr(List *list) { auto it = m_lists.find(list); diff --git a/src/engine/internal/cpp/cppcodebuilder.h b/src/engine/internal/cpp/cppcodebuilder.h index 943b1bf4b..8ec698c10 100644 --- a/src/engine/internal/cpp/cppcodebuilder.h +++ b/src/engine/internal/cpp/cppcodebuilder.h @@ -164,6 +164,7 @@ class LIBSCRATCHCPP_TEST_EXPORT CppCodeBuilder : public ICodeBuilder const std::string &variablePtr(Variable *variable); const std::string &listPtr(List *list); + std::string entityPtr(const void *ptr); CppCompilerContext *m_ctx = nullptr; BlockPrototype *m_procedurePrototype = nullptr; diff --git a/src/engine/internal/cpp/cppruntime.cpp b/src/engine/internal/cpp/cppruntime.cpp index 007edddc1..d27ab70b3 100644 --- a/src/engine/internal/cpp/cppruntime.cpp +++ b/src/engine/internal/cpp/cppruntime.cpp @@ -4,6 +4,9 @@ #include #include "cppruntime.h" +#include "cppcodebuilderfactory.h" +#include "cppcodebasegenerator.h" +#include "../../compiler_p.h" #include "cppexecutioncontext.h" using namespace libscratchcpp; @@ -46,3 +49,16 @@ extern "C" return static_cast(ctx)->finished(); } } + +extern "C" +{ + LIBSCRATCHCPP_EXPORT void cpp_use_cpp_backend() + { + CompilerPrivate::setBuilderFactory(CppCodeBuilderFactory::instance().get()); + } + + 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/cpp/cppruntime.h b/src/engine/internal/cpp/cppruntime.h index 7cc68bba2..91e3c712f 100644 --- a/src/engine/internal/cpp/cppruntime.h +++ b/src/engine/internal/cpp/cppruntime.h @@ -18,6 +18,12 @@ extern "C" LIBSCRATCHCPP_EXPORT void cpp_register_function(uint64_t moduleKey, const char *name, void *function); LIBSCRATCHCPP_EXPORT void cpp_mark_thread_as_finished(ExecutionContext *ctx); LIBSCRATCHCPP_EXPORT bool cpp_is_thread_finished(ExecutionContext *ctx); + + // Makes the compiler use the C++ backend (used by generated codebases) + LIBSCRATCHCPP_EXPORT void cpp_use_cpp_backend(); + + // Generates a standalone C++ codebase from a Scratch project + LIBSCRATCHCPP_EXPORT bool cpp_generate_codebase(const char *projectFile, const char *outputDir, const char *projectName); } } // namespace libscratchcpp diff --git a/test/cpp/cases.h b/test/cpp/cases.h index 403942fab..b32f83e59 100644 --- a/test/cpp/cases.h +++ b/test/cpp/cases.h @@ -38,9 +38,9 @@ struct CppTestCase { std::string name; std::function(CppTestEnv &)> build; - std::string expectedOutput; // standard output after running the code - int expectedRuns = -1; // the number of run() calls until the script finishes (-1 to skip) - std::string expectedValue; // the return value of reporters and hat predicates + std::string expectedOutput; // standard output after running the code + int expectedRuns = -1; // the number of run() calls until the script finishes (-1 to skip) + std::string expectedValue; // the return value of reporters and hat predicates Compiler::CodeType codeType = Compiler::CodeType::Script; }; diff --git a/test/cpp/cpp_test.cpp b/test/cpp/cpp_test.cpp index 95fb49d54..1189f5576 100644 --- a/test/cpp/cpp_test.cpp +++ b/test/cpp/cpp_test.cpp @@ -8,6 +8,9 @@ #include #include #include +#include +#include +#include #include #include "cases.h" @@ -15,8 +18,7 @@ using namespace libscratchcpp; class CppCodeBuilderTest : public testing::TestWithParam -{ -}; +{ }; static std::string caseName(const testing::TestParamInfo &info) { @@ -143,3 +145,36 @@ TEST(CppExecutableCodeTest, Unresolved) ASSERT_FALSE(code->isResolved()); ASSERT_EQ(code->functionName(), "script0"); } + +TEST(CppCodebaseGeneratorTest, Generate) +{ + std::filesystem::path dir = std::filesystem::temp_directory_path() / "scratchcpp_cpp_codebase_test"; + std::filesystem::remove_all(dir); + ASSERT_TRUE(CppCodebaseGenerator::generate(DATA_DIR "/custom_blocks.sb3", dir.string(), "my project")); + + auto read = [&dir](const std::string &name) { + std::ifstream file(dir / name); + std::stringstream ss; + ss << file.rdbuf(); + return ss.str(); + }; + + std::string code = read("project.cpp"); + ASSERT_NE(code.find("struct Frame"), std::string::npos); + ASSERT_NE(code.find("cpp_register_function"), std::string::npos); + ASSERT_NE(code.find("proc_"), std::string::npos); + ASSERT_NE(read("main.cpp").find("cpp_use_cpp_backend()"), std::string::npos); + ASSERT_NE(read("project_data.cpp").find("scratchcpp_project_data[]"), std::string::npos); + ASSERT_NE(read("CMakeLists.txt").find("add_executable(my_project main.cpp project.cpp project_data.cpp)"), std::string::npos); + + ASSERT_FALSE(CppCodebaseGenerator::generate("invalid.sb3", dir.string())); + std::filesystem::remove_all(dir); +} + +TEST(CppCodebaseGeneratorTest, DataSource) +{ + ASSERT_EQ( + CppCodebaseGenerator::dataSource("ab"), + "// Generated by libscratchcpp, do not edit\n\nextern const unsigned long long scratchcpp_project_size = 2ULL;\n\nextern const unsigned char scratchcpp_project_data[] = {\n 0x61, 0x62,\n " + " 0x00\n};\n"); +} diff --git a/transpiler/CMakeLists.txt b/transpiler/CMakeLists.txt new file mode 100644 index 000000000..9aaf04404 --- /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 000000000..4bf0331f2 --- /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; +} From da3bb9875ca630038e745fbb7f55717e1d09cee2 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sat, 17 Oct 2026 11:11:43 +0000 Subject: [PATCH 3/3] Use a portable PI constant in generated code Co-Authored-By: zemanadam893@gmail.com --- src/engine/internal/cpp/cppcodebuilder.cpp | 6 +++--- src/engine/internal/cpp/cppprelude.h | 8 +++++--- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/engine/internal/cpp/cppcodebuilder.cpp b/src/engine/internal/cpp/cppcodebuilder.cpp index e696b22fe..107b26644 100644 --- a/src/engine/internal/cpp/cppcodebuilder.cpp +++ b/src/engine/internal/cpp/cppcodebuilder.cpp @@ -398,17 +398,17 @@ CompilerValue *CppCodeBuilder::createTan(CompilerValue *num) CompilerValue *CppCodeBuilder::createAsin(CompilerValue *num) { - return mathFunction(num, "std::asin", " * 180.0 / M_PI + 0.0"); + return mathFunction(num, "std::asin", " * 180.0 / cpp::PI + 0.0"); } CompilerValue *CppCodeBuilder::createAcos(CompilerValue *num) { - return mathFunction(num, "std::acos", " * 180.0 / M_PI"); + return mathFunction(num, "std::acos", " * 180.0 / cpp::PI"); } CompilerValue *CppCodeBuilder::createAtan(CompilerValue *num) { - return mathFunction(num, "std::atan", " * 180.0 / M_PI"); + return mathFunction(num, "std::atan", " * 180.0 / cpp::PI"); } CompilerValue *CppCodeBuilder::createLn(CompilerValue *num) diff --git a/src/engine/internal/cpp/cppprelude.h b/src/engine/internal/cpp/cppprelude.h index 8dfd51103..2c69e2f85 100644 --- a/src/engine/internal/cpp/cppprelude.h +++ b/src/engine/internal/cpp/cppprelude.h @@ -63,6 +63,8 @@ struct Frame int state = 0; }; +static constexpr double PI = 3.14159265358979323846; + struct Str { Str() : @@ -231,12 +233,12 @@ inline double round(double x) inline double sin(double x) { - return std::round(std::sin(x * M_PI / 180.0) * 1e10) / 1e10 + 0.0; + return std::round(std::sin(x * PI / 180.0) * 1e10) / 1e10 + 0.0; } inline double cos(double x) { - return std::round(std::cos(x * M_PI / 180.0) * 1e10) / 1e10; + return std::round(std::cos(x * PI / 180.0) * 1e10) / 1e10; } inline double tan(double x) @@ -248,7 +250,7 @@ inline double tan(double x) else if (m == -90.0 || m == 270.0) return -std::numeric_limits::infinity(); - return std::round(std::tan(x * M_PI / 180.0) * 1e10) / 1e10 + 0.0; + return std::round(std::tan(x * PI / 180.0) * 1e10) / 1e10 + 0.0; } inline double random(ExecutionContext *ctx, double from, double to)