diff --git a/CMakeLists.txt b/CMakeLists.txt index c08d26ae..1aaebf77 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -60,6 +60,7 @@ target_sources(scratchcpp include/scratchcpp/textbubble.h include/scratchcpp/itimer.h include/scratchcpp/istacktimer.h + include/scratchcpp/irandomgenerator.h include/scratchcpp/keyevent.h include/scratchcpp/rect.h include/scratchcpp/igraphicseffect.h diff --git a/include/scratchcpp/dev/compiler.h b/include/scratchcpp/dev/compiler.h index b7f5cc73..eb70df4d 100644 --- a/include/scratchcpp/dev/compiler.h +++ b/include/scratchcpp/dev/compiler.h @@ -65,6 +65,8 @@ class LIBSCRATCHCPP_EXPORT Compiler CompilerValue *createMul(CompilerValue *operand1, CompilerValue *operand2); CompilerValue *createDiv(CompilerValue *operand1, CompilerValue *operand2); + CompilerValue *createRandom(CompilerValue *from, CompilerValue *to); + CompilerValue *createCmpEQ(CompilerValue *operand1, CompilerValue *operand2); CompilerValue *createCmpGT(CompilerValue *operand1, CompilerValue *operand2); CompilerValue *createCmpLT(CompilerValue *operand1, CompilerValue *operand2); diff --git a/include/scratchcpp/dev/executioncontext.h b/include/scratchcpp/dev/executioncontext.h index 970041c0..69d11945 100644 --- a/include/scratchcpp/dev/executioncontext.h +++ b/include/scratchcpp/dev/executioncontext.h @@ -12,6 +12,7 @@ class Thread; class IEngine; class Promise; class IStackTimer; +class IRandomGenerator; class ExecutionContextPrivate; /*! \brief The ExecutionContext represents the execution context of a target (can be a clone) with variables, lists, etc. */ @@ -31,6 +32,9 @@ class LIBSCRATCHCPP_EXPORT ExecutionContext IStackTimer *stackTimer() const; void setStackTimer(IStackTimer *newStackTimer); + IRandomGenerator *rng() const; + void setRng(IRandomGenerator *newRng); + private: spimpl::unique_impl_ptr impl; }; diff --git a/include/scratchcpp/irandomgenerator.h b/include/scratchcpp/irandomgenerator.h new file mode 100644 index 00000000..1fa1d79c --- /dev/null +++ b/include/scratchcpp/irandomgenerator.h @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: Apache-2.0 + +#pragma once + +#include "global.h" + +namespace libscratchcpp +{ + +/*! \brief The IRandomGenerator interface represents a random number generator that can be received e. g. from an ExecutionContext. */ +class LIBSCRATCHCPP_EXPORT IRandomGenerator +{ + public: + virtual ~IRandomGenerator() { } + + /*! Returns a random integer in the given range (inclusive). */ + virtual long randint(long start, long end) const = 0; + + /*! Returns a random double in the given range (inclusive). */ + virtual double randintDouble(double start, double end) const = 0; + + /*! Returns a random integer in the given range (inclusive) except the given integer. */ + virtual long randintExcept(long start, long end, long except) const = 0; +}; + +} // namespace libscratchcpp diff --git a/include/scratchcpp/value_functions.h b/include/scratchcpp/value_functions.h index d4409a0b..1542d2b9 100644 --- a/include/scratchcpp/value_functions.h +++ b/include/scratchcpp/value_functions.h @@ -34,6 +34,8 @@ extern "C" LIBSCRATCHCPP_EXPORT char *value_toCString(const ValueData *v); LIBSCRATCHCPP_EXPORT void value_toUtf16(const ValueData *v, std::u16string *dst); + LIBSCRATCHCPP_EXPORT bool value_doubleIsInt(double v); + LIBSCRATCHCPP_EXPORT char *value_doubleToCString(double v); LIBSCRATCHCPP_EXPORT const char *value_boolToCString(bool v); LIBSCRATCHCPP_EXPORT double value_stringToDouble(const char *s); diff --git a/src/dev/blocks/operatorblocks.cpp b/src/dev/blocks/operatorblocks.cpp index 8d4cbb92..b962d3c7 100644 --- a/src/dev/blocks/operatorblocks.cpp +++ b/src/dev/blocks/operatorblocks.cpp @@ -1,5 +1,12 @@ // SPDX-License-Identifier: Apache-2.0 +#include +#include +#include +#include +#include +#include + #include "operatorblocks.h" using namespace libscratchcpp; @@ -16,4 +23,206 @@ std::string OperatorBlocks::description() const void OperatorBlocks::registerBlocks(IEngine *engine) { + engine->addCompileFunction(this, "operator_add", &compileAdd); + engine->addCompileFunction(this, "operator_subtract", &compileSubtract); + engine->addCompileFunction(this, "operator_multiply", &compileMultiply); + engine->addCompileFunction(this, "operator_divide", &compileDivide); + engine->addCompileFunction(this, "operator_random", &compileRandom); + engine->addCompileFunction(this, "operator_lt", &compileLt); + engine->addCompileFunction(this, "operator_equals", &compileEquals); + engine->addCompileFunction(this, "operator_gt", &compileGt); + engine->addCompileFunction(this, "operator_and", &compileAnd); + engine->addCompileFunction(this, "operator_or", &compileOr); + engine->addCompileFunction(this, "operator_not", &compileNot); + engine->addCompileFunction(this, "operator_join", &compileJoin); + engine->addCompileFunction(this, "operator_letter_of", &compileLetterOf); + engine->addCompileFunction(this, "operator_length", &compileLength); + engine->addCompileFunction(this, "operator_contains", &compileContains); + engine->addCompileFunction(this, "operator_mod", &compileMod); + engine->addCompileFunction(this, "operator_round", &compileRound); + engine->addCompileFunction(this, "operator_mathop", &compileMathOp); +} + +CompilerValue *OperatorBlocks::compileAdd(Compiler *compiler) +{ + return compiler->createAdd(compiler->addInput("NUM1"), compiler->addInput("NUM2")); +} + +CompilerValue *OperatorBlocks::compileSubtract(Compiler *compiler) +{ + return compiler->createSub(compiler->addInput("NUM1"), compiler->addInput("NUM2")); +} + +CompilerValue *OperatorBlocks::compileMultiply(Compiler *compiler) +{ + return compiler->createMul(compiler->addInput("NUM1"), compiler->addInput("NUM2")); +} + +CompilerValue *OperatorBlocks::compileDivide(Compiler *compiler) +{ + return compiler->createDiv(compiler->addInput("NUM1"), compiler->addInput("NUM2")); +} + +CompilerValue *OperatorBlocks::compileRandom(Compiler *compiler) +{ + auto from = compiler->addInput("FROM"); + auto to = compiler->addInput("TO"); + return compiler->createRandom(from, to); +} + +CompilerValue *OperatorBlocks::compileLt(Compiler *compiler) +{ + return compiler->createCmpLT(compiler->addInput("OPERAND1"), compiler->addInput("OPERAND2")); +} + +CompilerValue *OperatorBlocks::compileEquals(Compiler *compiler) +{ + return compiler->createCmpEQ(compiler->addInput("OPERAND1"), compiler->addInput("OPERAND2")); +} + +CompilerValue *OperatorBlocks::compileGt(Compiler *compiler) +{ + return compiler->createCmpGT(compiler->addInput("OPERAND1"), compiler->addInput("OPERAND2")); +} + +CompilerValue *OperatorBlocks::compileAnd(Compiler *compiler) +{ + return compiler->createAnd(compiler->addInput("OPERAND1"), compiler->addInput("OPERAND2")); +} + +CompilerValue *OperatorBlocks::compileOr(Compiler *compiler) +{ + return compiler->createOr(compiler->addInput("OPERAND1"), compiler->addInput("OPERAND2")); +} + +CompilerValue *OperatorBlocks::compileNot(Compiler *compiler) +{ + return compiler->createNot(compiler->addInput("OPERAND")); +} + +CompilerValue *OperatorBlocks::compileJoin(Compiler *compiler) +{ + auto string1 = compiler->addInput("STRING1"); + auto string2 = compiler->addInput("STRING2"); + return compiler->addFunctionCall("operator_join", Compiler::StaticType::String, { Compiler::StaticType::String, Compiler::StaticType::String }, { string1, string2 }); +} + +CompilerValue *OperatorBlocks::compileLetterOf(Compiler *compiler) +{ + auto letter = compiler->addInput("LETTER"); + auto string = compiler->addInput("STRING"); + return compiler->addFunctionCall("operator_letter_of", Compiler::StaticType::String, { Compiler::StaticType::Number, Compiler::StaticType::String }, { letter, string }); +} + +CompilerValue *OperatorBlocks::compileLength(Compiler *compiler) +{ + auto string = compiler->addInput("STRING"); + return compiler->addFunctionCall("operator_length", Compiler::StaticType::Number, { Compiler::StaticType::String }, { string }); +} + +CompilerValue *OperatorBlocks::compileContains(Compiler *compiler) +{ + auto string1 = compiler->addInput("STRING1"); + auto string2 = compiler->addInput("STRING2"); + return compiler->addFunctionCall("operator_contains", Compiler::StaticType::Bool, { Compiler::StaticType::String, Compiler::StaticType::String }, { string1, string2 }); +} + +CompilerValue *OperatorBlocks::compileMod(Compiler *compiler) +{ + return compiler->createMod(compiler->addInput("NUM1"), compiler->addInput("NUM2")); +} + +CompilerValue *OperatorBlocks::compileRound(Compiler *compiler) +{ + return compiler->createRound(compiler->addInput("NUM")); +} + +CompilerValue *OperatorBlocks::compileMathOp(Compiler *compiler) +{ + Field *opField = compiler->field("OPERATOR"); + const std::string numInput = "NUM"; + const std::string &op = opField->value().toString(); + + if (op == "abs") + return compiler->createAbs(compiler->addInput(numInput)); + else if (op == "floor") + return compiler->createFloor(compiler->addInput(numInput)); + else if (op == "ceiling") + return compiler->createCeil(compiler->addInput(numInput)); + else if (op == "sqrt") + return compiler->createSqrt(compiler->addInput(numInput)); + else if (op == "sin") + return compiler->createSin(compiler->addInput(numInput)); + else if (op == "cos") + return compiler->createCos(compiler->addInput(numInput)); + else if (op == "tan") + return compiler->createTan(compiler->addInput(numInput)); + else if (op == "asin") + return compiler->createAsin(compiler->addInput(numInput)); + else if (op == "acos") + return compiler->createAcos(compiler->addInput(numInput)); + else if (op == "atan") + return compiler->createAtan(compiler->addInput(numInput)); + else if (op == "ln") + return compiler->createLn(compiler->addInput(numInput)); + else if (op == "log") + return compiler->createLog10(compiler->addInput(numInput)); + else if (op == "e ^") + return compiler->createExp(compiler->addInput(numInput)); + else if (op == "10 ^") + return compiler->createExp10(compiler->addInput(numInput)); + else + return compiler->addConstValue(Value()); +} + +extern "C" char *operator_join(const char *string1, const char *string2) +{ + const size_t len1 = strlen(string1); + const size_t len2 = strlen(string2); + + char *ret = (char *)malloc((len1 + len2 + 1) * sizeof(char)); + size_t i; + + for (i = 0; i < len1; i++) + ret[i] = string1[i]; + + for (i = 0; i < len2 + 1; i++) // +1: null-terminate + ret[len1 + i] = string2[i]; + + return ret; +} + +extern "C" char *operator_letter_of(double letter, const char *string) +{ + const size_t len = strlen(string); + + if (letter < 1 || letter > len) { + char *ret = (char *)malloc(sizeof(char)); + ret[0] = '\0'; + return ret; + } + + // TODO: Rewrite this + std::u16string u16 = utf8::utf8to16(std::string(string)); + std::string str = utf8::utf16to8(std::u16string({ u16[(size_t)letter - 1] })); + char *ret = (char *)malloc((str.size() + 1) * sizeof(char)); + strcpy(ret, str.c_str()); + + return ret; +} + +extern "C" double operator_length(const char *string) +{ + // TODO: Rewrite this + return utf8::utf8to16(std::string(string)).size(); +} + +extern "C" bool operator_contains(const char *string1, const char *string2) +{ + // TODO: Rewrite this + std::u16string u16string1 = utf8::utf8to16(std::string(string1)); + std::u16string u16string2 = utf8::utf8to16(std::string(string2)); + std::transform(u16string1.begin(), u16string1.end(), u16string1.begin(), ::tolower); + std::transform(u16string2.begin(), u16string2.end(), u16string2.begin(), ::tolower); + return (u16string1.find(u16string2) != std::u16string::npos); } diff --git a/src/dev/blocks/operatorblocks.h b/src/dev/blocks/operatorblocks.h index af5bd7b5..1804e931 100644 --- a/src/dev/blocks/operatorblocks.h +++ b/src/dev/blocks/operatorblocks.h @@ -7,6 +7,8 @@ namespace libscratchcpp { +class IRandomGenerator; + class OperatorBlocks : public IExtension { public: @@ -14,6 +16,26 @@ class OperatorBlocks : public IExtension std::string description() const override; void registerBlocks(IEngine *engine) override; + + private: + static CompilerValue *compileAdd(Compiler *compiler); + static CompilerValue *compileSubtract(Compiler *compiler); + static CompilerValue *compileMultiply(Compiler *compiler); + static CompilerValue *compileDivide(Compiler *compiler); + static CompilerValue *compileRandom(Compiler *compiler); + static CompilerValue *compileLt(Compiler *compiler); + static CompilerValue *compileEquals(Compiler *compiler); + static CompilerValue *compileGt(Compiler *compiler); + static CompilerValue *compileAnd(Compiler *compiler); + static CompilerValue *compileOr(Compiler *compiler); + static CompilerValue *compileNot(Compiler *compiler); + static CompilerValue *compileJoin(Compiler *compiler); + static CompilerValue *compileLetterOf(Compiler *compiler); + static CompilerValue *compileLength(Compiler *compiler); + static CompilerValue *compileContains(Compiler *compiler); + static CompilerValue *compileMod(Compiler *compiler); + static CompilerValue *compileRound(Compiler *compiler); + static CompilerValue *compileMathOp(Compiler *compiler); }; } // namespace libscratchcpp diff --git a/src/dev/engine/compiler.cpp b/src/dev/engine/compiler.cpp index 0c633c72..807fa11f 100644 --- a/src/dev/engine/compiler.cpp +++ b/src/dev/engine/compiler.cpp @@ -193,6 +193,12 @@ CompilerValue *Compiler::createDiv(CompilerValue *operand1, CompilerValue *opera return impl->builder->createDiv(operand1, operand2); } +/*! Creates a random instruction (Scratch behavior). */ +CompilerValue *Compiler::createRandom(CompilerValue *from, CompilerValue *to) +{ + return impl->builder->createRandom(from, to); +} + /*! Creates an equality comparison instruction. */ CompilerValue *Compiler::createCmpEQ(CompilerValue *operand1, CompilerValue *operand2) { diff --git a/src/dev/engine/executioncontext.cpp b/src/dev/engine/executioncontext.cpp index 5c72cd54..fb0d9327 100644 --- a/src/dev/engine/executioncontext.cpp +++ b/src/dev/engine/executioncontext.cpp @@ -48,3 +48,15 @@ void ExecutionContext::setStackTimer(IStackTimer *newStackTimer) { impl->stackTimer = newStackTimer; } + +/*! Returns the random number generator of this context. */ +IRandomGenerator *ExecutionContext::rng() const +{ + return impl->rng; +} + +/*! Sets a custom random number generator. */ +void ExecutionContext::setRng(IRandomGenerator *newRng) +{ + impl->rng = newRng; +} diff --git a/src/dev/engine/executioncontext_p.cpp b/src/dev/engine/executioncontext_p.cpp index 03970aa7..94dd1072 100644 --- a/src/dev/engine/executioncontext_p.cpp +++ b/src/dev/engine/executioncontext_p.cpp @@ -1,12 +1,14 @@ // SPDX-License-Identifier: Apache-2.0 #include "executioncontext_p.h" +#include "../../engine/internal/randomgenerator.h" using namespace libscratchcpp; ExecutionContextPrivate::ExecutionContextPrivate(Thread *thread) : thread(thread), defaultStackTimer(std::make_unique()), - stackTimer(defaultStackTimer.get()) + stackTimer(defaultStackTimer.get()), + rng(RandomGenerator::instance().get()) { } diff --git a/src/dev/engine/executioncontext_p.h b/src/dev/engine/executioncontext_p.h index c556839d..367452aa 100644 --- a/src/dev/engine/executioncontext_p.h +++ b/src/dev/engine/executioncontext_p.h @@ -11,6 +11,7 @@ namespace libscratchcpp class Thread; class Promise; +class IRandomGenerator; struct ExecutionContextPrivate { @@ -20,6 +21,7 @@ struct ExecutionContextPrivate std::shared_ptr promise; std::unique_ptr defaultStackTimer; IStackTimer *stackTimer = nullptr; + IRandomGenerator *rng = nullptr; }; } // namespace libscratchcpp diff --git a/src/dev/engine/internal/icodebuilder.h b/src/dev/engine/internal/icodebuilder.h index c92c3222..d65a24f0 100644 --- a/src/dev/engine/internal/icodebuilder.h +++ b/src/dev/engine/internal/icodebuilder.h @@ -36,6 +36,8 @@ class ICodeBuilder virtual CompilerValue *createMul(CompilerValue *operand1, CompilerValue *operand2) = 0; virtual CompilerValue *createDiv(CompilerValue *operand1, CompilerValue *operand2) = 0; + virtual CompilerValue *createRandom(CompilerValue *from, CompilerValue *to) = 0; + virtual CompilerValue *createCmpEQ(CompilerValue *operand1, CompilerValue *operand2) = 0; virtual CompilerValue *createCmpGT(CompilerValue *operand1, CompilerValue *operand2) = 0; virtual CompilerValue *createCmpLT(CompilerValue *operand1, CompilerValue *operand2) = 0; diff --git a/src/dev/engine/internal/llvm/CMakeLists.txt b/src/dev/engine/internal/llvm/CMakeLists.txt index 4ef97a05..315be55a 100644 --- a/src/dev/engine/internal/llvm/CMakeLists.txt +++ b/src/dev/engine/internal/llvm/CMakeLists.txt @@ -15,6 +15,7 @@ target_sources(scratchcpp llvmprocedure.h llvmtypes.cpp llvmtypes.h + llvmfunctions.cpp llvmexecutablecode.cpp llvmexecutablecode.h llvmexecutioncontext.cpp diff --git a/src/dev/engine/internal/llvm/llvmcodebuilder.cpp b/src/dev/engine/internal/llvm/llvmcodebuilder.cpp index 069f7bd9..74698bf2 100644 --- a/src/dev/engine/internal/llvm/llvmcodebuilder.cpp +++ b/src/dev/engine/internal/llvm/llvmcodebuilder.cpp @@ -53,6 +53,7 @@ std::shared_ptr LLVMCodeBuilder::finalize() // Set fast math flags llvm::FastMathFlags fmf; fmf.setFast(true); + fmf.setNoInfs(false); fmf.setNoNaNs(false); fmf.setNoSignedZeros(false); m_builder.setFastMathFlags(fmf); @@ -190,6 +191,38 @@ std::shared_ptr LLVMCodeBuilder::finalize() break; } + case LLVMInstruction::Type::Random: { + assert(step.args.size() == 2); + const auto &arg1 = step.args[0]; + const auto &arg2 = step.args[1]; + LLVMRegister *reg1 = arg1.second; + LLVMRegister *reg2 = arg2.second; + + if (reg1->type() == Compiler::StaticType::Bool && reg2->type() == Compiler::StaticType::Bool) { + llvm::Value *bool1 = castValue(arg1.second, Compiler::StaticType::Bool); + llvm::Value *bool2 = castValue(arg2.second, Compiler::StaticType::Bool); + step.functionReturnReg->value = m_builder.CreateCall(resolve_llvm_random_bool(), { executionContextPtr, bool1, bool2 }); + } else { + llvm::Constant *inf = llvm::ConstantFP::getInfinity(m_builder.getDoubleTy(), false); + llvm::Value *num1 = removeNaN(castValue(arg1.second, Compiler::StaticType::Number)); + llvm::Value *num2 = removeNaN(castValue(arg2.second, Compiler::StaticType::Number)); + llvm::Value *sum = m_builder.CreateFAdd(num1, num2); + llvm::Value *sumDiv = m_builder.CreateFDiv(sum, inf); + llvm::Value *isInfOrNaN = isNaN(sumDiv); + + // NOTE: The random function will be called even in edge cases where it isn't needed, but they're rare, so it shouldn't be an issue + if (reg1->type() == Compiler::StaticType::Number && reg2->type() == Compiler::StaticType::Number) + step.functionReturnReg->value = m_builder.CreateSelect(isInfOrNaN, sum, m_builder.CreateCall(resolve_llvm_random_double(), { executionContextPtr, num1, num2 })); + else { + llvm::Value *value1 = createValue(reg1); + llvm::Value *value2 = createValue(reg2); + step.functionReturnReg->value = m_builder.CreateSelect(isInfOrNaN, sum, m_builder.CreateCall(resolve_llvm_random(), { executionContextPtr, value1, value2 })); + } + } + + break; + } + case LLVMInstruction::Type::CmpEQ: { assert(step.args.size() == 2); const auto &arg1 = step.args[0].second; @@ -1088,6 +1121,11 @@ CompilerValue *LLVMCodeBuilder::createDiv(CompilerValue *operand1, CompilerValue return createOp(LLVMInstruction::Type::Div, Compiler::StaticType::Number, Compiler::StaticType::Number, { operand1, operand2 }); } +CompilerValue *LLVMCodeBuilder::createRandom(CompilerValue *from, CompilerValue *to) +{ + return createOp(LLVMInstruction::Type::Random, Compiler::StaticType::Number, Compiler::StaticType::Unknown, { from, to }); +} + CompilerValue *LLVMCodeBuilder::createCmpEQ(CompilerValue *operand1, CompilerValue *operand2) { return createOp(LLVMInstruction::Type::CmpEQ, Compiler::StaticType::Bool, Compiler::StaticType::Number, { operand1, operand2 }); @@ -2333,6 +2371,25 @@ llvm::FunctionCallee LLVMCodeBuilder::resolve_list_to_string() return resolveFunction("list_to_string", llvm::FunctionType::get(pointerType, { pointerType }, false)); } +llvm::FunctionCallee LLVMCodeBuilder::resolve_llvm_random() +{ + llvm::Type *pointerType = llvm::PointerType::get(llvm::Type::getInt8Ty(m_ctx), 0); + llvm::Type *valuePtr = m_valueDataType->getPointerTo(); + return resolveFunction("llvm_random", llvm::FunctionType::get(m_builder.getDoubleTy(), { pointerType, valuePtr, valuePtr }, false)); +} + +llvm::FunctionCallee LLVMCodeBuilder::resolve_llvm_random_double() +{ + llvm::Type *pointerType = llvm::PointerType::get(llvm::Type::getInt8Ty(m_ctx), 0); + return resolveFunction("llvm_random_double", llvm::FunctionType::get(m_builder.getDoubleTy(), { pointerType, m_builder.getDoubleTy(), m_builder.getDoubleTy() }, false)); +} + +llvm::FunctionCallee LLVMCodeBuilder::resolve_llvm_random_bool() +{ + llvm::Type *pointerType = llvm::PointerType::get(llvm::Type::getInt8Ty(m_ctx), 0); + return resolveFunction("llvm_random_bool", llvm::FunctionType::get(m_builder.getDoubleTy(), { pointerType, m_builder.getInt1Ty(), m_builder.getInt1Ty() }, false)); +} + llvm::FunctionCallee LLVMCodeBuilder::resolve_strcasecmp() { llvm::Type *pointerType = llvm::PointerType::get(llvm::Type::getInt8Ty(m_ctx), 0); diff --git a/src/dev/engine/internal/llvm/llvmcodebuilder.h b/src/dev/engine/internal/llvm/llvmcodebuilder.h index afdf12de..62e4aadf 100644 --- a/src/dev/engine/internal/llvm/llvmcodebuilder.h +++ b/src/dev/engine/internal/llvm/llvmcodebuilder.h @@ -43,6 +43,8 @@ class LLVMCodeBuilder : public ICodeBuilder CompilerValue *createMul(CompilerValue *operand1, CompilerValue *operand2) override; CompilerValue *createDiv(CompilerValue *operand1, CompilerValue *operand2) override; + CompilerValue *createRandom(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; @@ -166,6 +168,9 @@ class LLVMCodeBuilder : public ICodeBuilder llvm::FunctionCallee resolve_list_size_ptr(); llvm::FunctionCallee resolve_list_alloc_size_ptr(); llvm::FunctionCallee resolve_list_to_string(); + llvm::FunctionCallee resolve_llvm_random(); + llvm::FunctionCallee resolve_llvm_random_double(); + llvm::FunctionCallee resolve_llvm_random_bool(); llvm::FunctionCallee resolve_strcasecmp(); Target *m_target = nullptr; diff --git a/src/dev/engine/internal/llvm/llvmfunctions.cpp b/src/dev/engine/internal/llvm/llvmfunctions.cpp new file mode 100644 index 00000000..d07be391 --- /dev/null +++ b/src/dev/engine/internal/llvm/llvmfunctions.cpp @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: Apache-2.0 + +#include +#include +#include + +namespace libscratchcpp +{ + +extern "C" +{ + double llvm_random(ExecutionContext *ctx, ValueData *from, ValueData *to) + { + return value_isInt(from) && value_isInt(to) ? ctx->rng()->randint(value_toLong(from), value_toLong(to)) : ctx->rng()->randintDouble(value_toDouble(from), value_toDouble(to)); + } + + double llvm_random_double(ExecutionContext *ctx, double from, double to) + { + return value_doubleIsInt(from) && value_doubleIsInt(to) ? ctx->rng()->randint(from, to) : ctx->rng()->randintDouble(from, to); + } + + double llvm_random_bool(ExecutionContext *ctx, bool from, bool to) + { + return ctx->rng()->randint(from, to); + } +} + +} // namespace libscratchcpp diff --git a/src/dev/engine/internal/llvm/llvminstruction.h b/src/dev/engine/internal/llvm/llvminstruction.h index 9b81b563..f3d2fa7b 100644 --- a/src/dev/engine/internal/llvm/llvminstruction.h +++ b/src/dev/engine/internal/llvm/llvminstruction.h @@ -18,6 +18,7 @@ struct LLVMInstruction Sub, Mul, Div, + Random, CmpEQ, CmpGT, CmpLT, diff --git a/src/engine/CMakeLists.txt b/src/engine/CMakeLists.txt index 506489de..d1b47737 100644 --- a/src/engine/CMakeLists.txt +++ b/src/engine/CMakeLists.txt @@ -20,7 +20,6 @@ target_sources(scratchcpp internal/stacktimer.h internal/randomgenerator.h internal/randomgenerator.cpp - internal/irandomgenerator.h ) if(NOT LIBSCRATCHCPP_USE_LLVM) diff --git a/src/engine/internal/irandomgenerator.h b/src/engine/internal/irandomgenerator.h deleted file mode 100644 index 2228c933..00000000 --- a/src/engine/internal/irandomgenerator.h +++ /dev/null @@ -1,18 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 - -#pragma once - -namespace libscratchcpp -{ - -class IRandomGenerator -{ - public: - virtual ~IRandomGenerator() { } - - virtual long randint(long start, long end) const = 0; - virtual double randintDouble(double start, double end) const = 0; - virtual long randintExcept(long start, long end, long except) const = 0; -}; - -} // namespace libscratchcpp diff --git a/src/engine/internal/randomgenerator.h b/src/engine/internal/randomgenerator.h index 1358de37..69d7cabd 100644 --- a/src/engine/internal/randomgenerator.h +++ b/src/engine/internal/randomgenerator.h @@ -2,11 +2,10 @@ #pragma once +#include #include #include -#include "irandomgenerator.h" - namespace libscratchcpp { diff --git a/src/scratch/value_functions.cpp b/src/scratch/value_functions.cpp index 710facd5..0da37ff3 100644 --- a/src/scratch/value_functions.cpp +++ b/src/scratch/value_functions.cpp @@ -164,14 +164,9 @@ extern "C" case ValueType::Bool: return true; - case ValueType::Number: { - if (std::isinf(v->numberValue) || std::isnan(v->numberValue)) - return true; + case ValueType::Number: + return value_doubleIsInt(v->numberValue); - double intpart; - std::modf(v->numberValue, &intpart); - return v->numberValue == intpart; - } case ValueType::String: return value_checkString(v->stringValue) == 1; } @@ -280,6 +275,17 @@ extern "C" dst->assign(utf8::utf8to16(s)); } + /*! Returns true if the given number represents a round integer. */ + bool value_doubleIsInt(double v) + { + if (std::isinf(v) || std::isnan(v)) + return true; + + double intpart; + std::modf(v, &intpart); + return v == intpart; + } + /*! * Converts the given number to string. * \note It is the caller's responsibility to free allocated memory. diff --git a/test/dev/blocks/CMakeLists.txt b/test/dev/blocks/CMakeLists.txt index 3fc113f7..c36e43cf 100644 --- a/test/dev/blocks/CMakeLists.txt +++ b/test/dev/blocks/CMakeLists.txt @@ -132,6 +132,7 @@ if (LIBSCRATCHCPP_ENABLE_OPERATOR_BLOCKS) GTest::gmock_main scratchcpp scratchcpp_mocks + block_test_deps ) gtest_discover_tests(operator_blocks_test) diff --git a/test/dev/blocks/operator_blocks_test.cpp b/test/dev/blocks/operator_blocks_test.cpp index f29aacec..c67bde95 100644 --- a/test/dev/blocks/operator_blocks_test.cpp +++ b/test/dev/blocks/operator_blocks_test.cpp @@ -1,15 +1,762 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include #include +#include #include "../common.h" #include "dev/blocks/operatorblocks.h" +#include "util.h" using namespace libscratchcpp; +using namespace libscratchcpp::test; + +using ::testing::Return; class OperatorBlocksTest : public testing::Test { public: - void SetUp() override { m_extension = std::make_unique(); } + void SetUp() override + { + m_extension = std::make_unique(); + m_engine = m_project.engine().get(); + m_extension->registerBlocks(m_engine); + registerBlocks(m_engine, m_extension.get()); + } std::unique_ptr m_extension; + Project m_project; + IEngine *m_engine = nullptr; EngineMock m_engineMock; + RandomGeneratorMock m_rng; }; + +TEST_F(OperatorBlocksTest, Add) +{ + auto target = std::make_shared(); + ScriptBuilder builder(m_extension.get(), m_engine, target); + + builder.addBlock("operator_add"); + builder.addValueInput("NUM1", 5.7); + builder.addValueInput("NUM2", 2.5); + builder.captureBlockReturnValue(); + + builder.build(); + builder.run(); + + List *valueList = builder.capturedValues(); + ValueData *values = valueList->data(); + ASSERT_EQ(valueList->size(), 1); + ASSERT_EQ(Value(values[0]), 8.2); +} + +TEST_F(OperatorBlocksTest, Subtract) +{ + auto target = std::make_shared(); + ScriptBuilder builder(m_extension.get(), m_engine, target); + + builder.addBlock("operator_subtract"); + builder.addValueInput("NUM1", 5.7); + builder.addValueInput("NUM2", 2.5); + builder.captureBlockReturnValue(); + + builder.build(); + builder.run(); + + List *valueList = builder.capturedValues(); + ValueData *values = valueList->data(); + ASSERT_EQ(valueList->size(), 1); + ASSERT_EQ(Value(values[0]), 3.2); +} + +TEST_F(OperatorBlocksTest, Multiply) +{ + auto target = std::make_shared(); + ScriptBuilder builder(m_extension.get(), m_engine, target); + + builder.addBlock("operator_multiply"); + builder.addValueInput("NUM1", 5.7); + builder.addValueInput("NUM2", 2.5); + builder.captureBlockReturnValue(); + + builder.build(); + builder.run(); + + List *valueList = builder.capturedValues(); + ValueData *values = valueList->data(); + ASSERT_EQ(valueList->size(), 1); + ASSERT_EQ(Value(values[0]), 14.25); +} + +TEST_F(OperatorBlocksTest, Divide) +{ + auto target = std::make_shared(); + ScriptBuilder builder(m_extension.get(), m_engine, target); + + builder.addBlock("operator_divide"); + builder.addValueInput("NUM1", 5.7); + builder.addValueInput("NUM2", 2.5); + builder.captureBlockReturnValue(); + + builder.build(); + builder.run(); + + List *valueList = builder.capturedValues(); + ValueData *values = valueList->data(); + ASSERT_EQ(valueList->size(), 1); + ASSERT_EQ(std::round(value_toDouble(&values[0]) * 100) / 100, 2.28); +} + +TEST_F(OperatorBlocksTest, Random) +{ + auto target = std::make_shared(); + ScriptBuilder builder(m_extension.get(), m_engine, target); + + auto addRandomTest = [&builder](const Value &from, const Value &to) { + auto block = std::make_shared("", "operator_random"); + auto input = std::make_shared("FROM", Input::Type::Shadow); + input->setPrimaryValue(from); + block->addInput(input); + input = std::make_shared("TO", Input::Type::Shadow); + input->setPrimaryValue(to); + block->addInput(input); + + builder.addBlock("test_print"); + builder.addObscuredInput("STRING", block); + return builder.currentBlock(); + }; + + auto block = addRandomTest(-45, 12); + addRandomTest(12, 6.05); + addRandomTest(-78.686, -45); + addRandomTest(6.05, -78.686); + + builder.build(); + + Compiler compiler(&m_engineMock, target.get()); + auto code = compiler.compile(block); + Script script(target.get(), block, &m_engineMock); + script.setCode(code); + Thread thread(target.get(), &m_engineMock, &script); + auto ctx = code->createExecutionContext(&thread); + ctx->setRng(&m_rng); + + static const std::string expected = + "-18\n" + "3.486789\n" + "-59.468873\n" + "-28.648764\n"; + + EXPECT_CALL(m_rng, randint(-45, 12)).WillOnce(Return(-18)); + EXPECT_CALL(m_rng, randintDouble(12, 6.05)).WillOnce(Return(3.486789)); + EXPECT_CALL(m_rng, randintDouble(-78.686, -45)).WillOnce(Return(-59.468873)); + EXPECT_CALL(m_rng, randintDouble(6.05, -78.686)).WillOnce(Return(-28.648764)); + testing::internal::CaptureStdout(); + code->run(ctx.get()); + ASSERT_EQ(testing::internal::GetCapturedStdout(), expected); +} + +TEST_F(OperatorBlocksTest, Lt) +{ + auto target = std::make_shared(); + ScriptBuilder builder(m_extension.get(), m_engine, target); + + builder.addBlock("operator_lt"); + builder.addValueInput("OPERAND1", 5.4645); + builder.addValueInput("OPERAND2", 12.486); + builder.captureBlockReturnValue(); + + builder.addBlock("operator_lt"); + builder.addValueInput("OPERAND1", 153.25); + builder.addValueInput("OPERAND2", 96.5); + builder.captureBlockReturnValue(); + + builder.addBlock("operator_lt"); + builder.addValueInput("OPERAND1", 2.8465); + builder.addValueInput("OPERAND2", 2.8465); + builder.captureBlockReturnValue(); + + builder.build(); + builder.run(); + + List *valueList = builder.capturedValues(); + ValueData *values = valueList->data(); + ASSERT_EQ(valueList->size(), 3); + ASSERT_EQ(Value(values[0]), true); + ASSERT_EQ(Value(values[1]), false); + ASSERT_EQ(Value(values[2]), false); +} + +TEST_F(OperatorBlocksTest, Equals) +{ + auto target = std::make_shared(); + ScriptBuilder builder(m_extension.get(), m_engine, target); + + builder.addBlock("operator_equals"); + builder.addValueInput("OPERAND1", 5.4645); + builder.addValueInput("OPERAND2", 12.486); + builder.captureBlockReturnValue(); + + builder.addBlock("operator_equals"); + builder.addValueInput("OPERAND1", 153.25); + builder.addValueInput("OPERAND2", 96.5); + builder.captureBlockReturnValue(); + + builder.addBlock("operator_equals"); + builder.addValueInput("OPERAND1", 2.8465); + builder.addValueInput("OPERAND2", 2.8465); + builder.captureBlockReturnValue(); + + builder.build(); + builder.run(); + + List *valueList = builder.capturedValues(); + ValueData *values = valueList->data(); + ASSERT_EQ(valueList->size(), 3); + ASSERT_EQ(Value(values[0]), false); + ASSERT_EQ(Value(values[1]), false); + ASSERT_EQ(Value(values[2]), true); +} + +TEST_F(OperatorBlocksTest, Gt) +{ + auto target = std::make_shared(); + ScriptBuilder builder(m_extension.get(), m_engine, target); + + builder.addBlock("operator_gt"); + builder.addValueInput("OPERAND1", 5.4645); + builder.addValueInput("OPERAND2", 12.486); + builder.captureBlockReturnValue(); + + builder.addBlock("operator_gt"); + builder.addValueInput("OPERAND1", 153.25); + builder.addValueInput("OPERAND2", 96.5); + builder.captureBlockReturnValue(); + + builder.addBlock("operator_gt"); + builder.addValueInput("OPERAND1", 2.8465); + builder.addValueInput("OPERAND2", 2.8465); + builder.captureBlockReturnValue(); + + builder.build(); + builder.run(); + + List *valueList = builder.capturedValues(); + ValueData *values = valueList->data(); + ASSERT_EQ(valueList->size(), 3); + ASSERT_EQ(Value(values[0]), false); + ASSERT_EQ(Value(values[1]), true); + ASSERT_EQ(Value(values[2]), false); +} + +TEST_F(OperatorBlocksTest, And) +{ + auto target = std::make_shared(); + ScriptBuilder builder(m_extension.get(), m_engine, target); + + builder.addBlock("operator_and"); + builder.addValueInput("OPERAND1", false); + builder.addValueInput("OPERAND2", false); + builder.captureBlockReturnValue(); + + builder.addBlock("operator_and"); + builder.addValueInput("OPERAND1", true); + builder.addValueInput("OPERAND2", false); + builder.captureBlockReturnValue(); + + builder.addBlock("operator_and"); + builder.addValueInput("OPERAND1", false); + builder.addValueInput("OPERAND2", true); + builder.captureBlockReturnValue(); + + builder.addBlock("operator_and"); + builder.addValueInput("OPERAND1", true); + builder.addValueInput("OPERAND2", true); + builder.captureBlockReturnValue(); + + builder.build(); + builder.run(); + + List *valueList = builder.capturedValues(); + ValueData *values = valueList->data(); + ASSERT_EQ(valueList->size(), 4); + ASSERT_EQ(Value(values[0]), false); + ASSERT_EQ(Value(values[1]), false); + ASSERT_EQ(Value(values[2]), false); + ASSERT_EQ(Value(values[3]), true); +} + +TEST_F(OperatorBlocksTest, Or) +{ + auto target = std::make_shared(); + ScriptBuilder builder(m_extension.get(), m_engine, target); + + builder.addBlock("operator_or"); + builder.addValueInput("OPERAND1", false); + builder.addValueInput("OPERAND2", false); + builder.captureBlockReturnValue(); + + builder.addBlock("operator_or"); + builder.addValueInput("OPERAND1", true); + builder.addValueInput("OPERAND2", false); + builder.captureBlockReturnValue(); + + builder.addBlock("operator_or"); + builder.addValueInput("OPERAND1", false); + builder.addValueInput("OPERAND2", true); + builder.captureBlockReturnValue(); + + builder.addBlock("operator_or"); + builder.addValueInput("OPERAND1", true); + builder.addValueInput("OPERAND2", true); + builder.captureBlockReturnValue(); + + builder.build(); + builder.run(); + + List *valueList = builder.capturedValues(); + ValueData *values = valueList->data(); + ASSERT_EQ(valueList->size(), 4); + ASSERT_EQ(Value(values[0]), false); + ASSERT_EQ(Value(values[1]), true); + ASSERT_EQ(Value(values[2]), true); + ASSERT_EQ(Value(values[3]), true); +} + +TEST_F(OperatorBlocksTest, Not) +{ + auto target = std::make_shared(); + ScriptBuilder builder(m_extension.get(), m_engine, target); + + builder.addBlock("operator_not"); + builder.addValueInput("OPERAND", false); + builder.captureBlockReturnValue(); + + builder.addBlock("operator_not"); + builder.addValueInput("OPERAND", true); + builder.captureBlockReturnValue(); + + builder.build(); + builder.run(); + + List *valueList = builder.capturedValues(); + ValueData *values = valueList->data(); + ASSERT_EQ(valueList->size(), 2); + ASSERT_EQ(Value(values[0]), true); + ASSERT_EQ(Value(values[1]), false); +} + +TEST_F(OperatorBlocksTest, Join) +{ + auto target = std::make_shared(); + ScriptBuilder builder(m_extension.get(), m_engine, target); + + builder.addBlock("operator_join"); + builder.addValueInput("STRING1", "abc"); + builder.addValueInput("STRING2", "def"); + builder.captureBlockReturnValue(); + + builder.addBlock("operator_join"); + builder.addValueInput("STRING1", "Hello "); + builder.addValueInput("STRING2", "world"); + builder.captureBlockReturnValue(); + + builder.build(); + builder.run(); + + List *valueList = builder.capturedValues(); + ValueData *values = valueList->data(); + ASSERT_EQ(valueList->size(), 2); + ASSERT_EQ(Value(values[0]), "abcdef"); + ASSERT_EQ(Value(values[1]), "Hello world"); +} + +TEST_F(OperatorBlocksTest, LetterOf) +{ + auto target = std::make_shared(); + ScriptBuilder builder(m_extension.get(), m_engine, target); + + builder.addBlock("operator_letter_of"); + builder.addValueInput("LETTER", 2); + builder.addValueInput("STRING", "abc"); + builder.captureBlockReturnValue(); + + builder.addBlock("operator_letter_of"); + builder.addValueInput("LETTER", 7); + builder.addValueInput("STRING", "Hello world"); + builder.captureBlockReturnValue(); + + builder.addBlock("operator_letter_of"); + builder.addValueInput("LETTER", 0); + builder.addValueInput("STRING", "Hello world"); + builder.captureBlockReturnValue(); + + builder.addBlock("operator_letter_of"); + builder.addValueInput("LETTER", 12); + builder.addValueInput("STRING", "Hello world"); + builder.captureBlockReturnValue(); + + builder.addBlock("operator_letter_of"); + builder.addValueInput("LETTER", 1); + builder.addValueInput("STRING", "Ábč"); + builder.captureBlockReturnValue(); + + builder.build(); + builder.run(); + + List *valueList = builder.capturedValues(); + ValueData *values = valueList->data(); + ASSERT_EQ(valueList->size(), 5); + ASSERT_EQ(Value(values[0]), "b"); + ASSERT_EQ(Value(values[1]), "w"); + ASSERT_EQ(Value(values[2]), ""); + ASSERT_EQ(Value(values[3]), ""); + ASSERT_EQ(Value(values[4]), "Á"); +} + +TEST_F(OperatorBlocksTest, Length) +{ + auto target = std::make_shared(); + ScriptBuilder builder(m_extension.get(), m_engine, target); + + builder.addBlock("operator_length"); + builder.addValueInput("STRING", "abc"); + builder.captureBlockReturnValue(); + + builder.addBlock("operator_length"); + builder.addValueInput("STRING", "Hello world"); + builder.captureBlockReturnValue(); + + builder.addBlock("operator_length"); + builder.addValueInput("STRING", "dOádčĐaší"); + builder.captureBlockReturnValue(); + + builder.build(); + builder.run(); + + List *valueList = builder.capturedValues(); + ValueData *values = valueList->data(); + ASSERT_EQ(valueList->size(), 3); + ASSERT_EQ(Value(values[0]), 3); + ASSERT_EQ(Value(values[1]), 11); + ASSERT_EQ(Value(values[2]), 9); +} + +TEST_F(OperatorBlocksTest, Contains) +{ + auto target = std::make_shared(); + ScriptBuilder builder(m_extension.get(), m_engine, target); + + builder.addBlock("operator_contains"); + builder.addValueInput("STRING1", "abc"); + builder.addValueInput("STRING2", "a"); + builder.captureBlockReturnValue(); + + builder.addBlock("operator_contains"); + builder.addValueInput("STRING1", "abc"); + builder.addValueInput("STRING2", "e"); + builder.captureBlockReturnValue(); + + builder.addBlock("operator_contains"); + builder.addValueInput("STRING1", "abc"); + builder.addValueInput("STRING2", "C"); + builder.captureBlockReturnValue(); + + builder.addBlock("operator_contains"); + builder.addValueInput("STRING1", "Hello world"); + builder.addValueInput("STRING2", "ello"); + builder.captureBlockReturnValue(); + + builder.addBlock("operator_contains"); + builder.addValueInput("STRING1", "Hello world"); + builder.addValueInput("STRING2", "olld"); + builder.captureBlockReturnValue(); + + builder.addBlock("operator_contains"); + builder.addValueInput("STRING1", "ábČ"); + builder.addValueInput("STRING2", "á"); + builder.captureBlockReturnValue(); + + builder.addBlock("operator_contains"); + builder.addValueInput("STRING1", "ábČ"); + builder.addValueInput("STRING2", "bČ"); + builder.captureBlockReturnValue(); + + builder.addBlock("operator_contains"); + builder.addValueInput("STRING1", "ábČ"); + builder.addValueInput("STRING2", "ďá"); + builder.captureBlockReturnValue(); + + builder.build(); + builder.run(); + + List *valueList = builder.capturedValues(); + ValueData *values = valueList->data(); + ASSERT_EQ(valueList->size(), 8); + ASSERT_EQ(Value(values[0]), true); + ASSERT_EQ(Value(values[1]), false); + ASSERT_EQ(Value(values[2]), true); + ASSERT_EQ(Value(values[3]), true); + ASSERT_EQ(Value(values[4]), false); + ASSERT_EQ(Value(values[5]), true); + ASSERT_EQ(Value(values[6]), true); + ASSERT_EQ(Value(values[7]), false); +} + +TEST_F(OperatorBlocksTest, Mod) +{ + auto target = std::make_shared(); + ScriptBuilder builder(m_extension.get(), m_engine, target); + + builder.addBlock("operator_mod"); + builder.addValueInput("NUM1", 5.7); + builder.addValueInput("NUM2", 2.5); + builder.captureBlockReturnValue(); + + builder.addBlock("operator_mod"); + builder.addValueInput("NUM1", -5.7); + builder.addValueInput("NUM2", 2.5); + builder.captureBlockReturnValue(); + + builder.build(); + builder.run(); + + List *valueList = builder.capturedValues(); + ValueData *values = valueList->data(); + ASSERT_EQ(valueList->size(), 2); + ASSERT_EQ(std::round(value_toDouble(&values[0]) * 100) / 100, 0.7); + ASSERT_EQ(std::round(value_toDouble(&values[1]) * 100) / 100, 1.8); +} + +TEST_F(OperatorBlocksTest, Round) +{ + auto target = std::make_shared(); + ScriptBuilder builder(m_extension.get(), m_engine, target); + + builder.addBlock("operator_round"); + builder.addValueInput("NUM", 5.7); + builder.captureBlockReturnValue(); + + builder.addBlock("operator_round"); + builder.addValueInput("NUM", 2.3); + builder.captureBlockReturnValue(); + + builder.build(); + builder.run(); + + List *valueList = builder.capturedValues(); + ValueData *values = valueList->data(); + ASSERT_EQ(valueList->size(), 2); + ASSERT_EQ(Value(values[0]), 6); + ASSERT_EQ(Value(values[1]), 2); +} + +TEST_F(OperatorBlocksTest, MathOp) +{ + auto target = std::make_shared(); + ScriptBuilder builder(m_extension.get(), m_engine, target); + + // abs + builder.addBlock("operator_mathop"); + builder.addDropdownField("OPERATOR", "abs"); + builder.addValueInput("NUM", 5.7); + builder.captureBlockReturnValue(); + + builder.addBlock("operator_mathop"); + builder.addDropdownField("OPERATOR", "abs"); + builder.addValueInput("NUM", -5.7); + builder.captureBlockReturnValue(); + + // floor + builder.addBlock("operator_mathop"); + builder.addDropdownField("OPERATOR", "floor"); + builder.addValueInput("NUM", 3.2); + builder.captureBlockReturnValue(); + + builder.addBlock("operator_mathop"); + builder.addDropdownField("OPERATOR", "floor"); + builder.addValueInput("NUM", 5.7); + builder.captureBlockReturnValue(); + + // ceiling + builder.addBlock("operator_mathop"); + builder.addDropdownField("OPERATOR", "ceiling"); + builder.addValueInput("NUM", 3.2); + builder.captureBlockReturnValue(); + + builder.addBlock("operator_mathop"); + builder.addDropdownField("OPERATOR", "ceiling"); + builder.addValueInput("NUM", 5.7); + builder.captureBlockReturnValue(); + + // sqrt + builder.addBlock("operator_mathop"); + builder.addDropdownField("OPERATOR", "sqrt"); + builder.addValueInput("NUM", 16); + builder.captureBlockReturnValue(); + + builder.addBlock("operator_mathop"); + builder.addDropdownField("OPERATOR", "sqrt"); + builder.addValueInput("NUM", 2); + builder.captureBlockReturnValue(); + + // sin + builder.addBlock("operator_mathop"); + builder.addDropdownField("OPERATOR", "sin"); + builder.addValueInput("NUM", 90); + builder.captureBlockReturnValue(); + + builder.addBlock("operator_mathop"); + builder.addDropdownField("OPERATOR", "sin"); + builder.addValueInput("NUM", 30); + builder.captureBlockReturnValue(); + + // cos + builder.addBlock("operator_mathop"); + builder.addDropdownField("OPERATOR", "cos"); + builder.addValueInput("NUM", 0); + builder.captureBlockReturnValue(); + + builder.addBlock("operator_mathop"); + builder.addDropdownField("OPERATOR", "cos"); + builder.addValueInput("NUM", 60); + builder.captureBlockReturnValue(); + + // tan + builder.addBlock("operator_mathop"); + builder.addDropdownField("OPERATOR", "tan"); + builder.addValueInput("NUM", 30); + builder.captureBlockReturnValue(); + + builder.addBlock("operator_mathop"); + builder.addDropdownField("OPERATOR", "tan"); + builder.addValueInput("NUM", 45); + builder.captureBlockReturnValue(); + + // asin + builder.addBlock("operator_mathop"); + builder.addDropdownField("OPERATOR", "asin"); + builder.addValueInput("NUM", 1); + builder.captureBlockReturnValue(); + + builder.addBlock("operator_mathop"); + builder.addDropdownField("OPERATOR", "asin"); + builder.addValueInput("NUM", 0.5); + builder.captureBlockReturnValue(); + + // acos + builder.addBlock("operator_mathop"); + builder.addDropdownField("OPERATOR", "acos"); + builder.addValueInput("NUM", 1); + builder.captureBlockReturnValue(); + + builder.addBlock("operator_mathop"); + builder.addDropdownField("OPERATOR", "acos"); + builder.addValueInput("NUM", 0.5); + builder.captureBlockReturnValue(); + + // atan + builder.addBlock("operator_mathop"); + builder.addDropdownField("OPERATOR", "atan"); + builder.addValueInput("NUM", 0.5); + builder.captureBlockReturnValue(); + + builder.addBlock("operator_mathop"); + builder.addDropdownField("OPERATOR", "atan"); + builder.addValueInput("NUM", 1); + builder.captureBlockReturnValue(); + + // ln + builder.addBlock("operator_mathop"); + builder.addDropdownField("OPERATOR", "ln"); + builder.addValueInput("NUM", 1); + builder.captureBlockReturnValue(); + + builder.addBlock("operator_mathop"); + builder.addDropdownField("OPERATOR", "ln"); + builder.addValueInput("NUM", 10); + builder.captureBlockReturnValue(); + + // log + builder.addBlock("operator_mathop"); + builder.addDropdownField("OPERATOR", "log"); + builder.addValueInput("NUM", 1); + builder.captureBlockReturnValue(); + + builder.addBlock("operator_mathop"); + builder.addDropdownField("OPERATOR", "log"); + builder.addValueInput("NUM", 100); + builder.captureBlockReturnValue(); + + // e ^ + builder.addBlock("operator_mathop"); + builder.addDropdownField("OPERATOR", "e ^"); + builder.addValueInput("NUM", 1); + builder.captureBlockReturnValue(); + + builder.addBlock("operator_mathop"); + builder.addDropdownField("OPERATOR", "e ^"); + builder.addValueInput("NUM", 8.2); + builder.captureBlockReturnValue(); + + // 10 ^ + builder.addBlock("operator_mathop"); + builder.addDropdownField("OPERATOR", "10 ^"); + builder.addValueInput("NUM", 1); + builder.captureBlockReturnValue(); + + builder.addBlock("operator_mathop"); + builder.addDropdownField("OPERATOR", "10 ^"); + builder.addValueInput("NUM", 8.2); + builder.captureBlockReturnValue(); + + // invalid + builder.addBlock("operator_mathop"); + builder.addDropdownField("OPERATOR", "invalid"); + builder.addValueInput("NUM", -5.54); + builder.captureBlockReturnValue(); + + builder.build(); + builder.run(); + + List *valueList = builder.capturedValues(); + ValueData *values = valueList->data(); + ASSERT_EQ(valueList->size(), 29); + ASSERT_EQ(std::round(value_toDouble(&values[0]) * 100) / 100, 5.7); + ASSERT_EQ(std::round(value_toDouble(&values[1]) * 100) / 100, 5.7); + ASSERT_EQ(std::round(value_toDouble(&values[2]) * 100) / 100, 3); + ASSERT_EQ(std::round(value_toDouble(&values[3]) * 100) / 100, 5); + ASSERT_EQ(std::round(value_toDouble(&values[4]) * 100) / 100, 4); + ASSERT_EQ(std::round(value_toDouble(&values[5]) * 100) / 100, 6); + ASSERT_EQ(std::round(value_toDouble(&values[6]) * 100) / 100, 4); + ASSERT_EQ(std::round(value_toDouble(&values[7]) * 100) / 100, 1.41); + ASSERT_EQ(std::round(value_toDouble(&values[8]) * 100) / 100, 1); + ASSERT_EQ(std::round(value_toDouble(&values[9]) * 100) / 100, 0.5); + ASSERT_EQ(std::round(value_toDouble(&values[10]) * 100) / 100, 1); + ASSERT_EQ(std::round(value_toDouble(&values[11]) * 100) / 100, 0.5); + ASSERT_EQ(std::round(value_toDouble(&values[12]) * 100) / 100, 0.58); + ASSERT_EQ(std::round(value_toDouble(&values[13]) * 100) / 100, 1); + ASSERT_EQ(std::round(value_toDouble(&values[14]) * 100) / 100, 90); + ASSERT_EQ(std::round(value_toDouble(&values[15]) * 100) / 100, 30); + ASSERT_EQ(std::round(value_toDouble(&values[16]) * 100) / 100, 0); + ASSERT_EQ(std::round(value_toDouble(&values[17]) * 100) / 100, 60); + ASSERT_EQ(std::round(value_toDouble(&values[18]) * 100) / 100, 26.57); + ASSERT_EQ(std::round(value_toDouble(&values[19]) * 100) / 100, 45); + ASSERT_EQ(std::round(value_toDouble(&values[20]) * 100) / 100, 0); + ASSERT_EQ(std::round(value_toDouble(&values[21]) * 100) / 100, 2.3); + ASSERT_EQ(std::round(value_toDouble(&values[22]) * 100) / 100, 0); + ASSERT_EQ(std::round(value_toDouble(&values[23]) * 100) / 100, 2); + ASSERT_EQ(std::round(value_toDouble(&values[24]) * 100) / 100, 2.72); + ASSERT_EQ(std::round(value_toDouble(&values[25]) * 100) / 100, 3640.95); + ASSERT_EQ(std::round(value_toDouble(&values[26]) * 100) / 100, 10); + ASSERT_EQ(std::round(value_toDouble(&values[27]) * 100) / 100, 158489319.25); + ASSERT_EQ(std::round(value_toDouble(&values[28]) * 100) / 100, 0); +} diff --git a/test/dev/compiler/compiler_test.cpp b/test/dev/compiler/compiler_test.cpp index cacd80b5..c2abc8d2 100644 --- a/test/dev/compiler/compiler_test.cpp +++ b/test/dev/compiler/compiler_test.cpp @@ -466,6 +466,24 @@ TEST_F(CompilerTest, CreateDiv) compile(compiler, block); } +TEST_F(CompilerTest, CreateRandom) +{ + Compiler compiler(&m_engine, &m_target); + auto block = std::make_shared("", ""); + + block->setCompileFunction([](Compiler *compiler) -> CompilerValue * { + CompilerValue arg1(Compiler::StaticType::Unknown); + CompilerValue arg2(Compiler::StaticType::Unknown); + CompilerValue ret(Compiler::StaticType::Unknown); + EXPECT_CALL(*m_builder, createRandom(&arg1, &arg2)).WillOnce(Return(&ret)); + EXPECT_EQ(compiler->createRandom(&arg1, &arg2), &ret); + + return nullptr; + }); + + compile(compiler, block); +} + TEST_F(CompilerTest, CreateCmpEQ) { Compiler compiler(&m_engine, &m_target); diff --git a/test/dev/executioncontext/executioncontext_test.cpp b/test/dev/executioncontext/executioncontext_test.cpp index 48504cd1..d6945813 100644 --- a/test/dev/executioncontext/executioncontext_test.cpp +++ b/test/dev/executioncontext/executioncontext_test.cpp @@ -4,6 +4,7 @@ #include #include +#include #include "../../common.h" @@ -40,3 +41,13 @@ TEST(ExecutionContextTest, StackTimer) ctx.setStackTimer(&timer); ASSERT_EQ(ctx.stackTimer(), &timer); } + +TEST(ExecutionContextTest, Rng) +{ + ExecutionContext ctx(nullptr); + ASSERT_TRUE(ctx.rng()); + + RandomGeneratorMock rng; + ctx.setRng(&rng); + ASSERT_EQ(ctx.rng(), &rng); +} diff --git a/test/dev/llvm/llvmcodebuilder_test.cpp b/test/dev/llvm/llvmcodebuilder_test.cpp index 6fd62ac1..fa631be4 100644 --- a/test/dev/llvm/llvmcodebuilder_test.cpp +++ b/test/dev/llvm/llvmcodebuilder_test.cpp @@ -1,5 +1,6 @@ #include #include +#include #include #include #include @@ -10,6 +11,7 @@ #include #include #include +#include #include "testfunctions.h" @@ -27,6 +29,7 @@ class LLVMCodeBuilderTest : public testing::Test Sub, Mul, Div, + Random, CmpEQ, CmpGT, CmpLT, @@ -49,7 +52,6 @@ class LLVMCodeBuilderTest : public testing::Test Log10, Exp, Exp10 - }; void SetUp() override @@ -93,6 +95,9 @@ class LLVMCodeBuilderTest : public testing::Test case OpType::Div: return m_builder->createDiv(arg1, arg2); + case OpType::Random: + return m_builder->createRandom(arg1, arg2); + case OpType::CmpEQ: return m_builder->createCmpEQ(arg1, arg2); @@ -189,6 +194,15 @@ class LLVMCodeBuilderTest : public testing::Test case OpType::Div: return v1 / v2; + case OpType::Random: { + const double sum = v1.toDouble() + v2.toDouble(); + + if (std::isnan(sum) || std::isinf(sum)) + return sum; + + return v1.isInt() && v2.isInt() ? m_rng.randint(v1.toLong(), v2.toLong()) : m_rng.randintDouble(v1.toDouble(), v2.toDouble()); + } + case OpType::CmpEQ: return v1 == v2; @@ -225,7 +239,7 @@ class LLVMCodeBuilderTest : public testing::Test } } - void runOpTest(OpType type, const Value &v1, const Value &v2) + void runOpTestCommon(OpType type, const Value &v1, const Value &v2) { createBuilder(true); @@ -241,20 +255,37 @@ class LLVMCodeBuilderTest : public testing::Test ret = addOp(type, arg1, arg2); m_builder->addFunctionCall("test_print_string", Compiler::StaticType::Void, { Compiler::StaticType::String }, { ret }); - std::string str = doOp(type, v1, v2).toString() + '\n'; - std::string expected = str + str; - auto code = m_builder->finalize(); Script script(&m_target, nullptr, nullptr); script.setCode(code); Thread thread(&m_target, nullptr, &script); auto ctx = code->createExecutionContext(&thread); + ctx->setRng(&m_rng); testing::internal::CaptureStdout(); code->run(ctx.get()); + } + + void checkOpTest(const Value &v1, const Value &v2, const std::string &expected) + { const std::string quotes1 = v1.isString() ? "\"" : ""; const std::string quotes2 = v2.isString() ? "\"" : ""; ASSERT_THAT(testing::internal::GetCapturedStdout(), Eq(expected)) << quotes1 << v1.toString() << quotes1 << " " << quotes2 << v2.toString() << quotes2; + } + + void runOpTest(OpType type, const Value &v1, const Value &v2, const Value &expected) + { + std::string str = expected.toString(); + runOpTestCommon(type, v1, v2); + checkOpTest(v1, v2, str + '\n' + str + '\n'); + }; + + void runOpTest(OpType type, const Value &v1, const Value &v2) + { + runOpTestCommon(type, v1, v2); + std::string str = doOp(type, v1, v2).toString() + '\n'; + std::string expected = str + str; + checkOpTest(v1, v2, expected); }; void runOpTest(OpType type, const Value &v) @@ -317,6 +348,7 @@ class LLVMCodeBuilderTest : public testing::Test std::unique_ptr m_builder; TargetMock m_target; // NOTE: isStage() is used for call expectations + RandomGeneratorMock m_rng; }; TEST_F(LLVMCodeBuilderTest, FunctionCalls) @@ -605,6 +637,86 @@ TEST_F(LLVMCodeBuilderTest, Divide) runOpTest(OpType::Div, 0, 0); } +TEST_F(LLVMCodeBuilderTest, Random) +{ + EXPECT_CALL(m_rng, randint(-45, 12)).Times(3).WillRepeatedly(Return(-18)); + runOpTest(OpType::Random, -45, 12); + + EXPECT_CALL(m_rng, randint(-45, 12)).Times(3).WillRepeatedly(Return(5)); + runOpTest(OpType::Random, -45.0, 12.0); + + EXPECT_CALL(m_rng, randintDouble(12, 6.05)).Times(3).WillRepeatedly(Return(3.486789)); + runOpTest(OpType::Random, 12, 6.05); + + EXPECT_CALL(m_rng, randintDouble(-78.686, -45)).Times(3).WillRepeatedly(Return(-59.468873)); + runOpTest(OpType::Random, -78.686, -45); + + EXPECT_CALL(m_rng, randintDouble(6.05, -78.686)).Times(3).WillRepeatedly(Return(-28.648764)); + runOpTest(OpType::Random, 6.05, -78.686); + + EXPECT_CALL(m_rng, randint(-45, 12)).Times(3).WillRepeatedly(Return(0)); + runOpTest(OpType::Random, "-45", "12"); + + EXPECT_CALL(m_rng, randintDouble(-45, 12)).Times(3).WillRepeatedly(Return(5.2)); + runOpTest(OpType::Random, "-45.0", "12"); + + EXPECT_CALL(m_rng, randintDouble(-45, 12)).Times(3).WillRepeatedly(Return(-15.5787)); + runOpTest(OpType::Random, "-45", "12.0"); + + EXPECT_CALL(m_rng, randintDouble(-45, 12)).Times(3).WillRepeatedly(Return(2.587964)); + runOpTest(OpType::Random, "-45.0", "12.0"); + + EXPECT_CALL(m_rng, randintDouble(6.05, -78.686)).Times(3).WillRepeatedly(Return(5.648764)); + runOpTest(OpType::Random, "6.05", "-78.686"); + + EXPECT_CALL(m_rng, randint(-45, 12)).Times(3).WillRepeatedly(Return(0)); + runOpTest(OpType::Random, "-45", 12); + + EXPECT_CALL(m_rng, randint(-45, 12)).Times(3).WillRepeatedly(Return(0)); + runOpTest(OpType::Random, -45, "12"); + + EXPECT_CALL(m_rng, randintDouble(-45, 12)).Times(3).WillRepeatedly(Return(5.2)); + runOpTest(OpType::Random, "-45.0", 12); + + EXPECT_CALL(m_rng, randintDouble(-45, 12)).Times(3).WillRepeatedly(Return(-15.5787)); + runOpTest(OpType::Random, -45, "12.0"); + + EXPECT_CALL(m_rng, randintDouble(6.05, -78.686)).Times(3).WillRepeatedly(Return(5.648764)); + runOpTest(OpType::Random, 6.05, "-78.686"); + + EXPECT_CALL(m_rng, randintDouble(6.05, -78.686)).Times(3).WillRepeatedly(Return(5.648764)); + runOpTest(OpType::Random, "6.05", -78.686); + + EXPECT_CALL(m_rng, randint(0, 1)).Times(3).WillRepeatedly(Return(1)); + runOpTest(OpType::Random, false, true); + + EXPECT_CALL(m_rng, randint(1, 5)).Times(3).WillRepeatedly(Return(1)); + runOpTest(OpType::Random, true, 5); + + EXPECT_CALL(m_rng, randint(8, 0)).Times(3).WillRepeatedly(Return(1)); + runOpTest(OpType::Random, 8, false); + + const double inf = std::numeric_limits::infinity(); + const double nan = std::numeric_limits::quiet_NaN(); + EXPECT_CALL(m_rng, randint).WillRepeatedly(Return(0)); + EXPECT_CALL(m_rng, randintDouble).WillRepeatedly(Return(0)); + + runOpTest(OpType::Random, inf, 2, inf); + runOpTest(OpType::Random, -8, inf, inf); + runOpTest(OpType::Random, -inf, -2, -inf); + runOpTest(OpType::Random, 8, -inf, -inf); + + runOpTest(OpType::Random, inf, 2.5, inf); + runOpTest(OpType::Random, -8.09, inf, inf); + runOpTest(OpType::Random, -inf, -2.5, -inf); + runOpTest(OpType::Random, 8.09, -inf, -inf); + + runOpTest(OpType::Random, inf, inf, inf); + runOpTest(OpType::Random, -inf, -inf, -inf); + runOpTest(OpType::Random, inf, -inf, nan); + runOpTest(OpType::Random, -inf, inf, nan); +} + TEST_F(LLVMCodeBuilderTest, EqualComparison) { runOpTest(OpType::CmpEQ, 10, 10); diff --git a/test/mocks/codebuildermock.h b/test/mocks/codebuildermock.h index 478252d6..bd381144 100644 --- a/test/mocks/codebuildermock.h +++ b/test/mocks/codebuildermock.h @@ -26,6 +26,8 @@ class CodeBuilderMock : public ICodeBuilder MOCK_METHOD(CompilerValue *, createMul, (CompilerValue *, CompilerValue *), (override)); MOCK_METHOD(CompilerValue *, createDiv, (CompilerValue *, CompilerValue *), (override)); + MOCK_METHOD(CompilerValue *, createRandom, (CompilerValue *, CompilerValue *), (override)); + MOCK_METHOD(CompilerValue *, createCmpEQ, (CompilerValue *, CompilerValue *), (override)); MOCK_METHOD(CompilerValue *, createCmpGT, (CompilerValue *, CompilerValue *), (override)); MOCK_METHOD(CompilerValue *, createCmpLT, (CompilerValue *, CompilerValue *), (override)); diff --git a/test/mocks/randomgeneratormock.h b/test/mocks/randomgeneratormock.h index 6e07f881..eea08335 100644 --- a/test/mocks/randomgeneratormock.h +++ b/test/mocks/randomgeneratormock.h @@ -1,6 +1,6 @@ #pragma once -#include +#include #include using namespace libscratchcpp; diff --git a/test/scratch_classes/value_test.cpp b/test/scratch_classes/value_test.cpp index 04cce470..f59f3078 100644 --- a/test/scratch_classes/value_test.cpp +++ b/test/scratch_classes/value_test.cpp @@ -2795,6 +2795,16 @@ TEST(ValueTest, ComparisonOperators) } } +TEST(ValueTest, DoubleIsInt) +{ + ASSERT_TRUE(value_doubleIsInt(0.0)); + ASSERT_TRUE(value_doubleIsInt(15.0)); + ASSERT_TRUE(value_doubleIsInt(-468.0)); + ASSERT_FALSE(value_doubleIsInt(0.1)); + ASSERT_FALSE(value_doubleIsInt(1.2)); + ASSERT_FALSE(value_doubleIsInt(-12.5852)); +} + TEST(ValueTest, DoubleToCString) { char *ret;