From d93225779cb9206a23e966d31b9cf2715175874d Mon Sep 17 00:00:00 2001 From: adazem009 <68537469+adazem009@users.noreply.github.com> Date: Wed, 11 Dec 2024 08:44:45 +0100 Subject: [PATCH 01/26] Implement control_forever --- src/dev/blocks/controlblocks.cpp | 15 +++++++ src/dev/blocks/controlblocks.h | 3 ++ test/dev/blocks/CMakeLists.txt | 13 ++++++ test/dev/blocks/control_blocks_test.cpp | 57 ++++++++++++++++++++++++- test/dev/blocks/util.cpp | 32 ++++++++++++++ test/dev/blocks/util.h | 11 +++++ 6 files changed, 130 insertions(+), 1 deletion(-) create mode 100644 test/dev/blocks/util.cpp create mode 100644 test/dev/blocks/util.h diff --git a/src/dev/blocks/controlblocks.cpp b/src/dev/blocks/controlblocks.cpp index 937e5943..4f5d4fb1 100644 --- a/src/dev/blocks/controlblocks.cpp +++ b/src/dev/blocks/controlblocks.cpp @@ -1,5 +1,11 @@ // SPDX-License-Identifier: Apache-2.0 +#include +#include +#include +#include +#include + #include "controlblocks.h" using namespace libscratchcpp; @@ -16,4 +22,13 @@ std::string ControlBlocks::description() const void ControlBlocks::registerBlocks(IEngine *engine) { + engine->addCompileFunction(this, "control_forever", &compileForever); +} + +CompilerValue *ControlBlocks::compileForever(Compiler *compiler) +{ + auto substack = compiler->input("SUBSTACK"); + compiler->beginLoopCondition(); + compiler->moveToWhileLoop(compiler->addConstValue(true), substack ? substack->valueBlock() : nullptr); + return nullptr; } diff --git a/src/dev/blocks/controlblocks.h b/src/dev/blocks/controlblocks.h index 155f9296..60ad0511 100644 --- a/src/dev/blocks/controlblocks.h +++ b/src/dev/blocks/controlblocks.h @@ -14,6 +14,9 @@ class ControlBlocks : public IExtension std::string description() const override; void registerBlocks(IEngine *engine) override; + + private: + static CompilerValue *compileForever(Compiler *compiler); }; } // 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..f7db1734 100644 --- a/test/dev/blocks/control_blocks_test.cpp +++ b/test/dev/blocks/control_blocks_test.cpp @@ -1,15 +1,70 @@ +#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; 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()); + } + } +} diff --git a/test/dev/blocks/util.cpp b/test/dev/blocks/util.cpp new file mode 100644 index 00000000..38258807 --- /dev/null +++ b/test/dev/blocks/util.cpp @@ -0,0 +1,32 @@ +#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; + }); +} + +extern "C" void test_print(const char *str) +{ + std::cout << str << std::endl; +} + +} // namespace libscratchcpp diff --git a/test/dev/blocks/util.h b/test/dev/blocks/util.h new file mode 100644 index 00000000..c82c34a4 --- /dev/null +++ b/test/dev/blocks/util.h @@ -0,0 +1,11 @@ +#pragma once + +namespace libscratchcpp +{ + +class IEngine; +class IExtension; + +void registerBlocks(IEngine *engine, IExtension *extension); + +} // namespace libscratchcpp From cf1730cf028dccba5c8da68c76fa4e4ed5150ca8 Mon Sep 17 00:00:00 2001 From: adazem009 <68537469+adazem009@users.noreply.github.com> Date: Wed, 11 Dec 2024 10:16:36 +0100 Subject: [PATCH 02/26] LLVMCodeBuilder: Fix infinite count in repeat loop --- .../engine/internal/llvm/llvmcodebuilder.cpp | 4 +++- test/dev/llvm/llvmcodebuilder_test.cpp | 18 ++++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/src/dev/engine/internal/llvm/llvmcodebuilder.cpp b/src/dev/engine/internal/llvm/llvmcodebuilder.cpp index da31cea5..f2f2e8fb 100644 --- a/src/dev/engine/internal/llvm/llvmcodebuilder.cpp +++ b/src/dev/engine/internal/llvm/llvmcodebuilder.cpp @@ -784,6 +784,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))); @@ -795,6 +796,7 @@ std::shared_ptr LLVMCodeBuilder::finalize() 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.CreateSelect(isInf, zero, count); // Jump to condition branch m_builder.CreateBr(loop.conditionBranch); @@ -808,7 +810,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 diff --git a/test/dev/llvm/llvmcodebuilder_test.cpp b/test/dev/llvm/llvmcodebuilder_test.cpp index a51729d6..e3ba1946 100644 --- a/test/dev/llvm/llvmcodebuilder_test.cpp +++ b/test/dev/llvm/llvmcodebuilder_test.cpp @@ -3008,6 +3008,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) From 30ae4871b133d7faee119fac2959f8bc0b54585e Mon Sep 17 00:00:00 2001 From: adazem009 <68537469+adazem009@users.noreply.github.com> Date: Wed, 11 Dec 2024 10:16:53 +0100 Subject: [PATCH 03/26] Implement control_repeat --- src/dev/blocks/controlblocks.cpp | 8 ++++++ src/dev/blocks/controlblocks.h | 1 + test/dev/blocks/control_blocks_test.cpp | 37 +++++++++++++++++++++++++ 3 files changed, 46 insertions(+) diff --git a/src/dev/blocks/controlblocks.cpp b/src/dev/blocks/controlblocks.cpp index 4f5d4fb1..89fff45d 100644 --- a/src/dev/blocks/controlblocks.cpp +++ b/src/dev/blocks/controlblocks.cpp @@ -23,6 +23,7 @@ std::string ControlBlocks::description() const void ControlBlocks::registerBlocks(IEngine *engine) { engine->addCompileFunction(this, "control_forever", &compileForever); + engine->addCompileFunction(this, "control_repeat", &compileRepeat); } CompilerValue *ControlBlocks::compileForever(Compiler *compiler) @@ -32,3 +33,10 @@ CompilerValue *ControlBlocks::compileForever(Compiler *compiler) 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; +} diff --git a/src/dev/blocks/controlblocks.h b/src/dev/blocks/controlblocks.h index 60ad0511..2b3b7c5c 100644 --- a/src/dev/blocks/controlblocks.h +++ b/src/dev/blocks/controlblocks.h @@ -17,6 +17,7 @@ class ControlBlocks : public IExtension private: static CompilerValue *compileForever(Compiler *compiler); + static CompilerValue *compileRepeat(Compiler *compiler); }; } // namespace libscratchcpp diff --git a/test/dev/blocks/control_blocks_test.cpp b/test/dev/blocks/control_blocks_test.cpp index f7db1734..14352acf 100644 --- a/test/dev/blocks/control_blocks_test.cpp +++ b/test/dev/blocks/control_blocks_test.cpp @@ -68,3 +68,40 @@ TEST_F(ControlBlocksTest, Forever) } } } + +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.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()); + } + } +} From 94de97e585a6d05e137acd61766e6685aa7361c4 Mon Sep 17 00:00:00 2001 From: adazem009 <68537469+adazem009@users.noreply.github.com> Date: Wed, 11 Dec 2024 10:29:20 +0100 Subject: [PATCH 04/26] Implement control_if --- src/dev/blocks/controlblocks.cpp | 8 ++++++ src/dev/blocks/controlblocks.h | 1 + test/dev/blocks/control_blocks_test.cpp | 37 +++++++++++++++++++++++++ 3 files changed, 46 insertions(+) diff --git a/src/dev/blocks/controlblocks.cpp b/src/dev/blocks/controlblocks.cpp index 89fff45d..ba7ea851 100644 --- a/src/dev/blocks/controlblocks.cpp +++ b/src/dev/blocks/controlblocks.cpp @@ -24,6 +24,7 @@ void ControlBlocks::registerBlocks(IEngine *engine) { engine->addCompileFunction(this, "control_forever", &compileForever); engine->addCompileFunction(this, "control_repeat", &compileRepeat); + engine->addCompileFunction(this, "control_if", &compileIf); } CompilerValue *ControlBlocks::compileForever(Compiler *compiler) @@ -40,3 +41,10 @@ CompilerValue *ControlBlocks::compileRepeat(Compiler *compiler) 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; +} diff --git a/src/dev/blocks/controlblocks.h b/src/dev/blocks/controlblocks.h index 2b3b7c5c..bccfe0ad 100644 --- a/src/dev/blocks/controlblocks.h +++ b/src/dev/blocks/controlblocks.h @@ -18,6 +18,7 @@ class ControlBlocks : public IExtension private: static CompilerValue *compileForever(Compiler *compiler); static CompilerValue *compileRepeat(Compiler *compiler); + static CompilerValue *compileIf(Compiler *compiler); }; } // namespace libscratchcpp diff --git a/test/dev/blocks/control_blocks_test.cpp b/test/dev/blocks/control_blocks_test.cpp index 14352acf..2e54576d 100644 --- a/test/dev/blocks/control_blocks_test.cpp +++ b/test/dev/blocks/control_blocks_test.cpp @@ -105,3 +105,40 @@ TEST_F(ControlBlocksTest, Repeat) } } } + +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.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(); + } +} From 6784aaf14de8e5191b9dc4e21a2e5f803d5fe15e Mon Sep 17 00:00:00 2001 From: adazem009 <68537469+adazem009@users.noreply.github.com> Date: Wed, 11 Dec 2024 10:37:32 +0100 Subject: [PATCH 05/26] Add obscured input tests to control blocks test --- test/dev/blocks/control_blocks_test.cpp | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/test/dev/blocks/control_blocks_test.cpp b/test/dev/blocks/control_blocks_test.cpp index 2e54576d..109574af 100644 --- a/test/dev/blocks/control_blocks_test.cpp +++ b/test/dev/blocks/control_blocks_test.cpp @@ -81,6 +81,11 @@ TEST_F(ControlBlocksTest, Repeat) 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(); @@ -118,6 +123,11 @@ TEST_F(ControlBlocksTest, If) 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); From d2200b998e424cc8693bcab0398a37690e22186c Mon Sep 17 00:00:00 2001 From: adazem009 <68537469+adazem009@users.noreply.github.com> Date: Wed, 11 Dec 2024 10:37:50 +0100 Subject: [PATCH 06/26] Implement control_if_else --- src/dev/blocks/controlblocks.cpp | 9 +++ src/dev/blocks/controlblocks.h | 1 + test/dev/blocks/control_blocks_test.cpp | 98 +++++++++++++++++++++++++ test/dev/blocks/util.cpp | 6 ++ 4 files changed, 114 insertions(+) diff --git a/src/dev/blocks/controlblocks.cpp b/src/dev/blocks/controlblocks.cpp index ba7ea851..288388e5 100644 --- a/src/dev/blocks/controlblocks.cpp +++ b/src/dev/blocks/controlblocks.cpp @@ -25,6 +25,7 @@ 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); } CompilerValue *ControlBlocks::compileForever(Compiler *compiler) @@ -48,3 +49,11 @@ CompilerValue *ControlBlocks::compileIf(Compiler *compiler) 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; +} diff --git a/src/dev/blocks/controlblocks.h b/src/dev/blocks/controlblocks.h index bccfe0ad..b58e7856 100644 --- a/src/dev/blocks/controlblocks.h +++ b/src/dev/blocks/controlblocks.h @@ -19,6 +19,7 @@ class ControlBlocks : public IExtension static CompilerValue *compileForever(Compiler *compiler); static CompilerValue *compileRepeat(Compiler *compiler); static CompilerValue *compileIf(Compiler *compiler); + static CompilerValue *compileIfElse(Compiler *compiler); }; } // namespace libscratchcpp diff --git a/test/dev/blocks/control_blocks_test.cpp b/test/dev/blocks/control_blocks_test.cpp index 109574af..73c9f4db 100644 --- a/test/dev/blocks/control_blocks_test.cpp +++ b/test/dev/blocks/control_blocks_test.cpp @@ -152,3 +152,101 @@ TEST_F(ControlBlocksTest, If) 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(); + } +} diff --git a/test/dev/blocks/util.cpp b/test/dev/blocks/util.cpp index 38258807..4cc5d279 100644 --- a/test/dev/blocks/util.cpp +++ b/test/dev/blocks/util.cpp @@ -22,6 +22,12 @@ void registerBlocks(IEngine *engine, IExtension *extension) 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; + }); } extern "C" void test_print(const char *str) From 59daa77aa4c0ab7936664729afd63d50152fcea1 Mon Sep 17 00:00:00 2001 From: adazem009 <68537469+adazem009@users.noreply.github.com> Date: Wed, 11 Dec 2024 17:02:31 +0100 Subject: [PATCH 07/26] LLVMCodeBuilder: Add createStop() method --- src/dev/engine/internal/icodebuilder.h | 2 + .../engine/internal/llvm/llvmcodebuilder.cpp | 16 +++++ .../engine/internal/llvm/llvmcodebuilder.h | 2 + .../engine/internal/llvm/llvminstruction.h | 3 +- test/dev/llvm/llvmcodebuilder_test.cpp | 69 +++++++++++++++++++ test/mocks/codebuildermock.h | 2 + 6 files changed, 93 insertions(+), 1 deletion(-) diff --git a/src/dev/engine/internal/icodebuilder.h b/src/dev/engine/internal/icodebuilder.h index 5132f1a9..3b1f5419 100644 --- a/src/dev/engine/internal/icodebuilder.h +++ b/src/dev/engine/internal/icodebuilder.h @@ -81,6 +81,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 f2f2e8fb..eac2f868 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 @@ -898,9 +899,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); @@ -1283,6 +1294,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..3f96bc0a 100644 --- a/src/dev/engine/internal/llvm/llvmcodebuilder.h +++ b/src/dev/engine/internal/llvm/llvmcodebuilder.h @@ -89,6 +89,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..a016365b 100644 --- a/src/dev/engine/internal/llvm/llvminstruction.h +++ b/src/dev/engine/internal/llvm/llvminstruction.h @@ -61,7 +61,8 @@ struct LLVMInstruction BeginWhileLoop, BeginRepeatUntilLoop, BeginLoopCondition, - EndLoop + EndLoop, + Stop }; LLVMInstruction(Type type) : diff --git a/test/dev/llvm/llvmcodebuilder_test.cpp b/test/dev/llvm/llvmcodebuilder_test.cpp index e3ba1946..02bb877d 100644 --- a/test/dev/llvm/llvmcodebuilder_test.cpp +++ b/test/dev/llvm/llvmcodebuilder_test.cpp @@ -3568,3 +3568,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/mocks/codebuildermock.h b/test/mocks/codebuildermock.h index cf1738ea..f66408e7 100644 --- a/test/mocks/codebuildermock.h +++ b/test/mocks/codebuildermock.h @@ -71,4 +71,6 @@ class CodeBuilderMock : public ICodeBuilder MOCK_METHOD(void, endLoop, (), (override)); MOCK_METHOD(void, yield, (), (override)); + + MOCK_METHOD(void, createStop, (), (override)); }; From 5b37719244dacd7de06a2758641f54c5103521c5 Mon Sep 17 00:00:00 2001 From: adazem009 <68537469+adazem009@users.noreply.github.com> Date: Wed, 11 Dec 2024 18:20:10 +0100 Subject: [PATCH 08/26] Compiler: Add createStop() method --- include/scratchcpp/dev/compiler.h | 2 ++ src/dev/engine/compiler.cpp | 6 ++++++ test/dev/compiler/compiler_test.cpp | 15 +++++++++++++++ 3 files changed, 23 insertions(+) diff --git a/include/scratchcpp/dev/compiler.h b/include/scratchcpp/dev/compiler.h index 1c1d441e..ec326608 100644 --- a/include/scratchcpp/dev/compiler.h +++ b/include/scratchcpp/dev/compiler.h @@ -115,6 +115,8 @@ class LIBSCRATCHCPP_EXPORT Compiler void moveToRepeatUntilLoop(CompilerValue *cond, std::shared_ptr substack); void warp(); + void createStop(); + Input *input(const std::string &name) const; Field *field(const std::string &name) const; diff --git a/src/dev/engine/compiler.cpp b/src/dev/engine/compiler.cpp index db4c0b54..5bdeacf7 100644 --- a/src/dev/engine/compiler.cpp +++ b/src/dev/engine/compiler.cpp @@ -495,6 +495,12 @@ void Compiler::warp() impl->warp = true; } +/*! 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/test/dev/compiler/compiler_test.cpp b/test/dev/compiler/compiler_test.cpp index 7e7ed85f..70ab740d 100644 --- a/test/dev/compiler/compiler_test.cpp +++ b/test/dev/compiler/compiler_test.cpp @@ -1418,6 +1418,21 @@ TEST_F(CompilerTest, MoveToRepeatUntilLoop) compile(compiler, l1); } +TEST_F(CompilerTest, CreateStop) +{ + Compiler compiler(&m_engine, &m_target); + auto block = std::make_shared("", ""); + + block->setCompileFunction([](Compiler *compiler) -> CompilerValue * { + CompilerValue arg(Compiler::StaticType::Unknown); + EXPECT_CALL(*m_builder, createStop()); + compiler->createStop(); + return nullptr; + }); + + compile(compiler, block); +} + TEST_F(CompilerTest, Input) { Compiler compiler(&m_engine, &m_target); From 77f687e7fafe35266bccde12e9ffa08720a9fec7 Mon Sep 17 00:00:00 2001 From: adazem009 <68537469+adazem009@users.noreply.github.com> Date: Wed, 11 Dec 2024 18:31:50 +0100 Subject: [PATCH 09/26] Implement control_stop --- src/dev/blocks/controlblocks.cpp | 33 ++++++++++ src/dev/blocks/controlblocks.h | 1 + test/dev/blocks/control_blocks_test.cpp | 82 +++++++++++++++++++++++++ 3 files changed, 116 insertions(+) diff --git a/src/dev/blocks/controlblocks.cpp b/src/dev/blocks/controlblocks.cpp index 288388e5..5a3bbb03 100644 --- a/src/dev/blocks/controlblocks.cpp +++ b/src/dev/blocks/controlblocks.cpp @@ -5,6 +5,9 @@ #include #include #include +#include +#include +#include #include "controlblocks.h" @@ -26,6 +29,7 @@ void ControlBlocks::registerBlocks(IEngine *engine) 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); } CompilerValue *ControlBlocks::compileForever(Compiler *compiler) @@ -57,3 +61,32 @@ CompilerValue *ControlBlocks::compileIfElse(Compiler *compiler) 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; +} + +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); +} diff --git a/src/dev/blocks/controlblocks.h b/src/dev/blocks/controlblocks.h index b58e7856..c2956a57 100644 --- a/src/dev/blocks/controlblocks.h +++ b/src/dev/blocks/controlblocks.h @@ -20,6 +20,7 @@ class ControlBlocks : public IExtension static CompilerValue *compileRepeat(Compiler *compiler); static CompilerValue *compileIf(Compiler *compiler); static CompilerValue *compileIfElse(Compiler *compiler); + static CompilerValue *compileStop(Compiler *compiler); }; } // namespace libscratchcpp diff --git a/test/dev/blocks/control_blocks_test.cpp b/test/dev/blocks/control_blocks_test.cpp index 73c9f4db..b98edfd3 100644 --- a/test/dev/blocks/control_blocks_test.cpp +++ b/test/dev/blocks/control_blocks_test.cpp @@ -4,6 +4,8 @@ #include #include #include +#include +#include #include #include "../common.h" @@ -250,3 +252,83 @@ TEST_F(ControlBlocksTest, IfElse) 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(); + } +} From 21df2fe5456ca6329a4f4fb236553eb9677eb6a0 Mon Sep 17 00:00:00 2001 From: adazem009 <68537469+adazem009@users.noreply.github.com> Date: Thu, 12 Dec 2024 22:58:58 +0100 Subject: [PATCH 10/26] Add StackTimer --- CMakeLists.txt | 1 + include/scratchcpp/istacktimer.h | 33 +++++++++++++++++++++ src/engine/CMakeLists.txt | 2 ++ src/engine/internal/stacktimer.cpp | 45 +++++++++++++++++++++++++++++ src/engine/internal/stacktimer.h | 33 +++++++++++++++++++++ test/mocks/stacktimermock.h | 16 +++++++++++ test/timer/CMakeLists.txt | 1 + test/timer/stacktimer_test.cpp | 46 ++++++++++++++++++++++++++++++ 8 files changed, 177 insertions(+) create mode 100644 include/scratchcpp/istacktimer.h create mode 100644 src/engine/internal/stacktimer.cpp create mode 100644 src/engine/internal/stacktimer.h create mode 100644 test/mocks/stacktimermock.h create mode 100644 test/timer/stacktimer_test.cpp 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/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/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/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()); +} From 5744b3dd8413a2d349961219ac4806b762b60985 Mon Sep 17 00:00:00 2001 From: adazem009 <68537469+adazem009@users.noreply.github.com> Date: Thu, 12 Dec 2024 23:03:10 +0100 Subject: [PATCH 11/26] ExecutionContext: Add stack timer --- include/scratchcpp/dev/executioncontext.h | 4 ++++ src/dev/engine/executioncontext.cpp | 12 ++++++++++++ src/dev/engine/executioncontext_p.cpp | 4 +++- src/dev/engine/executioncontext_p.h | 5 ++++- test/dev/executioncontext/executioncontext_test.cpp | 11 +++++++++++ 5 files changed, 34 insertions(+), 2 deletions(-) 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/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/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); +} From f6ee5581b8298dbadded17d5096bb6f88132438f Mon Sep 17 00:00:00 2001 From: adazem009 <68537469+adazem009@users.noreply.github.com> Date: Thu, 12 Dec 2024 23:35:12 +0100 Subject: [PATCH 12/26] Compiler: Add createYield() method --- include/scratchcpp/dev/compiler.h | 1 + src/dev/engine/compiler.cpp | 6 ++++++ test/dev/compiler/compiler_test.cpp | 15 ++++++++++++++- 3 files changed, 21 insertions(+), 1 deletion(-) diff --git a/include/scratchcpp/dev/compiler.h b/include/scratchcpp/dev/compiler.h index ec326608..94d9c5da 100644 --- a/include/scratchcpp/dev/compiler.h +++ b/include/scratchcpp/dev/compiler.h @@ -115,6 +115,7 @@ 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; diff --git a/src/dev/engine/compiler.cpp b/src/dev/engine/compiler.cpp index 5bdeacf7..0ed85e99 100644 --- a/src/dev/engine/compiler.cpp +++ b/src/dev/engine/compiler.cpp @@ -495,6 +495,12 @@ void Compiler::warp() impl->warp = true; } +/*! Creates a suspend instruction. */ +void Compiler::createYield() +{ + impl->builder->yield(); +} + /*! Creates a stop script instruction. */ void Compiler::createStop() { diff --git a/test/dev/compiler/compiler_test.cpp b/test/dev/compiler/compiler_test.cpp index 70ab740d..3e68d9af 100644 --- a/test/dev/compiler/compiler_test.cpp +++ b/test/dev/compiler/compiler_test.cpp @@ -1418,13 +1418,26 @@ 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 * { - CompilerValue arg(Compiler::StaticType::Unknown); EXPECT_CALL(*m_builder, createStop()); compiler->createStop(); return nullptr; From 5a672685b818562cedee5e8c5057d5a1ce50b871 Mon Sep 17 00:00:00 2001 From: adazem009 <68537469+adazem009@users.noreply.github.com> Date: Thu, 12 Dec 2024 23:35:53 +0100 Subject: [PATCH 13/26] Implement control_wait --- src/dev/blocks/controlblocks.cpp | 26 +++++++++ src/dev/blocks/controlblocks.h | 1 + test/dev/blocks/control_blocks_test.cpp | 72 +++++++++++++++++++++++++ 3 files changed, 99 insertions(+) diff --git a/src/dev/blocks/controlblocks.cpp b/src/dev/blocks/controlblocks.cpp index 5a3bbb03..4f2bc8dd 100644 --- a/src/dev/blocks/controlblocks.cpp +++ b/src/dev/blocks/controlblocks.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include "controlblocks.h" @@ -30,6 +31,7 @@ void ControlBlocks::registerBlocks(IEngine *engine) 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); } CompilerValue *ControlBlocks::compileForever(Compiler *compiler) @@ -80,6 +82,20 @@ CompilerValue *ControlBlocks::compileStop(Compiler *compiler) return nullptr; } +CompilerValue *ControlBlocks::compileWait(Compiler *compiler) +{ + auto duration = compiler->addInput("DURATION"); + compiler->addFunctionCallWithCtx("control_start_stack_timer", 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; +} + extern "C" void control_stop_all(ExecutionContext *ctx) { ctx->engine()->stop(); @@ -90,3 +106,13 @@ 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_stack_timer(ExecutionContext *ctx, double seconds) +{ + ctx->stackTimer()->start(seconds); +} + +extern "C" bool control_stack_timer_elapsed(ExecutionContext *ctx) +{ + return ctx->stackTimer()->elapsed(); +} diff --git a/src/dev/blocks/controlblocks.h b/src/dev/blocks/controlblocks.h index c2956a57..00908872 100644 --- a/src/dev/blocks/controlblocks.h +++ b/src/dev/blocks/controlblocks.h @@ -21,6 +21,7 @@ class ControlBlocks : public IExtension static CompilerValue *compileIf(Compiler *compiler); static CompilerValue *compileIfElse(Compiler *compiler); static CompilerValue *compileStop(Compiler *compiler); + static CompilerValue *compileWait(Compiler *compiler); }; } // namespace libscratchcpp diff --git a/test/dev/blocks/control_blocks_test.cpp b/test/dev/blocks/control_blocks_test.cpp index b98edfd3..e5eb425f 100644 --- a/test/dev/blocks/control_blocks_test.cpp +++ b/test/dev/blocks/control_blocks_test.cpp @@ -6,7 +6,11 @@ #include #include #include +#include +#include + #include +#include #include "../common.h" #include "dev/blocks/controlblocks.h" @@ -15,6 +19,8 @@ using namespace libscratchcpp; using namespace libscratchcpp::test; +using ::testing::Return; + class ControlBlocksTest : public testing::Test { public: @@ -332,3 +338,69 @@ TEST_F(ControlBlocksTest, Stop) 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)); + code->run(ctx.get()); + ASSERT_FALSE(code->isFinished(ctx.get())); + + EXPECT_CALL(timer, elapsed()).WillOnce(Return(false)); + code->run(ctx.get()); + ASSERT_FALSE(code->isFinished(ctx.get())); + + EXPECT_CALL(timer, elapsed()).WillOnce(Return(false)); + code->run(ctx.get()); + ASSERT_FALSE(code->isFinished(ctx.get())); + + EXPECT_CALL(timer, elapsed()).WillOnce(Return(true)); + 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)); + code->run(ctx.get()); + ASSERT_FALSE(code->isFinished(ctx.get())); + + EXPECT_CALL(timer, elapsed()).WillOnce(Return(true)); + code->run(ctx.get()); + ASSERT_TRUE(code->isFinished(ctx.get())); + } +} From 880e30ed713910d2e2b9ad27ad70b29374ecff7f Mon Sep 17 00:00:00 2001 From: adazem009 <68537469+adazem009@users.noreply.github.com> Date: Sat, 14 Dec 2024 12:45:44 +0100 Subject: [PATCH 14/26] Request redraw in wait block --- src/dev/blocks/controlblocks.cpp | 5 +++-- test/dev/blocks/control_blocks_test.cpp | 6 ++++++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/dev/blocks/controlblocks.cpp b/src/dev/blocks/controlblocks.cpp index 4f2bc8dd..3fd04962 100644 --- a/src/dev/blocks/controlblocks.cpp +++ b/src/dev/blocks/controlblocks.cpp @@ -85,7 +85,7 @@ CompilerValue *ControlBlocks::compileStop(Compiler *compiler) CompilerValue *ControlBlocks::compileWait(Compiler *compiler) { auto duration = compiler->addInput("DURATION"); - compiler->addFunctionCallWithCtx("control_start_stack_timer", Compiler::StaticType::Void, { Compiler::StaticType::Number }, { duration }); + compiler->addFunctionCallWithCtx("control_start_wait", Compiler::StaticType::Void, { Compiler::StaticType::Number }, { duration }); compiler->createYield(); compiler->beginLoopCondition(); @@ -107,9 +107,10 @@ extern "C" void control_stop_other_scripts_in_target(ExecutionContext *ctx) ctx->engine()->stopTarget(thread->target(), thread); } -extern "C" void control_start_stack_timer(ExecutionContext *ctx, double seconds) +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) diff --git a/test/dev/blocks/control_blocks_test.cpp b/test/dev/blocks/control_blocks_test.cpp index e5eb425f..3f423213 100644 --- a/test/dev/blocks/control_blocks_test.cpp +++ b/test/dev/blocks/control_blocks_test.cpp @@ -360,18 +360,22 @@ TEST_F(ControlBlocksTest, Wait) 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())); } @@ -396,10 +400,12 @@ TEST_F(ControlBlocksTest, Wait) 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())); } From 90b566c327e46b29bda5b51f2e727dacb6523ffa Mon Sep 17 00:00:00 2001 From: adazem009 <68537469+adazem009@users.noreply.github.com> Date: Sat, 14 Dec 2024 13:11:30 +0100 Subject: [PATCH 15/26] Implement control_wait_until --- src/dev/blocks/controlblocks.cpp | 9 ++++ src/dev/blocks/controlblocks.h | 1 + test/dev/blocks/control_blocks_test.cpp | 62 +++++++++++++++++++++++++ test/dev/blocks/util.cpp | 7 +++ test/dev/blocks/util.h | 2 + 5 files changed, 81 insertions(+) diff --git a/src/dev/blocks/controlblocks.cpp b/src/dev/blocks/controlblocks.cpp index 3fd04962..54f95d9a 100644 --- a/src/dev/blocks/controlblocks.cpp +++ b/src/dev/blocks/controlblocks.cpp @@ -32,6 +32,7 @@ void ControlBlocks::registerBlocks(IEngine *engine) 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); } CompilerValue *ControlBlocks::compileForever(Compiler *compiler) @@ -96,6 +97,14 @@ CompilerValue *ControlBlocks::compileWait(Compiler *compiler) return nullptr; } +CompilerValue *ControlBlocks::compileWaitUntil(Compiler *compiler) +{ + compiler->beginLoopCondition(); + compiler->beginRepeatUntilLoop(compiler->addInput("CONDITION")); + compiler->endLoop(); + return nullptr; +} + extern "C" void control_stop_all(ExecutionContext *ctx) { ctx->engine()->stop(); diff --git a/src/dev/blocks/controlblocks.h b/src/dev/blocks/controlblocks.h index 00908872..9d1e72e3 100644 --- a/src/dev/blocks/controlblocks.h +++ b/src/dev/blocks/controlblocks.h @@ -22,6 +22,7 @@ class ControlBlocks : public IExtension static CompilerValue *compileIfElse(Compiler *compiler); static CompilerValue *compileStop(Compiler *compiler); static CompilerValue *compileWait(Compiler *compiler); + static CompilerValue *compileWaitUntil(Compiler *compiler); }; } // namespace libscratchcpp diff --git a/test/dev/blocks/control_blocks_test.cpp b/test/dev/blocks/control_blocks_test.cpp index 3f423213..ac85410f 100644 --- a/test/dev/blocks/control_blocks_test.cpp +++ b/test/dev/blocks/control_blocks_test.cpp @@ -410,3 +410,65 @@ TEST_F(ControlBlocksTest, Wait) 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()); + } +} diff --git a/test/dev/blocks/util.cpp b/test/dev/blocks/util.cpp index 4cc5d279..a6890119 100644 --- a/test/dev/blocks/util.cpp +++ b/test/dev/blocks/util.cpp @@ -28,6 +28,8 @@ void registerBlocks(IEngine *engine, IExtension *extension) 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); }); } extern "C" void test_print(const char *str) @@ -35,4 +37,9 @@ 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 index c82c34a4..9fc7e76f 100644 --- a/test/dev/blocks/util.h +++ b/test/dev/blocks/util.h @@ -6,6 +6,8 @@ namespace libscratchcpp class IEngine; class IExtension; +bool conditionReturnValue = false; + void registerBlocks(IEngine *engine, IExtension *extension); } // namespace libscratchcpp From 834b91c72ac995c8191deae16ffb314a9a8af726 Mon Sep 17 00:00:00 2001 From: adazem009 <68537469+adazem009@users.noreply.github.com> Date: Sat, 14 Dec 2024 14:31:44 +0100 Subject: [PATCH 16/26] Implement control_repeat_until --- src/dev/blocks/controlblocks.cpp | 9 +++ src/dev/blocks/controlblocks.h | 1 + test/dev/blocks/control_blocks_test.cpp | 87 +++++++++++++++++++++++++ 3 files changed, 97 insertions(+) diff --git a/src/dev/blocks/controlblocks.cpp b/src/dev/blocks/controlblocks.cpp index 54f95d9a..41e8d24f 100644 --- a/src/dev/blocks/controlblocks.cpp +++ b/src/dev/blocks/controlblocks.cpp @@ -33,6 +33,7 @@ void ControlBlocks::registerBlocks(IEngine *engine) 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); } CompilerValue *ControlBlocks::compileForever(Compiler *compiler) @@ -105,6 +106,14 @@ CompilerValue *ControlBlocks::compileWaitUntil(Compiler *compiler) 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; +} + extern "C" void control_stop_all(ExecutionContext *ctx) { ctx->engine()->stop(); diff --git a/src/dev/blocks/controlblocks.h b/src/dev/blocks/controlblocks.h index 9d1e72e3..e9f902cc 100644 --- a/src/dev/blocks/controlblocks.h +++ b/src/dev/blocks/controlblocks.h @@ -23,6 +23,7 @@ class ControlBlocks : public IExtension static CompilerValue *compileStop(Compiler *compiler); static CompilerValue *compileWait(Compiler *compiler); static CompilerValue *compileWaitUntil(Compiler *compiler); + static CompilerValue *compileRepeatUntil(Compiler *compiler); }; } // namespace libscratchcpp diff --git a/test/dev/blocks/control_blocks_test.cpp b/test/dev/blocks/control_blocks_test.cpp index ac85410f..91aa04f3 100644 --- a/test/dev/blocks/control_blocks_test.cpp +++ b/test/dev/blocks/control_blocks_test.cpp @@ -472,3 +472,90 @@ TEST_F(ControlBlocksTest, WaitUntil) 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()); + } + } +} From f62c2e0770a523dd5f12c0b3cc91493cced1cc20 Mon Sep 17 00:00:00 2001 From: adazem009 <68537469+adazem009@users.noreply.github.com> Date: Sat, 14 Dec 2024 14:39:38 +0100 Subject: [PATCH 17/26] Implement control_while --- src/dev/blocks/controlblocks.cpp | 9 +++ src/dev/blocks/controlblocks.h | 1 + test/dev/blocks/control_blocks_test.cpp | 87 +++++++++++++++++++++++++ 3 files changed, 97 insertions(+) diff --git a/src/dev/blocks/controlblocks.cpp b/src/dev/blocks/controlblocks.cpp index 41e8d24f..411940e4 100644 --- a/src/dev/blocks/controlblocks.cpp +++ b/src/dev/blocks/controlblocks.cpp @@ -34,6 +34,7 @@ void ControlBlocks::registerBlocks(IEngine *engine) 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); } CompilerValue *ControlBlocks::compileForever(Compiler *compiler) @@ -114,6 +115,14 @@ CompilerValue *ControlBlocks::compileRepeatUntil(Compiler *compiler) 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; +} + extern "C" void control_stop_all(ExecutionContext *ctx) { ctx->engine()->stop(); diff --git a/src/dev/blocks/controlblocks.h b/src/dev/blocks/controlblocks.h index e9f902cc..d2722809 100644 --- a/src/dev/blocks/controlblocks.h +++ b/src/dev/blocks/controlblocks.h @@ -24,6 +24,7 @@ class ControlBlocks : public IExtension static CompilerValue *compileWait(Compiler *compiler); static CompilerValue *compileWaitUntil(Compiler *compiler); static CompilerValue *compileRepeatUntil(Compiler *compiler); + static CompilerValue *compileWhile(Compiler *compiler); }; } // namespace libscratchcpp diff --git a/test/dev/blocks/control_blocks_test.cpp b/test/dev/blocks/control_blocks_test.cpp index 91aa04f3..19215797 100644 --- a/test/dev/blocks/control_blocks_test.cpp +++ b/test/dev/blocks/control_blocks_test.cpp @@ -559,3 +559,90 @@ TEST_F(ControlBlocksTest, RepeatUntil) } } } + +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()); + } + } +} From b389f56d71c41b6e3eb92d899910d814cc5464e8 Mon Sep 17 00:00:00 2001 From: adazem009 <68537469+adazem009@users.noreply.github.com> Date: Sat, 14 Dec 2024 16:10:08 +0100 Subject: [PATCH 18/26] LLVMCodeBuilder: Use unsigned integer in repeat loop --- src/dev/engine/internal/llvm/llvmcodebuilder.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/dev/engine/internal/llvm/llvmcodebuilder.cpp b/src/dev/engine/internal/llvm/llvmcodebuilder.cpp index eac2f868..5c4b8c64 100644 --- a/src/dev/engine/internal/llvm/llvmcodebuilder.cpp +++ b/src/dev/engine/internal/llvm/llvmcodebuilder.cpp @@ -796,7 +796,7 @@ 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 @@ -884,7 +884,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); } From f274cf65a023e5cf94cead2e69517c5a830510c9 Mon Sep 17 00:00:00 2001 From: adazem009 <68537469+adazem009@users.noreply.github.com> Date: Sat, 14 Dec 2024 16:10:55 +0100 Subject: [PATCH 19/26] LLVMCodeBuilder: Add addLoopIndex() method --- src/dev/engine/internal/icodebuilder.h | 1 + .../engine/internal/llvm/llvmcodebuilder.cpp | 13 +++++++++++ .../engine/internal/llvm/llvmcodebuilder.h | 1 + .../engine/internal/llvm/llvminstruction.h | 1 + test/dev/llvm/llvmcodebuilder_test.cpp | 23 ++++++++++--------- test/mocks/codebuildermock.h | 1 + 6 files changed, 29 insertions(+), 11 deletions(-) diff --git a/src/dev/engine/internal/icodebuilder.h b/src/dev/engine/internal/icodebuilder.h index 3b1f5419..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; diff --git a/src/dev/engine/internal/llvm/llvmcodebuilder.cpp b/src/dev/engine/internal/llvm/llvmcodebuilder.cpp index 5c4b8c64..069f7bd9 100644 --- a/src/dev/engine/internal/llvm/llvmcodebuilder.cpp +++ b/src/dev/engine/internal/llvm/llvmcodebuilder.cpp @@ -822,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(); @@ -993,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); diff --git a/src/dev/engine/internal/llvm/llvmcodebuilder.h b/src/dev/engine/internal/llvm/llvmcodebuilder.h index 3f96bc0a..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; diff --git a/src/dev/engine/internal/llvm/llvminstruction.h b/src/dev/engine/internal/llvm/llvminstruction.h index a016365b..9b81b563 100644 --- a/src/dev/engine/internal/llvm/llvminstruction.h +++ b/src/dev/engine/internal/llvm/llvminstruction.h @@ -58,6 +58,7 @@ struct LLVMInstruction BeginElse, EndIf, BeginRepeatLoop, + LoopIndex, BeginWhileLoop, BeginRepeatUntilLoop, BeginLoopCondition, diff --git a/test/dev/llvm/llvmcodebuilder_test.cpp b/test/dev/llvm/llvmcodebuilder_test.cpp index 02bb877d..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(); diff --git a/test/mocks/codebuildermock.h b/test/mocks/codebuildermock.h index f66408e7..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)); From cc2aca906bdeaa598de86e57c97037a4cc8a39c9 Mon Sep 17 00:00:00 2001 From: adazem009 <68537469+adazem009@users.noreply.github.com> Date: Sat, 14 Dec 2024 16:56:27 +0100 Subject: [PATCH 20/26] Compiler: Add addLoopIndex() method --- include/scratchcpp/dev/compiler.h | 1 + src/dev/engine/compiler.cpp | 6 ++++++ test/dev/compiler/compiler_test.cpp | 19 +++++++++++++++++++ 3 files changed, 26 insertions(+) diff --git a/include/scratchcpp/dev/compiler.h b/include/scratchcpp/dev/compiler.h index 94d9c5da..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); diff --git a/src/dev/engine/compiler.cpp b/src/dev/engine/compiler.cpp index 0ed85e99..772de15a 100644 --- a/src/dev/engine/compiler.cpp +++ b/src/dev/engine/compiler.cpp @@ -115,6 +115,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) { diff --git a/test/dev/compiler/compiler_test.cpp b/test/dev/compiler/compiler_test.cpp index 3e68d9af..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); From 53e30e23a8f01b4c8aa417aed5d84971afd6bf5f Mon Sep 17 00:00:00 2001 From: adazem009 <68537469+adazem009@users.noreply.github.com> Date: Sat, 14 Dec 2024 16:57:08 +0100 Subject: [PATCH 21/26] Compiler: Allow custom code in empty substacks --- src/dev/engine/compiler.cpp | 14 ++++++++++---- src/dev/engine/compiler_p.h | 1 + 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/dev/engine/compiler.cpp b/src/dev/engine/compiler.cpp index 772de15a..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); @@ -456,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. */ @@ -468,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. */ @@ -480,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. */ @@ -492,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. */ 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; From 9e8440df123266b8f2dc512795f892c88e4dd868 Mon Sep 17 00:00:00 2001 From: adazem009 <68537469+adazem009@users.noreply.github.com> Date: Sat, 14 Dec 2024 18:05:19 +0100 Subject: [PATCH 22/26] ScriptBuilder: Add support for substack sequences --- src/dev/test/scriptbuilder.cpp | 19 +++++++++++++++++-- test/dev/test_api/scriptbuilder_test.cpp | 19 +++++++++++++++++++ test/dev/test_api/testextension.cpp | 8 ++++++++ test/dev/test_api/testextension.h | 1 + 4 files changed, 45 insertions(+), 2 deletions(-) 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/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 From f98e1d1bc8bfc1ec407eae0de2bb82b6075369fc Mon Sep 17 00:00:00 2001 From: adazem009 <68537469+adazem009@users.noreply.github.com> Date: Sat, 14 Dec 2024 18:05:44 +0100 Subject: [PATCH 23/26] Implement control_foreach --- src/dev/blocks/controlblocks.cpp | 13 +++ src/dev/blocks/controlblocks.h | 1 + test/dev/blocks/control_blocks_test.cpp | 108 ++++++++++++++++++++++++ test/dev/blocks/util.cpp | 8 ++ 4 files changed, 130 insertions(+) diff --git a/src/dev/blocks/controlblocks.cpp b/src/dev/blocks/controlblocks.cpp index 411940e4..6da0e883 100644 --- a/src/dev/blocks/controlblocks.cpp +++ b/src/dev/blocks/controlblocks.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include "controlblocks.h" @@ -35,6 +36,7 @@ void ControlBlocks::registerBlocks(IEngine *engine) 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); } CompilerValue *ControlBlocks::compileForever(Compiler *compiler) @@ -123,6 +125,17 @@ CompilerValue *ControlBlocks::compileWhile(Compiler *compiler) 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; +} + extern "C" void control_stop_all(ExecutionContext *ctx) { ctx->engine()->stop(); diff --git a/src/dev/blocks/controlblocks.h b/src/dev/blocks/controlblocks.h index d2722809..a68490d1 100644 --- a/src/dev/blocks/controlblocks.h +++ b/src/dev/blocks/controlblocks.h @@ -25,6 +25,7 @@ class ControlBlocks : public IExtension static CompilerValue *compileWaitUntil(Compiler *compiler); static CompilerValue *compileRepeatUntil(Compiler *compiler); static CompilerValue *compileWhile(Compiler *compiler); + static CompilerValue *compileForEach(Compiler *compiler); }; } // namespace libscratchcpp diff --git a/test/dev/blocks/control_blocks_test.cpp b/test/dev/blocks/control_blocks_test.cpp index 19215797..bad62dc6 100644 --- a/test/dev/blocks/control_blocks_test.cpp +++ b/test/dev/blocks/control_blocks_test.cpp @@ -4,10 +4,12 @@ #include #include #include +#include #include #include #include #include +#include #include #include @@ -646,3 +648,109 @@ TEST_F(ControlBlocksTest, While) } } } + +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); + } +} diff --git a/test/dev/blocks/util.cpp b/test/dev/blocks/util.cpp index a6890119..2881c3f5 100644 --- a/test/dev/blocks/util.cpp +++ b/test/dev/blocks/util.cpp @@ -2,6 +2,8 @@ #include #include #include +#include +#include #include #include "util.h" @@ -30,6 +32,12 @@ void registerBlocks(IEngine *engine, IExtension *extension) }); engine->addCompileFunction(extension, "test_condition", [](Compiler *compiler) -> CompilerValue * { return compiler->addFunctionCall("test_condition", Compiler::StaticType::Bool); }); + + 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) From 6482b86223dffa65ddc2c62a56f539c699d88c79 Mon Sep 17 00:00:00 2001 From: adazem009 <68537469+adazem009@users.noreply.github.com> Date: Mon, 16 Dec 2024 07:47:20 +0100 Subject: [PATCH 24/26] Implement control_start_as_clone --- src/dev/blocks/controlblocks.cpp | 7 +++++++ src/dev/blocks/controlblocks.h | 1 + test/dev/blocks/control_blocks_test.cpp | 13 +++++++++++++ 3 files changed, 21 insertions(+) diff --git a/src/dev/blocks/controlblocks.cpp b/src/dev/blocks/controlblocks.cpp index 6da0e883..e49c2b25 100644 --- a/src/dev/blocks/controlblocks.cpp +++ b/src/dev/blocks/controlblocks.cpp @@ -37,6 +37,7 @@ void ControlBlocks::registerBlocks(IEngine *engine) 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); } CompilerValue *ControlBlocks::compileForever(Compiler *compiler) @@ -136,6 +137,12 @@ CompilerValue *ControlBlocks::compileForEach(Compiler *compiler) return nullptr; } +CompilerValue *ControlBlocks::compileStartAsClone(Compiler *compiler) +{ + compiler->engine()->addCloneInitScript(compiler->block()); + return nullptr; +} + extern "C" void control_stop_all(ExecutionContext *ctx) { ctx->engine()->stop(); diff --git a/src/dev/blocks/controlblocks.h b/src/dev/blocks/controlblocks.h index a68490d1..4e7e3b16 100644 --- a/src/dev/blocks/controlblocks.h +++ b/src/dev/blocks/controlblocks.h @@ -26,6 +26,7 @@ class ControlBlocks : public IExtension static CompilerValue *compileRepeatUntil(Compiler *compiler); static CompilerValue *compileWhile(Compiler *compiler); static CompilerValue *compileForEach(Compiler *compiler); + static CompilerValue *compileStartAsClone(Compiler *compiler); }; } // namespace libscratchcpp diff --git a/test/dev/blocks/control_blocks_test.cpp b/test/dev/blocks/control_blocks_test.cpp index bad62dc6..9416b56b 100644 --- a/test/dev/blocks/control_blocks_test.cpp +++ b/test/dev/blocks/control_blocks_test.cpp @@ -754,3 +754,16 @@ TEST_F(ControlBlocksTest, ForEach) 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); +} From 1cd40167c9fde36b975abbc89f8c0eaf7138606d Mon Sep 17 00:00:00 2001 From: adazem009 <68537469+adazem009@users.noreply.github.com> Date: Mon, 16 Dec 2024 15:26:23 +0100 Subject: [PATCH 25/26] Implement control_create_clone_of --- src/dev/blocks/controlblocks.cpp | 52 +++++ src/dev/blocks/controlblocks.h | 1 + test/dev/blocks/control_blocks_test.cpp | 296 ++++++++++++++++++++++++ test/dev/blocks/util.cpp | 2 + 4 files changed, 351 insertions(+) diff --git a/src/dev/blocks/controlblocks.cpp b/src/dev/blocks/controlblocks.cpp index e49c2b25..d9c66837 100644 --- a/src/dev/blocks/controlblocks.cpp +++ b/src/dev/blocks/controlblocks.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include "controlblocks.h" @@ -38,6 +39,7 @@ void ControlBlocks::registerBlocks(IEngine *engine) 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); } CompilerValue *ControlBlocks::compileForever(Compiler *compiler) @@ -143,6 +145,28 @@ CompilerValue *ControlBlocks::compileStartAsClone(Compiler *compiler) 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; +} + extern "C" void control_stop_all(ExecutionContext *ctx) { ctx->engine()->stop(); @@ -164,3 +188,31 @@ 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(); + } +} diff --git a/src/dev/blocks/controlblocks.h b/src/dev/blocks/controlblocks.h index 4e7e3b16..51012ad0 100644 --- a/src/dev/blocks/controlblocks.h +++ b/src/dev/blocks/controlblocks.h @@ -27,6 +27,7 @@ class ControlBlocks : public IExtension static CompilerValue *compileWhile(Compiler *compiler); static CompilerValue *compileForEach(Compiler *compiler); static CompilerValue *compileStartAsClone(Compiler *compiler); + static CompilerValue *compileCreateCloneOf(Compiler *compiler); }; } // namespace libscratchcpp diff --git a/test/dev/blocks/control_blocks_test.cpp b/test/dev/blocks/control_blocks_test.cpp index 9416b56b..09091052 100644 --- a/test/dev/blocks/control_blocks_test.cpp +++ b/test/dev/blocks/control_blocks_test.cpp @@ -2,6 +2,7 @@ #include #include #include +#include #include #include #include @@ -22,6 +23,8 @@ using namespace libscratchcpp; using namespace libscratchcpp::test; using ::testing::Return; +using ::testing::SaveArg; +using ::testing::_; class ControlBlocksTest : public testing::Test { @@ -767,3 +770,296 @@ TEST_F(ControlBlocksTest, StartAsClone) 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(); + } +} diff --git a/test/dev/blocks/util.cpp b/test/dev/blocks/util.cpp index 2881c3f5..78345800 100644 --- a/test/dev/blocks/util.cpp +++ b/test/dev/blocks/util.cpp @@ -33,6 +33,8 @@ void registerBlocks(IEngine *engine, IExtension *extension) 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")); From 2388d9f0935357ad54256799313d66767435357f Mon Sep 17 00:00:00 2001 From: adazem009 <68537469+adazem009@users.noreply.github.com> Date: Mon, 16 Dec 2024 21:39:48 +0100 Subject: [PATCH 26/26] Implement control_delete_this_clone --- src/dev/blocks/controlblocks.cpp | 15 ++++++++ src/dev/blocks/controlblocks.h | 1 + test/dev/blocks/control_blocks_test.cpp | 50 +++++++++++++++++++++++++ 3 files changed, 66 insertions(+) diff --git a/src/dev/blocks/controlblocks.cpp b/src/dev/blocks/controlblocks.cpp index d9c66837..9567f69f 100644 --- a/src/dev/blocks/controlblocks.cpp +++ b/src/dev/blocks/controlblocks.cpp @@ -40,6 +40,7 @@ void ControlBlocks::registerBlocks(IEngine *engine) 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) @@ -167,6 +168,12 @@ CompilerValue *ControlBlocks::compileCreateCloneOf(Compiler *compiler) 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(); @@ -216,3 +223,11 @@ extern "C" void control_create_clone(ExecutionContext *ctx, const char *spriteNa 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 51012ad0..17850824 100644 --- a/src/dev/blocks/controlblocks.h +++ b/src/dev/blocks/controlblocks.h @@ -28,6 +28,7 @@ class ControlBlocks : public IExtension 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/test/dev/blocks/control_blocks_test.cpp b/test/dev/blocks/control_blocks_test.cpp index 09091052..9f054691 100644 --- a/test/dev/blocks/control_blocks_test.cpp +++ b/test/dev/blocks/control_blocks_test.cpp @@ -1063,3 +1063,53 @@ TEST_F(ControlBlocksTest, CreateCloneOfStage) 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(); +}