diff --git a/CMakeLists.txt b/CMakeLists.txt index 27fc54d0..c08d26ae 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -59,6 +59,7 @@ target_sources(scratchcpp include/scratchcpp/sprite.h include/scratchcpp/textbubble.h include/scratchcpp/itimer.h + include/scratchcpp/istacktimer.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 1c1d441e..b7f5cc73 100644 --- a/include/scratchcpp/dev/compiler.h +++ b/include/scratchcpp/dev/compiler.h @@ -51,6 +51,7 @@ class LIBSCRATCHCPP_EXPORT Compiler CompilerValue *addTargetFunctionCall(const std::string &functionName, StaticType returnType = StaticType::Void, const ArgTypes &argTypes = {}, const Args &args = {}); CompilerValue *addFunctionCallWithCtx(const std::string &functionName, StaticType returnType = StaticType::Void, const ArgTypes &argTypes = {}, const Args &args = {}); CompilerConstant *addConstValue(const Value &value); + CompilerValue *addLoopIndex(); CompilerValue *addVariableValue(Variable *variable); CompilerValue *addListContents(List *list); CompilerValue *addListItem(List *list, CompilerValue *index); @@ -115,6 +116,9 @@ class LIBSCRATCHCPP_EXPORT Compiler void moveToRepeatUntilLoop(CompilerValue *cond, std::shared_ptr substack); void warp(); + void createYield(); + void createStop(); + Input *input(const std::string &name) const; Field *field(const std::string &name) const; diff --git a/include/scratchcpp/dev/executioncontext.h b/include/scratchcpp/dev/executioncontext.h index 2f41ea65..970041c0 100644 --- a/include/scratchcpp/dev/executioncontext.h +++ b/include/scratchcpp/dev/executioncontext.h @@ -11,6 +11,7 @@ namespace libscratchcpp class Thread; class IEngine; class Promise; +class IStackTimer; class ExecutionContextPrivate; /*! \brief The ExecutionContext represents the execution context of a target (can be a clone) with variables, lists, etc. */ @@ -27,6 +28,9 @@ class LIBSCRATCHCPP_EXPORT ExecutionContext std::shared_ptr promise() const; void setPromise(std::shared_ptr promise); + IStackTimer *stackTimer() const; + void setStackTimer(IStackTimer *newStackTimer); + private: spimpl::unique_impl_ptr impl; }; diff --git a/include/scratchcpp/istacktimer.h b/include/scratchcpp/istacktimer.h new file mode 100644 index 00000000..c482042b --- /dev/null +++ b/include/scratchcpp/istacktimer.h @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: Apache-2.0 + +#pragma once + +#include "global.h" + +namespace libscratchcpp +{ + +/*! + * \brief The IStackTimer interface represents a timer that can be used by blocks. + * + * You can get a stack timer using ExecutionContext#stackTimer(). + */ +class LIBSCRATCHCPP_EXPORT IStackTimer +{ + public: + virtual ~IStackTimer() { } + + /*! Starts the timer. */ + virtual void start(double seconds) = 0; + + /*! Stops the timer. */ + virtual void stop() = 0; + + /*! Returns true if the timer has been stopped using stop() or wasn't used at all. */ + virtual bool stopped() const = 0; + + /*! Returns true if the timer has elapsed. */ + virtual bool elapsed() const = 0; +}; + +} // namespace libscratchcpp diff --git a/src/dev/blocks/controlblocks.cpp b/src/dev/blocks/controlblocks.cpp index 937e5943..9567f69f 100644 --- a/src/dev/blocks/controlblocks.cpp +++ b/src/dev/blocks/controlblocks.cpp @@ -1,5 +1,17 @@ // SPDX-License-Identifier: Apache-2.0 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + #include "controlblocks.h" using namespace libscratchcpp; @@ -16,4 +28,206 @@ std::string ControlBlocks::description() const void ControlBlocks::registerBlocks(IEngine *engine) { + engine->addCompileFunction(this, "control_forever", &compileForever); + engine->addCompileFunction(this, "control_repeat", &compileRepeat); + engine->addCompileFunction(this, "control_if", &compileIf); + engine->addCompileFunction(this, "control_if_else", &compileIfElse); + engine->addCompileFunction(this, "control_stop", &compileStop); + engine->addCompileFunction(this, "control_wait", &compileWait); + engine->addCompileFunction(this, "control_wait_until", &compileWaitUntil); + engine->addCompileFunction(this, "control_repeat_until", &compileRepeatUntil); + engine->addCompileFunction(this, "control_while", &compileWhile); + engine->addCompileFunction(this, "control_for_each", &compileForEach); + engine->addCompileFunction(this, "control_start_as_clone", &compileStartAsClone); + engine->addCompileFunction(this, "control_create_clone_of", &compileCreateCloneOf); + engine->addCompileFunction(this, "control_delete_this_clone", &compileDeleteThisClone); +} + +CompilerValue *ControlBlocks::compileForever(Compiler *compiler) +{ + auto substack = compiler->input("SUBSTACK"); + compiler->beginLoopCondition(); + compiler->moveToWhileLoop(compiler->addConstValue(true), substack ? substack->valueBlock() : nullptr); + return nullptr; +} + +CompilerValue *ControlBlocks::compileRepeat(Compiler *compiler) +{ + auto substack = compiler->input("SUBSTACK"); + compiler->moveToRepeatLoop(compiler->addInput("TIMES"), substack ? substack->valueBlock() : nullptr); + return nullptr; +} + +CompilerValue *ControlBlocks::compileIf(Compiler *compiler) +{ + auto substack = compiler->input("SUBSTACK"); + compiler->moveToIf(compiler->addInput("CONDITION"), substack ? substack->valueBlock() : nullptr); + return nullptr; +} + +CompilerValue *ControlBlocks::compileIfElse(Compiler *compiler) +{ + auto substack = compiler->input("SUBSTACK"); + auto substack2 = compiler->input("SUBSTACK2"); + compiler->moveToIfElse(compiler->addInput("CONDITION"), substack ? substack->valueBlock() : nullptr, substack2 ? substack2->valueBlock() : nullptr); + return nullptr; +} + +CompilerValue *ControlBlocks::compileStop(Compiler *compiler) +{ + Field *option = compiler->field("STOP_OPTION"); + + if (option) { + std::string str = option->value().toString(); + + if (str == "all") + compiler->addFunctionCallWithCtx("control_stop_all", Compiler::StaticType::Void); + else if (str == "this script") + compiler->createStop(); + else if (str == "other scripts in sprite" || str == "other scripts in stage") + compiler->addFunctionCallWithCtx("control_stop_other_scripts_in_target", Compiler::StaticType::Void); + } + + return nullptr; +} + +CompilerValue *ControlBlocks::compileWait(Compiler *compiler) +{ + auto duration = compiler->addInput("DURATION"); + compiler->addFunctionCallWithCtx("control_start_wait", Compiler::StaticType::Void, { Compiler::StaticType::Number }, { duration }); + compiler->createYield(); + + compiler->beginLoopCondition(); + auto elapsed = compiler->addFunctionCallWithCtx("control_stack_timer_elapsed", Compiler::StaticType::Bool); + compiler->beginRepeatUntilLoop(elapsed); + compiler->endLoop(); + + return nullptr; +} + +CompilerValue *ControlBlocks::compileWaitUntil(Compiler *compiler) +{ + compiler->beginLoopCondition(); + compiler->beginRepeatUntilLoop(compiler->addInput("CONDITION")); + compiler->endLoop(); + return nullptr; +} + +CompilerValue *ControlBlocks::compileRepeatUntil(Compiler *compiler) +{ + auto substack = compiler->input("SUBSTACK"); + compiler->beginLoopCondition(); + compiler->moveToRepeatUntilLoop(compiler->addInput("CONDITION"), substack ? substack->valueBlock() : nullptr); + return nullptr; +} + +CompilerValue *ControlBlocks::compileWhile(Compiler *compiler) +{ + auto substack = compiler->input("SUBSTACK"); + compiler->beginLoopCondition(); + compiler->moveToWhileLoop(compiler->addInput("CONDITION"), substack ? substack->valueBlock() : nullptr); + return nullptr; +} + +CompilerValue *ControlBlocks::compileForEach(Compiler *compiler) +{ + Variable *var = static_cast(compiler->field("VARIABLE")->valuePtr().get()); + assert(var); + auto substack = compiler->input("SUBSTACK"); + compiler->moveToRepeatLoop(compiler->addInput("VALUE"), substack ? substack->valueBlock() : nullptr); + auto index = compiler->createAdd(compiler->addLoopIndex(), compiler->addConstValue(1)); + compiler->createVariableWrite(var, index); + return nullptr; +} + +CompilerValue *ControlBlocks::compileStartAsClone(Compiler *compiler) +{ + compiler->engine()->addCloneInitScript(compiler->block()); + return nullptr; +} + +CompilerValue *ControlBlocks::compileCreateCloneOf(Compiler *compiler) +{ + Input *input = compiler->input("CLONE_OPTION"); + + if (input->pointsToDropdownMenu()) { + std::string spriteName = input->selectedMenuItem(); + + if (spriteName == "_myself_") + compiler->addTargetFunctionCall("control_create_clone_of_myself"); + else { + auto index = compiler->engine()->findTarget(spriteName); + CompilerValue *arg = compiler->addConstValue(index); + compiler->addFunctionCallWithCtx("control_create_clone_by_index", Compiler::StaticType::Void, { Compiler::StaticType::Number }, { arg }); + } + } else { + CompilerValue *arg = compiler->addInput("CLONE_OPTION"); + compiler->addFunctionCallWithCtx("control_create_clone", Compiler::StaticType::Void, { Compiler::StaticType::String }, { arg }); + } + + return nullptr; +} + +CompilerValue *ControlBlocks::compileDeleteThisClone(Compiler *compiler) +{ + compiler->addTargetFunctionCall("control_delete_this_clone"); + return nullptr; +} + +extern "C" void control_stop_all(ExecutionContext *ctx) +{ + ctx->engine()->stop(); +} + +extern "C" void control_stop_other_scripts_in_target(ExecutionContext *ctx) +{ + Thread *thread = ctx->thread(); + ctx->engine()->stopTarget(thread->target(), thread); +} + +extern "C" void control_start_wait(ExecutionContext *ctx, double seconds) +{ + ctx->stackTimer()->start(seconds); + ctx->engine()->requestRedraw(); +} + +extern "C" bool control_stack_timer_elapsed(ExecutionContext *ctx) +{ + return ctx->stackTimer()->elapsed(); +} + +extern "C" void control_create_clone_of_myself(Target *target) +{ + if (!target->isStage()) + static_cast(target)->clone(); +} + +extern "C" void control_create_clone_by_index(ExecutionContext *ctx, double index) +{ + Target *target = ctx->engine()->targetAt(index); + + if (!target->isStage()) + static_cast(target)->clone(); +} + +extern "C" void control_create_clone(ExecutionContext *ctx, const char *spriteName) +{ + if (strcmp(spriteName, "_myself_") == 0) + control_create_clone_of_myself(ctx->thread()->target()); + else { + IEngine *engine = ctx->engine(); + auto index = engine->findTarget(spriteName); + Target *target = engine->targetAt(index); + + if (!target->isStage()) + static_cast(target)->clone(); + } +} + +extern "C" void control_delete_this_clone(Target *target) +{ + if (!target->isStage()) { + target->engine()->stopTarget(target, nullptr); + static_cast(target)->deleteClone(); + } } diff --git a/src/dev/blocks/controlblocks.h b/src/dev/blocks/controlblocks.h index 155f9296..17850824 100644 --- a/src/dev/blocks/controlblocks.h +++ b/src/dev/blocks/controlblocks.h @@ -14,6 +14,21 @@ class ControlBlocks : public IExtension std::string description() const override; void registerBlocks(IEngine *engine) override; + + private: + static CompilerValue *compileForever(Compiler *compiler); + static CompilerValue *compileRepeat(Compiler *compiler); + static CompilerValue *compileIf(Compiler *compiler); + static CompilerValue *compileIfElse(Compiler *compiler); + static CompilerValue *compileStop(Compiler *compiler); + static CompilerValue *compileWait(Compiler *compiler); + static CompilerValue *compileWaitUntil(Compiler *compiler); + static CompilerValue *compileRepeatUntil(Compiler *compiler); + static CompilerValue *compileWhile(Compiler *compiler); + static CompilerValue *compileForEach(Compiler *compiler); + static CompilerValue *compileStartAsClone(Compiler *compiler); + static CompilerValue *compileCreateCloneOf(Compiler *compiler); + static CompilerValue *compileDeleteThisClone(Compiler *compiler); }; } // namespace libscratchcpp diff --git a/src/dev/engine/compiler.cpp b/src/dev/engine/compiler.cpp index db4c0b54..0c633c72 100644 --- a/src/dev/engine/compiler.cpp +++ b/src/dev/engine/compiler.cpp @@ -42,6 +42,7 @@ std::shared_ptr Compiler::compile(std::shared_ptr startBl impl->builder = impl->builderFactory->create(impl->target, startBlock->id(), false); impl->substackTree.clear(); impl->substackHit = false; + impl->emptySubstack = false; impl->warp = false; impl->block = startBlock; @@ -55,6 +56,11 @@ std::shared_ptr Compiler::compile(std::shared_ptr startBl assert(false); } + if (impl->emptySubstack) { + impl->emptySubstack = false; + impl->substackEnd(); + } + if (impl->customLoopCount > 0) { std::cerr << "error: loop created by block '" << impl->block->opcode() << "' not terminated" << std::endl; assert(false); @@ -115,6 +121,12 @@ CompilerConstant *Compiler::addConstValue(const Value &value) return static_cast(impl->builder->addConstValue(value)); } +/*! Adds the index of the current repeat loop to the compiled code. */ +CompilerValue *Compiler::addLoopIndex() +{ + return impl->builder->addLoopIndex(); +} + /*! Adds the value of the given variable to the code. */ CompilerValue *Compiler::addVariableValue(Variable *variable) { @@ -450,7 +462,7 @@ void Compiler::moveToIfElse(CompilerValue *cond, std::shared_ptr substack impl->builder->beginIfStatement(cond); if (!impl->block) - impl->substackEnd(); + impl->emptySubstack = true; } /*! Jumps to the given repeat loop substack. */ @@ -462,7 +474,7 @@ void Compiler::moveToRepeatLoop(CompilerValue *count, std::shared_ptr sub impl->builder->beginRepeatLoop(count); if (!impl->block) - impl->substackEnd(); + impl->emptySubstack = true; } /*! Jumps to the given while loop substack. */ @@ -474,7 +486,7 @@ void Compiler::moveToWhileLoop(CompilerValue *cond, std::shared_ptr subst impl->builder->beginWhileLoop(cond); if (!impl->block) - impl->substackEnd(); + impl->emptySubstack = true; } /*! Jumps to the given until loop substack. */ @@ -486,7 +498,7 @@ void Compiler::moveToRepeatUntilLoop(CompilerValue *cond, std::shared_ptr impl->builder->beginRepeatUntilLoop(cond); if (!impl->block) - impl->substackEnd(); + impl->emptySubstack = true; } /*! Makes current script run without screen refresh. */ @@ -495,6 +507,18 @@ void Compiler::warp() impl->warp = true; } +/*! Creates a suspend instruction. */ +void Compiler::createYield() +{ + impl->builder->yield(); +} + +/*! Creates a stop script instruction. */ +void Compiler::createStop() +{ + impl->builder->createStop(); +} + /*! Convenience method which returns the field with the given name. */ Input *Compiler::input(const std::string &name) const { diff --git a/src/dev/engine/compiler_p.h b/src/dev/engine/compiler_p.h index 740d3be2..add1021c 100644 --- a/src/dev/engine/compiler_p.h +++ b/src/dev/engine/compiler_p.h @@ -36,6 +36,7 @@ struct CompilerPrivate int customLoopCount = 0; std::vector, std::shared_ptr>, SubstackType>> substackTree; bool substackHit = false; + bool emptySubstack = false; bool warp = false; static inline ICodeBuilderFactory *builderFactory = nullptr; diff --git a/src/dev/engine/executioncontext.cpp b/src/dev/engine/executioncontext.cpp index b0e74926..5c72cd54 100644 --- a/src/dev/engine/executioncontext.cpp +++ b/src/dev/engine/executioncontext.cpp @@ -36,3 +36,15 @@ void ExecutionContext::setPromise(std::shared_ptr promise) { impl->promise = promise; } + +/*! Returns the stack timer of this context. Can be used for wait blocks. */ +IStackTimer *ExecutionContext::stackTimer() const +{ + return impl->stackTimer; +} + +/*! Sets a custom stack timer. */ +void ExecutionContext::setStackTimer(IStackTimer *newStackTimer) +{ + impl->stackTimer = newStackTimer; +} diff --git a/src/dev/engine/executioncontext_p.cpp b/src/dev/engine/executioncontext_p.cpp index bb42caac..03970aa7 100644 --- a/src/dev/engine/executioncontext_p.cpp +++ b/src/dev/engine/executioncontext_p.cpp @@ -5,6 +5,8 @@ using namespace libscratchcpp; ExecutionContextPrivate::ExecutionContextPrivate(Thread *thread) : - thread(thread) + thread(thread), + defaultStackTimer(std::make_unique()), + stackTimer(defaultStackTimer.get()) { } diff --git a/src/dev/engine/executioncontext_p.h b/src/dev/engine/executioncontext_p.h index 19322c13..c556839d 100644 --- a/src/dev/engine/executioncontext_p.h +++ b/src/dev/engine/executioncontext_p.h @@ -4,11 +4,12 @@ #include +#include "../../engine/internal/stacktimer.h" + namespace libscratchcpp { class Thread; -class Target; class Promise; struct ExecutionContextPrivate @@ -17,6 +18,8 @@ struct ExecutionContextPrivate Thread *thread = nullptr; std::shared_ptr promise; + std::unique_ptr defaultStackTimer; + IStackTimer *stackTimer = nullptr; }; } // namespace libscratchcpp diff --git a/src/dev/engine/internal/icodebuilder.h b/src/dev/engine/internal/icodebuilder.h index 5132f1a9..c92c3222 100644 --- a/src/dev/engine/internal/icodebuilder.h +++ b/src/dev/engine/internal/icodebuilder.h @@ -23,6 +23,7 @@ class ICodeBuilder virtual CompilerValue *addTargetFunctionCall(const std::string &functionName, Compiler::StaticType returnType, const Compiler::ArgTypes &argTypes, const Compiler::Args &args) = 0; virtual CompilerValue *addFunctionCallWithCtx(const std::string &functionName, Compiler::StaticType returnType, const Compiler::ArgTypes &argTypes, const Compiler::Args &args) = 0; virtual CompilerConstant *addConstValue(const Value &value) = 0; + virtual CompilerValue *addLoopIndex() = 0; virtual CompilerValue *addVariableValue(Variable *variable) = 0; virtual CompilerValue *addListContents(List *list) = 0; virtual CompilerValue *addListItem(List *list, CompilerValue *index) = 0; @@ -81,6 +82,8 @@ class ICodeBuilder virtual void endLoop() = 0; virtual void yield() = 0; + + virtual void createStop() = 0; }; } // namespace libscratchcpp diff --git a/src/dev/engine/internal/llvm/llvmcodebuilder.cpp b/src/dev/engine/internal/llvm/llvmcodebuilder.cpp index da31cea5..069f7bd9 100644 --- a/src/dev/engine/internal/llvm/llvmcodebuilder.cpp +++ b/src/dev/engine/internal/llvm/llvmcodebuilder.cpp @@ -68,6 +68,7 @@ std::shared_ptr LLVMCodeBuilder::finalize() llvm::Value *targetLists = func->getArg(3); llvm::BasicBlock *entry = llvm::BasicBlock::Create(m_ctx, "entry", func); + llvm::BasicBlock *endBranch = llvm::BasicBlock::Create(m_ctx, "end", func); m_builder.SetInsertPoint(entry); // Init coroutine @@ -784,6 +785,7 @@ std::shared_ptr LLVMCodeBuilder::finalize() const auto ® = step.args[0]; assert(reg.first == Compiler::StaticType::Number); llvm::Value *count = castValue(reg.second, reg.first); + llvm::Value *isInf = m_builder.CreateFCmpOEQ(count, llvm::ConstantFP::getInfinity(m_builder.getDoubleTy(), false)); // Clamp count if <= 0 (we can skip the loop if count is not positive) llvm::Value *comparison = m_builder.CreateFCmpULE(count, llvm::ConstantFP::get(m_ctx, llvm::APFloat(0.0))); @@ -794,7 +796,8 @@ std::shared_ptr LLVMCodeBuilder::finalize() m_builder.SetInsertPoint(roundBranch); llvm::Function *roundFunc = llvm::Intrinsic::getDeclaration(m_module.get(), llvm::Intrinsic::round, { count->getType() }); count = m_builder.CreateCall(roundFunc, { count }); - count = m_builder.CreateFPToSI(count, m_builder.getInt64Ty()); // cast to signed integer + count = m_builder.CreateFPToUI(count, m_builder.getInt64Ty()); // cast to unsigned integer + count = m_builder.CreateSelect(isInf, zero, count); // Jump to condition branch m_builder.CreateBr(loop.conditionBranch); @@ -808,7 +811,7 @@ std::shared_ptr LLVMCodeBuilder::finalize() loop.afterLoop = llvm::BasicBlock::Create(m_ctx, "", func); llvm::Value *currentIndex = m_builder.CreateLoad(m_builder.getInt64Ty(), loop.index); - comparison = m_builder.CreateICmpULT(currentIndex, count); + comparison = m_builder.CreateOr(isInf, m_builder.CreateICmpULT(currentIndex, count)); m_builder.CreateCondBr(comparison, body, loop.afterLoop); // Switch to body branch @@ -819,6 +822,14 @@ std::shared_ptr LLVMCodeBuilder::finalize() break; } + case LLVMInstruction::Type::LoopIndex: { + assert(!loops.empty()); + LLVMLoop &loop = loops.back(); + llvm::Value *index = m_builder.CreateLoad(m_builder.getInt64Ty(), loop.index); + step.functionReturnReg->value = m_builder.CreateUIToFP(index, m_builder.getDoubleTy()); + break; + } + case LLVMInstruction::Type::BeginWhileLoop: { assert(!loops.empty()); LLVMLoop &loop = loops.back(); @@ -881,7 +892,7 @@ std::shared_ptr LLVMCodeBuilder::finalize() if (loop.isRepeatLoop) { // Increment index llvm::Value *currentIndex = m_builder.CreateLoad(m_builder.getInt64Ty(), loop.index); - llvm::Value *incremented = m_builder.CreateAdd(currentIndex, llvm::ConstantInt::get(m_builder.getInt64Ty(), 1, true)); + llvm::Value *incremented = m_builder.CreateAdd(currentIndex, llvm::ConstantInt::get(m_builder.getInt64Ty(), 1, false)); m_builder.CreateStore(incremented, loop.index); } @@ -896,9 +907,19 @@ std::shared_ptr LLVMCodeBuilder::finalize() popScopeLevel(); break; } + + case LLVMInstruction::Type::Stop: { + m_builder.CreateBr(endBranch); + llvm::BasicBlock *nextBranch = llvm::BasicBlock::Create(m_ctx, "", func); + m_builder.SetInsertPoint(nextBranch); + break; + } } } + m_builder.CreateBr(endBranch); + + m_builder.SetInsertPoint(endBranch); freeHeap(); syncVariables(targetVariables); @@ -980,6 +1001,11 @@ CompilerConstant *LLVMCodeBuilder::addConstValue(const Value &value) return static_cast(addReg(reg)); } +CompilerValue *LLVMCodeBuilder::addLoopIndex() +{ + return createOp(LLVMInstruction::Type::LoopIndex, Compiler::StaticType::Number, {}, {}); +} + CompilerValue *LLVMCodeBuilder::addVariableValue(Variable *variable) { LLVMInstruction ins(LLVMInstruction::Type::ReadVariable); @@ -1281,6 +1307,11 @@ void LLVMCodeBuilder::yield() m_instructions.push_back({ LLVMInstruction::Type::Yield }); } +void LLVMCodeBuilder::createStop() +{ + m_instructions.push_back({ LLVMInstruction::Type::Stop }); +} + void LLVMCodeBuilder::initTypes() { m_valueDataType = LLVMTypes::createValueDataType(&m_builder); diff --git a/src/dev/engine/internal/llvm/llvmcodebuilder.h b/src/dev/engine/internal/llvm/llvmcodebuilder.h index 1978ef30..afdf12de 100644 --- a/src/dev/engine/internal/llvm/llvmcodebuilder.h +++ b/src/dev/engine/internal/llvm/llvmcodebuilder.h @@ -30,6 +30,7 @@ class LLVMCodeBuilder : public ICodeBuilder 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 *addLoopIndex() override; CompilerValue *addVariableValue(Variable *variable) override; CompilerValue *addListContents(List *list) override; CompilerValue *addListItem(List *list, CompilerValue *index) override; @@ -89,6 +90,8 @@ class LLVMCodeBuilder : public ICodeBuilder void yield() override; + void createStop() override; + private: enum class Comparison { diff --git a/src/dev/engine/internal/llvm/llvminstruction.h b/src/dev/engine/internal/llvm/llvminstruction.h index b770008e..9b81b563 100644 --- a/src/dev/engine/internal/llvm/llvminstruction.h +++ b/src/dev/engine/internal/llvm/llvminstruction.h @@ -58,10 +58,12 @@ struct LLVMInstruction BeginElse, EndIf, BeginRepeatLoop, + LoopIndex, BeginWhileLoop, BeginRepeatUntilLoop, BeginLoopCondition, - EndLoop + EndLoop, + Stop }; LLVMInstruction(Type type) : diff --git a/src/dev/test/scriptbuilder.cpp b/src/dev/test/scriptbuilder.cpp index 97d3aac2..ea232b07 100644 --- a/src/dev/test/scriptbuilder.cpp +++ b/src/dev/test/scriptbuilder.cpp @@ -103,8 +103,23 @@ void ScriptBuilder::addObscuredInput(const std::string &name, std::shared_ptrlastBlock) return; - valueBlock->setId(std::to_string(impl->blockId++)); - impl->inputBlocks.push_back(valueBlock); + auto block = valueBlock; + + while (block) { + block->setId(std::to_string(impl->blockId++)); + impl->inputBlocks.push_back(block); + + auto parent = block->parent(); + auto next = block->next(); + + if (parent) + parent->setNext(block); + + if (next) + next->setParent(block); + + block = next; + } auto input = std::make_shared(name, Input::Type::ObscuredShadow); input->setValueBlock(valueBlock); diff --git a/src/engine/CMakeLists.txt b/src/engine/CMakeLists.txt index 71402cef..506489de 100644 --- a/src/engine/CMakeLists.txt +++ b/src/engine/CMakeLists.txt @@ -16,6 +16,8 @@ target_sources(scratchcpp internal/iclock.h internal/timer.cpp internal/timer.h + internal/stacktimer.cpp + internal/stacktimer.h internal/randomgenerator.h internal/randomgenerator.cpp internal/irandomgenerator.h diff --git a/src/engine/internal/stacktimer.cpp b/src/engine/internal/stacktimer.cpp new file mode 100644 index 00000000..01ae3bc7 --- /dev/null +++ b/src/engine/internal/stacktimer.cpp @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: Apache-2.0 + +#include +#include + +#include "stacktimer.h" +#include "clock.h" + +using namespace libscratchcpp; + +StackTimer::StackTimer() +{ + m_clock = Clock::instance().get(); +} + +StackTimer::StackTimer(IClock *clock) : + m_clock(clock) +{ + assert(clock); +} + +void StackTimer::start(double seconds) +{ + m_startTime = m_clock->currentSteadyTime(); + m_timeLimit = seconds * 1000; + m_stopped = false; +} + +void StackTimer::stop() +{ + m_stopped = true; +} + +bool StackTimer::stopped() const +{ + return m_stopped; +} + +bool StackTimer::elapsed() const +{ + if (m_stopped) + return false; + + return std::chrono::duration_cast(m_clock->currentSteadyTime() - m_startTime).count() >= m_timeLimit; +} diff --git a/src/engine/internal/stacktimer.h b/src/engine/internal/stacktimer.h new file mode 100644 index 00000000..54ece0c8 --- /dev/null +++ b/src/engine/internal/stacktimer.h @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: Apache-2.0 + +#pragma once + +#include +#include + +namespace libscratchcpp +{ + +class IClock; + +class StackTimer : public IStackTimer +{ + public: + StackTimer(); + StackTimer(IClock *clock); + StackTimer(const StackTimer &) = delete; + + void start(double seconds) override; + void stop() override; + + bool stopped() const override; + bool elapsed() const override; + + private: + std::chrono::steady_clock::time_point m_startTime; + bool m_stopped = true; + long m_timeLimit = 0; + IClock *m_clock = nullptr; +}; + +} // namespace libscratchcpp diff --git a/test/dev/blocks/CMakeLists.txt b/test/dev/blocks/CMakeLists.txt index 3e32a0d0..3fc113f7 100644 --- a/test/dev/blocks/CMakeLists.txt +++ b/test/dev/blocks/CMakeLists.txt @@ -1,3 +1,15 @@ +add_library( + block_test_deps SHARED + util.cpp + util.h +) + +target_link_libraries( + block_test_deps + GTest::gtest_main + scratchcpp +) + # motion_blocks_test if (LIBSCRATCHCPP_ENABLE_MOTION_BLOCKS) add_executable( @@ -83,6 +95,7 @@ if (LIBSCRATCHCPP_ENABLE_CONTROL_BLOCKS) GTest::gmock_main scratchcpp scratchcpp_mocks + block_test_deps ) gtest_discover_tests(control_blocks_test) diff --git a/test/dev/blocks/control_blocks_test.cpp b/test/dev/blocks/control_blocks_test.cpp index b5cff150..9f054691 100644 --- a/test/dev/blocks/control_blocks_test.cpp +++ b/test/dev/blocks/control_blocks_test.cpp @@ -1,15 +1,1115 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + #include +#include #include "../common.h" #include "dev/blocks/controlblocks.h" +#include "util.h" using namespace libscratchcpp; +using namespace libscratchcpp::test; + +using ::testing::Return; +using ::testing::SaveArg; +using ::testing::_; class ControlBlocksTest : 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; }; + +TEST_F(ControlBlocksTest, Forever) +{ + auto target = std::make_shared(); + + { + ScriptBuilder builder(m_extension.get(), m_engine, target); + + builder.addBlock("control_forever"); + auto substack = std::make_shared("", "test_print_test"); + builder.addObscuredInput("SUBSTACK", substack); + + builder.build(); + m_engine->start(); + + for (int i = 0; i < 2; i++) { + testing::internal::CaptureStdout(); + m_engine->step(); + ASSERT_EQ(testing::internal::GetCapturedStdout().substr(0, 10), "test\ntest\n"); + ASSERT_TRUE(m_engine->isRunning()); + } + } + + m_engine->clear(); + target = std::make_shared(); + + { + ScriptBuilder builder(m_extension.get(), m_engine, target); + builder.addBlock("control_forever"); + + builder.build(); + m_engine->start(); + + for (int i = 0; i < 2; i++) { + m_engine->step(); + ASSERT_TRUE(m_engine->isRunning()); + } + } +} + +TEST_F(ControlBlocksTest, Repeat) +{ + auto target = std::make_shared(); + + { + ScriptBuilder builder(m_extension.get(), m_engine, target); + + builder.addBlock("control_repeat"); + auto substack = std::make_shared("", "test_print_test"); + builder.addObscuredInput("SUBSTACK", substack); + builder.addValueInput("TIMES", 5); + + builder.addBlock("control_repeat"); + substack = std::make_shared("", "test_print_test"); + builder.addObscuredInput("SUBSTACK", substack); + builder.addNullObscuredInput("TIMES"); + + builder.build(); + + testing::internal::CaptureStdout(); + builder.run(); + ASSERT_EQ(testing::internal::GetCapturedStdout(), "test\ntest\ntest\ntest\ntest\n"); + } + + m_engine->clear(); + target = std::make_shared(); + + { + ScriptBuilder builder(m_extension.get(), m_engine, target); + builder.addBlock("control_repeat"); + builder.addValueInput("TIMES", "Infinity"); + + builder.build(); + m_engine->start(); + + for (int i = 0; i < 2; i++) { + m_engine->step(); + ASSERT_TRUE(m_engine->isRunning()); + } + } +} + +TEST_F(ControlBlocksTest, If) +{ + auto target = std::make_shared(); + + { + ScriptBuilder builder(m_extension.get(), m_engine, target); + + builder.addBlock("control_if"); + auto substack = std::make_shared("", "test_print_test"); + builder.addValueInput("CONDITION", false); + builder.addObscuredInput("SUBSTACK", substack); + + builder.addBlock("control_if"); + substack = std::make_shared("", "test_print_test"); + builder.addNullObscuredInput("CONDITION"); + builder.addObscuredInput("SUBSTACK", substack); + + builder.addBlock("control_if"); + substack = std::make_shared("", "test_print_test"); + builder.addValueInput("CONDITION", true); + builder.addObscuredInput("SUBSTACK", substack); + + builder.build(); + + testing::internal::CaptureStdout(); + builder.run(); + ASSERT_EQ(testing::internal::GetCapturedStdout(), "test\n"); + } + + m_engine->clear(); + target = std::make_shared(); + + { + ScriptBuilder builder(m_extension.get(), m_engine, target); + builder.addBlock("control_if"); + builder.addValueInput("CONDITION", true); + + builder.build(); + builder.run(); + } +} + +TEST_F(ControlBlocksTest, IfElse) +{ + auto target = std::make_shared(); + + { + ScriptBuilder builder(m_extension.get(), m_engine, target); + + builder.addBlock("control_if_else"); + builder.addValueInput("CONDITION", false); + auto substack = std::make_shared("", "test_print_test"); + builder.addObscuredInput("SUBSTACK", substack); + auto substack2 = std::make_shared("", "test_print_test2"); + builder.addObscuredInput("SUBSTACK2", substack2); + + builder.addBlock("control_if_else"); + builder.addNullObscuredInput("CONDITION"); + substack = std::make_shared("", "test_print_test"); + builder.addObscuredInput("SUBSTACK", substack); + substack2 = std::make_shared("", "test_print_test2"); + builder.addObscuredInput("SUBSTACK2", substack2); + + builder.addBlock("control_if_else"); + builder.addValueInput("CONDITION", true); + substack = std::make_shared("", "test_print_test"); + builder.addObscuredInput("SUBSTACK", substack); + substack2 = std::make_shared("", "test_print_test2"); + builder.addObscuredInput("SUBSTACK2", substack2); + + builder.build(); + + testing::internal::CaptureStdout(); + builder.run(); + ASSERT_EQ(testing::internal::GetCapturedStdout(), "test2\ntest2\ntest\n"); + } + + m_engine->clear(); + target = std::make_shared(); + + { + ScriptBuilder builder(m_extension.get(), m_engine, target); + + builder.addBlock("control_if_else"); + builder.addValueInput("CONDITION", false); + auto substack2 = std::make_shared("", "test_print_test2"); + builder.addObscuredInput("SUBSTACK2", substack2); + + builder.addBlock("control_if_else"); + builder.addValueInput("CONDITION", true); + substack2 = std::make_shared("", "test_print_test2"); + builder.addObscuredInput("SUBSTACK2", substack2); + + builder.build(); + + testing::internal::CaptureStdout(); + builder.run(); + ASSERT_EQ(testing::internal::GetCapturedStdout(), "test2\n"); + } + + m_engine->clear(); + target = std::make_shared(); + + { + ScriptBuilder builder(m_extension.get(), m_engine, target); + + builder.addBlock("control_if_else"); + builder.addValueInput("CONDITION", false); + auto substack = std::make_shared("", "test_print_test"); + builder.addObscuredInput("SUBSTACK", substack); + + builder.addBlock("control_if_else"); + builder.addValueInput("CONDITION", true); + substack = std::make_shared("", "test_print_test"); + builder.addObscuredInput("SUBSTACK", substack); + + builder.build(); + + testing::internal::CaptureStdout(); + builder.run(); + ASSERT_EQ(testing::internal::GetCapturedStdout(), "test\n"); + } + + m_engine->clear(); + target = std::make_shared(); + + { + ScriptBuilder builder(m_extension.get(), m_engine, target); + + builder.addBlock("control_if_else"); + builder.addValueInput("CONDITION", false); + + builder.addBlock("control_if_else"); + builder.addValueInput("CONDITION", true); + + builder.build(); + builder.run(); + } +} + +TEST_F(ControlBlocksTest, Stop) +{ + auto target = std::make_shared(); + + // Stop all + { + ScriptBuilder builder(m_extension.get(), m_engine, target); + + builder.addBlock("control_stop"); + builder.addDropdownField("STOP_OPTION", "all"); + auto block = builder.currentBlock(); + + 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); + + EXPECT_CALL(m_engineMock, stop()); + thread.run(); + } + + m_engine->clear(); + target = std::make_shared(); + + // Stop this script + { + ScriptBuilder builder(m_extension.get(), m_engine, target); + + builder.addBlock("control_stop"); + builder.addDropdownField("STOP_OPTION", "this script"); + builder.addBlock("test_print_test"); + + builder.build(); + + testing::internal::CaptureStdout(); + builder.run(); + ASSERT_TRUE(testing::internal::GetCapturedStdout().empty()); + } + + m_engine->clear(); + target = std::make_shared(); + + // Stop other scripts in sprite + { + ScriptBuilder builder(m_extension.get(), m_engine, target); + + builder.addBlock("control_stop"); + builder.addDropdownField("STOP_OPTION", "other scripts in sprite"); + auto block = builder.currentBlock(); + + 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); + + EXPECT_CALL(m_engineMock, stopTarget(target.get(), &thread)); + thread.run(); + } + + // Stop other scripts in stage + { + ScriptBuilder builder(m_extension.get(), m_engine, target); + + builder.addBlock("control_stop"); + builder.addDropdownField("STOP_OPTION", "other scripts in stage"); + auto block = builder.currentBlock(); + + 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); + + EXPECT_CALL(m_engineMock, stopTarget(target.get(), &thread)); + thread.run(); + } +} + +TEST_F(ControlBlocksTest, Wait) +{ + auto target = std::make_shared(); + + { + ScriptBuilder builder(m_extension.get(), m_engine, target); + + builder.addBlock("control_wait"); + builder.addValueInput("DURATION", 2.5); + auto block = builder.currentBlock(); + + 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); + StackTimerMock timer; + ctx->setStackTimer(&timer); + + EXPECT_CALL(timer, start(2.5)); + EXPECT_CALL(m_engineMock, requestRedraw()); + code->run(ctx.get()); + ASSERT_FALSE(code->isFinished(ctx.get())); + + EXPECT_CALL(timer, elapsed()).WillOnce(Return(false)); + EXPECT_CALL(m_engineMock, requestRedraw()).Times(0); + code->run(ctx.get()); + ASSERT_FALSE(code->isFinished(ctx.get())); + + EXPECT_CALL(timer, elapsed()).WillOnce(Return(false)); + EXPECT_CALL(m_engineMock, requestRedraw()).Times(0); + code->run(ctx.get()); + ASSERT_FALSE(code->isFinished(ctx.get())); + + EXPECT_CALL(timer, elapsed()).WillOnce(Return(true)); + EXPECT_CALL(m_engineMock, requestRedraw()).Times(0); + code->run(ctx.get()); + ASSERT_TRUE(code->isFinished(ctx.get())); + } + + m_engine->clear(); + target = std::make_shared(); + + { + ScriptBuilder builder(m_extension.get(), m_engine, target); + + builder.addBlock("control_wait"); + builder.addNullObscuredInput("DURATION"); + auto block = builder.currentBlock(); + + 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); + StackTimerMock timer; + ctx->setStackTimer(&timer); + + EXPECT_CALL(timer, start(0.0)); + EXPECT_CALL(m_engineMock, requestRedraw()); + code->run(ctx.get()); + ASSERT_FALSE(code->isFinished(ctx.get())); + + EXPECT_CALL(timer, elapsed()).WillOnce(Return(true)); + EXPECT_CALL(m_engineMock, requestRedraw()).Times(0); + code->run(ctx.get()); + ASSERT_TRUE(code->isFinished(ctx.get())); + } +} + +TEST_F(ControlBlocksTest, WaitUntil) +{ + auto target = std::make_shared(); + + { + ScriptBuilder builder(m_extension.get(), m_engine, target); + + builder.addBlock("control_wait_until"); + builder.addValueInput("CONDITION", false); + builder.build(); + m_engine->start(); + + m_engine->step(); + ASSERT_TRUE(m_engine->isRunning()); + + m_engine->step(); + ASSERT_TRUE(m_engine->isRunning()); + } + + m_engine->clear(); + target = std::make_shared(); + + { + ScriptBuilder builder(m_extension.get(), m_engine, target); + + builder.addBlock("control_wait_until"); + builder.addValueInput("CONDITION", true); + builder.build(); + m_engine->start(); + + m_engine->step(); + m_engine->step(); + ASSERT_FALSE(m_engine->isRunning()); + } + + m_engine->clear(); + target = std::make_shared(); + + { + ScriptBuilder builder(m_extension.get(), m_engine, target); + + builder.addBlock("control_wait_until"); + auto block = std::make_shared("", "test_condition"); + builder.addObscuredInput("CONDITION", block); + builder.build(); + + conditionReturnValue = false; + m_engine->start(); + + m_engine->step(); + ASSERT_TRUE(m_engine->isRunning()); + + m_engine->step(); + ASSERT_TRUE(m_engine->isRunning()); + + conditionReturnValue = true; + m_engine->step(); + m_engine->step(); + ASSERT_FALSE(m_engine->isRunning()); + } +} + +TEST_F(ControlBlocksTest, RepeatUntil) +{ + auto target = std::make_shared(); + + { + ScriptBuilder builder(m_extension.get(), m_engine, target); + + builder.addBlock("control_repeat_until"); + auto substack = std::make_shared("", "test_print_test"); + builder.addObscuredInput("SUBSTACK", substack); + builder.addValueInput("CONDITION", false); + builder.build(); + m_engine->start(); + + for (int i = 0; i < 2; i++) { + testing::internal::CaptureStdout(); + m_engine->step(); + ASSERT_EQ(testing::internal::GetCapturedStdout().substr(0, 10), "test\ntest\n"); + ASSERT_TRUE(m_engine->isRunning()); + } + } + + m_engine->clear(); + target = std::make_shared(); + + { + ScriptBuilder builder(m_extension.get(), m_engine, target); + + builder.addBlock("control_repeat_until"); + auto substack = std::make_shared("", "test_print_test"); + builder.addObscuredInput("SUBSTACK", substack); + builder.addValueInput("CONDITION", true); + builder.build(); + m_engine->start(); + + testing::internal::CaptureStdout(); + m_engine->step(); + m_engine->step(); + ASSERT_TRUE(testing::internal::GetCapturedStdout().empty()); + ASSERT_FALSE(m_engine->isRunning()); + } + + m_engine->clear(); + target = std::make_shared(); + + { + ScriptBuilder builder(m_extension.get(), m_engine, target); + + builder.addBlock("control_repeat_until"); + auto substack = std::make_shared("", "test_print_test"); + builder.addObscuredInput("SUBSTACK", substack); + auto block = std::make_shared("", "test_condition"); + builder.addObscuredInput("CONDITION", block); + builder.build(); + + conditionReturnValue = false; + m_engine->start(); + + testing::internal::CaptureStdout(); + m_engine->step(); + ASSERT_EQ(testing::internal::GetCapturedStdout().substr(0, 10), "test\ntest\n"); + ASSERT_TRUE(m_engine->isRunning()); + + conditionReturnValue = true; + m_engine->step(); + m_engine->step(); + ASSERT_FALSE(m_engine->isRunning()); + } + + m_engine->clear(); + target = std::make_shared(); + + { + ScriptBuilder builder(m_extension.get(), m_engine, target); + builder.addBlock("control_repeat_until"); + builder.addValueInput("CONDITION", false); + + builder.build(); + m_engine->start(); + + for (int i = 0; i < 2; i++) { + m_engine->step(); + ASSERT_TRUE(m_engine->isRunning()); + } + } +} + +TEST_F(ControlBlocksTest, While) +{ + auto target = std::make_shared(); + + { + ScriptBuilder builder(m_extension.get(), m_engine, target); + + builder.addBlock("control_while"); + auto substack = std::make_shared("", "test_print_test"); + builder.addObscuredInput("SUBSTACK", substack); + builder.addValueInput("CONDITION", true); + builder.build(); + m_engine->start(); + + for (int i = 0; i < 2; i++) { + testing::internal::CaptureStdout(); + m_engine->step(); + ASSERT_EQ(testing::internal::GetCapturedStdout().substr(0, 10), "test\ntest\n"); + ASSERT_TRUE(m_engine->isRunning()); + } + } + + m_engine->clear(); + target = std::make_shared(); + + { + ScriptBuilder builder(m_extension.get(), m_engine, target); + + builder.addBlock("control_while"); + auto substack = std::make_shared("", "test_print_test"); + builder.addObscuredInput("SUBSTACK", substack); + builder.addValueInput("CONDITION", false); + builder.build(); + m_engine->start(); + + testing::internal::CaptureStdout(); + m_engine->step(); + m_engine->step(); + ASSERT_TRUE(testing::internal::GetCapturedStdout().empty()); + ASSERT_FALSE(m_engine->isRunning()); + } + + m_engine->clear(); + target = std::make_shared(); + + { + ScriptBuilder builder(m_extension.get(), m_engine, target); + + builder.addBlock("control_while"); + auto substack = std::make_shared("", "test_print_test"); + builder.addObscuredInput("SUBSTACK", substack); + auto block = std::make_shared("", "test_condition"); + builder.addObscuredInput("CONDITION", block); + builder.build(); + + conditionReturnValue = true; + m_engine->start(); + + testing::internal::CaptureStdout(); + m_engine->step(); + ASSERT_EQ(testing::internal::GetCapturedStdout().substr(0, 10), "test\ntest\n"); + ASSERT_TRUE(m_engine->isRunning()); + + conditionReturnValue = false; + m_engine->step(); + m_engine->step(); + ASSERT_FALSE(m_engine->isRunning()); + } + + m_engine->clear(); + target = std::make_shared(); + + { + ScriptBuilder builder(m_extension.get(), m_engine, target); + builder.addBlock("control_while"); + builder.addValueInput("CONDITION", true); + + builder.build(); + m_engine->start(); + + for (int i = 0; i < 2; i++) { + m_engine->step(); + ASSERT_TRUE(m_engine->isRunning()); + } + } +} + +TEST_F(ControlBlocksTest, ForEach) +{ + auto target = std::make_shared(); + auto var1 = std::make_shared("", ""); + auto var2 = std::make_shared("", ""); + target->addVariable(var1); + target->addVariable(var2); + + { + ScriptBuilder builder(m_extension.get(), m_engine, target); + + builder.addBlock("control_for_each"); + + auto substack = std::make_shared("", "test_print"); + auto input = std::make_shared("STRING", Input::Type::ObscuredShadow); + input->primaryValue()->setValuePtr(var1); + substack->addInput(input); + + builder.addObscuredInput("SUBSTACK", substack); + + builder.addValueInput("VALUE", 5); + builder.addEntityField("VARIABLE", var1); + + builder.addBlock("control_for_each"); + substack = std::make_shared("", "test_print_test"); + builder.addObscuredInput("SUBSTACK", substack); + builder.addNullObscuredInput("VALUE"); + builder.addEntityField("VARIABLE", var2); + + builder.build(); + + var1->setValue(10); + testing::internal::CaptureStdout(); + builder.run(); + ASSERT_EQ(testing::internal::GetCapturedStdout(), "1\n2\n3\n4\n5\n"); + ASSERT_EQ(var1->value(), 5); + } + + m_engine->clear(); + target = std::make_shared(); + target->addVariable(var1); + + { + ScriptBuilder builder(m_extension.get(), m_engine, target); + + builder.addBlock("control_for_each"); + + auto substack = std::make_shared("", "test_print"); + auto input = std::make_shared("STRING", Input::Type::ObscuredShadow); + input->primaryValue()->setValuePtr(var1); + substack->addInput(input); + + auto setVar = std::make_shared("", "test_set_var"); + substack->setNext(setVar); + setVar->setParent(substack); + auto field = std::make_shared("VARIABLE", ""); + setVar->addField(field); + input = std::make_shared("VALUE", Input::Type::Shadow); + input->setPrimaryValue(0); + setVar->addInput(input); + + auto printAgain = std::make_shared("", "test_print"); + setVar->setNext(printAgain); + printAgain->setParent(setVar); + input = std::make_shared("STRING", Input::Type::ObscuredShadow); + printAgain->addInput(input); + + builder.addObscuredInput("SUBSTACK", substack); + + builder.addValueInput("VALUE", 3); + builder.addEntityField("VARIABLE", var1); + + field->setValuePtr(var1); + input->primaryValue()->setValuePtr(var1); + + builder.build(); + + var1->setValue(7); + testing::internal::CaptureStdout(); + builder.run(); + ASSERT_EQ(testing::internal::GetCapturedStdout(), "1\n0\n2\n0\n3\n0\n"); + ASSERT_EQ(var1->value(), 0); + } + + m_engine->clear(); + target = std::make_shared(); + target->addVariable(var1); + + { + ScriptBuilder builder(m_extension.get(), m_engine, target); + builder.addBlock("control_for_each"); + builder.addValueInput("VALUE", "Infinity"); + builder.addEntityField("VARIABLE", var1); + + builder.build(); + m_engine->start(); + + for (int i = 0; i < 2; i++) { + m_engine->step(); + ASSERT_TRUE(m_engine->isRunning()); + } + + ASSERT_GT(var1->value(), 0); + } +} + +TEST_F(ControlBlocksTest, StartAsClone) +{ + auto target = std::make_shared(); + ScriptBuilder builder(m_extension.get(), m_engine, target); + + builder.addBlock("control_start_as_clone"); + auto block = builder.currentBlock(); + + Compiler compiler(&m_engineMock, target.get()); + EXPECT_CALL(m_engineMock, addCloneInitScript(block)); + compiler.compile(block); +} + +TEST_F(ControlBlocksTest, CreateCloneOfSprite) +{ + EXPECT_CALL(m_engineMock, cloneLimit()).WillRepeatedly(Return(-1)); + EXPECT_CALL(m_engineMock, requestRedraw()).WillRepeatedly(Return()); + auto target = std::make_shared(); + target->setEngine(&m_engineMock); + + // create clone of [Sprite1] + { + ScriptBuilder builder(m_extension.get(), m_engine, target); + + builder.addBlock("control_create_clone_of"); + builder.addDropdownInput("CLONE_OPTION", "Sprite1"); + auto block = builder.currentBlock(); + + EXPECT_CALL(m_engineMock, findTarget("Sprite1")).WillOnce(Return(4)); + 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); + + Sprite sprite; + sprite.setEngine(&m_engineMock); + std::shared_ptr clone; + EXPECT_CALL(m_engineMock, targetAt(4)).WillOnce(Return(&sprite)); + EXPECT_CALL(m_engineMock, initClone(_)).WillOnce(SaveArg<0>(&clone)); + EXPECT_CALL(m_engineMock, moveDrawableBehindOther(_, &sprite)); + thread.run(); + ASSERT_TRUE(clone); + ASSERT_EQ(clone->cloneSprite(), &sprite); + } + + m_engine->clear(); + target = std::make_shared(); + target->setEngine(&m_engineMock); + + // create clone of [myself] + { + ScriptBuilder builder(m_extension.get(), m_engine, target); + + builder.addBlock("control_create_clone_of"); + builder.addDropdownInput("CLONE_OPTION", "_myself_"); + auto block = builder.currentBlock(); + + EXPECT_CALL(m_engineMock, findTarget).Times(0); + 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); + + std::shared_ptr clone; + EXPECT_CALL(m_engineMock, initClone(_)).WillOnce(SaveArg<0>(&clone)); + EXPECT_CALL(m_engineMock, moveDrawableBehindOther(_, target.get())); + thread.run(); + ASSERT_TRUE(clone); + ASSERT_EQ(clone->cloneSprite(), target.get()); + } + + m_engine->clear(); + target = std::make_shared(); + target->setEngine(&m_engineMock); + + // create clone of ["_mYself_"] + { + ScriptBuilder builder(m_extension.get(), m_engine, target); + + builder.addBlock("control_create_clone_of"); + builder.addDropdownInput("CLONE_OPTION", "_mYself_"); + auto block = builder.currentBlock(); + + EXPECT_CALL(m_engineMock, findTarget("_mYself_")).WillOnce(Return(4)); + 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); + + Sprite sprite; + sprite.setEngine(&m_engineMock); + std::shared_ptr clone; + EXPECT_CALL(m_engineMock, targetAt(4)).WillOnce(Return(&sprite)); + EXPECT_CALL(m_engineMock, initClone(_)).WillOnce(SaveArg<0>(&clone)); + EXPECT_CALL(m_engineMock, moveDrawableBehindOther(_, &sprite)); + thread.run(); + ASSERT_TRUE(clone); + ASSERT_EQ(clone->cloneSprite(), &sprite); + } + + m_engine->clear(); + target = std::make_shared(); + target->setEngine(&m_engineMock); + + // create clone of (null block) + { + ScriptBuilder builder(m_extension.get(), m_engine, target); + + builder.addBlock("control_create_clone_of"); + builder.addNullObscuredInput("CLONE_OPTION"); + auto block = builder.currentBlock(); + + EXPECT_CALL(m_engineMock, findTarget).Times(0); + 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); + + Sprite sprite; + sprite.setEngine(&m_engineMock); + std::shared_ptr clone; + EXPECT_CALL(m_engineMock, findTarget("0")).WillOnce(Return(2)); + EXPECT_CALL(m_engineMock, targetAt(2)).WillOnce(Return(&sprite)); + EXPECT_CALL(m_engineMock, initClone(_)).WillOnce(SaveArg<0>(&clone)); + EXPECT_CALL(m_engineMock, moveDrawableBehindOther(_, &sprite)); + thread.run(); + ASSERT_TRUE(clone); + ASSERT_EQ(clone->cloneSprite(), &sprite); + } + + m_engine->clear(); + target = std::make_shared(); + target->setEngine(&m_engineMock); + + // create clone of ("_myself_") + { + ScriptBuilder builder(m_extension.get(), m_engine, target); + + builder.addBlock("control_create_clone_of"); + auto valueBlock = std::make_shared("", "test_input"); + auto input = std::make_shared("INPUT", Input::Type::Shadow); + input->setPrimaryValue("_myself_"); + valueBlock->addInput(input); + builder.addObscuredInput("CLONE_OPTION", valueBlock); + auto block = builder.currentBlock(); + + EXPECT_CALL(m_engineMock, findTarget).Times(0); + 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); + + std::shared_ptr clone; + EXPECT_CALL(m_engineMock, findTarget).Times(0); + EXPECT_CALL(m_engineMock, initClone(_)).WillOnce(SaveArg<0>(&clone)); + EXPECT_CALL(m_engineMock, moveDrawableBehindOther(_, target.get())); + thread.run(); + ASSERT_TRUE(clone); + ASSERT_EQ(clone->cloneSprite(), target.get()); + } + + // create clone of ("_mYself_") + { + ScriptBuilder builder(m_extension.get(), m_engine, target); + + builder.addBlock("control_create_clone_of"); + auto valueBlock = std::make_shared("", "test_input"); + auto input = std::make_shared("INPUT", Input::Type::Shadow); + input->setPrimaryValue("_mYself_"); + valueBlock->addInput(input); + builder.addObscuredInput("CLONE_OPTION", valueBlock); + auto block = builder.currentBlock(); + + EXPECT_CALL(m_engineMock, findTarget).Times(0); + 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); + + Sprite sprite; + sprite.setEngine(&m_engineMock); + std::shared_ptr clone; + EXPECT_CALL(m_engineMock, findTarget("_mYself_")).WillOnce(Return(2)); + EXPECT_CALL(m_engineMock, targetAt(2)).WillOnce(Return(&sprite)); + EXPECT_CALL(m_engineMock, initClone(_)).WillOnce(SaveArg<0>(&clone)); + EXPECT_CALL(m_engineMock, moveDrawableBehindOther(_, &sprite)); + thread.run(); + ASSERT_TRUE(clone); + ASSERT_EQ(clone->cloneSprite(), &sprite); + } +} + +TEST_F(ControlBlocksTest, CreateCloneOfStage) +{ + EXPECT_CALL(m_engineMock, cloneLimit()).WillRepeatedly(Return(-1)); + EXPECT_CALL(m_engineMock, requestRedraw()).WillRepeatedly(Return()); + auto target = std::make_shared(); + target->setEngine(&m_engineMock); + + // create clone of [Stage] + { + ScriptBuilder builder(m_extension.get(), m_engine, target); + + builder.addBlock("control_create_clone_of"); + builder.addDropdownInput("CLONE_OPTION", "_stage_"); + auto block = builder.currentBlock(); + + EXPECT_CALL(m_engineMock, findTarget("_stage_")).WillOnce(Return(8)); + 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); + + Stage stage; + stage.setEngine(&m_engineMock); + EXPECT_CALL(m_engineMock, targetAt(8)).WillOnce(Return(&stage)); + EXPECT_CALL(m_engineMock, initClone).Times(0); + thread.run(); + } + + m_engine->clear(); + target = std::make_shared(); + target->setEngine(&m_engineMock); + + // create clone of [myself] + { + ScriptBuilder builder(m_extension.get(), m_engine, target); + + builder.addBlock("control_create_clone_of"); + builder.addDropdownInput("CLONE_OPTION", "_myself_"); + auto block = builder.currentBlock(); + + EXPECT_CALL(m_engineMock, findTarget).Times(0); + 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); + + EXPECT_CALL(m_engineMock, initClone).Times(0); + thread.run(); + } + + m_engine->clear(); + target = std::make_shared(); + target->setEngine(&m_engineMock); + + // create clone of (null block) + { + ScriptBuilder builder(m_extension.get(), m_engine, target); + + builder.addBlock("control_create_clone_of"); + builder.addNullObscuredInput("CLONE_OPTION"); + auto block = builder.currentBlock(); + + EXPECT_CALL(m_engineMock, findTarget).Times(0); + 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); + + Stage stage; + stage.setEngine(&m_engineMock); + EXPECT_CALL(m_engineMock, findTarget("0")).WillOnce(Return(2)); + EXPECT_CALL(m_engineMock, targetAt(2)).WillOnce(Return(&stage)); + EXPECT_CALL(m_engineMock, initClone).Times(0); + thread.run(); + } + + m_engine->clear(); + target = std::make_shared(); + target->setEngine(&m_engineMock); + + // create clone of ("_myself_") + { + ScriptBuilder builder(m_extension.get(), m_engine, target); + + builder.addBlock("control_create_clone_of"); + auto valueBlock = std::make_shared("", "test_input"); + auto input = std::make_shared("INPUT", Input::Type::Shadow); + input->setPrimaryValue("_myself_"); + valueBlock->addInput(input); + builder.addObscuredInput("CLONE_OPTION", valueBlock); + auto block = builder.currentBlock(); + + EXPECT_CALL(m_engineMock, findTarget).Times(0); + 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); + + EXPECT_CALL(m_engineMock, findTarget).Times(0); + EXPECT_CALL(m_engineMock, initClone).Times(0); + thread.run(); + } +} + +TEST_F(ControlBlocksTest, DeleteThisClone) +{ + Sprite sprite; + sprite.setEngine(&m_engineMock); + + std::shared_ptr clone; + EXPECT_CALL(m_engineMock, cloneLimit()).WillRepeatedly(Return(-1)); + EXPECT_CALL(m_engineMock, initClone(_)).WillOnce(SaveArg<0>(&clone)); + EXPECT_CALL(m_engineMock, moveDrawableBehindOther(_, &sprite)); + EXPECT_CALL(m_engineMock, requestRedraw()); + sprite.clone(); + ASSERT_TRUE(clone); + + ScriptBuilder builder(m_extension.get(), m_engine, clone); + + builder.addBlock("control_delete_this_clone"); + auto block = builder.currentBlock(); + + Compiler compiler(&m_engineMock, clone.get()); + auto code = compiler.compile(block); + Script script(clone.get(), block, &m_engineMock); + script.setCode(code); + Thread thread(clone.get(), &m_engineMock, &script); + + EXPECT_CALL(m_engineMock, stopTarget(clone.get(), nullptr)); + EXPECT_CALL(m_engineMock, deinitClone(clone)); + thread.run(); +} + +TEST_F(ControlBlocksTest, DeleteThisCloneStage) +{ + auto target = std::make_shared(); + target->setEngine(&m_engineMock); + + ScriptBuilder builder(m_extension.get(), m_engine, target); + + builder.addBlock("control_delete_this_clone"); + auto block = builder.currentBlock(); + + 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); + + EXPECT_CALL(m_engineMock, stopTarget).Times(0); + EXPECT_CALL(m_engineMock, deinitClone).Times(0); + thread.run(); +} diff --git a/test/dev/blocks/util.cpp b/test/dev/blocks/util.cpp new file mode 100644 index 00000000..78345800 --- /dev/null +++ b/test/dev/blocks/util.cpp @@ -0,0 +1,55 @@ +#include +#include +#include +#include +#include +#include +#include + +#include "util.h" + +namespace libscratchcpp +{ + +void registerBlocks(IEngine *engine, IExtension *extension) +{ + engine->addCompileFunction(extension, "test_print", [](Compiler *compiler) -> CompilerValue * { + auto input = compiler->addInput("STRING"); + compiler->addFunctionCall("test_print", Compiler::StaticType::Void, { Compiler::StaticType::String }, { input }); + return nullptr; + }); + + engine->addCompileFunction(extension, "test_print_test", [](Compiler *compiler) -> CompilerValue * { + auto input = compiler->addConstValue("test"); + compiler->addFunctionCall("test_print", Compiler::StaticType::Void, { Compiler::StaticType::String }, { input }); + return nullptr; + }); + + engine->addCompileFunction(extension, "test_print_test2", [](Compiler *compiler) -> CompilerValue * { + auto input = compiler->addConstValue("test2"); + compiler->addFunctionCall("test_print", Compiler::StaticType::Void, { Compiler::StaticType::String }, { input }); + return nullptr; + }); + + engine->addCompileFunction(extension, "test_condition", [](Compiler *compiler) -> CompilerValue * { return compiler->addFunctionCall("test_condition", Compiler::StaticType::Bool); }); + + engine->addCompileFunction(extension, "test_input", [](Compiler *compiler) -> CompilerValue * { return compiler->addInput("INPUT"); }); + + engine->addCompileFunction(extension, "test_set_var", [](Compiler *compiler) -> CompilerValue * { + Variable *var = static_cast(compiler->field("VARIABLE")->valuePtr().get()); + compiler->createVariableWrite(var, compiler->addInput("VALUE")); + return nullptr; + }); +} + +extern "C" void test_print(const char *str) +{ + std::cout << str << std::endl; +} + +extern "C" bool test_condition() +{ + return conditionReturnValue; +} + +} // namespace libscratchcpp diff --git a/test/dev/blocks/util.h b/test/dev/blocks/util.h new file mode 100644 index 00000000..9fc7e76f --- /dev/null +++ b/test/dev/blocks/util.h @@ -0,0 +1,13 @@ +#pragma once + +namespace libscratchcpp +{ + +class IEngine; +class IExtension; + +bool conditionReturnValue = false; + +void registerBlocks(IEngine *engine, IExtension *extension); + +} // namespace libscratchcpp diff --git a/test/dev/compiler/compiler_test.cpp b/test/dev/compiler/compiler_test.cpp index 7e7ed85f..cacd80b5 100644 --- a/test/dev/compiler/compiler_test.cpp +++ b/test/dev/compiler/compiler_test.cpp @@ -161,6 +161,25 @@ TEST_F(CompilerTest, AddConstValue) compile(compiler, block); } +TEST_F(CompilerTest, AddLoopIndex) +{ + Compiler compiler(&m_engine, &m_target); + auto block = std::make_shared("a", ""); + block->setCompileFunction([](Compiler *compiler) -> CompilerValue * { + CompilerValue ret(Compiler::StaticType::Unknown); + + EXPECT_CALL(*m_builder, addLoopIndex()).WillOnce(Return(&ret)); + EXPECT_EQ(compiler->addLoopIndex(), &ret); + + EXPECT_CALL(*m_builder, addLoopIndex()).WillOnce(Return(nullptr)); + EXPECT_EQ(compiler->addLoopIndex(), nullptr); + + return nullptr; + }); + + compile(compiler, block); +} + TEST_F(CompilerTest, AddVariableValue) { Compiler compiler(&m_engine, &m_target); @@ -1418,6 +1437,34 @@ TEST_F(CompilerTest, MoveToRepeatUntilLoop) compile(compiler, l1); } +TEST_F(CompilerTest, CreateYield) +{ + Compiler compiler(&m_engine, &m_target); + auto block = std::make_shared("", ""); + + block->setCompileFunction([](Compiler *compiler) -> CompilerValue * { + EXPECT_CALL(*m_builder, yield()); + compiler->createYield(); + return nullptr; + }); + + compile(compiler, block); +} + +TEST_F(CompilerTest, CreateStop) +{ + Compiler compiler(&m_engine, &m_target); + auto block = std::make_shared("", ""); + + block->setCompileFunction([](Compiler *compiler) -> CompilerValue * { + EXPECT_CALL(*m_builder, createStop()); + compiler->createStop(); + return nullptr; + }); + + compile(compiler, block); +} + TEST_F(CompilerTest, Input) { Compiler compiler(&m_engine, &m_target); diff --git a/test/dev/executioncontext/executioncontext_test.cpp b/test/dev/executioncontext/executioncontext_test.cpp index 38122f6f..48504cd1 100644 --- a/test/dev/executioncontext/executioncontext_test.cpp +++ b/test/dev/executioncontext/executioncontext_test.cpp @@ -3,6 +3,7 @@ #include #include +#include #include "../../common.h" @@ -29,3 +30,13 @@ TEST(ExecutionContextTest, Promise) ctx.setPromise(nullptr); ASSERT_EQ(ctx.promise(), nullptr); } + +TEST(ExecutionContextTest, StackTimer) +{ + ExecutionContext ctx(nullptr); + ASSERT_TRUE(ctx.stackTimer()); + + StackTimerMock timer; + ctx.setStackTimer(&timer); + ASSERT_EQ(ctx.stackTimer(), &timer); +} diff --git a/test/dev/llvm/llvmcodebuilder_test.cpp b/test/dev/llvm/llvmcodebuilder_test.cpp index a51729d6..6fd62ac1 100644 --- a/test/dev/llvm/llvmcodebuilder_test.cpp +++ b/test/dev/llvm/llvmcodebuilder_test.cpp @@ -2907,7 +2907,8 @@ TEST_F(LLVMCodeBuilderTest, RepeatLoop) v = m_builder->addConstValue(2); v = callConstFuncForType(ValueType::Number, v); m_builder->beginRepeatLoop(v); - m_builder->addTargetFunctionCall("test_function_no_args", Compiler::StaticType::Void, {}, {}); + CompilerValue *index = m_builder->addLoopIndex(); + m_builder->addTargetFunctionCall("test_print_number", Compiler::StaticType::Void, { Compiler::StaticType::Number }, { index }); m_builder->endLoop(); // Nested @@ -2928,8 +2929,8 @@ TEST_F(LLVMCodeBuilderTest, RepeatLoop) v = m_builder->addConstValue(3); m_builder->beginRepeatLoop(v); { - v = m_builder->addConstValue(3); - m_builder->addTargetFunctionCall("test_function_1_arg", Compiler::StaticType::Void, { Compiler::StaticType::String }, { v }); + index = m_builder->addLoopIndex(); + m_builder->addTargetFunctionCall("test_print_number", Compiler::StaticType::Void, { Compiler::StaticType::Number }, { index }); } m_builder->endLoop(); } @@ -2950,20 +2951,20 @@ TEST_F(LLVMCodeBuilderTest, RepeatLoop) "1_arg 1\n" "1_arg 1\n" "1_arg 1\n" - "no_args\n" - "no_args\n" + "0\n" + "1\n" "1_arg 1\n" "1_arg 1\n" "1_arg 2\n" - "1_arg 3\n" - "1_arg 3\n" - "1_arg 3\n" + "0\n" + "1\n" + "2\n" "1_arg 1\n" "1_arg 1\n" "1_arg 2\n" - "1_arg 3\n" - "1_arg 3\n" - "1_arg 3\n"; + "0\n" + "1\n" + "2\n"; EXPECT_CALL(m_target, isStage).WillRepeatedly(Return(false)); testing::internal::CaptureStdout(); @@ -3008,6 +3009,24 @@ TEST_F(LLVMCodeBuilderTest, RepeatLoop) ctx = code->createExecutionContext(&thread); code->run(ctx.get()); ASSERT_TRUE(code->isFinished(ctx.get())); + + // Infinite no warp loop + createBuilder(false); + + v = m_builder->addConstValue("Infinity"); + m_builder->beginRepeatLoop(v); + m_builder->addTargetFunctionCall("test_function_no_args", Compiler::StaticType::Void, {}, {}); + m_builder->endLoop(); + + code = m_builder->finalize(); + ctx = code->createExecutionContext(&thread); + + for (int i = 0; i < 10; i++) { + testing::internal::CaptureStdout(); + code->run(ctx.get()); + ASSERT_EQ(testing::internal::GetCapturedStdout(), "no_args\n"); + ASSERT_FALSE(code->isFinished(ctx.get())); + } } TEST_F(LLVMCodeBuilderTest, WhileLoop) @@ -3550,3 +3569,72 @@ TEST_F(LLVMCodeBuilderTest, LoopLists) code->run(ctx.get()); ASSERT_EQ(testing::internal::GetCapturedStdout(), expected); } + +TEST_F(LLVMCodeBuilderTest, StopNoWarp) +{ + Sprite sprite; + createBuilder(&sprite, false); + + m_builder->beginLoopCondition(); + CompilerValue *v = m_builder->addConstValue(true); + m_builder->beginWhileLoop(v); + m_builder->createStop(); + m_builder->endLoop(); + + m_builder->addTargetFunctionCall("test_function_no_args", Compiler::StaticType::Void, {}, {}); + + std::string expected = ""; + + auto code = m_builder->finalize(); + Script script(&sprite, nullptr, nullptr); + script.setCode(code); + Thread thread(&sprite, nullptr, &script); + auto ctx = code->createExecutionContext(&thread); + testing::internal::CaptureStdout(); + code->run(ctx.get()); + ASSERT_EQ(testing::internal::GetCapturedStdout(), expected); +} + +TEST_F(LLVMCodeBuilderTest, StopWarp) +{ + Sprite sprite; + createBuilder(&sprite, true); + + CompilerValue *v = m_builder->addConstValue(true); + m_builder->beginIfStatement(v); + m_builder->createStop(); + m_builder->endIf(); + + m_builder->addTargetFunctionCall("test_function_no_args", Compiler::StaticType::Void, {}, {}); + + std::string expected = ""; + + auto code = m_builder->finalize(); + Script script(&sprite, nullptr, nullptr); + script.setCode(code); + Thread thread(&sprite, nullptr, &script); + auto ctx = code->createExecutionContext(&thread); + testing::internal::CaptureStdout(); + code->run(ctx.get()); + ASSERT_EQ(testing::internal::GetCapturedStdout(), expected); +} + +TEST_F(LLVMCodeBuilderTest, StopAndReturn) +{ + Sprite sprite; + createBuilder(&sprite, true); + + m_builder->addTargetFunctionCall("test_function_no_args", Compiler::StaticType::Void, {}, {}); + m_builder->createStop(); + + std::string expected = "no_args\n"; + + auto code = m_builder->finalize(); + Script script(&sprite, nullptr, nullptr); + script.setCode(code); + Thread thread(&sprite, nullptr, &script); + auto ctx = code->createExecutionContext(&thread); + testing::internal::CaptureStdout(); + code->run(ctx.get()); + ASSERT_EQ(testing::internal::GetCapturedStdout(), expected); +} diff --git a/test/dev/test_api/scriptbuilder_test.cpp b/test/dev/test_api/scriptbuilder_test.cpp index a2d4eb0e..3191d63f 100644 --- a/test/dev/test_api/scriptbuilder_test.cpp +++ b/test/dev/test_api/scriptbuilder_test.cpp @@ -110,6 +110,25 @@ TEST_F(ScriptBuilderTest, AddObscuredInput) ASSERT_EQ(testing::internal::GetCapturedStdout(), "test\n"); } +TEST_F(ScriptBuilderTest, AddObscuredInputMultipleBlocks) +{ + m_builder->addBlock("test_substack"); + auto substack = std::make_shared("", "test_simple"); + auto block1 = std::make_shared("", "test_simple"); + substack->setNext(block1); + block1->setParent(substack); + auto block2 = std::make_shared("", "test_simple"); + block1->setNext(block2); + block2->setParent(block1); + m_builder->addObscuredInput("SUBSTACK", substack); + + m_builder->build(); + + testing::internal::CaptureStdout(); + m_builder->run(); + ASSERT_EQ(testing::internal::GetCapturedStdout(), "test\ntest\ntest\n"); +} + TEST_F(ScriptBuilderTest, AddNullObscuredInput) { m_builder->addBlock("test_print"); diff --git a/test/dev/test_api/testextension.cpp b/test/dev/test_api/testextension.cpp index d24cbf3a..7e6ef22d 100644 --- a/test/dev/test_api/testextension.cpp +++ b/test/dev/test_api/testextension.cpp @@ -29,6 +29,7 @@ void TestExtension::registerBlocks(IEngine *engine) engine->addCompileFunction(this, "test_print_field", &compilePrintField); engine->addCompileFunction(this, "test_teststr", &compileTestStr); engine->addCompileFunction(this, "test_input", &compileInput); + engine->addCompileFunction(this, "test_substack", &compileSubstack); } CompilerValue *TestExtension::compileSimple(Compiler *compiler) @@ -69,6 +70,13 @@ CompilerValue *TestExtension::compileInput(Compiler *compiler) return compiler->addInput("INPUT"); } +CompilerValue *TestExtension::compileSubstack(Compiler *compiler) +{ + auto substack = compiler->input("SUBSTACK"); + compiler->moveToIf(compiler->addConstValue(true), substack->valueBlock()); + return nullptr; +} + extern "C" void test_simple() { std::cout << "test" << std::endl; diff --git a/test/dev/test_api/testextension.h b/test/dev/test_api/testextension.h index 699d520f..6c88ad25 100644 --- a/test/dev/test_api/testextension.h +++ b/test/dev/test_api/testextension.h @@ -20,6 +20,7 @@ class TestExtension : public IExtension static CompilerValue *compilePrintField(Compiler *compiler); static CompilerValue *compileTestStr(Compiler *compiler); static CompilerValue *compileInput(Compiler *compiler); + static CompilerValue *compileSubstack(Compiler *compiler); }; } // namespace libscratchcpp diff --git a/test/mocks/codebuildermock.h b/test/mocks/codebuildermock.h index cf1738ea..478252d6 100644 --- a/test/mocks/codebuildermock.h +++ b/test/mocks/codebuildermock.h @@ -13,6 +13,7 @@ class CodeBuilderMock : public ICodeBuilder MOCK_METHOD(CompilerValue *, addTargetFunctionCall, (const std::string &, Compiler::StaticType, const Compiler::ArgTypes &, const Compiler::Args &), (override)); MOCK_METHOD(CompilerValue *, addFunctionCallWithCtx, (const std::string &, Compiler::StaticType, const Compiler::ArgTypes &, const Compiler::Args &), (override)); MOCK_METHOD(CompilerConstant *, addConstValue, (const Value &), (override)); + MOCK_METHOD(CompilerValue *, addLoopIndex, (), (override)); MOCK_METHOD(CompilerValue *, addVariableValue, (Variable *), (override)); MOCK_METHOD(CompilerValue *, addListContents, (List *), (override)); MOCK_METHOD(CompilerValue *, addListItem, (List *, CompilerValue *), (override)); @@ -71,4 +72,6 @@ class CodeBuilderMock : public ICodeBuilder MOCK_METHOD(void, endLoop, (), (override)); MOCK_METHOD(void, yield, (), (override)); + + MOCK_METHOD(void, createStop, (), (override)); }; diff --git a/test/mocks/stacktimermock.h b/test/mocks/stacktimermock.h new file mode 100644 index 00000000..9a614d3e --- /dev/null +++ b/test/mocks/stacktimermock.h @@ -0,0 +1,16 @@ +#pragma once + +#include +#include + +using namespace libscratchcpp; + +class StackTimerMock : public IStackTimer +{ + public: + MOCK_METHOD(void, start, (double), (override)); + MOCK_METHOD(void, stop, (), (override)); + + MOCK_METHOD(bool, stopped, (), (const, override)); + MOCK_METHOD(bool, elapsed, (), (const, override)); +}; diff --git a/test/timer/CMakeLists.txt b/test/timer/CMakeLists.txt index 5041f24e..3157ae45 100644 --- a/test/timer/CMakeLists.txt +++ b/test/timer/CMakeLists.txt @@ -1,6 +1,7 @@ add_executable( timer_test timer_test.cpp + stacktimer_test.cpp ) target_link_libraries( diff --git a/test/timer/stacktimer_test.cpp b/test/timer/stacktimer_test.cpp new file mode 100644 index 00000000..6d06da90 --- /dev/null +++ b/test/timer/stacktimer_test.cpp @@ -0,0 +1,46 @@ +#include + +#include + +using namespace libscratchcpp; + +using ::testing::Return; + +TEST(StackTimerTest, StartStopElapsed) +{ + ClockMock clock; + StackTimer timer(&clock); + + ASSERT_TRUE(timer.stopped()); + + EXPECT_CALL(clock, currentSteadyTime).Times(0); + ASSERT_FALSE(timer.elapsed()); + + std::chrono::steady_clock::time_point time2(std::chrono::milliseconds(73)); + EXPECT_CALL(clock, currentSteadyTime()).WillOnce(Return(time2)); + timer.start(0.5); + + EXPECT_CALL(clock, currentSteadyTime()).WillOnce(Return(time2)); + ASSERT_FALSE(timer.elapsed()); + ASSERT_FALSE(timer.stopped()); + + std::chrono::steady_clock::time_point time3(std::chrono::milliseconds(520)); + EXPECT_CALL(clock, currentSteadyTime()).WillOnce(Return(time3)); + ASSERT_FALSE(timer.elapsed()); + ASSERT_FALSE(timer.stopped()); + + std::chrono::steady_clock::time_point time4(std::chrono::milliseconds(573)); + EXPECT_CALL(clock, currentSteadyTime()).WillOnce(Return(time4)); + ASSERT_TRUE(timer.elapsed()); + ASSERT_FALSE(timer.stopped()); + + std::chrono::steady_clock::time_point time5(std::chrono::milliseconds(580)); + EXPECT_CALL(clock, currentSteadyTime()).WillOnce(Return(time5)); + ASSERT_TRUE(timer.elapsed()); + ASSERT_FALSE(timer.stopped()); + + timer.stop(); + EXPECT_CALL(clock, currentSteadyTime).Times(0); + ASSERT_FALSE(timer.elapsed()); + ASSERT_TRUE(timer.stopped()); +}