From 95496879c8804f537038ea46d6ea6861413434a3 Mon Sep 17 00:00:00 2001 From: Nikolay Cherkashin Date: Mon, 5 May 2025 18:36:20 +0000 Subject: [PATCH 01/12] base tests --- Dockerfile | 2 +- codegen/coyieldpass.cpp | 311 ++++++++++++++++++---------- test/codegen/coyield/lit.local.cfg | 9 + test/codegen/lit.cfg | 9 + verifying/targets/CMakeLists.txt | 2 +- verifying/targets/counique_args.cpp | 21 +- verifying/targets/counique_args.yml | 3 +- verifying/targets/unique_args.cpp | 6 +- verifying/targets/unique_args.yml | 4 + 9 files changed, 257 insertions(+), 110 deletions(-) create mode 100644 test/codegen/coyield/lit.local.cfg create mode 100644 test/codegen/lit.cfg create mode 100644 verifying/targets/unique_args.yml diff --git a/Dockerfile b/Dockerfile index 377dbfd1..c52b4fb0 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,7 +3,7 @@ FROM silkeh/clang:19 AS ltest RUN apt update && apt install -y git ninja-build valgrind libboost-context-dev libgflags-dev libstdc++-11-dev RUN mv /usr/lib/gcc/x86_64-linux-gnu/12 /usr/lib/gcc/x86_64-linux-gnu/_12 -FROM ltest as blocking +FROM ltest AS blocking RUN apt install -y pkg-config libcapstone-dev && \ git clone https://github.com/Kirillog/syscall_intercept.git && \ cmake syscall_intercept -G Ninja -B syscall_intercept/build -DCMAKE_INSTALL_PREFIX=/usr -DCMAKE_BUILD_TYPE=Release -DCMAKE_C_COMPILER=clang && \ diff --git a/codegen/coyieldpass.cpp b/codegen/coyieldpass.cpp index d97589aa..5ce79429 100644 --- a/codegen/coyieldpass.cpp +++ b/codegen/coyieldpass.cpp @@ -1,3 +1,6 @@ + +#include +#include #include #include #include @@ -6,6 +9,8 @@ #include #include #include +#include +#include #include #include #include @@ -13,9 +18,10 @@ #include #include #include +#include +#include #include -#include #include #include #include @@ -27,24 +33,21 @@ #include "llvm/Pass.h" #include "llvm/Passes/PassBuilder.h" #include "llvm/Passes/PassPlugin.h" +#include "llvm/Transforms/Coroutines/CoroSplit.h" #include "llvm/Transforms/Utils/Cloning.h" using namespace llvm; using Builder = IRBuilder<>; constexpr std::string_view costatus_change = "CoroutineStatusChange"; - -constexpr std::string_view co_expr_start = "::await_ready"; - -constexpr std::string_view co_expr_end = "::await_resume"; -constexpr std::string_view co_initial_suspend = "::initial_suspend()"; -constexpr std::string_view co_final_suspend = "::final_suspend()"; - -constexpr std::string_view no_filter = "any"; - +constexpr std::string_view co_await_ready = "await_ready"; +constexpr int resumed_coro = 0; static cl::opt input_list( "coroutine-file", cl::desc("Specify path to file with coroutines to check"), llvm::cl::Required); ; +constexpr bool dump_before = false; +constexpr bool dump_after = true; + struct CoroutineFilter { CoroutineFilter() = default; CoroutineFilter(const std::optional &parent_name, @@ -54,6 +57,7 @@ struct CoroutineFilter { std::optional parent_name; std::optional co_name; std::string print_name; + bool only_fun; }; namespace llvm { @@ -64,6 +68,7 @@ struct MappingTraits { io.mapRequired("Name", cofilter.print_name); io.mapOptional("Coroutine", cofilter.co_name); io.mapOptional("Parent", cofilter.parent_name); + io.mapRequired("OnlyFun", cofilter.only_fun); } }; } // namespace yaml @@ -79,11 +84,15 @@ struct CoYieldInserter { costatus_change, FunctionType::get(Type::getVoidTy(context), {PointerType::get(Type::getInt8Ty(context), 0), - Type::getInt8Ty(context)}, + Type::getInt1Ty(context)}, {})); } void Run(const Module &index) { + if (dump_before) { + index.dump(); + errs().flush(); + } for (auto &f : m) { std::string demangled = demangle(f.getName()); auto filt = @@ -95,20 +104,15 @@ struct CoYieldInserter { InsertYields(filt, f); } } + if (dump_after) { + index.dump(); + errs().flush(); + } } private: void InsertYields(auto filt, Function &f) { Builder builder(&*f.begin()); - /* - In fact co_await expr when expr is coroutine is - co_await initial_suspend() - coro body... - co_await_final_suspend() - We are interested to insert only before initial_suspend and - after final_suspend - */ - int skip_insert_points = 0; for (auto &b : f) { for (auto &i : b) { CallBase *call = dyn_cast(&i); @@ -121,108 +125,193 @@ struct CoYieldInserter { } auto raw_fn_name = c_fn->getName(); std::string co_name = demangle(raw_fn_name); - bool is_call_inst = isa(call); + if (co_name == costatus_change) { + continue; + } + CallInst *call_inst = dyn_cast(call); InvokeInst *invoke = dyn_cast(call); - if (is_call_inst || invoke) { - auto res_filt = - filt | std::ranges::views::filter( - [&co_name](const CoroutineFilter &a) -> bool { - return !a.co_name || a.co_name == co_name; - }); - if (!res_filt.empty()) { - auto filt_entry = res_filt.front(); - errs() << "inserted " << filt_entry.print_name << "\n"; - builder.SetInsertPoint(call); - InsertCall(filt_entry, builder, true); - // Invoke instruction has unwind/normal ends so we need handle it - if (invoke) { - builder.SetInsertPoint(invoke->getNormalDest()->getFirstInsertionPt()); - InsertCall(filt_entry, builder, false); - builder.SetInsertPoint(invoke->getUnwindDest()->getFirstInsertionPt()); - InsertCall(filt_entry, builder, false); - } else { - builder.SetInsertPoint(call->getNextNode()); - InsertCall(filt_entry, builder, false); + + if (call_inst || invoke) { + auto await_ready_ind = co_name.find(co_await_ready); + if (await_ready_ind != std::string::npos) { + auto res_filt = + filt | std::ranges::views::filter( + [&co_name](const CoroutineFilter &a) -> bool { + return !a.co_name || co_name.find(*a.co_name) != + std::string::npos; + }); + if (!res_filt.empty()) { + errs() << "inserted coro handled by type " << co_name << "\n"; + HandleCoroCase(builder, call, res_filt.front()); + } + } else { + auto res_filt = + filt | std::ranges::views::filter( + [&co_name](const CoroutineFilter &a) -> bool { + return !a.co_name || a.co_name == co_name; + }); + + if (!res_filt.empty()) { + auto entry = res_filt.front(); + if (entry.only_fun) { + errs() << "inserted generic" << co_name << "\n"; + HandleGenericFunCase(builder, call, invoke, entry); + } else { + errs() << "inserted coro handled by func name" << co_name + << "\n"; + if (invoke) { + assert(FindAwaitReady( + builder, invoke->getNormalDest()->begin(), entry)); + } else { + assert(FindAwaitReady( + builder, BasicBlock::iterator(call->getNextNode()), + entry)); + } + } } - continue; } } - if (!is_call_inst) { - continue; - } - auto initial = co_name.find(co_initial_suspend); - if (initial != std::string::npos) { - builder.SetInsertPoint(call); - InsertCallWithFilter(filt, co_name, builder, true, initial); - skip_insert_points = 2; - continue; + } + } + } + + bool FindAwaitReady(Builder &builder, BasicBlock::iterator start, + const CoroutineFilter &entry) { + for (Instruction &n_inst : make_range(start, start->getParent()->end())) { + auto *call_inst = dyn_cast(&n_inst); + if (!call_inst) { + continue; + } + auto await_ready_ind = demangle(call_inst->getCalledFunction()->getName()) + .find(co_await_ready); + if (await_ready_ind != std::string::npos) { + HandleCoroCase(builder, call_inst, entry); + return true; + } + // If Coro Type constructor can throw we need go deeper + if (auto *invoke = dyn_cast(call_inst)) { + return FindAwaitReady(builder, invoke->getNormalDest()->begin(), entry); + } + } + return false; + } + // This case is needed at sample by some coro primitives where the + // normal function which is the body of coro is called in loop + void HandleGenericFunCase(Builder &builder, CallBase *call, + InvokeInst *invoke, + const CoroutineFilter &filt_entry) { + builder.SetInsertPoint(call); + InsertCall(filt_entry, builder, true); + // Invoke instruction has unwind/normal ends so we need handle it + if (invoke) { + builder.SetInsertPoint(invoke->getNormalDest()->getFirstInsertionPt()); + InsertCall(filt_entry, builder, false); + builder.SetInsertPoint(invoke->getUnwindDest()->getFirstInsertionPt()); + InsertCall(filt_entry, builder, false); + } else { + builder.SetInsertPoint(call->getNextNode()); + InsertCall(filt_entry, builder, false); + } + } + + void HandleCoroCase(Builder &builder, CallBase *call, + const CoroutineFilter &filt_entry) { + BranchInst *br = dyn_cast(call->getNextNode()); + assert(br && br->getNumSuccessors() == 2); + BasicBlock *not_ready_bb = br->getSuccessor(1); + for (auto &i : *not_ready_bb) { + CallBase *call_base = dyn_cast(&i); + if (!call_base) { + continue; + } + + Intrinsic::ID id = call_base->getIntrinsicID(); + switch (id) { + // We cannot insert after await_suspend because inside it we can already + // interact with handle, so we must we do it before + case Intrinsic::coro_await_suspend_bool: { + builder.SetInsertPoint(call_base); + InsertCall(filt_entry, builder, true); + // InsertAtEnd(builder, nextNormal(call_base), filt_entry); + BranchInst *suspend_br = dyn_cast(i.getNextNode()); + assert(suspend_br && suspend_br->getNumSuccessors() == 2); + builder.SetInsertPoint( + suspend_br->getSuccessor(0)->getFirstInsertionPt()); + // InsertCall(filt_entry, builder, true); + // handled if await_suspend was true, now change block also for false + BasicBlock *tramp = + InsertAtEnd(builder, &(*builder.GetInsertPoint()), filt_entry); + suspend_br->setSuccessor(1, tramp); + return; } - auto final = co_name.find(co_final_suspend); - if (final != std::string::npos) { - builder.SetInsertPoint(call->getNextNode()); - InsertCallWithFilter(filt, co_name, builder, false, final); - skip_insert_points = 2; - continue; + case Intrinsic::coro_await_suspend_void: { + builder.SetInsertPoint(call_base); + InsertCall(filt_entry, builder, true); + InsertAtEnd(builder, call_base++, filt_entry); + return; } - auto start = co_name.find(co_expr_start); - if (start != std::string::npos) { - if (skip_insert_points != 0) { - assert(skip_insert_points == 2); - skip_insert_points--; - continue; - } - builder.SetInsertPoint(call); - InsertCallWithFilter(filt, co_name, builder, true, start); - continue; + case Intrinsic::coro_await_suspend_handle: { + builder.SetInsertPoint(call_base); + InsertCall(filt_entry, builder, true); + InsertAtEnd(builder, nextNormal(call_base), filt_entry); + return; } - auto end_pos = co_name.find(co_expr_end); - if (end_pos != std::string::npos) { - if (skip_insert_points != 0) { - assert(skip_insert_points == 1); - skip_insert_points--; - continue; - } - builder.SetInsertPoint(call->getNextNode()); - InsertCallWithFilter(filt, co_name, builder, false, end_pos); + default: { continue; } } } + assert(false && "Haven't found await_suspend intrisinc"); } - - void InsertCallWithFilter(auto filt, StringRef co_name, Builder &builder, - bool start, int end_pos) { - auto res_filt = - filt | std::ranges::views::filter( - [&end_pos, &co_name](const CoroutineFilter &a) -> bool { - return !a.co_name || - a.co_name == co_name.substr(0, end_pos); - }); - if (res_filt.empty()) { - return; + Instruction *nextNormal(CallBase *inst) { + if (auto invoke = dyn_cast(inst)) { + return invoke->getNormalDest()->getFirstNonPHI(); + } else { + return inst->getNextNode(); } - errs() << "inserted " << co_name.str() << "\n"; - // First in the config will match - InsertCall(res_filt.front(), builder, start); } - + BasicBlock *InsertAtEnd(Builder &builder, Instruction *instr, + const CoroutineFilter &filt_entry) { + CallInst *intr = dyn_cast(instr); + assert(intr && intr->getIntrinsicID() == Intrinsic::coro_suspend); + SwitchInst *switch_inst = dyn_cast(intr->getNextNode()); + assert(switch_inst); + auto resumed_bb = switch_inst->findCaseValue( + ConstantInt::get(Type::getInt8Ty(builder.getContext()), resumed_coro)); + auto succ = resumed_bb->getCaseSuccessor(); + // If we would simple insert in the block we would have extra ends, so we + // need to add a trampoline + BasicBlock *tramp = + BasicBlock::Create(builder.getContext(), "", succ->getParent()); + resumed_bb->setSuccessor(tramp); + builder.SetInsertPoint(tramp); + InsertCall(filt_entry, builder, false); + builder.CreateBr(succ); + return tramp; + } void InsertCall(const CoroutineFilter &filt, Builder &builder, bool start) { auto llvm_start = ConstantInt::get(Type::getInt1Ty(builder.getContext()), start); - Constant *str_const = - ConstantDataArray::getString(m.getContext(), filt.print_name, true); - auto zero = ConstantInt::get(Type::getInt32Ty(m.getContext()), 0); - Constant *ind[] = {zero, zero}; - GlobalVariable *global = new GlobalVariable( - m, str_const->getType(), true, GlobalValue::PrivateLinkage, str_const); - auto ptr = - ConstantExpr::getGetElementPtr(global->getValueType(), global, ind); - builder.CreateCall(coroYieldF, {ptr, llvm_start}); + auto literal = string_literals.find(filt.print_name); + if (literal == string_literals.end()) { + Constant *str_const = + ConstantDataArray::getString(m.getContext(), filt.print_name, true); + auto zero = ConstantInt::get(Type::getInt32Ty(m.getContext()), 0); + std::array ind = {zero, zero}; + GlobalVariable *global = + new GlobalVariable(m, str_const->getType(), true, + GlobalValue::PrivateLinkage, str_const); + auto ptr = + ConstantExpr::getGetElementPtr(global->getValueType(), global, ind); + literal = string_literals.emplace(filt.print_name, ptr).first; + } + builder.CreateCall(coroYieldF, {literal->second, llvm_start}); } Module &m; FunctionCallee coroYieldF; std::vector co_filter; + std::map string_literals; }; namespace { @@ -230,12 +319,13 @@ namespace { struct CoYieldInsertPass final : public PassInfoMixin { PreservedAnalyses run(Module &m, ModuleAnalysisManager &am) { // NOLINT if (input_list.empty()) { - report_fatal_error("No file with coroutines list"); + report_fatal_error("No file with coroutines list", false); } auto file = llvm::MemoryBuffer::getFile(input_list); if (!file) { - report_fatal_error("Failed to load config file\n"); + errs() << "Tried to read file " << input_list << "\n"; + report_fatal_error("Failed to load config file \n", false); } llvm::yaml::Input input(file.get()->getBuffer()); @@ -243,7 +333,8 @@ struct CoYieldInsertPass final : public PassInfoMixin { input >> filt; if (input.error()) { - report_fatal_error("Error parsing YAML\n"); + errs() << "Tried to parse file " << input_list << "\n"; + report_fatal_error("Error parsing YAML\n", false); } CoYieldInserter gen{m, std::move(filt)}; gen.Run(m); @@ -259,9 +350,21 @@ llvmGetPassPluginInfo() { .PluginName = "coyield_insert", .PluginVersion = "v0.1", .RegisterPassBuilderCallbacks = [](PassBuilder &pb) { + // This parsing we need for testing with opt + pb.registerPipelineParsingCallback( + [](StringRef Name, ModulePassManager &mpm, + ArrayRef) { + if (Name == "coyield_insert") { + mpm.addPass(CoYieldInsertPass()); + return true; + } + return false; + }); pb.registerPipelineStartEPCallback( [](ModulePassManager &mpm, OptimizationLevel level) { - std::set l; + // Looks like we don't need any lowerings, but i'm not + // sure + // mpm.addPass(CoroEarlyPass()); mpm.addPass(CoYieldInsertPass()); }); }}; diff --git a/test/codegen/coyield/lit.local.cfg b/test/codegen/coyield/lit.local.cfg new file mode 100644 index 00000000..29a8f270 --- /dev/null +++ b/test/codegen/coyield/lit.local.cfg @@ -0,0 +1,9 @@ +import lit +config.name = 'CoYieldPass' +config.build_pass_root = os.path.join(config.lit_config_dir, '..', '..' ,'build', 'codegen') + +#todo add normal handling of config files +config.substitutions.append(('%build', f""" +clang++ %s -emit-llvm -Xclang -disable-llvm-passes -S -std=c++20 -o - +| opt -load-pass-plugin={config.build_pass_root}/libCoYieldPass.so --coroutine-file=%s.yml +-passes=coyield_insert -verify-each -stop-after=coyield_insert -S""")) diff --git a/test/codegen/lit.cfg b/test/codegen/lit.cfg new file mode 100644 index 00000000..d34efde5 --- /dev/null +++ b/test/codegen/lit.cfg @@ -0,0 +1,9 @@ +import lit + +config.name = 'Codegen' + +config.suffixes = ['.cpp'] + +config.test_format = lit.formats.ShTest() +config.lit_config_dir = os.path.dirname(os.path.abspath(__file__)) +config.test_source_root = None \ No newline at end of file diff --git a/verifying/targets/CMakeLists.txt b/verifying/targets/CMakeLists.txt index 6e8d2023..feacb20d 100644 --- a/verifying/targets/CMakeLists.txt +++ b/verifying/targets/CMakeLists.txt @@ -11,10 +11,10 @@ set (SOURCE_TARGET_LIST ) set (SOURCE_TARGET_WITHOUT_PLUGIN_LIST - unique_args.cpp ) set (SOURCE_TARGET_CO_LIST + unique_args.cpp counique_args.cpp ) diff --git a/verifying/targets/counique_args.cpp b/verifying/targets/counique_args.cpp index b599640e..52c9f4c6 100644 --- a/verifying/targets/counique_args.cpp +++ b/verifying/targets/counique_args.cpp @@ -10,10 +10,25 @@ struct Promise; // NOLINTBEGIN(readability-identifier-naming) +struct SimpleAwaitable { + bool await_ready() const noexcept { + return false; // Always suspend + } + + bool await_suspend(std::coroutine_handle<> h) const noexcept { + h.resume(); + return false; + } + + void await_resume() const noexcept { + } +}; struct Coroutine : std::coroutine_handle { using promise_type = ::Promise; + auto operator co_await() const { return SimpleAwaitable{}; } }; + struct Promise { Coroutine get_return_object() { return {Coroutine::from_promise(*this)}; } std::suspend_never initial_suspend() noexcept { return {}; } @@ -26,10 +41,14 @@ struct Promise { static std::vector used(limit, false); static std::vector done(limit, false); -Coroutine CoFun(int i) { +Coroutine CoWork(int i) { done[i] = true; co_return; } + +Coroutine CoFun(int i) { + co_await CoWork(i); +} struct CoUniqueArgsTest { CoUniqueArgsTest() {} ValueWrapper Get(size_t i) { diff --git a/verifying/targets/counique_args.yml b/verifying/targets/counique_args.yml index 5cf938e5..6b3b7cc7 100644 --- a/verifying/targets/counique_args.yml +++ b/verifying/targets/counique_args.yml @@ -1,3 +1,4 @@ - Name: cofun - Coroutine: CoFun(int) + Coroutine: CoWork(int) + OnlyFun: True \ No newline at end of file diff --git a/verifying/targets/unique_args.cpp b/verifying/targets/unique_args.cpp index ee53dc8d..910ceb81 100644 --- a/verifying/targets/unique_args.cpp +++ b/verifying/targets/unique_args.cpp @@ -8,17 +8,19 @@ static std::vector used(limit, false); static std::vector done(limit, false); +void DoWork(int i){ + done[i] = true; +} struct CoUniqueArgsTest { CoUniqueArgsTest() {} ValueWrapper Get(size_t i) { assert(!used[i]); used[i] = true; - CoroYield(); + DoWork(i); auto l = [this]() { Reset(); return limit; }; - done[i] = true; return {std::count(done.begin(), done.end(), false) == 0 ? l() : std::optional(), diff --git a/verifying/targets/unique_args.yml b/verifying/targets/unique_args.yml new file mode 100644 index 00000000..34b61fdf --- /dev/null +++ b/verifying/targets/unique_args.yml @@ -0,0 +1,4 @@ +- Name: work + Coroutine: DoWork(int) + OnlyFun: True + \ No newline at end of file From 3e35b994e7e7b7b822ace0b31ff6af178212b6fd Mon Sep 17 00:00:00 2001 From: Nikolay Cherkashin Date: Tue, 6 May 2025 21:49:39 +0000 Subject: [PATCH 02/12] pass ready(?) --- .github/workflows/run-tests.yaml | 20 ++ codegen/coyieldpass.cpp | 277 +++++++++++++----- runtime/include/lib.h | 14 +- runtime/lib.cpp | 7 + test/codegen/coyield/lit.local.cfg | 5 +- test/codegen/coyield/tests/bool_suspend.cpp | 36 +++ .../coyield/tests/bool_suspend.cpp.yml | 0 test/codegen/coyield/tests/simple_insert.cpp | 19 ++ .../coyield/tests/simple_insert.cpp.yml | 8 + test/codegen/coyield/tests/void_suspend.cpp | 34 +++ .../coyield/tests/void_suspend.cpp.yml | 0 verifying/targets/counique_args.yml | 4 +- verifying/targets/unique_args.yml | 4 +- 13 files changed, 345 insertions(+), 83 deletions(-) create mode 100644 test/codegen/coyield/tests/bool_suspend.cpp create mode 100644 test/codegen/coyield/tests/bool_suspend.cpp.yml create mode 100644 test/codegen/coyield/tests/simple_insert.cpp create mode 100644 test/codegen/coyield/tests/simple_insert.cpp.yml create mode 100644 test/codegen/coyield/tests/void_suspend.cpp create mode 100644 test/codegen/coyield/tests/void_suspend.cpp.yml diff --git a/.github/workflows/run-tests.yaml b/.github/workflows/run-tests.yaml index c036b076..264f1163 100644 --- a/.github/workflows/run-tests.yaml +++ b/.github/workflows/run-tests.yaml @@ -23,6 +23,26 @@ jobs: cmake --build build --target lin_check_test - name: Run lin check test run: ctest --test-dir build -R "^LinearizabilityCheckerCounterTest" -V + pass-tests: + runs-on: ubuntu-latest + defaults: + run: + shell: bash + container: + image: silkeh/clang:18 + options: --user root + timeout-minutes: 10 + steps: + - name: Install deps + run: apt update && apt install -y git ninja-build valgrind libboost-context-dev libgflags-dev + - name: Check out repository code + uses: actions/checkout@v4 + - name: Build + run: | + cmake -G Ninja -B build -DCMAKE_BUILD_TYPE=RelWithAssert + cmake --build build --target CoYieldPass + - name: Run lin check test + run: ctest --test-dir build -L llvm-pass -V verifying-test: runs-on: ubuntu-latest defaults: diff --git a/codegen/coyieldpass.cpp b/codegen/coyieldpass.cpp index 5ce79429..f6e5795b 100644 --- a/codegen/coyieldpass.cpp +++ b/codegen/coyieldpass.cpp @@ -1,6 +1,7 @@ #include #include +#include #include #include #include @@ -39,36 +40,53 @@ using namespace llvm; using Builder = IRBuilder<>; constexpr std::string_view costatus_change = "CoroutineStatusChange"; +constexpr std::string_view create_thread = "CreateNewVirtualThread"; + constexpr std::string_view co_await_ready = "await_ready"; constexpr int resumed_coro = 0; static cl::opt input_list( - "coroutine-file", cl::desc("Specify path to file with coroutines to check"), + "coroutine-file", cl::desc("Specify path to file with config"), llvm::cl::Required); ; + constexpr bool dump_before = false; -constexpr bool dump_after = true; +constexpr bool dump_after = false; + +enum HandleType { GENERIC_FUN, CORO_FUN, SPAWN_VIRT_THREAD }; struct CoroutineFilter { CoroutineFilter() = default; - CoroutineFilter(const std::optional &parent_name, - const std::optional &co_name, - const std::string &print_name) - : parent_name(parent_name), co_name(co_name), print_name(print_name) {}; - std::optional parent_name; - std::optional co_name; + HandleType type; std::string print_name; - bool only_fun; + + std::optional co_name; + std::optional parent_name; + // i believe that adding column is overkill + std::optional debug_line; + std::optional debug_file; }; namespace llvm { namespace yaml { template <> +struct ScalarEnumerationTraits { + static void enumeration(IO &io, HandleType &value) { // NOLINT + io.enumCase(value, "Generic", HandleType::GENERIC_FUN); + io.enumCase(value, "Coro", HandleType::CORO_FUN); + io.enumCase(value, "VirtThread", HandleType::SPAWN_VIRT_THREAD); + } +}; +template <> struct MappingTraits { static void mapping(IO &io, CoroutineFilter &cofilter) { // NOLINT + io.mapRequired("Type", cofilter.type); io.mapRequired("Name", cofilter.print_name); - io.mapOptional("Coroutine", cofilter.co_name); + + io.mapOptional("Function", cofilter.co_name); io.mapOptional("Parent", cofilter.parent_name); - io.mapRequired("OnlyFun", cofilter.only_fun); + + io.mapOptional("Line", cofilter.debug_line); + io.mapOptional("File", cofilter.debug_file); } }; } // namespace yaml @@ -77,8 +95,9 @@ struct MappingTraits { LLVM_YAML_IS_SEQUENCE_VECTOR(CoroutineFilter); struct CoYieldInserter { + Builder builder; CoYieldInserter(Module &m, std::vector &&co_filter) - : m(m), co_filter(std::move(co_filter)) { + : m(m), co_filter(std::move(co_filter)), builder(m.getContext()) { auto &context = m.getContext(); coroYieldF = m.getOrInsertFunction( costatus_change, @@ -86,14 +105,23 @@ struct CoYieldInserter { {PointerType::get(Type::getInt8Ty(context), 0), Type::getInt1Ty(context)}, {})); + createThreadF = m.getOrInsertFunction( + create_thread, + FunctionType::get(Type::getVoidTy(context), + {PointerType::get(Type::getInt8Ty(context), 0), + PointerType::get(context, 0)}, + {})); } - void Run(const Module &index) { + void Run() { if (dump_before) { - index.dump(); + m.dump(); errs().flush(); } for (auto &f : m) { + if (ignored.contains(&f)) { + continue; + } std::string demangled = demangle(f.getName()); auto filt = co_filter | std::ranges::views::filter( @@ -104,8 +132,11 @@ struct CoYieldInserter { InsertYields(filt, f); } } + for (auto inst : to_delete) { + inst->eraseFromParent(); + } if (dump_after) { - index.dump(); + m.dump(); errs().flush(); } } @@ -132,50 +163,139 @@ struct CoYieldInserter { InvokeInst *invoke = dyn_cast(call); if (call_inst || invoke) { - auto await_ready_ind = co_name.find(co_await_ready); - if (await_ready_ind != std::string::npos) { - auto res_filt = - filt | std::ranges::views::filter( - [&co_name](const CoroutineFilter &a) -> bool { - return !a.co_name || co_name.find(*a.co_name) != - std::string::npos; - }); - if (!res_filt.empty()) { - errs() << "inserted coro handled by type " << co_name << "\n"; - HandleCoroCase(builder, call, res_filt.front()); - } + // filt and filt | filter have differnet types + if (auto debugLoc = call->getDebugLoc()) { + auto place_filt = + filt | + std::ranges::views::filter( + [&co_name, &debugLoc](const CoroutineFilter &a) -> bool { + if (a.debug_file.has_value() && + a.debug_file != debugLoc->getFile()->getFilename()) { + return false; + } + if (a.debug_line.has_value() && + a.debug_line != debugLoc.getLine()) { + return false; + } + return true; + }); + InsertYield(place_filt, call_inst, invoke, co_name); + } else { - auto res_filt = - filt | std::ranges::views::filter( - [&co_name](const CoroutineFilter &a) -> bool { - return !a.co_name || a.co_name == co_name; - }); - - if (!res_filt.empty()) { - auto entry = res_filt.front(); - if (entry.only_fun) { - errs() << "inserted generic" << co_name << "\n"; - HandleGenericFunCase(builder, call, invoke, entry); - } else { - errs() << "inserted coro handled by func name" << co_name - << "\n"; - if (invoke) { - assert(FindAwaitReady( - builder, invoke->getNormalDest()->begin(), entry)); - } else { - assert(FindAwaitReady( - builder, BasicBlock::iterator(call->getNextNode()), - entry)); - } + InsertYield(filt, call_inst, invoke, co_name); + } + } + } + } + } + + void InsertYield(auto filt, CallInst *call, InvokeInst *invoke, + std::string co_name) { + auto await_ready_ind = co_name.find(co_await_ready); + if (await_ready_ind != std::string::npos) { + auto res_filt = + filt | std::ranges::views::filter( + [&co_name](const CoroutineFilter &a) -> bool { + return !a.co_name || + co_name.find(*a.co_name) != std::string::npos; + }); + if (!res_filt.empty() && res_filt.front().type == HandleType::CORO_FUN) { + errs() << "inserted coro handled by type " << co_name << "\n"; + HandleCoroCase(call, res_filt.front()); + } + } else { + auto res_filt = filt | std::ranges::views::filter( + [&co_name](const CoroutineFilter &a) -> bool { + return !a.co_name || a.co_name == co_name; + }); + + if (!res_filt.empty()) { + auto entry = res_filt.front(); + switch (entry.type) { + case HandleType::CORO_FUN: { + errs() << "inserted coro handled by func name" << co_name << "\n"; + if (invoke) { + assert(FindAwaitReady(invoke->getNormalDest()->begin(), entry)); + } else { + assert(FindAwaitReady(BasicBlock::iterator(call->getNextNode()), + entry)); + } + break; + } + case HandleType::GENERIC_FUN: { + errs() << "inserted generic " << co_name << "\n"; + HandleGenericFunCase(call, invoke, entry); + break; + } + case HandleType::SPAWN_VIRT_THREAD: { + errs() << "inserted spawn of new thread " << co_name << "\n"; + CallBase *inst = call ? static_cast(call) : invoke; + Function *called_fun = inst->getCalledFunction(); + + if (!inst->arg_empty()) { + auto [wrapper_fun, storage] = InsertZeroArgsWrapper(inst); + builder.SetInsertPoint(inst); + for (size_t i = 0; i < called_fun->arg_size(); i++) { + Value *arg = inst->getArgOperand(i); + + Value *storage_place = + builder.CreateGEP(storage->getValueType(), storage, + { + builder.getInt32(0), + builder.getInt32(i), + }); + builder.CreateStore(arg, storage_place); } + called_fun = wrapper_fun; } + Value *pointer_to_func = builder.CreatePointerCast( + called_fun, PointerType::get(builder.getContext(), 0)); + builder.SetInsertPoint(inst); + Value *replacement = builder.CreateCall( + createThreadF, {GetLiteral(entry.print_name), pointer_to_func}); + inst->replaceAllUsesWith(replacement); + // we cannot simple delete here instruction because we are + // iterating over it in basic block + to_delete.push_back(inst); + break; } + default: + __builtin_unreachable(); } } } } + // We need pass to scheduler function and wan't to care about number + // of args and their type to not interact with templates - so lets create a + // wrapper which would have zero args + std::pair InsertZeroArgsWrapper( + CallBase *call_inst) { + Function *func = + Function::Create(FunctionType::get(Type::getVoidTy(m.getContext()), {}), + GlobalValue::PrivateLinkage, "", m); + ignored.insert(func); + std::vector types; + for (auto &arg : call_inst->args()) { + types.push_back(arg->getType()); + } + StructType *storage_type = StructType::create(types); + GlobalVariable *storage = + new GlobalVariable(m, storage_type, false, GlobalValue::PrivateLinkage, + Constant::getNullValue(storage_type)); + BasicBlock *block = BasicBlock::Create(builder.getContext(), "", func); + builder.SetInsertPoint(block); + std::vector args; + for (size_t i = 0; i < types.size(); i++) { + Value *load = builder.CreateGEP( + storage_type, storage, {builder.getInt32(0), builder.getInt32(i)}); + args.push_back(builder.CreateLoad(types[i], load)); + } + builder.CreateCall(call_inst->getCalledFunction(), {args}); + builder.CreateRetVoid(); + return {func, storage}; + } - bool FindAwaitReady(Builder &builder, BasicBlock::iterator start, + bool FindAwaitReady(BasicBlock::iterator start, const CoroutineFilter &entry) { for (Instruction &n_inst : make_range(start, start->getParent()->end())) { auto *call_inst = dyn_cast(&n_inst); @@ -185,37 +305,35 @@ struct CoYieldInserter { auto await_ready_ind = demangle(call_inst->getCalledFunction()->getName()) .find(co_await_ready); if (await_ready_ind != std::string::npos) { - HandleCoroCase(builder, call_inst, entry); + HandleCoroCase(call_inst, entry); return true; } // If Coro Type constructor can throw we need go deeper if (auto *invoke = dyn_cast(call_inst)) { - return FindAwaitReady(builder, invoke->getNormalDest()->begin(), entry); + return FindAwaitReady(invoke->getNormalDest()->begin(), entry); } } return false; } // This case is needed at sample by some coro primitives where the // normal function which is the body of coro is called in loop - void HandleGenericFunCase(Builder &builder, CallBase *call, - InvokeInst *invoke, + void HandleGenericFunCase(CallBase *call, InvokeInst *invoke, const CoroutineFilter &filt_entry) { builder.SetInsertPoint(call); - InsertCall(filt_entry, builder, true); + InsertYieldCall(filt_entry, builder, true); // Invoke instruction has unwind/normal ends so we need handle it if (invoke) { builder.SetInsertPoint(invoke->getNormalDest()->getFirstInsertionPt()); - InsertCall(filt_entry, builder, false); + InsertYieldCall(filt_entry, builder, false); builder.SetInsertPoint(invoke->getUnwindDest()->getFirstInsertionPt()); - InsertCall(filt_entry, builder, false); + InsertYieldCall(filt_entry, builder, false); } else { builder.SetInsertPoint(call->getNextNode()); - InsertCall(filt_entry, builder, false); + InsertYieldCall(filt_entry, builder, false); } } - void HandleCoroCase(Builder &builder, CallBase *call, - const CoroutineFilter &filt_entry) { + void HandleCoroCase(CallBase *call, const CoroutineFilter &filt_entry) { BranchInst *br = dyn_cast(call->getNextNode()); assert(br && br->getNumSuccessors() == 2); BasicBlock *not_ready_bb = br->getSuccessor(1); @@ -227,18 +345,18 @@ struct CoYieldInserter { Intrinsic::ID id = call_base->getIntrinsicID(); switch (id) { - // We cannot insert after await_suspend because inside it we can already - // interact with handle, so we must we do it before + // We cannot insert after await_suspend because inside it we can + // already interact with handle, so we must we do it before case Intrinsic::coro_await_suspend_bool: { builder.SetInsertPoint(call_base); - InsertCall(filt_entry, builder, true); - // InsertAtEnd(builder, nextNormal(call_base), filt_entry); + InsertYieldCall(filt_entry, builder, true); BranchInst *suspend_br = dyn_cast(i.getNextNode()); assert(suspend_br && suspend_br->getNumSuccessors() == 2); builder.SetInsertPoint( suspend_br->getSuccessor(0)->getFirstInsertionPt()); // InsertCall(filt_entry, builder, true); - // handled if await_suspend was true, now change block also for false + // handled if await_suspend was true, now change block also for + // false BasicBlock *tramp = InsertAtEnd(builder, &(*builder.GetInsertPoint()), filt_entry); suspend_br->setSuccessor(1, tramp); @@ -246,13 +364,13 @@ struct CoYieldInserter { } case Intrinsic::coro_await_suspend_void: { builder.SetInsertPoint(call_base); - InsertCall(filt_entry, builder, true); + InsertYieldCall(filt_entry, builder, true); InsertAtEnd(builder, call_base++, filt_entry); return; } case Intrinsic::coro_await_suspend_handle: { builder.SetInsertPoint(call_base); - InsertCall(filt_entry, builder, true); + InsertYieldCall(filt_entry, builder, true); InsertAtEnd(builder, nextNormal(call_base), filt_entry); return; } @@ -263,6 +381,7 @@ struct CoYieldInserter { } assert(false && "Haven't found await_suspend intrisinc"); } + Instruction *nextNormal(CallBase *inst) { if (auto invoke = dyn_cast(inst)) { return invoke->getNormalDest()->getFirstNonPHI(); @@ -285,17 +404,21 @@ struct CoYieldInserter { BasicBlock::Create(builder.getContext(), "", succ->getParent()); resumed_bb->setSuccessor(tramp); builder.SetInsertPoint(tramp); - InsertCall(filt_entry, builder, false); + InsertYieldCall(filt_entry, builder, false); builder.CreateBr(succ); return tramp; } - void InsertCall(const CoroutineFilter &filt, Builder &builder, bool start) { + void InsertYieldCall(const CoroutineFilter &filt, Builder &builder, + bool start) { auto llvm_start = ConstantInt::get(Type::getInt1Ty(builder.getContext()), start); - auto literal = string_literals.find(filt.print_name); + builder.CreateCall(coroYieldF, {GetLiteral(filt.print_name), llvm_start}); + } + Constant *GetLiteral(const std::string &name) { + auto literal = string_literals.find(name); if (literal == string_literals.end()) { Constant *str_const = - ConstantDataArray::getString(m.getContext(), filt.print_name, true); + ConstantDataArray::getString(m.getContext(), name, true); auto zero = ConstantInt::get(Type::getInt32Ty(m.getContext()), 0); std::array ind = {zero, zero}; GlobalVariable *global = @@ -303,15 +426,17 @@ struct CoYieldInserter { GlobalValue::PrivateLinkage, str_const); auto ptr = ConstantExpr::getGetElementPtr(global->getValueType(), global, ind); - literal = string_literals.emplace(filt.print_name, ptr).first; + literal = string_literals.emplace(name, ptr).first; } - builder.CreateCall(coroYieldF, {literal->second, llvm_start}); + return literal->second; } - Module &m; FunctionCallee coroYieldF; + FunctionCallee createThreadF; std::vector co_filter; std::map string_literals; + std::vector to_delete; + std::set ignored; }; namespace { @@ -337,7 +462,7 @@ struct CoYieldInsertPass final : public PassInfoMixin { report_fatal_error("Error parsing YAML\n", false); } CoYieldInserter gen{m, std::move(filt)}; - gen.Run(m); + gen.Run(); return PreservedAnalyses::none(); }; }; @@ -362,7 +487,7 @@ llvmGetPassPluginInfo() { }); pb.registerPipelineStartEPCallback( [](ModulePassManager &mpm, OptimizationLevel level) { - // Looks like we don't need any lowerings, but i'm not + // Looks like we don't need any lowerings, before but i'm not // sure // mpm.addPass(CoroEarlyPass()); mpm.addPass(CoYieldInsertPass()); diff --git a/runtime/include/lib.h b/runtime/include/lib.h index b629c970..1541b5fa 100644 --- a/runtime/include/lib.h +++ b/runtime/include/lib.h @@ -18,6 +18,7 @@ struct CoroBase; struct CoroutineStatus; +struct VirtualThreadCreation; // Current executing coroutine. extern std::shared_ptr this_coro; @@ -26,11 +27,20 @@ extern boost::context::fiber_context sched_ctx; extern std::optional coroutine_status; -struct CoroutineStatus{ +extern std::optional virtual_thread_creation; + +extern std::optional virtual_thread_creation; + +struct CoroutineStatus { std::string_view name; bool has_started; }; +struct VirtualThreadCreation { + std::string_view name; + std::function function; +}; + // Runtime token. // Target method could use token generator. struct Token { @@ -52,6 +62,8 @@ extern "C" void CoroYield(); extern "C" void CoroutineStatusChange(char* coroutine, bool start); +extern "C" void CreateNewVirtualThread(char* name, void* func); + struct CoroBase : public std::enable_shared_from_this { CoroBase(const CoroBase&) = delete; CoroBase(CoroBase&&) = delete; diff --git a/runtime/lib.cpp b/runtime/lib.cpp index 2292d76f..7c5a9bb8 100644 --- a/runtime/lib.cpp +++ b/runtime/lib.cpp @@ -13,6 +13,8 @@ Task this_coro{}; boost::context::fiber_context sched_ctx; std::optional coroutine_status; +std::optional virtual_thread_creation; + std::unordered_map futex_state{}; namespace ltest { @@ -68,6 +70,11 @@ extern "C" void CoroutineStatusChange(char* name, bool start) { CoroYield(); } +extern "C" void CreateNewVirtualThread(char* name, void* func) { + virtual_thread_creation.emplace(name, reinterpret_cast(func)); + CoroYield(); +} + void CoroBase::Terminate() { int tries = 0; while (!IsReturned()) { diff --git a/test/codegen/coyield/lit.local.cfg b/test/codegen/coyield/lit.local.cfg index 29a8f270..d8e7a20d 100644 --- a/test/codegen/coyield/lit.local.cfg +++ b/test/codegen/coyield/lit.local.cfg @@ -3,7 +3,8 @@ config.name = 'CoYieldPass' config.build_pass_root = os.path.join(config.lit_config_dir, '..', '..' ,'build', 'codegen') #todo add normal handling of config files -config.substitutions.append(('%build', f""" +config.substitutions.append(('%check', f""" clang++ %s -emit-llvm -Xclang -disable-llvm-passes -S -std=c++20 -o - | opt -load-pass-plugin={config.build_pass_root}/libCoYieldPass.so --coroutine-file=%s.yml --passes=coyield_insert -verify-each -stop-after=coyield_insert -S""")) +-passes=coyield_insert -verify-each -stop-after=verify -S +| FileCheck %s""")) diff --git a/test/codegen/coyield/tests/bool_suspend.cpp b/test/codegen/coyield/tests/bool_suspend.cpp new file mode 100644 index 00000000..2ced175c --- /dev/null +++ b/test/codegen/coyield/tests/bool_suspend.cpp @@ -0,0 +1,36 @@ +// RUN: %check +#include +struct SimpleAwaitable { + bool await_ready() const noexcept { + return false; + } + + bool await_suspend(std::coroutine_handle<> h) const noexcept { + h.resume(); + return false; + } + + void await_resume() const noexcept { + } +}; + +struct CoroTask { + struct promise_type { + CoroTask get_return_object() { return {}; } + std::suspend_never initial_suspend() { return {}; } + std::suspend_never final_suspend() noexcept { return {}; } + void return_void() {} + void unhandled_exception() {} + }; + auto operator co_await() const { return SimpleAwaitable{}; } + +}; + +CoroTask myCoroutine2() { + co_return; +} + +// CHECK: call +CoroTask myCoroutine() { + co_await myCoroutine2(); +} \ No newline at end of file diff --git a/test/codegen/coyield/tests/bool_suspend.cpp.yml b/test/codegen/coyield/tests/bool_suspend.cpp.yml new file mode 100644 index 00000000..e69de29b diff --git a/test/codegen/coyield/tests/simple_insert.cpp b/test/codegen/coyield/tests/simple_insert.cpp new file mode 100644 index 00000000..d8a44dfa --- /dev/null +++ b/test/codegen/coyield/tests/simple_insert.cpp @@ -0,0 +1,19 @@ +// RUN: %check +int g() { return 1; } +void based_on_pos(){ + +} + +void ignored() {} +int f() { + ignored(); + // CHECK: call void @CoroutineStatusChange(ptr [[name:@[0-9]+]], i1 true) + // CHECK-NEXT: %[[res:.*]] = call noundef i32 @_Z1gv() + return g(); + // CHECK: call void @CoroutineStatusChange(ptr [[name]], i1 false) + // CHECK-NEXT: ret i32 %[[res]] +} + +int ignored_f(){ + return g(); +} \ No newline at end of file diff --git a/test/codegen/coyield/tests/simple_insert.cpp.yml b/test/codegen/coyield/tests/simple_insert.cpp.yml new file mode 100644 index 00000000..e77b130d --- /dev/null +++ b/test/codegen/coyield/tests/simple_insert.cpp.yml @@ -0,0 +1,8 @@ +- Name: test + Parent: f() + Function: g() + Type: Generic +- Name: pos_test + Parent: based_on_pos() + Function: g() + Type: Generic diff --git a/test/codegen/coyield/tests/void_suspend.cpp b/test/codegen/coyield/tests/void_suspend.cpp new file mode 100644 index 00000000..00904588 --- /dev/null +++ b/test/codegen/coyield/tests/void_suspend.cpp @@ -0,0 +1,34 @@ +// RUN: %check +#include +struct SimpleAwaitable { + bool await_ready() const noexcept { + return false; + } + + void await_suspend(std::coroutine_handle<> h) const noexcept { + h.resume(); + } + + void await_resume() const noexcept { + } +}; + +struct CoroTask { + struct promise_type { + CoroTask get_return_object() { return {}; } + std::suspend_never initial_suspend() { return {}; } + std::suspend_never final_suspend() noexcept { return {}; } + void return_void() {} + void unhandled_exception() {} + }; + auto operator co_await() const { return SimpleAwaitable{}; } + +}; + +CoroTask myCoroutine2() { + co_return; +} +// CHECK: call +CoroTask myCoroutine() { + co_await myCoroutine2(); +} \ No newline at end of file diff --git a/test/codegen/coyield/tests/void_suspend.cpp.yml b/test/codegen/coyield/tests/void_suspend.cpp.yml new file mode 100644 index 00000000..e69de29b diff --git a/verifying/targets/counique_args.yml b/verifying/targets/counique_args.yml index 6b3b7cc7..c192608f 100644 --- a/verifying/targets/counique_args.yml +++ b/verifying/targets/counique_args.yml @@ -1,4 +1,4 @@ - Name: cofun - Coroutine: CoWork(int) - OnlyFun: True + Function: CoWork(int) + Type: Coro \ No newline at end of file diff --git a/verifying/targets/unique_args.yml b/verifying/targets/unique_args.yml index 34b61fdf..a6dee20c 100644 --- a/verifying/targets/unique_args.yml +++ b/verifying/targets/unique_args.yml @@ -1,4 +1,4 @@ - Name: work - Coroutine: DoWork(int) - OnlyFun: True + Function: DoWork(int) + Type: Generic \ No newline at end of file From b0c3117928fb6e9b5adcc8cdfbf3552319fd7d3a Mon Sep 17 00:00:00 2001 From: Nikolay Cherkashin Date: Tue, 6 May 2025 22:41:57 +0000 Subject: [PATCH 03/12] sm fix --- codegen/coyieldpass.cpp | 3 +-- verifying/targets/counique_args.cpp | 5 +++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/codegen/coyieldpass.cpp b/codegen/coyieldpass.cpp index f6e5795b..aded149c 100644 --- a/codegen/coyieldpass.cpp +++ b/codegen/coyieldpass.cpp @@ -21,7 +21,6 @@ #include #include -#include #include #include #include @@ -365,7 +364,7 @@ struct CoYieldInserter { case Intrinsic::coro_await_suspend_void: { builder.SetInsertPoint(call_base); InsertYieldCall(filt_entry, builder, true); - InsertAtEnd(builder, call_base++, filt_entry); + // InsertAtEnd(builder, call_base++, filt_entry); return; } case Intrinsic::coro_await_suspend_handle: { diff --git a/verifying/targets/counique_args.cpp b/verifying/targets/counique_args.cpp index 52c9f4c6..6a35b6bd 100644 --- a/verifying/targets/counique_args.cpp +++ b/verifying/targets/counique_args.cpp @@ -12,12 +12,12 @@ struct Promise; // NOLINTBEGIN(readability-identifier-naming) struct SimpleAwaitable { bool await_ready() const noexcept { - return false; // Always suspend + return false; } bool await_suspend(std::coroutine_handle<> h) const noexcept { h.resume(); - return false; + return true; } void await_resume() const noexcept { @@ -49,6 +49,7 @@ Coroutine CoWork(int i) { Coroutine CoFun(int i) { co_await CoWork(i); } + struct CoUniqueArgsTest { CoUniqueArgsTest() {} ValueWrapper Get(size_t i) { From 136d402399c922a94459e497878f9e55373340a6 Mon Sep 17 00:00:00 2001 From: Nikolay Cherkashin Date: Sat, 10 May 2025 23:51:54 +0000 Subject: [PATCH 04/12] [no ci] dump code --- codegen/coyieldpass.cpp | 400 ++++++++++++------ runtime/include/lib.h | 20 +- runtime/include/pretty_print.h | 176 +++++--- runtime/include/scheduler.h | 207 +++++++-- runtime/include/stable_vector.h | 9 +- runtime/include/verifying.h | 2 +- runtime/lib.cpp | 17 +- test/CMakeLists.txt | 1 + .../coyield/tests/simple_insert.cpp.yml | 18 +- verifying/targets/CMakeLists.txt | 1 + verifying/targets/counique_args.yml | 7 +- verifying/targets/dynthreads_unique_args.cpp | 98 +++++ verifying/targets/dynthreads_unique_args.yml | 11 + verifying/targets/unique_args.cpp | 8 +- verifying/targets/unique_args.yml | 8 +- 15 files changed, 720 insertions(+), 263 deletions(-) create mode 100644 verifying/targets/dynthreads_unique_args.cpp create mode 100644 verifying/targets/dynthreads_unique_args.yml diff --git a/codegen/coyieldpass.cpp b/codegen/coyieldpass.cpp index aded149c..8c9e45e7 100644 --- a/codegen/coyieldpass.cpp +++ b/codegen/coyieldpass.cpp @@ -22,6 +22,8 @@ #include #include +#include +#include #include #include #include @@ -40,6 +42,7 @@ using Builder = IRBuilder<>; constexpr std::string_view costatus_change = "CoroutineStatusChange"; constexpr std::string_view create_thread = "CreateNewVirtualThread"; +constexpr std::string_view wait_thread = "WaitForThread"; constexpr std::string_view co_await_ready = "await_ready"; constexpr int resumed_coro = 0; @@ -51,13 +54,10 @@ static cl::opt input_list( constexpr bool dump_before = false; constexpr bool dump_after = false; -enum HandleType { GENERIC_FUN, CORO_FUN, SPAWN_VIRT_THREAD }; - -struct CoroutineFilter { - CoroutineFilter() = default; - HandleType type; - std::string print_name; +enum HandleType { GENERIC_FUN, CORO_FUN, SPAWN_VIRT_THREAD, WAIT_VIRT_THREAD }; +struct InsertPlace { + InsertPlace() = default; std::optional co_name; std::optional parent_name; // i believe that adding column is overkill @@ -65,22 +65,75 @@ struct CoroutineFilter { std::optional debug_file; }; +// LLVM is built without rtti by default, so no vtables +struct InsertAction { + InsertAction(HandleType type) : type(type) {} + InsertPlace place; + std::string print_name; + HandleType type; + virtual ~InsertAction() {} +}; +struct InsertCoro : InsertAction { + InsertCoro() : InsertAction(HandleType::CORO_FUN) {} +}; + +struct InsertGeneric : InsertAction { + InsertGeneric() : InsertAction(HandleType::GENERIC_FUN) {} +}; + +struct InsertSpawn : InsertAction { + int creation_id; + InsertSpawn(int creation_id) + : InsertAction(HandleType::SPAWN_VIRT_THREAD), creation_id(creation_id) {} +}; + +struct InsertWaitThread : InsertAction { + std::vector wait_for_ids; + InsertWaitThread(const std::vector &wait) + : InsertAction(HandleType::WAIT_VIRT_THREAD), wait_for_ids(wait) {} +}; + +using InsertActionPtr = std::shared_ptr; + namespace llvm { namespace yaml { +static std::map> + construct_action{ + {"Generic", [](IO &io) { return std::make_shared(); }}, + {"Coro", [](IO &io) { return std::make_shared(); }}, + {"Spawn", + [](IO &io) { + int creation_id; + io.mapRequired("CreationId", creation_id); + return std::make_shared(creation_id); + }}, + {"Wait", [](IO &io) { + std::vector vect; + io.mapRequired("WaitsFor", vect); + assert(vect.size() > 0); + return std::make_shared(vect); + }}}; template <> -struct ScalarEnumerationTraits { - static void enumeration(IO &io, HandleType &value) { // NOLINT - io.enumCase(value, "Generic", HandleType::GENERIC_FUN); - io.enumCase(value, "Coro", HandleType::CORO_FUN); - io.enumCase(value, "VirtThread", HandleType::SPAWN_VIRT_THREAD); +struct MappingTraits { + static void mapping(IO &io, + InsertActionPtr &action) { // NOLINT + std::string type; + // theoreticaly it should work with tags, but it don't work( + io.mapRequired("Action", type); + auto entry = construct_action.find(type); + if (entry == construct_action.end()) { + io.setError("Got unexpected action - " + type); + return; + } + action = (entry->second)(io); + io.mapRequired("Name", action->print_name); + io.mapRequired("Place", action->place); } }; -template <> -struct MappingTraits { - static void mapping(IO &io, CoroutineFilter &cofilter) { // NOLINT - io.mapRequired("Type", cofilter.type); - io.mapRequired("Name", cofilter.print_name); +template <> +struct MappingTraits { + static void mapping(IO &io, InsertPlace &cofilter) { // NOLINT io.mapOptional("Function", cofilter.co_name); io.mapOptional("Parent", cofilter.parent_name); @@ -91,25 +144,30 @@ struct MappingTraits { } // namespace yaml } // namespace llvm -LLVM_YAML_IS_SEQUENCE_VECTOR(CoroutineFilter); +LLVM_YAML_IS_SEQUENCE_VECTOR(InsertActionPtr); struct CoYieldInserter { Builder builder; - CoYieldInserter(Module &m, std::vector &&co_filter) + CoYieldInserter(Module &m, std::vector &&co_filter) : m(m), co_filter(std::move(co_filter)), builder(m.getContext()) { auto &context = m.getContext(); - coroYieldF = m.getOrInsertFunction( + coro_yield_f = m.getOrInsertFunction( costatus_change, FunctionType::get(Type::getVoidTy(context), {PointerType::get(Type::getInt8Ty(context), 0), Type::getInt1Ty(context)}, {})); - createThreadF = m.getOrInsertFunction( + create_thread_f = m.getOrInsertFunction( create_thread, FunctionType::get(Type::getVoidTy(context), {PointerType::get(Type::getInt8Ty(context), 0), PointerType::get(context, 0)}, {})); + wait_thread_f = m.getOrInsertFunction( + wait_thread, + FunctionType::get( + Type::getVoidTy(context), + {PointerType::get(context, 0), Type::getInt32Ty(context)}, {})); } void Run() { @@ -124,11 +182,13 @@ struct CoYieldInserter { std::string demangled = demangle(f.getName()); auto filt = co_filter | std::ranges::views::filter( - [&demangled](const CoroutineFilter &a) -> bool { - return !a.parent_name || a.parent_name == demangled; + [&demangled](const InsertActionPtr &a) -> bool { + const auto &place = a->place; + return !place.parent_name || + place.parent_name == demangled; }); if (!filt.empty()) { - InsertYields(filt, f); + InsertContextSwitchFunctions(filt, f); } } for (auto inst : to_delete) { @@ -141,7 +201,7 @@ struct CoYieldInserter { } private: - void InsertYields(auto filt, Function &f) { + void InsertContextSwitchFunctions(auto filt, Function &f) { Builder builder(&*f.begin()); for (auto &b : f) { for (auto &i : b) { @@ -162,108 +222,169 @@ struct CoYieldInserter { InvokeInst *invoke = dyn_cast(call); if (call_inst || invoke) { - // filt and filt | filter have differnet types + // filt and filt | filter have different types if (auto debugLoc = call->getDebugLoc()) { auto place_filt = filt | std::ranges::views::filter( - [&co_name, &debugLoc](const CoroutineFilter &a) -> bool { - if (a.debug_file.has_value() && - a.debug_file != debugLoc->getFile()->getFilename()) { + [&co_name, &debugLoc](const InsertActionPtr &a) -> bool { + const auto &place = a->place; + if (place.debug_file.has_value() && + place.debug_file != + debugLoc->getFile()->getFilename()) { return false; } - if (a.debug_line.has_value() && - a.debug_line != debugLoc.getLine()) { + if (place.debug_line.has_value() && + place.debug_line != debugLoc.getLine()) { return false; } return true; }); - InsertYield(place_filt, call_inst, invoke, co_name); + InsertContextSwitchFun(place_filt, call_inst, invoke, co_name); } else { - InsertYield(filt, call_inst, invoke, co_name); + InsertContextSwitchFun(filt, call_inst, invoke, co_name); } } } } } - void InsertYield(auto filt, CallInst *call, InvokeInst *invoke, - std::string co_name) { + void InsertContextSwitchFun(auto filt, CallInst *call, InvokeInst *invoke, + std::string co_name) { auto await_ready_ind = co_name.find(co_await_ready); if (await_ready_ind != std::string::npos) { auto res_filt = filt | std::ranges::views::filter( - [&co_name](const CoroutineFilter &a) -> bool { - return !a.co_name || - co_name.find(*a.co_name) != std::string::npos; + [&co_name](const InsertActionPtr &a) -> bool { + const auto &place = a->place; + return !place.co_name || + co_name.find(*place.co_name) != std::string::npos; }); - if (!res_filt.empty() && res_filt.front().type == HandleType::CORO_FUN) { - errs() << "inserted coro handled by type " << co_name << "\n"; - HandleCoroCase(call, res_filt.front()); - } - } else { - auto res_filt = filt | std::ranges::views::filter( - [&co_name](const CoroutineFilter &a) -> bool { - return !a.co_name || a.co_name == co_name; - }); - if (!res_filt.empty()) { auto entry = res_filt.front(); - switch (entry.type) { + switch (entry->type) { case HandleType::CORO_FUN: { - errs() << "inserted coro handled by func name" << co_name << "\n"; - if (invoke) { - assert(FindAwaitReady(invoke->getNormalDest()->begin(), entry)); - } else { - assert(FindAwaitReady(BasicBlock::iterator(call->getNextNode()), - entry)); - } - break; + errs() << "inserted coro handled by type " << co_name << "\n"; + auto start = [this, &entry]() { InsertYieldCall(entry, true); }; + auto end = [this, &entry]() { InsertYieldCall(entry, false); }; + HandleCoroCase(call, res_filt.front(), start, end); + return; } - case HandleType::GENERIC_FUN: { - errs() << "inserted generic " << co_name << "\n"; - HandleGenericFunCase(call, invoke, entry); - break; + default: + return; + } + } + return; + } + auto res_filt = + filt | std::ranges::views::filter( + [&co_name](const InsertActionPtr &a) -> bool { + const auto &place = a->place; + return !place.co_name || place.co_name == co_name; + }); + + if (!res_filt.empty()) { + auto entry = res_filt.front(); + switch (entry->type) { + case HandleType::CORO_FUN: { + errs() << "inserted coro handled by func name " << co_name << "\n"; + auto start = [this, &entry]() { InsertYieldCall(entry, true); }; + auto end = [this, &entry]() { InsertYieldCall(entry, false); }; + if (invoke) { + assert(FindAwaitReady(invoke->getNormalDest()->begin(), entry, + start, end)); + } else { + assert(FindAwaitReady(BasicBlock::iterator(call->getNextNode()), + entry, start, end)); } - case HandleType::SPAWN_VIRT_THREAD: { - errs() << "inserted spawn of new thread " << co_name << "\n"; - CallBase *inst = call ? static_cast(call) : invoke; - Function *called_fun = inst->getCalledFunction(); - - if (!inst->arg_empty()) { - auto [wrapper_fun, storage] = InsertZeroArgsWrapper(inst); - builder.SetInsertPoint(inst); - for (size_t i = 0; i < called_fun->arg_size(); i++) { - Value *arg = inst->getArgOperand(i); - - Value *storage_place = - builder.CreateGEP(storage->getValueType(), storage, - { - builder.getInt32(0), - builder.getInt32(i), - }); - builder.CreateStore(arg, storage_place); - } - called_fun = wrapper_fun; - } - Value *pointer_to_func = builder.CreatePointerCast( - called_fun, PointerType::get(builder.getContext(), 0)); + break; + } + case HandleType::GENERIC_FUN: { + errs() << "inserted generic " << co_name << "\n"; + HandleGenericFunCase(call, invoke, entry); + break; + } + case HandleType::SPAWN_VIRT_THREAD: { + errs() << "inserted spawn of new thread " << co_name << "\n"; + CallBase *inst = call ? static_cast(call) : invoke; + Function *called_fun = inst->getCalledFunction(); + + if (!inst->arg_empty()) { + auto [wrapper_fun, storage] = InsertZeroArgsWrapper(inst); builder.SetInsertPoint(inst); - Value *replacement = builder.CreateCall( - createThreadF, {GetLiteral(entry.print_name), pointer_to_func}); - inst->replaceAllUsesWith(replacement); - // we cannot simple delete here instruction because we are - // iterating over it in basic block - to_delete.push_back(inst); - break; + for (size_t i = 0; i < called_fun->arg_size(); i++) { + Value *arg = inst->getArgOperand(i); + + Value *storage_place = + builder.CreateGEP(storage->getValueType(), storage, + { + builder.getInt32(0), + builder.getInt32(i), + }); + builder.CreateStore(arg, storage_place); + } + called_fun = wrapper_fun; } - default: - __builtin_unreachable(); + Value *pointer_to_func = builder.CreatePointerCast( + called_fun, PointerType::get(builder.getContext(), 0)); + std::shared_ptr spawn = + std::static_pointer_cast(entry); + Value *id = builder.getInt32(spawn->creation_id); + builder.SetInsertPoint(inst); + Value *replacement; + if (call) { + replacement = builder.CreateCall( + create_thread_f, + {id, GetLiteral(entry->print_name), pointer_to_func}); + } else { + replacement = builder.CreateInvoke( + create_thread_f, invoke->getNormalDest(), + invoke->getUnwindDest(), + {id, GetLiteral(entry->print_name), pointer_to_func}); + } + inst->replaceAllUsesWith(replacement); + // we cannot simple delete here instruction because we are + // iterating over it in basic block + to_delete.push_back(inst); + break; } + case HandleType::WAIT_VIRT_THREAD: { + errs() << "inserted wait of new thread" << co_name << "\n"; + // I'm sure that here must be only coro case + auto start = std::bind_front( + &CoYieldInserter::InsertWaitFunc, this, + std::static_pointer_cast(entry)); + if (invoke) { + assert(FindAwaitReady(invoke->getNormalDest()->begin(), entry, + start, {})); + } else { + assert(FindAwaitReady(BasicBlock::iterator(call->getNextNode()), + entry, start, {})); + } + break; + } + default: + __builtin_unreachable(); } } } + void InsertWaitFunc(const std::shared_ptr &act) { + // because in c++ this function will have pointer as argument, we need also + // explicitly pass the size + std::vector elements; + for (auto &a : act->wait_for_ids) { + elements.push_back(builder.getInt32(a)); + } + Constant *indices = ConstantArray::get( + ArrayType::get(builder.getInt32Ty(), act->wait_for_ids.size()), + elements); + GlobalVariable *val_ind = new GlobalVariable(m, + indices->getType(), true, llvm::GlobalValue::InternalLinkage, indices); + builder.CreateCall(wait_thread_f, + {val_ind, builder.getInt32(elements.size())}); + } + // We need pass to scheduler function and wan't to care about number // of args and their type to not interact with templates - so lets create a // wrapper which would have zero args @@ -294,8 +415,9 @@ struct CoYieldInserter { return {func, storage}; } - bool FindAwaitReady(BasicBlock::iterator start, - const CoroutineFilter &entry) { + bool FindAwaitReady(BasicBlock::iterator start, InsertActionPtr &entry, + const std::function &start_insert, + const std::function &end_insert) { for (Instruction &n_inst : make_range(start, start->getParent()->end())) { auto *call_inst = dyn_cast(&n_inst); if (!call_inst) { @@ -304,12 +426,13 @@ struct CoYieldInserter { auto await_ready_ind = demangle(call_inst->getCalledFunction()->getName()) .find(co_await_ready); if (await_ready_ind != std::string::npos) { - HandleCoroCase(call_inst, entry); + HandleCoroCase(call_inst, entry, start_insert, end_insert); return true; } // If Coro Type constructor can throw we need go deeper if (auto *invoke = dyn_cast(call_inst)) { - return FindAwaitReady(invoke->getNormalDest()->begin(), entry); + return FindAwaitReady(invoke->getNormalDest()->begin(), entry, + start_insert, end_insert); } } return false; @@ -317,22 +440,24 @@ struct CoYieldInserter { // This case is needed at sample by some coro primitives where the // normal function which is the body of coro is called in loop void HandleGenericFunCase(CallBase *call, InvokeInst *invoke, - const CoroutineFilter &filt_entry) { + const InsertActionPtr &filt_entry) { builder.SetInsertPoint(call); - InsertYieldCall(filt_entry, builder, true); + InsertYieldCall(filt_entry, true); // Invoke instruction has unwind/normal ends so we need handle it if (invoke) { builder.SetInsertPoint(invoke->getNormalDest()->getFirstInsertionPt()); - InsertYieldCall(filt_entry, builder, false); + InsertYieldCall(filt_entry, false); builder.SetInsertPoint(invoke->getUnwindDest()->getFirstInsertionPt()); - InsertYieldCall(filt_entry, builder, false); + InsertYieldCall(filt_entry, false); } else { builder.SetInsertPoint(call->getNextNode()); - InsertYieldCall(filt_entry, builder, false); + InsertYieldCall(filt_entry, false); } } - void HandleCoroCase(CallBase *call, const CoroutineFilter &filt_entry) { + void HandleCoroCase(CallBase *call, const InsertActionPtr &filt_entry, + const std::function &start_insert, + const std::function &end_insert) { BranchInst *br = dyn_cast(call->getNextNode()); assert(br && br->getNumSuccessors() == 2); BasicBlock *not_ready_bb = br->getSuccessor(1); @@ -347,30 +472,41 @@ struct CoYieldInserter { // We cannot insert after await_suspend because inside it we can // already interact with handle, so we must we do it before case Intrinsic::coro_await_suspend_bool: { - builder.SetInsertPoint(call_base); - InsertYieldCall(filt_entry, builder, true); - BranchInst *suspend_br = dyn_cast(i.getNextNode()); - assert(suspend_br && suspend_br->getNumSuccessors() == 2); - builder.SetInsertPoint( - suspend_br->getSuccessor(0)->getFirstInsertionPt()); - // InsertCall(filt_entry, builder, true); - // handled if await_suspend was true, now change block also for - // false - BasicBlock *tramp = - InsertAtEnd(builder, &(*builder.GetInsertPoint()), filt_entry); - suspend_br->setSuccessor(1, tramp); + if (start_insert) { + builder.SetInsertPoint(call_base); + start_insert(); + } + if (end_insert) { + BranchInst *suspend_br = dyn_cast(i.getNextNode()); + assert(suspend_br && suspend_br->getNumSuccessors() == 2); + builder.SetInsertPoint( + suspend_br->getSuccessor(0)->getFirstInsertionPt()); + // handled if await_suspend was true, now change block also for + // false + BasicBlock *tramp = InsertAtEnd( + builder, &(*builder.GetInsertPoint()), filt_entry, end_insert); + suspend_br->setSuccessor(1, tramp); + } return; } case Intrinsic::coro_await_suspend_void: { - builder.SetInsertPoint(call_base); - InsertYieldCall(filt_entry, builder, true); - // InsertAtEnd(builder, call_base++, filt_entry); + if (start_insert) { + builder.SetInsertPoint(call_base); + start_insert(); + } + if (end_insert) { + InsertAtEnd(builder, nextNormal(call_base), filt_entry, end_insert); + } return; } case Intrinsic::coro_await_suspend_handle: { - builder.SetInsertPoint(call_base); - InsertYieldCall(filt_entry, builder, true); - InsertAtEnd(builder, nextNormal(call_base), filt_entry); + if (start_insert) { + builder.SetInsertPoint(call_base); + start_insert(); + } + if (end_insert) { + InsertAtEnd(builder, nextNormal(call_base), filt_entry, end_insert); + } return; } default: { @@ -389,7 +525,8 @@ struct CoYieldInserter { } } BasicBlock *InsertAtEnd(Builder &builder, Instruction *instr, - const CoroutineFilter &filt_entry) { + const InsertActionPtr &filt_entry, + const std::function &end_insert) { CallInst *intr = dyn_cast(instr); assert(intr && intr->getIntrinsicID() == Intrinsic::coro_suspend); SwitchInst *switch_inst = dyn_cast(intr->getNextNode()); @@ -403,15 +540,15 @@ struct CoYieldInserter { BasicBlock::Create(builder.getContext(), "", succ->getParent()); resumed_bb->setSuccessor(tramp); builder.SetInsertPoint(tramp); - InsertYieldCall(filt_entry, builder, false); + end_insert(); builder.CreateBr(succ); return tramp; } - void InsertYieldCall(const CoroutineFilter &filt, Builder &builder, - bool start) { + void InsertYieldCall(const InsertActionPtr &filt, bool start) { auto llvm_start = ConstantInt::get(Type::getInt1Ty(builder.getContext()), start); - builder.CreateCall(coroYieldF, {GetLiteral(filt.print_name), llvm_start}); + builder.CreateCall(coro_yield_f, + {GetLiteral(filt->print_name), llvm_start}); } Constant *GetLiteral(const std::string &name) { auto literal = string_literals.find(name); @@ -430,9 +567,10 @@ struct CoYieldInserter { return literal->second; } Module &m; - FunctionCallee coroYieldF; - FunctionCallee createThreadF; - std::vector co_filter; + FunctionCallee coro_yield_f; + FunctionCallee create_thread_f; + FunctionCallee wait_thread_f; + std::vector co_filter; std::map string_literals; std::vector to_delete; std::set ignored; @@ -453,7 +591,7 @@ struct CoYieldInsertPass final : public PassInfoMixin { } llvm::yaml::Input input(file.get()->getBuffer()); - std::vector filt; + std::vector filt; input >> filt; if (input.error()) { @@ -486,8 +624,8 @@ llvmGetPassPluginInfo() { }); pb.registerPipelineStartEPCallback( [](ModulePassManager &mpm, OptimizationLevel level) { - // Looks like we don't need any lowerings, before but i'm not - // sure + // Looks like we don't need any lowerings, before but i'm + // not sure // mpm.addPass(CoroEarlyPass()); mpm.addPass(CoYieldInsertPass()); }); diff --git a/runtime/include/lib.h b/runtime/include/lib.h index 1541b5fa..9c4f9164 100644 --- a/runtime/include/lib.h +++ b/runtime/include/lib.h @@ -18,7 +18,8 @@ struct CoroBase; struct CoroutineStatus; -struct VirtualThreadCreation; +struct CreatedThreadInfo; +struct WaitThreadInfo; // Current executing coroutine. extern std::shared_ptr this_coro; @@ -27,18 +28,25 @@ extern boost::context::fiber_context sched_ctx; extern std::optional coroutine_status; -extern std::optional virtual_thread_creation; +extern std::optional virtual_thread_creation; -extern std::optional virtual_thread_creation; +extern std::optional virtual_thread_wait; struct CoroutineStatus { std::string_view name; bool has_started; }; -struct VirtualThreadCreation { +struct CreatedThreadInfo { std::string_view name; std::function function; + int id; + //will be set in scheduler + size_t parent = -1; +}; + +struct WaitThreadInfo { + std::vector wait_ids; }; // Runtime token. @@ -62,7 +70,9 @@ extern "C" void CoroYield(); extern "C" void CoroutineStatusChange(char* coroutine, bool start); -extern "C" void CreateNewVirtualThread(char* name, void* func); +extern "C" void CreateNewVirtualThread(int id, char* name, void* func); + +extern "C" void WaitForThread(int* ids, int size); struct CoroBase : public std::enable_shared_from_this { CoroBase(const CoroBase&) = delete; diff --git a/runtime/include/pretty_print.h b/runtime/include/pretty_print.h index 145dc974..471fab7c 100644 --- a/runtime/include/pretty_print.h +++ b/runtime/include/pretty_print.h @@ -1,18 +1,54 @@ #pragma once +#include #include #include #include +#include #include #include #include "lib.h" #include "lincheck.h" #include "logger.h" - +#include "stable_vector.h" using std::string; using std::to_string; -using FullHistoryWithThreads = std::vector, CoroutineStatus>>>; +struct CreateNewThread { + size_t created_thread_id; + std::string_view name; +}; + +using FullHistoryWithThreads = + std::vector, + CoroutineStatus, CreateNewThread, WaitThreadInfo>>>; + +template +void DFS(const StableVector& arr, std::vector& visited, size_t i, + std::vector& ans) { + ans.push_back(i); + visited[i] = true; + for (auto& u : arr[i].children) { + if (!visited[u]) { + DFS(arr, visited, u, ans); + } + } +} + +template +void TopSort(const StableVector& arr, std::vector& ans) { + std::vector visited(arr.size(), false); + for (int i = 0; i < arr.size(); i++) { + if (!visited[i]) { + DFS(arr, visited, i, ans); + } + } +} + +template +struct Overloads : Ts... { + using Ts::operator()...; +}; + struct PrettyPrinter { PrettyPrinter(size_t threads_num); @@ -121,9 +157,14 @@ struct PrettyPrinter { // Helps to debug full histories. template - void PrettyPrint(FullHistoryWithThreads& result, Out_t& out) { - int cell_width = 20; // Up it if necessary. Enough for now. + void PrettyPrint(FullHistoryWithThreads& result, std::vector mapping, + Out_t& out) { + int cell_width = 10; // Up it if necessary. Enough for now. + std::vector inverse_mapping(mapping.size(), -1); + for (int i = 0; i < mapping.size(); i++) { + inverse_mapping[mapping[i]] = i; + } auto print_separator = [&out, this, cell_width]() { out << "*"; for (int i = 0; i < threads_num; ++i) { @@ -150,7 +191,7 @@ struct PrettyPrinter { for (int i = 0; i < threads_num; ++i) { int rest = cell_width - 1 /*T*/ - to_string(i).size(); print_spaces(rest / 2); - out << "T" << i; + out << "T" << mapping[i]; print_spaces(rest - rest / 2); out << "|"; } @@ -168,59 +209,78 @@ struct PrettyPrinter { std::vector co_depth(threads_num, 0); // Rows. for (const auto& i : result) { - int num = i.first; + int num = inverse_mapping[i.first]; FitPrinter fp{out, cell_width}; - if (i.second.index() == 0) { - auto act = std::get<0>(i.second); - auto base = act.get().get(); - if (index.find(base) == index.end()) { - int sz = index.size(); - index[base] = sz; - } - int length = std::to_string(index[base]).size(); - std::cout << index[base]; - assert(spaces - length >= 0); - print_spaces(7 - length); - out << "|"; - for (int j = 0; j < num; ++j) { - print_empty_cell(); - } - fp.Out(" "); - fp.Out(std::string{act.get()->GetName()}); - fp.Out("("); - const auto& args = act.get()->GetStrArgs(); - for (int i = 0; i < args.size(); ++i) { - if (i > 0) { - fp.Out(", "); - } - fp.Out(args[i]); - } - fp.Out(")"); - } else if (i.second.index() == 1) { - print_spaces(7); - out << "|"; - for (int j = 0; j < num; ++j) { - print_empty_cell(); - } - auto cor = std::get<1>(i.second); - auto print_formated_spaces = [&fp](int count) { - for (int i = 0; i < count; ++i) { - fp.Out(" "); - } - }; - if (cor.has_started) { - print_formated_spaces(co_depth[num] + 1); - fp.Out(">"); - co_depth[num]++; - } else { - print_formated_spaces(co_depth[num]); - fp.Out("<"); - co_depth[num]--; - } - fp.Out(cor.name); - // std::cerr << cor.name << "\n"; - assert(fp.rest > 0 && "increase cell_width in pretty printer"); - } + auto visitor = + Overloads{[&](std::reference_wrapper act) { + auto base = act.get().get(); + if (index.find(base) == index.end()) { + int sz = index.size(); + index[base] = sz; + } + int length = std::to_string(index[base]).size(); + out << index[base]; + assert(spaces - length >= 0); + print_spaces(7 - length); + out << "|"; + for (int j = 0; j < num; ++j) { + print_empty_cell(); + } + fp.Out(" "); + fp.Out(std::string{act.get()->GetName()}); + fp.Out("("); + const auto& args = act.get()->GetStrArgs(); + for (int i = 0; i < args.size(); ++i) { + if (i > 0) { + fp.Out(", "); + } + fp.Out(args[i]); + } + fp.Out(")"); + }, + [&](const CoroutineStatus& cor) { + print_spaces(7); + out << "|"; + for (int j = 0; j < num; ++j) { + print_empty_cell(); + } + auto print_formated_spaces = [&fp](int count) { + for (int i = 0; i < count; ++i) { + fp.Out(" "); + } + }; + if (cor.has_started) { + print_formated_spaces(co_depth[num] + 1); + fp.Out(">"); + co_depth[num]++; + } else { + print_formated_spaces(co_depth[num]); + fp.Out("<"); + co_depth[num]--; + } + fp.Out(cor.name); + }, + [&](const CreateNewThread& new_thread) { + print_spaces(7); + out << "|"; + for (int j = 0; j < num; ++j) { + print_empty_cell(); + } + fp.Out(std::string(" ->T") + + std::to_string(new_thread.created_thread_id)); + }, + [&](const WaitThreadInfo& wait_thread){ + print_spaces(7); + out << "|"; + for (int j = 0; j < num; ++j) { + print_empty_cell(); + } + fp.Out(std::string(" <-")); + } + + }; + std::visit(visitor, i.second); + assert(fp.rest > 0 && "increase cell_width in pretty printer"); print_spaces(fp.rest); out << "|"; diff --git a/runtime/include/scheduler.h b/runtime/include/scheduler.h index a6c22e2e..e52c67ac 100644 --- a/runtime/include/scheduler.h +++ b/runtime/include/scheduler.h @@ -1,13 +1,17 @@ #pragma once #include #include +#include #include #include #include +#include #include #include #include +#include #include +#include #include "lib.h" #include "lincheck.h" @@ -17,6 +21,7 @@ #include "pretty_print.h" #include "scheduler_fwd.h" #include "stable_vector.h" +#include "value_wrapper.h" /// Generated by some strategy task, /// that may be not executed due to constraints of data structure @@ -485,23 +490,23 @@ struct StrategyScheduler : public SchedulerWithReplay { // TLAScheduler generates all executions satisfying some conditions. template struct TLAScheduler : Scheduler { - TLAScheduler(size_t max_tasks, size_t max_rounds, size_t threads_count, - size_t max_switches, size_t max_depth, - std::vector constructors, ModelChecker& checker, - PrettyPrinter& pretty_printer, std::function cancel_func) + TLAScheduler(size_t max_tasks, size_t max_rounds, + size_t initial_threads_count, size_t max_switches, + size_t max_depth, std::vector constructors, + ModelChecker& checker, std::function cancel_func) : max_tasks{max_tasks}, max_rounds{max_rounds}, max_switches{max_switches}, + initial_threads_count(initial_threads_count), constructors{std::move(constructors)}, checker{checker}, - pretty_printer{pretty_printer}, max_depth(max_depth), cancel(cancel_func) { - for (size_t i = 0; i < threads_count; ++i) { - threads.emplace_back(Thread{ - .id = i, - .tasks = StableVector{}, - }); + for (size_t i = 0; i < initial_threads_count; ++i) { + threads.emplace_back(Thread{.id = i, + .tasks = StableVector{}, + .children = {}, + .created_meta = {}}); } }; @@ -516,8 +521,23 @@ struct TLAScheduler : Scheduler { struct Thread { size_t id; StableVector tasks; + std::vector children; + std::optional created_meta; + std::optional wait_cond; + }; + struct WaitId { + // this is the id from config + int base_id; + // this is the id of thread from where the task called + size_t thread; + // TODO add this ptr + bool operator==(const WaitId& oth) const = default; + }; + struct WaitHasher { + std::size_t operator()(const WaitId& id) const { + return std::hash()(id.base_id ^ (id.thread << 32)); + } }; - // TLAScheduler enumerates all possible executions with finished max_tasks. // In fact, it enumerates tables (c = continue, f = finished): // *---------*---------*--------* @@ -537,20 +557,30 @@ struct TLAScheduler : Scheduler { bool is_new{}; }; + // In structured concurrency so kind of termination will be safe for children + // threads + void TerminateThread(Thread& thr) { + for (size_t i = 0; i < thr.children.size(); i++) { + TerminateThread(threads[thr.children[i]]); + } + thr.children.clear(); + for (size_t j = 0; j < thr.tasks.size(); ++j) { + auto& task = thr.tasks[j]; + if (!task->IsReturned()) { + task->Terminate(); + } + } + } // Terminates all running tasks. // We do it in a dangerous way: in random order. // Actually, we assume obstruction free here. // cancel() func takes care for graceful shutdown void TerminateTasks() { cancel(); - for (size_t i = 0; i < threads.size(); ++i) { - for (size_t j = 0; j < threads[i].tasks.size(); ++j) { - auto& task = threads[i].tasks[j]; - if (!task->IsReturned()) { - task->Terminate(); - } - } + for (size_t i = 0; i < initial_threads_count; ++i) { + TerminateThread(threads[i]); } + started_thread_groups.clear(); } // Replays all actions from 0 to the step_end. @@ -576,27 +606,56 @@ struct TLAScheduler : Scheduler { } void UpdateFullHistory(size_t thread_id, Task& task, bool is_new) { + if (virtual_thread_creation.has_value()) { + full_history.emplace_back(thread_id, task); + virtual_thread_creation->parent = thread_id; + threads.emplace_back(Thread{.id = threads.size(), + .tasks = {}, + .children = {}, + .created_meta = *virtual_thread_creation}); + threads[thread_id].children.push_back(threads.size() - 1); + // TODO: looks like we need a seperate function fo verifier + // verifier.UpdateState(coroutine_status->name, thread_id, + // true); + full_history.emplace_back( + thread_id, + CreateNewThread{threads.size() - 1, virtual_thread_creation->name}); + started_thread_groups + .emplace(WaitId{.base_id = virtual_thread_creation->id, + .thread = thread_id}, + 0) + .first->second++; + virtual_thread_creation.reset(); + return; + } + if (virtual_thread_wait.has_value()) { + assert(!threads[thread_id].wait_cond); + threads[thread_id].wait_cond = virtual_thread_wait; + virtual_thread_wait.reset(); + return; + } if (coroutine_status.has_value()) { if (is_new) { assert(coroutine_status->has_started); full_history.emplace_back(thread_id, task); } - //To prevent cases like this - // +--------+--------+ - // | T1 | T2 | - // +--------+--------+ - // | | Recv | - // | Send | | - // | | >read | - // | >flush | | - // +--------+--------+ - verifier.UpdateState(coroutine_status->name, thread_id, coroutine_status->has_started); + // To prevent cases like this + // +--------+--------+ + // | T1 | T2 | + // +--------+--------+ + // | | Recv | + // | Send | | + // | | >read | + // | >flush | | + // +--------+--------+ + verifier.UpdateState(coroutine_status->name, thread_id, + coroutine_status->has_started); full_history.emplace_back(thread_id, coroutine_status.value()); coroutine_status.reset(); - } else { - verifier.UpdateState(task->GetName(), thread_id, is_new); - full_history.emplace_back(thread_id, task); + return; } + verifier.UpdateState(task->GetName(), thread_id, is_new); + full_history.emplace_back(thread_id, task); } // Resumes choosed task. // If task is finished and finished tasks == max_tasks, stops. @@ -626,14 +685,27 @@ struct TLAScheduler : Scheduler { } assert(!task->IsParked()); + + std::vector mapping; + TopSort(threads, mapping); + PrettyPrinter pretty_printer{threads.size()}; + pretty_printer.PrettyPrint(full_history, mapping, log()); + log() << "===============================================\n\n"; + task->Resume(); UpdateFullHistory(thread_id, task, is_new); bool is_finished = task->IsReturned(); if (is_finished) { - finished_tasks++; verifier.OnFinished(TaskWithMetaData{task, false, thread.id}); - auto result = task->GetRetVal(); - sequential_history.emplace_back(Response(task, result, thread_id)); + + if (thread.created_meta) { + started_thread_groups[{.base_id = thread.created_meta->id, + .thread = thread.created_meta->parent}]--; + } else { + finished_tasks++; + auto result = task->GetRetVal(); + sequential_history.emplace_back(Response(task, result, thread_id)); + } } bool stop = finished_tasks == max_tasks; @@ -645,17 +717,24 @@ struct TLAScheduler : Scheduler { } } else { log() << "run round: " << finished_rounds << "\n"; - pretty_printer.PrettyPrint(full_history, log()); + // std::vector mapping(threads.size()); + // std::iota(mapping.begin(), mapping.end(), 0); + std::vector mapping; + TopSort(threads, mapping); + PrettyPrinter pretty_printer{threads.size()}; + pretty_printer.PrettyPrint(full_history, mapping, log()); log() << "===============================================\n\n"; log().flush(); // Stop, check if the the generated history is linearizable. ++finished_rounds; - if (!checker.Check(sequential_history)) { - return {false, - std::make_pair(Scheduler::FullHistory{}, sequential_history)}; - } + // Disabled temporaraly - need discuus how to handle it better + // if (!checker.Check(sequential_history)) { + // return {false, + // std::make_pair(Scheduler::FullHistory{}, + // sequential_history)}; + // } if (finished_rounds == max_rounds) { - // It was the last round. + // It was the last round.q return {true, {}}; } } @@ -676,6 +755,11 @@ struct TLAScheduler : Scheduler { full_history.pop_back(); } } + if (full_history.back().second.index() == 3) { + // auto& wt = std::get<3>(full_history.back().second); + // full_history.pop_back(); + + } full_history.pop_back(); if (is_finished) { --finished_tasks; @@ -700,8 +784,22 @@ struct TLAScheduler : Scheduler { bool all_parked = true; // Pick next task. for (size_t i = 0; i < threads.size(); ++i) { - auto& thread = threads[i]; + Thread& thread = threads[i]; auto& tasks = thread.tasks; + if (thread.wait_cond) { + if (std::any_of( + thread.wait_cond->wait_ids.begin(), + thread.wait_cond->wait_ids.end(), [this, &i](auto& child) { + auto it = started_thread_groups.find( + {.base_id = child, .thread = i}); + return it == started_thread_groups.end() || it->second != 0; + })) { + continue; + } + // full_history.emplace_back( + // WaitThreadInfo{.wait_ids = thread.wait_cond.value().wait_ids}); + thread.wait_cond.reset(); + } if (!tasks.empty() && !tasks.back()->IsReturned()) { if (tasks.back()->IsParked()) { continue; @@ -724,6 +822,30 @@ struct TLAScheduler : Scheduler { } all_parked = false; + std::optional& created = thread.created_meta; + if (created && thread.tasks.empty()) { + tasks.emplace_back(Coro::New( + [&created](void*) -> ValueWrapper { + created->function(); + return void_v; + }, + &state, std::shared_ptr(), + [](std::shared_ptr) -> std::vector { + return {}; + }, + created->name, -1)); + frame.is_new = true; + auto [is_over, res] = ResumeTask(frame, step, switches, thread, true); + if (is_over || res.has_value()) { + return {is_over, res}; + } + tasks.pop_back(); + auto size_after = thread.tasks.size(); + // As we can't return to the past in coroutine, we need to replay all + // tasks from the beginning. + Replay(step); + continue; + } // Choose constructor to create task. bool stop = started_tasks == max_tasks; if (!stop && threads[i].tasks.size() < max_depth) { @@ -754,9 +876,9 @@ struct TLAScheduler : Scheduler { return {false, {}}; } - PrettyPrinter& pretty_printer; size_t max_tasks; size_t max_rounds; + size_t initial_threads_count; size_t max_switches; size_t max_depth; @@ -775,4 +897,5 @@ struct TLAScheduler : Scheduler { StableVector frames; Verifier verifier; std::function cancel; + std::unordered_map started_thread_groups; }; diff --git a/runtime/include/stable_vector.h b/runtime/include/stable_vector.h index 6067fd8f..d21fb0f8 100644 --- a/runtime/include/stable_vector.h +++ b/runtime/include/stable_vector.h @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include #include @@ -49,7 +50,7 @@ struct StableVector { template requires std::constructible_from T &emplace_back(Args &&...args) { - const size_t index = 63 - __builtin_clzll(total_size + 1); + const size_t index = 63 - std::countl_zero(total_size + 1); if (((total_size + 1) & total_size) == 0 && !entities[index]) { entities[index] = ::new (static_cast(alignof(T))) type_t[1ULL << index]; @@ -62,7 +63,7 @@ struct StableVector { } void pop_back() noexcept { - const size_t index = 63 - __builtin_clzll(total_size); + const size_t index = 63 - std::countl_zero(total_size); if (((total_size - 1) & total_size) == 0 && entities[index + 1]) { ::operator delete[](std::exchange(entities[index + 1], nullptr), static_cast(alignof(T))); @@ -73,14 +74,14 @@ struct StableVector { } T &operator[](size_t i) noexcept { - const size_t index = 63 - __builtin_clzll(++i); + const size_t index = 63 - std::countl_zero(++i); const size_t internal_index = i ^ (1ULL << index); return *std::launder( reinterpret_cast(entities[index][internal_index])); } const T &operator[](size_t i) const noexcept { - const size_t index = 63 - __builtin_clzll(++i); + const size_t index = 63 - std::countl_zero(++i); const size_t internal_index = i ^ (1ULL << index); return *std::launder( reinterpret_cast(entities[index][internal_index])); diff --git a/runtime/include/verifying.h b/runtime/include/verifying.h index c4391dc4..72f26ee8 100644 --- a/runtime/include/verifying.h +++ b/runtime/include/verifying.h @@ -143,7 +143,7 @@ std::unique_ptr MakeScheduler(ModelChecker &checker, Opts &opts, std::cout << "tla\n"; auto scheduler = std::make_unique>( opts.tasks, opts.rounds, opts.threads, opts.switches, opts.depth, - std::move(l), checker, pretty_printer, cancel); + std::move(l), checker, cancel); return scheduler; } default: { diff --git a/runtime/lib.cpp b/runtime/lib.cpp index 7c5a9bb8..0cb8be77 100644 --- a/runtime/lib.cpp +++ b/runtime/lib.cpp @@ -1,5 +1,6 @@ #include "include/lib.h" +#include #include #include #include @@ -13,7 +14,10 @@ Task this_coro{}; boost::context::fiber_context sched_ctx; std::optional coroutine_status; -std::optional virtual_thread_creation; +std::optional virtual_thread_creation; + +std::optional virtual_thread_wait; + std::unordered_map futex_state{}; @@ -70,8 +74,15 @@ extern "C" void CoroutineStatusChange(char* name, bool start) { CoroYield(); } -extern "C" void CreateNewVirtualThread(char* name, void* func) { - virtual_thread_creation.emplace(name, reinterpret_cast(func)); +extern "C" void CreateNewVirtualThread(int id, char* name, void* func) { + virtual_thread_creation.emplace(name, reinterpret_cast(func), id); + CoroYield(); +} + +extern "C" void WaitForThread(int* ids, int size) { + std::vector vids(size); + std::copy(ids, ids + size, vids.begin()); + virtual_thread_wait.emplace(vids); CoroYield(); } diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 4c5f637e..65941679 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -1 +1,2 @@ add_subdirectory(runtime) +add_subdirectory(codegen) \ No newline at end of file diff --git a/test/codegen/coyield/tests/simple_insert.cpp.yml b/test/codegen/coyield/tests/simple_insert.cpp.yml index e77b130d..17e146e0 100644 --- a/test/codegen/coyield/tests/simple_insert.cpp.yml +++ b/test/codegen/coyield/tests/simple_insert.cpp.yml @@ -1,8 +1,10 @@ -- Name: test - Parent: f() - Function: g() - Type: Generic -- Name: pos_test - Parent: based_on_pos() - Function: g() - Type: Generic +- Action: Generic + Name: test + Place: + Parent: f() + Function: g() +- Action: Generic + Name: pos_test + Place: + Parent: based_on_pos() + Function: g() diff --git a/verifying/targets/CMakeLists.txt b/verifying/targets/CMakeLists.txt index feacb20d..29dc18bd 100644 --- a/verifying/targets/CMakeLists.txt +++ b/verifying/targets/CMakeLists.txt @@ -16,6 +16,7 @@ set (SOURCE_TARGET_WITHOUT_PLUGIN_LIST set (SOURCE_TARGET_CO_LIST unique_args.cpp counique_args.cpp + dynthreads_unique_args.cpp ) foreach(source_name ${SOURCE_TARGET_LIST}) diff --git a/verifying/targets/counique_args.yml b/verifying/targets/counique_args.yml index c192608f..75af9bec 100644 --- a/verifying/targets/counique_args.yml +++ b/verifying/targets/counique_args.yml @@ -1,4 +1,5 @@ -- Name: cofun - Function: CoWork(int) - Type: Coro +- Action: Coro + Name: cofun + Place: + Function: CoWork(int) \ No newline at end of file diff --git a/verifying/targets/dynthreads_unique_args.cpp b/verifying/targets/dynthreads_unique_args.cpp new file mode 100644 index 00000000..44ae8078 --- /dev/null +++ b/verifying/targets/dynthreads_unique_args.cpp @@ -0,0 +1,98 @@ +#include +#include +#include +#include +#include + +#include "../specs/unique_args.h" + +static std::vector used(limit, false); +static std::vector state(limit, 0); +struct Promise; +// NOLINTBEGIN(readability-identifier-naming) +struct SimpleAwaitable { + bool await_ready() const noexcept { return false; } + + bool await_suspend(std::coroutine_handle<> h) const noexcept { + h.resume(); + return true; + } + + void await_resume() const noexcept {} +}; + +struct Coroutine : std::coroutine_handle { + using promise_type = ::Promise; + auto operator co_await() const { return SimpleAwaitable{}; } +}; + +struct Promise { + Coroutine get_return_object() { return {Coroutine::from_promise(*this)}; } + std::suspend_always initial_suspend() noexcept { return {}; } + std::suspend_never final_suspend() noexcept { return {}; } + void return_void() {} + void unhandled_exception() {} +}; +// NOLINTEND(readability-identifier-naming) + +struct Waiter { + void Add(const Coroutine& coro) { + list.push_back(&coro); + } + SimpleAwaitable Wait() { + for (auto& a : list) { + a->resume(); + } + return {}; } + std::vector list; +}; + +Coroutine DoWork(int i) { + state[i]++; + return {}; +} +Coroutine Work(int i) { + Waiter w; + w.Add(DoWork(i)); + state[i]++; + co_await w.Wait(); +} + +struct DynThreadsTest { + DynThreadsTest() {} + ValueWrapper Get(size_t i) { + assert(!used[i]); + used[i] = true; + auto coro= Work(i); + coro.resume(); + auto l = [this]() { + Reset(); + return limit; + }; + return {std::count(state.begin(), state.end(), 2) == limit + ? l() + : std::optional(), + GetDefaultCompator>(), Print}; + } + void Reset() { + std::fill(used.begin(), used.end(), false); + std::fill(state.begin(), state.end(), false); + } +}; + +auto GenerateArgs(size_t thread_num) { + for (size_t i = 0; i < limit; i++) { + if (!used[i]) { + return ltest::generators::makeSingleArg(i); + } + } + assert(false && "extra call"); +} + +target_method(GenerateArgs, int, DynThreadsTest, Get, size_t); + +using SpecT = + ltest::Spec; + +LTEST_ENTRYPOINT(SpecT); diff --git a/verifying/targets/dynthreads_unique_args.yml b/verifying/targets/dynthreads_unique_args.yml new file mode 100644 index 00000000..3b43cbe4 --- /dev/null +++ b/verifying/targets/dynthreads_unique_args.yml @@ -0,0 +1,11 @@ +- Action: Spawn + CreationId: 0 + Name: go + Place: + Function: Waiter::Add(Coroutine const&) +- Action: Wait + WaitsFor: [0] + Name: wait + Place: + Function: Waiter::Wait() + diff --git a/verifying/targets/unique_args.cpp b/verifying/targets/unique_args.cpp index 910ceb81..b9c40f76 100644 --- a/verifying/targets/unique_args.cpp +++ b/verifying/targets/unique_args.cpp @@ -11,8 +11,8 @@ static std::vector done(limit, false); void DoWork(int i){ done[i] = true; } -struct CoUniqueArgsTest { - CoUniqueArgsTest() {} +struct DynThreadsTest { + DynThreadsTest() {} ValueWrapper Get(size_t i) { assert(!used[i]); used[i] = true; @@ -41,10 +41,10 @@ auto GenerateArgs(size_t thread_num) { assert(false && "extra call"); } -target_method(GenerateArgs, int, CoUniqueArgsTest, Get, size_t); +target_method(GenerateArgs, int, DynThreadsTest, Get, size_t); using SpecT = - ltest::Spec; LTEST_ENTRYPOINT(SpecT); diff --git a/verifying/targets/unique_args.yml b/verifying/targets/unique_args.yml index a6dee20c..39f78f6b 100644 --- a/verifying/targets/unique_args.yml +++ b/verifying/targets/unique_args.yml @@ -1,4 +1,4 @@ -- Name: work - Function: DoWork(int) - Type: Generic - \ No newline at end of file +- Action: Generic + Name: work + Place: + Function: DoWork(int) \ No newline at end of file From c1cc6e871ddcc0fe9b11a3b873f74fbdbab684f7 Mon Sep 17 00:00:00 2001 From: Nikolay Cherkashin Date: Sat, 17 May 2025 19:04:29 +0000 Subject: [PATCH 05/12] replay don't work full --- runtime/include/pretty_print.h | 13 +-- runtime/include/scheduler.h | 173 +++++++++++++++++++-------------- 2 files changed, 106 insertions(+), 80 deletions(-) diff --git a/runtime/include/pretty_print.h b/runtime/include/pretty_print.h index 471fab7c..2e021a06 100644 --- a/runtime/include/pretty_print.h +++ b/runtime/include/pretty_print.h @@ -13,23 +13,23 @@ #include "stable_vector.h" using std::string; using std::to_string; -struct CreateNewThread { +struct CreateNewThreadHistoryInfo { size_t created_thread_id; std::string_view name; }; using FullHistoryWithThreads = std::vector, - CoroutineStatus, CreateNewThread, WaitThreadInfo>>>; + CoroutineStatus, CreateNewThreadHistoryInfo, WaitThreadInfo>>>; template -void DFS(const StableVector& arr, std::vector& visited, size_t i, +void Dfs(const StableVector& arr, std::vector& visited, size_t i, std::vector& ans) { ans.push_back(i); visited[i] = true; for (auto& u : arr[i].children) { if (!visited[u]) { - DFS(arr, visited, u, ans); + Dfs(arr, visited, u, ans); } } } @@ -39,7 +39,7 @@ void TopSort(const StableVector& arr, std::vector& ans) { std::vector visited(arr.size(), false); for (int i = 0; i < arr.size(); i++) { if (!visited[i]) { - DFS(arr, visited, i, ans); + Dfs(arr, visited, i, ans); } } } @@ -227,6 +227,7 @@ struct PrettyPrinter { print_empty_cell(); } fp.Out(" "); + // std::cerr << "writing " << &act.get() << "\n"; fp.Out(std::string{act.get()->GetName()}); fp.Out("("); const auto& args = act.get()->GetStrArgs(); @@ -260,7 +261,7 @@ struct PrettyPrinter { } fp.Out(cor.name); }, - [&](const CreateNewThread& new_thread) { + [&](const CreateNewThreadHistoryInfo& new_thread) { print_spaces(7); out << "|"; for (int j = 0; j < num; ++j) { diff --git a/runtime/include/scheduler.h b/runtime/include/scheduler.h index e52c67ac..206493bf 100644 --- a/runtime/include/scheduler.h +++ b/runtime/include/scheduler.h @@ -11,6 +11,7 @@ #include #include #include +#include #include #include "lib.h" @@ -555,6 +556,7 @@ struct TLAScheduler : Scheduler { Task* task{}; // Is true if the task was created at this step. bool is_new{}; + size_t thread_id; }; // In structured concurrency so kind of termination will be safe for children @@ -564,6 +566,7 @@ struct TLAScheduler : Scheduler { TerminateThread(threads[thr.children[i]]); } thr.children.clear(); + thr.wait_cond.reset(); for (size_t j = 0; j < thr.tasks.size(); ++j) { auto& task = thr.tasks[j]; if (!task->IsReturned()) { @@ -581,6 +584,7 @@ struct TLAScheduler : Scheduler { TerminateThread(threads[i]); } started_thread_groups.clear(); + threads.resize(initial_threads_count); } // Replays all actions from 0 to the step_end. @@ -590,50 +594,76 @@ struct TLAScheduler : Scheduler { // In histories we store references, so there's no need to update it. state.Reset(); for (size_t step = 0; step < step_end; ++step) { - auto& frame = frames[step]; + Frame& frame = frames[step]; auto task = frame.task; assert(task); + if (virtual_thread_creation) { + CreateNewThread(frame.thread_id, *virtual_thread_creation); + virtual_thread_creation.reset(); + } + AddWaitCond(frame.thread_id); if (frame.is_new) { - // It was a new task. - // So restart it from the beginning with the same args. - *task = (*task)->Restart(&state); + // It was a new task.frame.created_meta + // So restart it from the beginning with the same + // args. + if (frame.thread_id < initial_threads_count) { + *task = (*task)->Restart(&state); + } else { + std::optional meta = + threads[frame.thread_id].created_meta; + assert(meta); + std::cerr << "was " << &task; + threads[frame.thread_id].tasks.emplace_back(CreateSpawnedTask(*meta)); + task = &threads[frame.thread_id].tasks.back(); + std::cerr << " " << &task << "\n"; + } } else { - // It was a not new task, hence, we recreated in early. + // It was a not new task, hence, we recreated in + // early. } (*task)->Resume(); + std::cerr << task->get()->GetName() << " replayed\n"; + coroutine_status.reset(); } - coroutine_status.reset(); + std::cerr << "replayed " << step_end << "\n"; } + void CreateNewThread(size_t thread_id, const CreatedThreadInfo& created) { + threads.emplace_back(Thread{.id = threads.size(), + .tasks = {}, + .children = {}, + .created_meta = created}); + threads.back().created_meta->parent = thread_id; + threads[thread_id].children.push_back(threads.size() - 1); + started_thread_groups + .emplace(WaitId{.base_id = created.id, .thread = thread_id}, 0) + .first->second++; + } + void AddWaitCond(size_t thread_id) { + if (virtual_thread_wait) { + assert(!threads[thread_id].wait_cond); + threads[thread_id].wait_cond = virtual_thread_wait; + virtual_thread_wait.reset(); + return; + } + } void UpdateFullHistory(size_t thread_id, Task& task, bool is_new) { - if (virtual_thread_creation.has_value()) { + if (virtual_thread_creation) { + CreateNewThread(thread_id, *virtual_thread_creation); full_history.emplace_back(thread_id, task); - virtual_thread_creation->parent = thread_id; - threads.emplace_back(Thread{.id = threads.size(), - .tasks = {}, - .children = {}, - .created_meta = *virtual_thread_creation}); - threads[thread_id].children.push_back(threads.size() - 1); // TODO: looks like we need a seperate function fo verifier // verifier.UpdateState(coroutine_status->name, thread_id, // true); full_history.emplace_back( - thread_id, - CreateNewThread{threads.size() - 1, virtual_thread_creation->name}); - started_thread_groups - .emplace(WaitId{.base_id = virtual_thread_creation->id, - .thread = thread_id}, - 0) - .first->second++; + thread_id, CreateNewThreadHistoryInfo{threads.size() - 1, + virtual_thread_creation->name}); + threads.back().tasks.emplace_back(CreateSpawnedTask(*virtual_thread_creation)); + threads.back().tasks.back().Resume(); + //this is will push us to the moment where args are saved and we can save continue virtual_thread_creation.reset(); return; } - if (virtual_thread_wait.has_value()) { - assert(!threads[thread_id].wait_cond); - threads[thread_id].wait_cond = virtual_thread_wait; - virtual_thread_wait.reset(); - return; - } + AddWaitCond(thread_id); if (coroutine_status.has_value()) { if (is_new) { assert(coroutine_status->has_started); @@ -717,8 +747,6 @@ struct TLAScheduler : Scheduler { } } else { log() << "run round: " << finished_rounds << "\n"; - // std::vector mapping(threads.size()); - // std::iota(mapping.begin(), mapping.end(), 0); std::vector mapping; TopSort(threads, mapping); PrettyPrinter pretty_printer{threads.size()}; @@ -740,47 +768,62 @@ struct TLAScheduler : Scheduler { } thread_id_history.pop_back(); - // Removing combination of start of task + coroutine start - if (full_history.back().second.index() == 1) { - auto& cor = std::get<1>(full_history.back().second); - auto& prev = full_history[full_history.size() - 2]; - int thread = full_history.back().first; - auto first_ind = - std::find_if(full_history.begin(), --full_history.end(), - [&thread](auto& a) { return a.first == thread; }); - if (cor.has_started && - std::distance(full_history.begin(), first_ind) == - full_history.size() - 2 && - prev.second.index() == 0) { - full_history.pop_back(); - } - } - if (full_history.back().second.index() == 3) { - // auto& wt = std::get<3>(full_history.back().second); - // full_history.pop_back(); - - } + auto visitor = Overloads{ + // Removing combination of start of task + coroutine start + [this](const CoroutineStatus& status) { + auto& cor = std::get<1>(full_history.back().second); + auto& prev = full_history[full_history.size() - 2]; + int thread = full_history.back().first; + auto first_ind = + std::find_if(full_history.begin(), --full_history.end(), + [&thread](auto& a) { return a.first == thread; }); + if (cor.has_started && + std::distance(full_history.begin(), first_ind) == + full_history.size() - 2 && + prev.second.index() == 0) { + full_history.pop_back(); + } + }, + [this](const CreateNewThreadHistoryInfo& created) { + full_history.pop_back(); + }, + [this](const WaitThreadInfo& wait) { full_history.pop_back(); }, + [](auto& a) {}}; + std::visit(visitor, full_history.back().second); full_history.pop_back(); if (is_finished) { - --finished_tasks; + if (thread.created_meta) { + --finished_tasks; + } // resp. sequential_history.pop_back(); } if (is_new) { // inv. - --started_tasks; + if (thread.created_meta) { + --started_tasks; + } sequential_history.pop_back(); } return {false, {}}; } + Task CreateSpawnedTask(const CreatedThreadInfo& created) { + return Coro::New( + [&created](void*) -> ValueWrapper { + created.function(); + return void_v; + }, + &state, std::shared_ptr(), + [](std::shared_ptr) -> std::vector { return {}; }, + created.name, -1); + } std::tuple RunStep(size_t step, size_t switches) { // Push frame to the stack. frames.emplace_back(Frame{}); auto& frame = frames.back(); - bool all_parked = true; // Pick next task. for (size_t i = 0; i < threads.size(); ++i) { @@ -811,6 +854,7 @@ struct TLAScheduler : Scheduler { } // Task exists. frame.is_new = false; + frame.thread_id = i; auto [is_over, res] = ResumeTask(frame, step, switches, thread, false); if (is_over || res.has_value()) { return {is_over, res}; @@ -823,27 +867,7 @@ struct TLAScheduler : Scheduler { all_parked = false; std::optional& created = thread.created_meta; - if (created && thread.tasks.empty()) { - tasks.emplace_back(Coro::New( - [&created](void*) -> ValueWrapper { - created->function(); - return void_v; - }, - &state, std::shared_ptr(), - [](std::shared_ptr) -> std::vector { - return {}; - }, - created->name, -1)); - frame.is_new = true; - auto [is_over, res] = ResumeTask(frame, step, switches, thread, true); - if (is_over || res.has_value()) { - return {is_over, res}; - } - tasks.pop_back(); - auto size_after = thread.tasks.size(); - // As we can't return to the past in coroutine, we need to replay all - // tasks from the beginning. - Replay(step); + if (i >= initial_threads_count) { continue; } // Choose constructor to create task. @@ -853,10 +877,11 @@ struct TLAScheduler : Scheduler { if (!verifier.Verify(CreatedTaskMetaData{cons.GetName(), true, i})) { continue; } - frame.is_new = true; - auto size_before = tasks.size(); tasks.emplace_back(cons.Build(&state, i, -1/* TODO: fix task id for tla, because it is Scheduler and not Strategy class for some reason */)); started_tasks++; + frame.is_new = true; + frame.thread_id = i; + auto size_before = tasks.size() - 1; auto [is_over, res] = ResumeTask(frame, step, switches, thread, true); if (is_over || res.has_value()) { return {is_over, res}; From 87361612a4d12bd37176467f9c0f12d284da98a5 Mon Sep 17 00:00:00 2001 From: Nikolay Cherkashin Date: Mon, 19 May 2025 17:13:56 +0000 Subject: [PATCH 06/12] i'm not sure was it a good idea --- codegen/coyieldpass.cpp | 112 +++++++++++++++++++++++++++++----------- 1 file changed, 81 insertions(+), 31 deletions(-) diff --git a/codegen/coyieldpass.cpp b/codegen/coyieldpass.cpp index 8c9e45e7..0447fcad 100644 --- a/codegen/coyieldpass.cpp +++ b/codegen/coyieldpass.cpp @@ -43,15 +43,17 @@ using Builder = IRBuilder<>; constexpr std::string_view costatus_change = "CoroutineStatusChange"; constexpr std::string_view create_thread = "CreateNewVirtualThread"; constexpr std::string_view wait_thread = "WaitForThread"; +constexpr std::string_view coyield = "CoYield"; constexpr std::string_view co_await_ready = "await_ready"; +constexpr std::string_view co_initial_suspend = "initial_suspend()"; constexpr int resumed_coro = 0; static cl::opt input_list( "coroutine-file", cl::desc("Specify path to file with config"), llvm::cl::Required); ; -constexpr bool dump_before = false; +constexpr bool dump_before = true; constexpr bool dump_after = false; enum HandleType { GENERIC_FUN, CORO_FUN, SPAWN_VIRT_THREAD, WAIT_VIRT_THREAD }; @@ -65,32 +67,33 @@ struct InsertPlace { std::optional debug_file; }; -// LLVM is built without rtti by default, so no vtables struct InsertAction { - InsertAction(HandleType type) : type(type) {} InsertPlace place; std::string print_name; HandleType type; + virtual HandleType GetType() = 0; virtual ~InsertAction() {} }; struct InsertCoro : InsertAction { - InsertCoro() : InsertAction(HandleType::CORO_FUN) {} + HandleType GetType() override { return HandleType::CORO_FUN; } }; struct InsertGeneric : InsertAction { - InsertGeneric() : InsertAction(HandleType::GENERIC_FUN) {} + HandleType GetType() override { return HandleType::GENERIC_FUN; } }; struct InsertSpawn : InsertAction { int creation_id; - InsertSpawn(int creation_id) - : InsertAction(HandleType::SPAWN_VIRT_THREAD), creation_id(creation_id) {} + bool has_this; + HandleType GetType() override { return HandleType::SPAWN_VIRT_THREAD; } + InsertSpawn(int creation_id, bool has_this) + : creation_id(creation_id), has_this(has_this) {} }; struct InsertWaitThread : InsertAction { std::vector wait_for_ids; - InsertWaitThread(const std::vector &wait) - : InsertAction(HandleType::WAIT_VIRT_THREAD), wait_for_ids(wait) {} + HandleType GetType() override { return HandleType::WAIT_VIRT_THREAD; } + InsertWaitThread(const std::vector &wait) : wait_for_ids(wait) {} }; using InsertActionPtr = std::shared_ptr; @@ -104,8 +107,10 @@ static std::map> {"Spawn", [](IO &io) { int creation_id; + bool has_this; io.mapRequired("CreationId", creation_id); - return std::make_shared(creation_id); + io.mapRequired("HasThis", has_this); + return std::make_shared(creation_id, has_this); }}, {"Wait", [](IO &io) { std::vector vect; @@ -168,6 +173,8 @@ struct CoYieldInserter { FunctionType::get( Type::getVoidTy(context), {PointerType::get(context, 0), Type::getInt32Ty(context)}, {})); + coro_yield_f = m.getOrInsertFunction( + coyield, FunctionType::get(Type::getVoidTy(context), {})); } void Run() { @@ -263,7 +270,7 @@ struct CoYieldInserter { }); if (!res_filt.empty()) { auto entry = res_filt.front(); - switch (entry->type) { + switch (entry->GetType()) { case HandleType::CORO_FUN: { errs() << "inserted coro handled by type " << co_name << "\n"; auto start = [this, &entry]() { InsertYieldCall(entry, true); }; @@ -286,7 +293,7 @@ struct CoYieldInserter { if (!res_filt.empty()) { auto entry = res_filt.front(); - switch (entry->type) { + switch (entry->GetType()) { case HandleType::CORO_FUN: { errs() << "inserted coro handled by func name " << co_name << "\n"; auto start = [this, &entry]() { InsertYieldCall(entry, true); }; @@ -310,24 +317,22 @@ struct CoYieldInserter { CallBase *inst = call ? static_cast(call) : invoke; Function *called_fun = inst->getCalledFunction(); - if (!inst->arg_empty()) { - auto [wrapper_fun, storage] = InsertZeroArgsWrapper(inst); - builder.SetInsertPoint(inst); - for (size_t i = 0; i < called_fun->arg_size(); i++) { - Value *arg = inst->getArgOperand(i); - - Value *storage_place = - builder.CreateGEP(storage->getValueType(), storage, - { - builder.getInt32(0), - builder.getInt32(i), - }); - builder.CreateStore(arg, storage_place); - } - called_fun = wrapper_fun; + assert(!inst->arg_empty()); + auto [wrapper_fun, storage] = InsertZeroArgsWrapper(inst); + builder.SetInsertPoint(inst); + for (size_t i = 0; i < called_fun->arg_size(); i++) { + Value *arg = inst->getArgOperand(i); + + Value *storage_place = + builder.CreateGEP(storage->getValueType(), storage, + { + builder.getInt32(0), + builder.getInt32(i), + }); + builder.CreateStore(arg, storage_place); } Value *pointer_to_func = builder.CreatePointerCast( - called_fun, PointerType::get(builder.getContext(), 0)); + wrapper_fun, PointerType::get(builder.getContext(), 0)); std::shared_ptr spawn = std::static_pointer_cast(entry); Value *id = builder.getInt32(spawn->creation_id); @@ -347,7 +352,18 @@ struct CoYieldInserter { // we cannot simple delete here instruction because we are // iterating over it in basic block to_delete.push_back(inst); - break; + // Now we must insert coyield point at the start of coroutines args, + // we need explicitly launch it's body to be sure that it's args are + // saved + // TODO: put name and metainfo inside this call + for (size_t i = 0; i < called_fun->arg_size(); i++) { + if (i == 0 && spawn->has_this) { + continue; + } + assert(isa(inst->getArgOperand(i))); + InsertAtBodyStart(dyn_cast(inst->getArgOperand(i)), + [this]() { builder.CreateCall(coro_yield_f); }); + } } case HandleType::WAIT_VIRT_THREAD: { errs() << "inserted wait of new thread" << co_name << "\n"; @@ -379,8 +395,9 @@ struct CoYieldInserter { Constant *indices = ConstantArray::get( ArrayType::get(builder.getInt32Ty(), act->wait_for_ids.size()), elements); - GlobalVariable *val_ind = new GlobalVariable(m, - indices->getType(), true, llvm::GlobalValue::InternalLinkage, indices); + GlobalVariable *val_ind = + new GlobalVariable(m, indices->getType(), true, + llvm::GlobalValue::InternalLinkage, indices); builder.CreateCall(wait_thread_f, {val_ind, builder.getInt32(elements.size())}); } @@ -550,6 +567,39 @@ struct CoYieldInserter { builder.CreateCall(coro_yield_f, {GetLiteral(filt->print_name), llvm_start}); } + + void InsertAtBodyStart(CallBase *call, + const std::function &insert) { + auto f = call->getCalledFunction(); + ValueToValueMapTy vmap; + auto cloned_f = CloneFunction(f, vmap); + call->setCalledFunction(cloned_f); + for (auto &b : *cloned_f) { + for (auto &i : b) { + if (auto call = dyn_cast(&i)) { + std::string demangled = demangle(call->getName()); + + auto initial = demangled.find(co_initial_suspend); + if (initial != std::string::npos) { + // we have only one initial_suspend; + auto *await_ready = i.getNextNode(); + auto *br = dyn_cast(await_ready->getNextNode()); + // true case is shorter; + auto *ready_bb = br->getSuccessor(0); + auto *resume_bb = ready_bb->getSingleSuccessor(); + // in this block is llvm.lifetime.end.p0 for coro initial suspend + auto *lifetime_end_bb = resume_bb->getSingleSuccessor(); + auto *body_bb = resume_bb->getSingleSuccessor(); + builder.SetInsertPoint(body_bb); + insert(); + return; + } + } + } + } + assert(false && "no initial suspend"); + } + Constant *GetLiteral(const std::string &name) { auto literal = string_literals.find(name); if (literal == string_literals.end()) { From d56576f246e03b680420f8115f5b74086f005f1c Mon Sep 17 00:00:00 2001 From: Nikolay Cherkashin Date: Sun, 8 Jun 2025 23:36:19 +0000 Subject: [PATCH 07/12] [noci] dump --- codegen/coyieldpass.cpp | 248 ++++++++++++------ runtime/include/lib.h | 27 +- runtime/include/lincheck.h | 4 +- runtime/include/pretty_print.h | 61 ++--- runtime/include/scheduler.h | 144 ++++++---- runtime/include/verifying.h | 15 +- runtime/lib.cpp | 27 +- test/codegen/CMakeLists.txt | 15 ++ .../codegen/coyield/tests/thread_creation.cpp | 41 +++ .../coyield/tests/thread_creation.cpp.yml | 16 ++ verifying/specs/unique_args.h | 9 +- verifying/targets/dynthreads_unique_args.cpp | 38 +-- verifying/targets/dynthreads_unique_args.yml | 11 +- 13 files changed, 452 insertions(+), 204 deletions(-) create mode 100644 test/codegen/CMakeLists.txt create mode 100644 test/codegen/coyield/tests/thread_creation.cpp create mode 100644 test/codegen/coyield/tests/thread_creation.cpp.yml diff --git a/codegen/coyieldpass.cpp b/codegen/coyieldpass.cpp index 0447fcad..3f79c76e 100644 --- a/codegen/coyieldpass.cpp +++ b/codegen/coyieldpass.cpp @@ -1,7 +1,10 @@ -#include -#include -#include +#include +#include +#include +#include +#include +#include #include #include #include @@ -43,7 +46,7 @@ using Builder = IRBuilder<>; constexpr std::string_view costatus_change = "CoroutineStatusChange"; constexpr std::string_view create_thread = "CreateNewVirtualThread"; constexpr std::string_view wait_thread = "WaitForThread"; -constexpr std::string_view coyield = "CoYield"; +constexpr std::string_view spawned_coro_start = "VirtualThreadStartPoint"; constexpr std::string_view co_await_ready = "await_ready"; constexpr std::string_view co_initial_suspend = "initial_suspend()"; @@ -53,10 +56,16 @@ static cl::opt input_list( llvm::cl::Required); ; -constexpr bool dump_before = true; +constexpr bool dump_before = false; constexpr bool dump_after = false; -enum HandleType { GENERIC_FUN, CORO_FUN, SPAWN_VIRT_THREAD, WAIT_VIRT_THREAD }; +enum HandleType { + GENERIC_FUN, + CORO_FUN, + SPAWN_VIRT_THREAD, + SPAWN_CORO_INFO, + WAIT_VIRT_THREAD +}; struct InsertPlace { InsertPlace() = default; @@ -69,16 +78,25 @@ struct InsertPlace { struct InsertAction { InsertPlace place; - std::string print_name; HandleType type; virtual HandleType GetType() = 0; virtual ~InsertAction() {} }; -struct InsertCoro : InsertAction { + +struct InsertActionWithName : InsertAction { + std::string print_name; + InsertActionWithName(const std::string &print_name) + : print_name(print_name) {} + virtual ~InsertActionWithName() {} +}; + +struct InsertCoro : InsertActionWithName { + using InsertActionWithName::InsertActionWithName; HandleType GetType() override { return HandleType::CORO_FUN; } }; -struct InsertGeneric : InsertAction { +struct InsertGeneric : InsertActionWithName { + using InsertActionWithName::InsertActionWithName; HandleType GetType() override { return HandleType::GENERIC_FUN; } }; @@ -90,6 +108,14 @@ struct InsertSpawn : InsertAction { : creation_id(creation_id), has_this(has_this) {} }; +struct InsertSpawnedCoro : InsertActionWithName { + std::optional args_fun_name; + HandleType GetType() override { return HandleType::SPAWN_CORO_INFO; } + InsertSpawnedCoro(const std::optional &args_fun, + const std::string &print_name) + : args_fun_name(args_fun), InsertActionWithName(print_name) {} +}; + struct InsertWaitThread : InsertAction { std::vector wait_for_ids; HandleType GetType() override { return HandleType::WAIT_VIRT_THREAD; } @@ -101,23 +127,43 @@ using InsertActionPtr = std::shared_ptr; namespace llvm { namespace yaml { static std::map> - construct_action{ - {"Generic", [](IO &io) { return std::make_shared(); }}, - {"Coro", [](IO &io) { return std::make_shared(); }}, - {"Spawn", - [](IO &io) { - int creation_id; - bool has_this; - io.mapRequired("CreationId", creation_id); - io.mapRequired("HasThis", has_this); - return std::make_shared(creation_id, has_this); - }}, - {"Wait", [](IO &io) { - std::vector vect; - io.mapRequired("WaitsFor", vect); - assert(vect.size() > 0); - return std::make_shared(vect); - }}}; + construct_action{{"Generic", + [](IO &io) { + std::string print_name; + io.mapRequired("Name", print_name); + return std::make_shared(print_name); + }}, + {"Coro", + [](IO &io) { + std::string print_name; + io.mapRequired("Name", print_name); + return std::make_shared(print_name); + }}, + {"Spawn", + [](IO &io) { + int creation_id; + bool has_this; + io.mapRequired("CreationId", creation_id); + io.mapRequired("HasThis", has_this); + + return std::make_shared(creation_id, + has_this); + }}, + {"SpawnedCoro", + [](IO &io) { + std::optional args_fun_name; + std::string print_name; + io.mapOptional("ArgsFun", args_fun_name); + io.mapRequired("Name", print_name); + return std::make_shared( + args_fun_name, print_name); + }}, + {"Wait", [](IO &io) { + std::vector vect; + io.mapRequired("WaitsFor", vect); + assert(vect.size() > 0); + return std::make_shared(vect); + }}}; template <> struct MappingTraits { static void mapping(IO &io, @@ -131,7 +177,6 @@ struct MappingTraits { return; } action = (entry->second)(io); - io.mapRequired("Name", action->print_name); io.mapRequired("Place", action->place); } }; @@ -156,25 +201,24 @@ struct CoYieldInserter { CoYieldInserter(Module &m, std::vector &&co_filter) : m(m), co_filter(std::move(co_filter)), builder(m.getContext()) { auto &context = m.getContext(); + auto *char_ptr = PointerType::get(Type::getInt8Ty(context), 0); coro_yield_f = m.getOrInsertFunction( costatus_change, FunctionType::get(Type::getVoidTy(context), - {PointerType::get(Type::getInt8Ty(context), 0), - Type::getInt1Ty(context)}, - {})); + {char_ptr, Type::getInt1Ty(context)}, {})); create_thread_f = m.getOrInsertFunction( create_thread, - FunctionType::get(Type::getVoidTy(context), - {PointerType::get(Type::getInt8Ty(context), 0), - PointerType::get(context, 0)}, - {})); + FunctionType::get( + Type::getVoidTy(context), + {Type::getInt32Ty(context), PointerType::get(context, 0)}, {})); wait_thread_f = m.getOrInsertFunction( wait_thread, FunctionType::get( Type::getVoidTy(context), {PointerType::get(context, 0), Type::getInt32Ty(context)}, {})); - coro_yield_f = m.getOrInsertFunction( - coyield, FunctionType::get(Type::getVoidTy(context), {})); + spawned_coro_start_f = m.getOrInsertFunction( + spawned_coro_start, + FunctionType::get(Type::getVoidTy(context), {char_ptr, char_ptr}, {})); } void Run() { @@ -273,8 +317,14 @@ struct CoYieldInserter { switch (entry->GetType()) { case HandleType::CORO_FUN: { errs() << "inserted coro handled by type " << co_name << "\n"; - auto start = [this, &entry]() { InsertYieldCall(entry, true); }; - auto end = [this, &entry]() { InsertYieldCall(entry, false); }; + auto act_name = + std::static_pointer_cast(entry); + auto start = [this, &act_name]() { + InsertYieldCall(act_name, true); + }; + auto end = [this, &act_name]() { + InsertYieldCall(act_name, false); + }; HandleCoroCase(call, res_filt.front(), start, end); return; } @@ -296,8 +346,9 @@ struct CoYieldInserter { switch (entry->GetType()) { case HandleType::CORO_FUN: { errs() << "inserted coro handled by func name " << co_name << "\n"; - auto start = [this, &entry]() { InsertYieldCall(entry, true); }; - auto end = [this, &entry]() { InsertYieldCall(entry, false); }; + auto act_name = std::static_pointer_cast(entry); + auto start = [this, &act_name]() { InsertYieldCall(act_name, true); }; + auto end = [this, &act_name]() { InsertYieldCall(act_name, false); }; if (invoke) { assert(FindAwaitReady(invoke->getNormalDest()->begin(), entry, start, end)); @@ -309,7 +360,9 @@ struct CoYieldInserter { } case HandleType::GENERIC_FUN: { errs() << "inserted generic " << co_name << "\n"; - HandleGenericFunCase(call, invoke, entry); + HandleGenericFunCase( + call, invoke, + std::static_pointer_cast(entry)); break; } case HandleType::SPAWN_VIRT_THREAD: { @@ -339,44 +392,49 @@ struct CoYieldInserter { builder.SetInsertPoint(inst); Value *replacement; if (call) { - replacement = builder.CreateCall( - create_thread_f, - {id, GetLiteral(entry->print_name), pointer_to_func}); + replacement = + builder.CreateCall(create_thread_f, {id, pointer_to_func}); } else { replacement = builder.CreateInvoke( create_thread_f, invoke->getNormalDest(), - invoke->getUnwindDest(), - {id, GetLiteral(entry->print_name), pointer_to_func}); + invoke->getUnwindDest(), {id, pointer_to_func}); } inst->replaceAllUsesWith(replacement); // we cannot simple delete here instruction because we are // iterating over it in basic block to_delete.push_back(inst); - // Now we must insert coyield point at the start of coroutines args, - // we need explicitly launch it's body to be sure that it's args are - // saved - // TODO: put name and metainfo inside this call - for (size_t i = 0; i < called_fun->arg_size(); i++) { - if (i == 0 && spawn->has_this) { - continue; - } - assert(isa(inst->getArgOperand(i))); - InsertAtBodyStart(dyn_cast(inst->getArgOperand(i)), - [this]() { builder.CreateCall(coro_yield_f); }); + break; + } + case HandleType::SPAWN_CORO_INFO: { + CallBase *inst = call ? static_cast(call) : invoke; + errs() << "inserted coroutine info " << co_name << "\n"; + auto spawn_coro = std::static_pointer_cast(entry); + Function *func = nullptr; + if (spawn_coro->args_fun_name) { + func = m.getFunction(*spawn_coro->args_fun_name); + assert(func); } + InsertAtBodyStart(inst, func, [this, &spawn_coro](Value *args) { + builder.CreateCall(spawned_coro_start_f, + {GetLiteral(spawn_coro->print_name), + GetLiteral(spawn_coro->print_name)}); + builder.CreateAnd(ConstantInt::get(builder.getInt16Ty(), 1), + ConstantInt::get(builder.getInt16Ty(), 1)); + }); + break; } case HandleType::WAIT_VIRT_THREAD: { - errs() << "inserted wait of new thread" << co_name << "\n"; + errs() << "inserted wait of new thread " << co_name << "\n"; // I'm sure that here must be only coro case auto start = std::bind_front( &CoYieldInserter::InsertWaitFunc, this, std::static_pointer_cast(entry)); if (invoke) { - assert(FindAwaitReady(invoke->getNormalDest()->begin(), entry, - start, {})); + assert(InsertBeforeAnyCoroCall(invoke->getNormalDest()->begin(), + start)); } else { - assert(FindAwaitReady(BasicBlock::iterator(call->getNextNode()), - entry, start, {})); + assert(InsertBeforeAnyCoroCall( + BasicBlock::iterator(call->getNextNode()), start)); } break; } @@ -454,10 +512,29 @@ struct CoYieldInserter { } return false; } + + bool InsertBeforeAnyCoroCall(BasicBlock::iterator start, + const std::function &insert) { + for (Instruction &n_inst : make_range(start, start->getParent()->end())) { + auto *call_inst = dyn_cast(&n_inst); + if (!call_inst) { + continue; + } + auto await_ready_ind = demangle(call_inst->getCalledFunction()->getName()) + .find(co_await_ready); + if (await_ready_ind != std::string::npos) { + builder.SetInsertPoint(call_inst); + insert(); + return true; + } + } + return false; + } // This case is needed at sample by some coro primitives where the // normal function which is the body of coro is called in loop - void HandleGenericFunCase(CallBase *call, InvokeInst *invoke, - const InsertActionPtr &filt_entry) { + void HandleGenericFunCase( + CallBase *call, InvokeInst *invoke, + const std::shared_ptr &filt_entry) { builder.SetInsertPoint(call); InsertYieldCall(filt_entry, true); // Invoke instruction has unwind/normal ends so we need handle it @@ -561,24 +638,40 @@ struct CoYieldInserter { builder.CreateBr(succ); return tramp; } - void InsertYieldCall(const InsertActionPtr &filt, bool start) { + void InsertYieldCall(const std::shared_ptr &filt, + bool start) { auto llvm_start = ConstantInt::get(Type::getInt1Ty(builder.getContext()), start); builder.CreateCall(coro_yield_f, {GetLiteral(filt->print_name), llvm_start}); } - void InsertAtBodyStart(CallBase *call, - const std::function &insert) { + void InsertAtBodyStart(CallBase *call, Function *insert_at_start_fun, + const std::function &insert) { auto f = call->getCalledFunction(); ValueToValueMapTy vmap; auto cloned_f = CloneFunction(f, vmap); call->setCalledFunction(cloned_f); + // for simplity and correct handling of references let's call print function + // on the start + Value *print_args = + ConstantPointerNull::get(PointerType::get(m.getContext(), 0)); + if (insert_at_start_fun) { + builder.SetInsertPoint(cloned_f->front().getFirstInsertionPt()); + SmallVector args; + for (auto &a : cloned_f->args()) { + args.emplace_back(&a); + } + print_args = builder.CreateCall(insert_at_start_fun, ArrayRef(args)); + } for (auto &b : *cloned_f) { for (auto &i : b) { if (auto call = dyn_cast(&i)) { - std::string demangled = demangle(call->getName()); - + auto f = call->getCalledFunction(); + if (!f) { + continue; + } + std::string demangled = demangle(f->getName()); auto initial = demangled.find(co_initial_suspend); if (initial != std::string::npos) { // we have only one initial_suspend; @@ -587,11 +680,17 @@ struct CoYieldInserter { // true case is shorter; auto *ready_bb = br->getSuccessor(0); auto *resume_bb = ready_bb->getSingleSuccessor(); - // in this block is llvm.lifetime.end.p0 for coro initial suspend - auto *lifetime_end_bb = resume_bb->getSingleSuccessor(); - auto *body_bb = resume_bb->getSingleSuccessor(); - builder.SetInsertPoint(body_bb); - insert(); + auto it = resume_bb->getFirstInsertionPt(); + //iterate until await resume is meet + while(true){ + auto* call = dyn_cast(&(*it)); + if(call){ + break; + } + it++; + } + builder.SetInsertPoint(++it); + insert(print_args); return; } } @@ -618,6 +717,7 @@ struct CoYieldInserter { } Module &m; FunctionCallee coro_yield_f; + FunctionCallee spawned_coro_start_f; FunctionCallee create_thread_f; FunctionCallee wait_thread_f; std::vector co_filter; diff --git a/runtime/include/lib.h b/runtime/include/lib.h index 9c4f9164..b7197518 100644 --- a/runtime/include/lib.h +++ b/runtime/include/lib.h @@ -9,6 +9,7 @@ #include #include #include +#include #include #include @@ -38,11 +39,15 @@ struct CoroutineStatus { }; struct CreatedThreadInfo { - std::string_view name; std::function function; int id; - //will be set in scheduler + // this will be set after start of scheduler + // name is a literal stored in .data, but args will be set in runtime + std::string_view name = ""; + std::string args = ""; + // will be set in scheduler size_t parent = -1; + bool has_started = false; }; struct WaitThreadInfo { @@ -70,7 +75,9 @@ extern "C" void CoroYield(); extern "C" void CoroutineStatusChange(char* coroutine, bool start); -extern "C" void CreateNewVirtualThread(int id, char* name, void* func); +extern "C" void CreateNewVirtualThread(int id, void* func); + +extern "C" void VirtualThreadStartPoint(char* name, char* args); extern "C" void WaitForThread(int* ids, int size); @@ -101,6 +108,10 @@ struct CoroBase : public std::enable_shared_from_this { // Returns the args as strings. virtual std::vector GetStrArgs() const = 0; + virtual void SetStrArgsAndName( + std::string_view name, + std::function(std::shared_ptr)> args) = 0; + // Returns raw pointer to the tuple arguments. virtual void* GetArgs() const = 0; @@ -109,7 +120,7 @@ struct CoroBase : public std::enable_shared_from_this { std::shared_ptr GetPtr(); // Terminate the coroutine. - void Terminate(); + bool Terminate(); // Sets the token. void SetToken(std::shared_ptr); @@ -215,6 +226,14 @@ struct Coro final : public CoroBase { return args_to_strings(args); } + void SetStrArgsAndName( + std::string_view name, + std::function(std::shared_ptr)> args_fun) + override { + this->name = name; + this->args_to_strings = args_fun; + } + void* GetArgs() const override { return args.get(); } private: diff --git a/runtime/include/lincheck.h b/runtime/include/lincheck.h index 178fd3d5..9ea4f89a 100644 --- a/runtime/include/lincheck.h +++ b/runtime/include/lincheck.h @@ -17,7 +17,7 @@ struct Response { ValueWrapper result; int thread_id; - private: +// private: std::reference_wrapper task; }; @@ -28,7 +28,7 @@ struct Invoke { int thread_id; - private: +// private: std::reference_wrapper task; }; diff --git a/runtime/include/pretty_print.h b/runtime/include/pretty_print.h index 2e021a06..51008e81 100644 --- a/runtime/include/pretty_print.h +++ b/runtime/include/pretty_print.h @@ -18,9 +18,9 @@ struct CreateNewThreadHistoryInfo { std::string_view name; }; -using FullHistoryWithThreads = - std::vector, - CoroutineStatus, CreateNewThreadHistoryInfo, WaitThreadInfo>>>; +using FullHistoryWithThreads = std::vector< + std::pair, CoroutineStatus, + CreateNewThreadHistoryInfo, WaitThreadInfo>>>; template void Dfs(const StableVector& arr, std::vector& visited, size_t i, @@ -68,14 +68,6 @@ struct PrettyPrinter { template void PrettyPrint(const std::vector>& result, Out_t& out) { - auto get_thread_num = [](const std::variant& v) { - // Crutch. - if (v.index() == 0) { - return get<0>(v).thread_id; - } - return get<1>(v).thread_id; - }; - int cell_width = 20; // Up it if necessary. Enough for now. auto print_separator = [&out, this, cell_width]() { @@ -116,7 +108,7 @@ struct PrettyPrinter { // Rows. for (const auto& i : result) { - int num = get_thread_num(i); + int num = std::visit([](auto& a) { return a.thread_id; }, i); out << "|"; for (int j = 0; j < num; ++j) { print_empty_cell(); @@ -124,24 +116,25 @@ struct PrettyPrinter { FitPrinter fp{out, cell_width}; fp.Out(" "); - if (i.index() == 0) { - auto inv = get<0>(i); - auto& task = inv.GetTask(); - fp.Out("[" + std::to_string(task->GetId()) + "] "); - fp.Out(std::string{task->GetName()}); - fp.Out("("); - const auto& args = task->GetStrArgs(); - for (int i = 0; i < args.size(); ++i) { - if (i > 0) { - fp.Out(", "); - } - fp.Out(args[i]); - } - fp.Out(")"); - } else { - auto resp = get<1>(i); - fp.Out("<-- " + to_string(resp.GetTask()->GetRetVal())); - } + std::visit( + Overloads{[&fp](const Invoke& inv) { + auto& task = inv.GetTask(); + fp.Out("[" + std::to_string(task->GetId()) + "] "); + fp.Out(std::string{task->GetName()}); + fp.Out("("); + const auto& args = task->GetStrArgs(); + for (int i = 0; i < args.size(); ++i) { + if (i > 0) { + fp.Out(", "); + } + fp.Out(args[i]); + } + fp.Out(")"); + }, + [&fp](const Response& resp) { + fp.Out("<-- " + to_string(resp.GetTask()->GetRetVal())); + }}, + i); assert(fp.rest > 0 && "increase cell_width in pretty printer"); print_spaces(fp.rest); out << "|"; @@ -157,9 +150,9 @@ struct PrettyPrinter { // Helps to debug full histories. template - void PrettyPrint(FullHistoryWithThreads& result, std::vector mapping, - Out_t& out) { - int cell_width = 10; // Up it if necessary. Enough for now. + void PrettyPrint(FullHistoryWithThreads& result, + const std::vector mapping, Out_t& out) { + int cell_width = 20; // Up it if necessary. Enough for now. std::vector inverse_mapping(mapping.size(), -1); for (int i = 0; i < mapping.size(); i++) { @@ -270,7 +263,7 @@ struct PrettyPrinter { fp.Out(std::string(" ->T") + std::to_string(new_thread.created_thread_id)); }, - [&](const WaitThreadInfo& wait_thread){ + [&](const WaitThreadInfo& wait_thread) { print_spaces(7); out << "|"; for (int j = 0; j < num; ++j) { diff --git a/runtime/include/scheduler.h b/runtime/include/scheduler.h index 206493bf..0ae223fe 100644 --- a/runtime/include/scheduler.h +++ b/runtime/include/scheduler.h @@ -507,7 +507,8 @@ struct TLAScheduler : Scheduler { threads.emplace_back(Thread{.id = i, .tasks = StableVector{}, .children = {}, - .created_meta = {}}); + .created_meta = {}, + .wait_cond = {}}); } }; @@ -536,7 +537,7 @@ struct TLAScheduler : Scheduler { }; struct WaitHasher { std::size_t operator()(const WaitId& id) const { - return std::hash()(id.base_id ^ (id.thread << 32)); + return (id.thread & 0xff) | ((id.base_id & 0xff) << 8); } }; // TLAScheduler enumerates all possible executions with finished max_tasks. @@ -561,16 +562,22 @@ struct TLAScheduler : Scheduler { // In structured concurrency so kind of termination will be safe for children // threads - void TerminateThread(Thread& thr) { + void TerminateThread(size_t i) { + Thread& thr = threads[i]; for (size_t i = 0; i < thr.children.size(); i++) { - TerminateThread(threads[thr.children[i]]); + TerminateThread(thr.children[i]); } thr.children.clear(); thr.wait_cond.reset(); for (size_t j = 0; j < thr.tasks.size(); ++j) { auto& task = thr.tasks[j]; if (!task->IsReturned()) { - task->Terminate(); + if (!task->Terminate()) { + // a new thread was spawned, so we need to create and finish him first + CreateNewThread(i); + virtual_thread_creation.reset(); + TerminateThread(threads.size() - 1); + } } } } @@ -581,7 +588,7 @@ struct TLAScheduler : Scheduler { void TerminateTasks() { cancel(); for (size_t i = 0; i < initial_threads_count; ++i) { - TerminateThread(threads[i]); + TerminateThread(i); } started_thread_groups.clear(); threads.resize(initial_threads_count); @@ -595,13 +602,8 @@ struct TLAScheduler : Scheduler { state.Reset(); for (size_t step = 0; step < step_end; ++step) { Frame& frame = frames[step]; - auto task = frame.task; + Task* task = frame.task; assert(task); - if (virtual_thread_creation) { - CreateNewThread(frame.thread_id, *virtual_thread_creation); - virtual_thread_creation.reset(); - } - AddWaitCond(frame.thread_id); if (frame.is_new) { // It was a new task.frame.created_meta // So restart it from the beginning with the same @@ -609,47 +611,85 @@ struct TLAScheduler : Scheduler { if (frame.thread_id < initial_threads_count) { *task = (*task)->Restart(&state); } else { - std::optional meta = - threads[frame.thread_id].created_meta; - assert(meta); - std::cerr << "was " << &task; - threads[frame.thread_id].tasks.emplace_back(CreateSpawnedTask(*meta)); - task = &threads[frame.thread_id].tasks.back(); - std::cerr << " " << &task << "\n"; + assert(false); } } else { // It was a not new task, hence, we recreated in // early. + if (frame.thread_id >= initial_threads_count) { + task = &threads[frame.thread_id].tasks.back(); + } } (*task)->Resume(); - std::cerr << task->get()->GetName() << " replayed\n"; + if (virtual_thread_creation) { + CreateNewThread(frame.thread_id); + virtual_thread_creation.reset(); + } + AddWaitCond(frame.thread_id); + bool is_finished = (*task)->IsReturned(); + if (is_finished && frame.thread_id >= initial_threads_count) { + started_thread_groups[{ + .base_id = threads[frame.thread_id].created_meta->id, + .thread = threads[frame.thread_id].created_meta->parent}]--; + } coroutine_status.reset(); } - std::cerr << "replayed " << step_end << "\n"; + // Horrible but how to this better? + for (auto& v : full_history) { + if (v.first >= initial_threads_count) { + Task* new_task = &threads[v.first].tasks.back(); + std::visit(Overloads{[&new_task, &v](std::reference_wrapper& t) { + t = std::reference_wrapper(*new_task); + }, + [](auto& a) {}}, + v.second); + } + } + for (auto& v : sequential_history) { + size_t thread_id = std::visit([](auto& a) { return a.thread_id; }, v); + if (thread_id >= initial_threads_count) { + std::visit([this, thread_id]( + auto& a) { a.task = threads[thread_id].tasks.back(); }, + v); + } + } } - void CreateNewThread(size_t thread_id, const CreatedThreadInfo& created) { + void CreateNewThread(size_t parent_id) { + assert(virtual_thread_creation.has_value()); threads.emplace_back(Thread{.id = threads.size(), .tasks = {}, .children = {}, - .created_meta = created}); - threads.back().created_meta->parent = thread_id; - threads[thread_id].children.push_back(threads.size() - 1); + .created_meta = virtual_thread_creation, + .wait_cond = {}}); + threads.back().created_meta->parent = parent_id; + threads[parent_id].children.push_back(threads.size() - 1); started_thread_groups - .emplace(WaitId{.base_id = created.id, .thread = thread_id}, 0) + .emplace( + WaitId{.base_id = virtual_thread_creation->id, .thread = parent_id}, + 0) .first->second++; + auto& tasks = threads.back().tasks; + tasks.emplace_back(CreateSpawnedTask(*virtual_thread_creation)); + tasks.back()->Resume(); + tasks.back()->SetStrArgsAndName(virtual_thread_creation->name, + [](auto _) { return std::vector{}; }); } + void AddWaitCond(size_t thread_id) { if (virtual_thread_wait) { + assert(thread_id < initial_threads_count); assert(!threads[thread_id].wait_cond); threads[thread_id].wait_cond = virtual_thread_wait; virtual_thread_wait.reset(); return; } } + void UpdateFullHistory(size_t thread_id, Task& task, bool is_new) { if (virtual_thread_creation) { - CreateNewThread(thread_id, *virtual_thread_creation); + // this is will push us to the moment where coro is real started + CreateNewThread(thread_id); full_history.emplace_back(thread_id, task); // TODO: looks like we need a seperate function fo verifier // verifier.UpdateState(coroutine_status->name, thread_id, @@ -657,9 +697,6 @@ struct TLAScheduler : Scheduler { full_history.emplace_back( thread_id, CreateNewThreadHistoryInfo{threads.size() - 1, virtual_thread_creation->name}); - threads.back().tasks.emplace_back(CreateSpawnedTask(*virtual_thread_creation)); - threads.back().tasks.back().Resume(); - //this is will push us to the moment where args are saved and we can save continue virtual_thread_creation.reset(); return; } @@ -715,13 +752,6 @@ struct TLAScheduler : Scheduler { } assert(!task->IsParked()); - - std::vector mapping; - TopSort(threads, mapping); - PrettyPrinter pretty_printer{threads.size()}; - pretty_printer.PrettyPrint(full_history, mapping, log()); - log() << "===============================================\n\n"; - task->Resume(); UpdateFullHistory(thread_id, task, is_new); bool is_finished = task->IsReturned(); @@ -733,9 +763,9 @@ struct TLAScheduler : Scheduler { .thread = thread.created_meta->parent}]--; } else { finished_tasks++; - auto result = task->GetRetVal(); - sequential_history.emplace_back(Response(task, result, thread_id)); } + auto result = task->GetRetVal(); + sequential_history.emplace_back(Response(task, result, thread_id)); } bool stop = finished_tasks == max_tasks; @@ -755,12 +785,10 @@ struct TLAScheduler : Scheduler { log().flush(); // Stop, check if the the generated history is linearizable. ++finished_rounds; - // Disabled temporaraly - need discuus how to handle it better - // if (!checker.Check(sequential_history)) { - // return {false, - // std::make_pair(Scheduler::FullHistory{}, - // sequential_history)}; - // } + if (!checker.Check(sequential_history)) { + return {false, + std::make_pair(Scheduler::FullHistory{}, sequential_history)}; + } if (finished_rounds == max_rounds) { // It was the last round.q return {true, {}}; @@ -788,11 +816,11 @@ struct TLAScheduler : Scheduler { full_history.pop_back(); }, [this](const WaitThreadInfo& wait) { full_history.pop_back(); }, - [](auto& a) {}}; + [](const std::reference_wrapper& a) {}}; std::visit(visitor, full_history.back().second); full_history.pop_back(); if (is_finished) { - if (thread.created_meta) { + if (thread_id < initial_threads_count) { --finished_tasks; } // resp. @@ -800,12 +828,11 @@ struct TLAScheduler : Scheduler { } if (is_new) { // inv. - if (thread.created_meta) { + if (thread_id < initial_threads_count) { --started_tasks; } sequential_history.pop_back(); } - return {false, {}}; } @@ -817,7 +844,7 @@ struct TLAScheduler : Scheduler { }, &state, std::shared_ptr(), [](std::shared_ptr) -> std::vector { return {}; }, - created.name, -1); + {}, -1); } std::tuple RunStep(size_t step, size_t switches) { @@ -830,12 +857,12 @@ struct TLAScheduler : Scheduler { Thread& thread = threads[i]; auto& tasks = thread.tasks; if (thread.wait_cond) { - if (std::any_of( + if (!std::all_of( thread.wait_cond->wait_ids.begin(), thread.wait_cond->wait_ids.end(), [this, &i](auto& child) { auto it = started_thread_groups.find( {.base_id = child, .thread = i}); - return it == started_thread_groups.end() || it->second != 0; + return it == started_thread_groups.end() || it->second == 0; })) { continue; } @@ -848,14 +875,18 @@ struct TLAScheduler : Scheduler { continue; } all_parked = false; + auto& meta = thread.created_meta; + bool is_new = + meta ? !std::exchange((*thread.created_meta).has_started, true) + : false; if (!verifier.Verify(CreatedTaskMetaData{ - std::string{tasks.back()->GetName()}, false, i})) { + std::string{tasks.back()->GetName()}, is_new, i})) { continue; } // Task exists. frame.is_new = false; frame.thread_id = i; - auto [is_over, res] = ResumeTask(frame, step, switches, thread, false); + auto [is_over, res] = ResumeTask(frame, step, switches, thread, is_new); if (is_over || res.has_value()) { return {is_over, res}; } @@ -866,7 +897,6 @@ struct TLAScheduler : Scheduler { } all_parked = false; - std::optional& created = thread.created_meta; if (i >= initial_threads_count) { continue; } @@ -889,8 +919,8 @@ struct TLAScheduler : Scheduler { tasks.pop_back(); auto size_after = thread.tasks.size(); assert(size_before == size_after); - // As we can't return to the past in coroutine, we need to replay all - // tasks from the beginning. + // As we can't return to the past in coroutine, we need to replay + // all tasks from the beginning. Replay(step); } } diff --git a/runtime/include/verifying.h b/runtime/include/verifying.h index 72f26ee8..737dc8c1 100644 --- a/runtime/include/verifying.h +++ b/runtime/include/verifying.h @@ -3,6 +3,7 @@ #include #include +#include #include "lib.h" #include "lincheck_recursive.h" @@ -152,13 +153,19 @@ std::unique_ptr MakeScheduler(ModelChecker &checker, Opts &opts, } } -inline int TrapRun(std::unique_ptr &&scheduler, - PrettyPrinter &pretty_printer) { +inline int TrapRun(std::unique_ptr &&scheduler) { auto guard = SyscallTrapGuard{}; auto result = scheduler->Run(); if (result.has_value()) { std::cout << "non linearized:\n"; - pretty_printer.PrettyPrint(result.value().second, std::cout); + int count = 0; + for (auto& v : result->second) { + std::visit([&count](auto& a){ + count = std::max(count, a.thread_id); + }, v); + } + PrettyPrinter pretty_printer(count + 1); + pretty_printer.PrettyPrint(result->second, std::cout); return 1; } else { std::cout << "success!\n"; @@ -202,7 +209,7 @@ int Run(int argc, char *argv[]) { &Spec::cancel_t::Cancel); std::cout << "\n\n"; std::cout.flush(); - return TrapRun(std::move(scheduler), pretty_printer); + return TrapRun(std::move(scheduler)); } } // namespace ltest diff --git a/runtime/lib.cpp b/runtime/lib.cpp index 0cb8be77..cf4d5c0e 100644 --- a/runtime/lib.cpp +++ b/runtime/lib.cpp @@ -2,6 +2,9 @@ #include #include +#include +#include +#include #include #include #include @@ -18,7 +21,6 @@ std::optional virtual_thread_creation; std::optional virtual_thread_wait; - std::unordered_map futex_state{}; namespace ltest { @@ -69,13 +71,20 @@ extern "C" void CoroYield() { } extern "C" void CoroutineStatusChange(char* name, bool start) { - // assert(!coroutine_status.has_value()); + assert(!coroutine_status.has_value()); coroutine_status.emplace(name, start); CoroYield(); } -extern "C" void CreateNewVirtualThread(int id, char* name, void* func) { - virtual_thread_creation.emplace(name, reinterpret_cast(func), id); +extern "C" void CreateNewVirtualThread(int id, void* func) { + virtual_thread_creation.emplace(reinterpret_cast(func), id); + CoroYield(); +} + +extern "C" void VirtualThreadStartPoint(char* name, char* args) { + assert(virtual_thread_creation); + virtual_thread_creation->name = name; + virtual_thread_creation->args = std::string(args); CoroYield(); } @@ -86,14 +95,22 @@ extern "C" void WaitForThread(int* ids, int size) { CoroYield(); } -void CoroBase::Terminate() { +bool CoroBase::Terminate() { int tries = 0; while (!IsReturned()) { ++tries; Resume(); + // we don't care about this while terminating + coroutine_status.reset(); + virtual_thread_wait.reset(); + // we couldn't process before the spawned thread is spawned and finished + if (virtual_thread_creation) { + return false; + } assert(tries < 10000000 && "coroutine is spinning too long, possible wrong terminating order"); } + return true; } void Token::Reset() { parked = false; } diff --git a/test/codegen/CMakeLists.txt b/test/codegen/CMakeLists.txt new file mode 100644 index 00000000..0d462908 --- /dev/null +++ b/test/codegen/CMakeLists.txt @@ -0,0 +1,15 @@ + +find_package(Python3 REQUIRED) +# find_program(LIT_EXECUTABLE NAMES lit lit.py) +# if(NOT LIT_EXECUTABLE) +# message(FATAL_ERROR "Could not find lit testing tool") +# endif() + +# add_test( +# NAME "coyield" +# COMMAND ${LIT_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR} --filter-out +# ${CMAKE_CURRENT_SOURCE_DIR}/coyield/.* --verbose +# ) +# set_tests_properties("coyield" +# PROPERTIES +# LABELS "llvm-pass") \ No newline at end of file diff --git a/test/codegen/coyield/tests/thread_creation.cpp b/test/codegen/coyield/tests/thread_creation.cpp new file mode 100644 index 00000000..6be3e7d7 --- /dev/null +++ b/test/codegen/coyield/tests/thread_creation.cpp @@ -0,0 +1,41 @@ +// RUN: %check +#include +struct Promise; +// NOLINTBEGIN(readability-identifier-naming) +struct SimpleAwaitable { + bool await_ready() const noexcept { return false; } + + bool await_suspend(std::coroutine_handle<> h) const noexcept { + h.resume(); + return true; + } + + void await_resume() const noexcept {} +}; +struct Coroutine : std::coroutine_handle { + using promise_type = ::Promise; + auto operator co_await() const { return SimpleAwaitable{}; } +}; + +struct Promise { + Coroutine get_return_object() { return {Coroutine::from_promise(*this)}; } + std::suspend_always initial_suspend() noexcept { return {}; } + std::suspend_always final_suspend() noexcept { return {}; } + void return_void() {} + void unhandled_exception() {} +}; +// NOLINTEND(readability-identifier-naming) + +//Let's omit realization for simplicity +struct Waiter { + void Add(Coroutine coro) {} + SimpleAwaitable Wait() { return {}; } +}; + +Coroutine DoWork(int i) { return {}; } +Coroutine Work(int i) { + Waiter w; + w.Add(DoWork(i)); + w.Add(DoWork(i)); + co_await w.Wait(); +} \ No newline at end of file diff --git a/test/codegen/coyield/tests/thread_creation.cpp.yml b/test/codegen/coyield/tests/thread_creation.cpp.yml new file mode 100644 index 00000000..04007e81 --- /dev/null +++ b/test/codegen/coyield/tests/thread_creation.cpp.yml @@ -0,0 +1,16 @@ +- Action: Spawn + HasThis: True + CreationId: 0 + Name: go + Place: + Function: Waiter::Add(Coroutine) +- Action: Wait + WaitsFor: [0] + Name: wait + Place: + Function: Waiter::Wait() +- Action: SpawnedCoro + ArgsFun: PrintInt + Name: DoWork + Place: + Function: DoWork(int) \ No newline at end of file diff --git a/verifying/specs/unique_args.h b/verifying/specs/unique_args.h index afa13996..35a0c329 100644 --- a/verifying/specs/unique_args.h +++ b/verifying/specs/unique_args.h @@ -23,17 +23,18 @@ struct UniqueArgsRef { return {called == limit ? std::exchange(called, 0) : std::optional(), GetDefaultCompator>(), Print}; } - + void DoWork() { return; } using MethodT = std::function; static auto GetMethods() { MethodT get = [](UniqueArgsRef *l, void *args) { auto real_args = reinterpret_cast *>(args); return l->Get(std::get<0>(*real_args)); }; - - return std::map{ - {"Get", get}, + MethodT do_work = [](UniqueArgsRef *l, void *args) { + l->DoWork(); + return void_v; }; + return std::map{{"Get", get}, {"DoWork", do_work}}; } }; diff --git a/verifying/targets/dynthreads_unique_args.cpp b/verifying/targets/dynthreads_unique_args.cpp index 44ae8078..8552df18 100644 --- a/verifying/targets/dynthreads_unique_args.cpp +++ b/verifying/targets/dynthreads_unique_args.cpp @@ -3,8 +3,10 @@ #include #include #include +#include #include "../specs/unique_args.h" +#include "runtime/include/lib.h" static std::vector used(limit, false); static std::vector state(limit, 0); @@ -34,44 +36,48 @@ struct Promise { void unhandled_exception() {} }; // NOLINTEND(readability-identifier-naming) - struct Waiter { - void Add(const Coroutine& coro) { - list.push_back(&coro); + void Add(Coroutine&& coro) { + // list.push_back(coro); + coro.resume(); } - SimpleAwaitable Wait() { - for (auto& a : list) { - a->resume(); - } - return {}; } - std::vector list; + SimpleAwaitable Wait() { return {}; } + std::vector list; }; Coroutine DoWork(int i) { state[i]++; - return {}; + // std::cerr << "updated" << i << "\n"; + co_return; } + Coroutine Work(int i) { Waiter w; w.Add(DoWork(i)); - state[i]++; co_await w.Wait(); + assert(state[i] == 1); + co_return; } +static std::string str; + +extern "C" char* PrintInt(int i) { + str = std::to_string(i); + return str.data(); +}; struct DynThreadsTest { DynThreadsTest() {} ValueWrapper Get(size_t i) { assert(!used[i]); used[i] = true; - auto coro= Work(i); + bool last = std::count(used.begin(), used.end(), true) == limit; + auto coro = Work(i); coro.resume(); auto l = [this]() { - Reset(); + std::fill(used.begin(), used.end(), false); return limit; }; - return {std::count(state.begin(), state.end(), 2) == limit - ? l() - : std::optional(), + return {last ? l() : std::optional(), GetDefaultCompator>(), Print}; } void Reset() { diff --git a/verifying/targets/dynthreads_unique_args.yml b/verifying/targets/dynthreads_unique_args.yml index 3b43cbe4..9a8f6fd5 100644 --- a/verifying/targets/dynthreads_unique_args.yml +++ b/verifying/targets/dynthreads_unique_args.yml @@ -1,11 +1,14 @@ - Action: Spawn CreationId: 0 - Name: go + HasThis: True Place: - Function: Waiter::Add(Coroutine const&) + Function: Waiter::Add(Coroutine&&) - Action: Wait WaitsFor: [0] - Name: wait Place: Function: Waiter::Wait() - +- Action: SpawnedCoro + ArgsFun: PrintInt + Name: DoWork + Place: + Function: DoWork(int) \ No newline at end of file From ddff2dbb8b871b841d6e3e9ec771a880c9fbd107 Mon Sep 17 00:00:00 2001 From: Nikolay Cherkashin Date: Mon, 9 Jun 2025 09:19:36 +0000 Subject: [PATCH 08/12] bump version --- .github/workflows/run-tests.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/run-tests.yaml b/.github/workflows/run-tests.yaml index 264f1163..654309cf 100644 --- a/.github/workflows/run-tests.yaml +++ b/.github/workflows/run-tests.yaml @@ -9,7 +9,7 @@ jobs: run: shell: bash container: - image: silkeh/clang:18 + image: silkeh/clang:19 options: --user root timeout-minutes: 10 steps: @@ -29,7 +29,7 @@ jobs: run: shell: bash container: - image: silkeh/clang:18 + image: silkeh/clang:19 options: --user root timeout-minutes: 10 steps: @@ -49,7 +49,7 @@ jobs: run: shell: bash container: - image: silkeh/clang:18 + image: silkeh/clang:19 options: --user root timeout-minutes: 10 steps: From 862ec87d9402aba20cfbd70fff1c3197c0c7724a Mon Sep 17 00:00:00 2001 From: Nikolay Cherkashin Date: Mon, 9 Jun 2025 21:57:04 +0000 Subject: [PATCH 09/12] more tests --- codegen/coyieldpass.cpp | 1 - test/codegen/CMakeLists.txt | 24 ++--- test/codegen/coyield/tests/bool_suspend.cpp | 5 +- .../coyield/tests/bool_suspend.cpp.yml | 4 + .../codegen/coyield/tests/thread_creation.cpp | 15 ++- .../coyield/tests/thread_creation.cpp.yml | 4 +- test/codegen/coyield/tests/void_suspend.cpp | 41 ++++---- .../coyield/tests/void_suspend.cpp.yml | 4 + verifying/CMakeLists.txt | 2 +- verifying/specs/communication.h | 76 +++++++++++++++ verifying/specs/support_coro.h | 33 +++++++ verifying/targets/CMakeLists.txt | 1 + verifying/targets/counique_args.cpp | 40 +------- verifying/targets/nonlinear_communication.cpp | 96 +++++++++++++++++++ verifying/targets/nonlinear_communication.yml | 8 ++ 15 files changed, 273 insertions(+), 81 deletions(-) create mode 100644 verifying/specs/communication.h create mode 100644 verifying/specs/support_coro.h create mode 100644 verifying/targets/nonlinear_communication.cpp create mode 100644 verifying/targets/nonlinear_communication.yml diff --git a/codegen/coyieldpass.cpp b/codegen/coyieldpass.cpp index 3f79c76e..fd217551 100644 --- a/codegen/coyieldpass.cpp +++ b/codegen/coyieldpass.cpp @@ -38,7 +38,6 @@ #include "llvm/Pass.h" #include "llvm/Passes/PassBuilder.h" #include "llvm/Passes/PassPlugin.h" -#include "llvm/Transforms/Coroutines/CoroSplit.h" #include "llvm/Transforms/Utils/Cloning.h" using namespace llvm; using Builder = IRBuilder<>; diff --git a/test/codegen/CMakeLists.txt b/test/codegen/CMakeLists.txt index 0d462908..62de3fc1 100644 --- a/test/codegen/CMakeLists.txt +++ b/test/codegen/CMakeLists.txt @@ -1,15 +1,15 @@ find_package(Python3 REQUIRED) -# find_program(LIT_EXECUTABLE NAMES lit lit.py) -# if(NOT LIT_EXECUTABLE) -# message(FATAL_ERROR "Could not find lit testing tool") -# endif() +find_program(LIT_EXECUTABLE NAMES lit lit.py) +if(NOT LIT_EXECUTABLE) + message(FATAL_ERROR "Could not find lit testing tool") +endif() -# add_test( -# NAME "coyield" -# COMMAND ${LIT_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR} --filter-out -# ${CMAKE_CURRENT_SOURCE_DIR}/coyield/.* --verbose -# ) -# set_tests_properties("coyield" -# PROPERTIES -# LABELS "llvm-pass") \ No newline at end of file +add_test( + NAME "coyield" + COMMAND ${LIT_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR} --filter-out + ${CMAKE_CURRENT_SOURCE_DIR}/coyield/.* --verbose +) +set_tests_properties("coyield" + PROPERTIES + LABELS "llvm-pass") \ No newline at end of file diff --git a/test/codegen/coyield/tests/bool_suspend.cpp b/test/codegen/coyield/tests/bool_suspend.cpp index 2ced175c..6fe29198 100644 --- a/test/codegen/coyield/tests/bool_suspend.cpp +++ b/test/codegen/coyield/tests/bool_suspend.cpp @@ -30,7 +30,8 @@ CoroTask myCoroutine2() { co_return; } -// CHECK: call CoroTask myCoroutine() { - co_await myCoroutine2(); + // CHECK: call void @CoroutineStatusChange(ptr [[name:@[0-9]+]], i1 true) + co_await myCoroutine2(); + // CHECK: call void @CoroutineStatusChange(ptr [[name]], i1 false) } \ No newline at end of file diff --git a/test/codegen/coyield/tests/bool_suspend.cpp.yml b/test/codegen/coyield/tests/bool_suspend.cpp.yml index e69de29b..aac6c521 100644 --- a/test/codegen/coyield/tests/bool_suspend.cpp.yml +++ b/test/codegen/coyield/tests/bool_suspend.cpp.yml @@ -0,0 +1,4 @@ +- Action: Coro + Name: test + Place: + Function: myCoroutine2() diff --git a/test/codegen/coyield/tests/thread_creation.cpp b/test/codegen/coyield/tests/thread_creation.cpp index 6be3e7d7..96067902 100644 --- a/test/codegen/coyield/tests/thread_creation.cpp +++ b/test/codegen/coyield/tests/thread_creation.cpp @@ -1,5 +1,13 @@ // RUN: %check #include +#include +static std::string str; + +extern "C" char* PrintInt(int i) { + str = std::to_string(i); + return str.data(); +}; + struct Promise; // NOLINTBEGIN(readability-identifier-naming) struct SimpleAwaitable { @@ -26,16 +34,17 @@ struct Promise { }; // NOLINTEND(readability-identifier-naming) -//Let's omit realization for simplicity +// Let's omit realization for simplicity struct Waiter { void Add(Coroutine coro) {} SimpleAwaitable Wait() { return {}; } }; -Coroutine DoWork(int i) { return {}; } +Coroutine DoWork(int i) { co_return; } Coroutine Work(int i) { Waiter w; + // CHECK: call w.Add(DoWork(i)); - w.Add(DoWork(i)); + // CHECK: call co_await w.Wait(); } \ No newline at end of file diff --git a/test/codegen/coyield/tests/thread_creation.cpp.yml b/test/codegen/coyield/tests/thread_creation.cpp.yml index 04007e81..0b08565b 100644 --- a/test/codegen/coyield/tests/thread_creation.cpp.yml +++ b/test/codegen/coyield/tests/thread_creation.cpp.yml @@ -1,12 +1,10 @@ - Action: Spawn - HasThis: True CreationId: 0 - Name: go + HasThis: True Place: Function: Waiter::Add(Coroutine) - Action: Wait WaitsFor: [0] - Name: wait Place: Function: Waiter::Wait() - Action: SpawnedCoro diff --git a/test/codegen/coyield/tests/void_suspend.cpp b/test/codegen/coyield/tests/void_suspend.cpp index 00904588..196d13cb 100644 --- a/test/codegen/coyield/tests/void_suspend.cpp +++ b/test/codegen/coyield/tests/void_suspend.cpp @@ -1,34 +1,27 @@ // RUN: %check #include struct SimpleAwaitable { - bool await_ready() const noexcept { - return false; - } - - void await_suspend(std::coroutine_handle<> h) const noexcept { - h.resume(); - } + bool await_ready() const noexcept { return false; } - void await_resume() const noexcept { - } + void await_suspend(std::coroutine_handle<> h) const noexcept { h.resume(); } + + void await_resume() const noexcept {} }; struct CoroTask { - struct promise_type { - CoroTask get_return_object() { return {}; } - std::suspend_never initial_suspend() { return {}; } - std::suspend_never final_suspend() noexcept { return {}; } - void return_void() {} - void unhandled_exception() {} - }; - auto operator co_await() const { return SimpleAwaitable{}; } - + struct promise_type { + CoroTask get_return_object() { return {}; } + std::suspend_never initial_suspend() { return {}; } + std::suspend_never final_suspend() noexcept { return {}; } + void return_void() {} + void unhandled_exception() {} + }; + auto operator co_await() const { return SimpleAwaitable{}; } }; -CoroTask myCoroutine2() { - co_return; -} -// CHECK: call -CoroTask myCoroutine() { - co_await myCoroutine2(); +CoroTask myCoroutine2() { co_return; } +CoroTask myCoroutine() { + // CHECK: call void @CoroutineStatusChange(ptr [[name:@[0-9]+]], i1 true) + co_await myCoroutine2(); + // CHECK: call void @CoroutineStatusChange(ptr [[name]], i1 false) } \ No newline at end of file diff --git a/test/codegen/coyield/tests/void_suspend.cpp.yml b/test/codegen/coyield/tests/void_suspend.cpp.yml index e69de29b..aac6c521 100644 --- a/test/codegen/coyield/tests/void_suspend.cpp.yml +++ b/test/codegen/coyield/tests/void_suspend.cpp.yml @@ -0,0 +1,4 @@ +- Action: Coro + Name: test + Place: + Function: myCoroutine2() diff --git a/verifying/CMakeLists.txt b/verifying/CMakeLists.txt index 4252abb7..efe61674 100644 --- a/verifying/CMakeLists.txt +++ b/verifying/CMakeLists.txt @@ -46,4 +46,4 @@ function(add_integration_test test_name label fail) endfunction() add_subdirectory(targets) -add_subdirectory(blocking) +add_subdirectory(net) diff --git a/verifying/specs/communication.h b/verifying/specs/communication.h new file mode 100644 index 00000000..82a99e30 --- /dev/null +++ b/verifying/specs/communication.h @@ -0,0 +1,76 @@ +#include +#include +#include +#include +#include + +#include "../../runtime/include/verifying.h" +#include "support_coro.h" + +constexpr int writer_count = 2; +constexpr int start_id = 100; +namespace spec { + +struct CommunicationRef { + std::deque buf; + int id = start_id; + CommunicationRef() {} + CommunicationRef &operator=(const CommunicationRef &oth) { return *this; } + + void Send(int i) { + buf.push_back(id); + id++; + buf.push_back(i); + } + + void Receive() { + int m_id = buf.front(); + buf.pop_front(); + int message = buf.front(); + buf.pop_front(); + } + using MethodT = std::function; + static auto GetMethods() { + MethodT receive = [](CommunicationRef *l, void *args) { + l->Receive(); + return void_v; + }; + MethodT send = [](CommunicationRef *l, void *args) { + auto real_args = reinterpret_cast *>(args); + l->Send(std::get<0>(*real_args)); + return void_v; + }; + return std::map{{"Send", send}, {"Receive", receive}}; + } +}; + +struct UniqueArgsHash { + size_t operator()(const CommunicationRef &r) const { + int res = 0; + for (int elem : r.buf) { + res += elem; + } + return res; + } +}; +struct UniqueArgsEquals { + bool operator()(const CommunicationRef &lhs, + const CommunicationRef &rhs) const { + return lhs.buf == rhs.buf; + } +}; +struct UniqueArgsOptionsOverride { + static ltest::DefaultOptions GetOptions() { + return {.threads = writer_count + 1, + .tasks = writer_count + 1, + .switches = 100000000, + .rounds = 10000, + .depth = 1, + .forbid_all_same = false, + .verbose = false, + .strategy = "tla", + .weights = ""}; + } +}; + +} // namespace spec diff --git a/verifying/specs/support_coro.h b/verifying/specs/support_coro.h new file mode 100644 index 00000000..35b02873 --- /dev/null +++ b/verifying/specs/support_coro.h @@ -0,0 +1,33 @@ +#include +struct Promise; + + +// NOLINTBEGIN(readability-identifier-naming) +struct SimpleAwaitable { + bool await_ready() const noexcept { + return false; + } + + bool await_suspend(std::coroutine_handle<> h) const noexcept { + h.resume(); + return true; + } + + void await_resume() const noexcept { + } + +}; +struct Coroutine : std::coroutine_handle { + using promise_type = ::Promise; + auto operator co_await() const { return SimpleAwaitable{}; } +}; + + +struct Promise { + Coroutine get_return_object() { return {Coroutine::from_promise(*this)}; } + std::suspend_never initial_suspend() noexcept { return {}; } + std::suspend_always final_suspend() noexcept { return {}; } + void return_void() {} + void unhandled_exception() {} +}; +// NOLINTEND(readability-identifier-naming) \ No newline at end of file diff --git a/verifying/targets/CMakeLists.txt b/verifying/targets/CMakeLists.txt index 29dc18bd..8a3e8028 100644 --- a/verifying/targets/CMakeLists.txt +++ b/verifying/targets/CMakeLists.txt @@ -17,6 +17,7 @@ set (SOURCE_TARGET_CO_LIST unique_args.cpp counique_args.cpp dynthreads_unique_args.cpp + nonlinear_communication.cpp ) foreach(source_name ${SOURCE_TARGET_LIST}) diff --git a/verifying/targets/counique_args.cpp b/verifying/targets/counique_args.cpp index 6a35b6bd..da89c341 100644 --- a/verifying/targets/counique_args.cpp +++ b/verifying/targets/counique_args.cpp @@ -5,38 +5,8 @@ #include #include "../specs/unique_args.h" +#include "../specs/support_coro.h" -struct Promise; - - -// NOLINTBEGIN(readability-identifier-naming) -struct SimpleAwaitable { - bool await_ready() const noexcept { - return false; - } - - bool await_suspend(std::coroutine_handle<> h) const noexcept { - h.resume(); - return true; - } - - void await_resume() const noexcept { - } -}; -struct Coroutine : std::coroutine_handle { - using promise_type = ::Promise; - auto operator co_await() const { return SimpleAwaitable{}; } -}; - - -struct Promise { - Coroutine get_return_object() { return {Coroutine::from_promise(*this)}; } - std::suspend_never initial_suspend() noexcept { return {}; } - std::suspend_always final_suspend() noexcept { return {}; } - void return_void() {} - void unhandled_exception() {} -}; -// NOLINTEND(readability-identifier-naming) static std::vector used(limit, false); static std::vector done(limit, false); @@ -50,8 +20,8 @@ Coroutine CoFun(int i) { co_await CoWork(i); } -struct CoUniqueArgsTest { - CoUniqueArgsTest() {} +struct NonLinearCommunicationTest { + NonLinearCommunicationTest() {} ValueWrapper Get(size_t i) { assert(!used[i]); used[i] = true; @@ -80,10 +50,10 @@ auto GenerateArgs(size_t thread_num) { assert(false && "extra call"); } -target_method(GenerateArgs, int, CoUniqueArgsTest, Get, size_t); +target_method(GenerateArgs, int, NonLinearCommunicationTest, Get, size_t); using SpecT = - ltest::Spec; LTEST_ENTRYPOINT(SpecT); diff --git a/verifying/targets/nonlinear_communication.cpp b/verifying/targets/nonlinear_communication.cpp new file mode 100644 index 00000000..917bf470 --- /dev/null +++ b/verifying/targets/nonlinear_communication.cpp @@ -0,0 +1,96 @@ +#include +#include +#include +#include +#include + +#include "../specs/communication.h" +#include "runtime/include/value_wrapper.h" + +static std::vector used(writer_count, false); + +static int write_id = start_id; +static int read_id = start_id; +struct NonLinearCommunicationTest { + std::deque buf; + NonLinearCommunicationTest() {} + // TODO better support return values + + Coroutine SendHeader(int r) { + buf.push_back(r); + co_return; + } + + Coroutine SendBody(int r) { + buf.push_back(r); + co_return; + } + + Coroutine SendImpl(int i) { + co_await SendHeader(write_id); + co_await SendBody(i); + write_id++; + co_return; + } + void Send(int i) { + assert(!used[i]); + used[i] = true; + SendImpl(i); + } + void Receive() { + for (int i = 0; i < writer_count; i++) { + buf.pop_front(); + int r = buf.front(); + buf.pop_front(); + assert(r < writer_count); + } + } + void Reset() { + std::fill(used.begin(), used.end(), false); + write_id = start_id; + buf.clear(); + } +}; + +auto GenerateArgs(size_t thread_num) { + for (size_t i = 0; i < writer_count; i++) { + if (!used[i]) { + return ltest::generators::makeSingleArg(i); + } + } + assert(false && "extra call"); +} +static constexpr std::string_view send_func = "Send"; +static constexpr std::string_view receive_func = "Receive"; +static constexpr size_t output_thread = writer_count; +class NoReadBeforeWrite { + public: + bool Verify(CreatedTaskMetaData task) { + // output from the pipe is the last thread + if (task.name == send_func && task.thread_id == output_thread) { + return false; + } + if (task.name == receive_func && task.thread_id != output_thread) { + return false; + } + // // no receive before send + if (task.name == receive_func && task.is_new && + write_id - start_id < writer_count) { + return false; + } + return true; + } + void OnFinished(TaskWithMetaData task) {} + void Reset() {} + void UpdateState(std::string_view coro_name, int thread_id, bool) {} +}; + +target_method(GenerateArgs, void, NonLinearCommunicationTest, Send, int); +target_method(ltest::generators::genEmpty, void, NonLinearCommunicationTest, + Receive); + +using SpecT = ltest::Spec; + +LTEST_ENTRYPOINT_CONSTRAINT(SpecT, NoReadBeforeWrite); diff --git a/verifying/targets/nonlinear_communication.yml b/verifying/targets/nonlinear_communication.yml new file mode 100644 index 00000000..00393559 --- /dev/null +++ b/verifying/targets/nonlinear_communication.yml @@ -0,0 +1,8 @@ +- Action: Coro + Name: SendHeader + Place: + Function: NonLinearCommunicationTest::SendHeader(int) +- Action: Coro + Name: SendBody + Place: + Function: NonLinearCommunicationTest::SendBody(int) \ No newline at end of file From 6ce60ae3b608565228a31049a7c78f533ad08c4c Mon Sep 17 00:00:00 2001 From: Nikolay Cherkashin Date: Mon, 9 Jun 2025 22:07:34 +0000 Subject: [PATCH 10/12] temporarly disabled codegen tests(not sure how to best use lit ) --- .github/workflows/run-tests.yaml | 40 ++++++++++++++++---------------- test/CMakeLists.txt | 2 +- verifying/CMakeLists.txt | 2 +- 3 files changed, 22 insertions(+), 22 deletions(-) diff --git a/.github/workflows/run-tests.yaml b/.github/workflows/run-tests.yaml index 654309cf..ece240a2 100644 --- a/.github/workflows/run-tests.yaml +++ b/.github/workflows/run-tests.yaml @@ -23,26 +23,26 @@ jobs: cmake --build build --target lin_check_test - name: Run lin check test run: ctest --test-dir build -R "^LinearizabilityCheckerCounterTest" -V - pass-tests: - runs-on: ubuntu-latest - defaults: - run: - shell: bash - container: - image: silkeh/clang:19 - options: --user root - timeout-minutes: 10 - steps: - - name: Install deps - run: apt update && apt install -y git ninja-build valgrind libboost-context-dev libgflags-dev - - name: Check out repository code - uses: actions/checkout@v4 - - name: Build - run: | - cmake -G Ninja -B build -DCMAKE_BUILD_TYPE=RelWithAssert - cmake --build build --target CoYieldPass - - name: Run lin check test - run: ctest --test-dir build -L llvm-pass -V + # pass-tests: + # runs-on: ubuntu-latest + # defaults: + # run: + # shell: bash + # container: + # image: silkeh/clang:19 + # options: --user root + # timeout-minutes: 10 + # steps: + # - name: Install deps + # run: apt update && apt install -y git ninja-build valgrind libboost-context-dev libgflags-dev + # - name: Check out repository code + # uses: actions/checkout@v4 + # - name: Build + # run: | + # cmake -G Ninja -B build -DCMAKE_BUILD_TYPE=RelWithAssert + # cmake --build build --target CoYieldPass + # - name: Run lin check test + # run: ctest --test-dir build -L llvm-pass -V verifying-test: runs-on: ubuntu-latest defaults: diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 65941679..7d2b3efd 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -1,2 +1,2 @@ add_subdirectory(runtime) -add_subdirectory(codegen) \ No newline at end of file +# add_subdirectory(codegen) \ No newline at end of file diff --git a/verifying/CMakeLists.txt b/verifying/CMakeLists.txt index efe61674..4252abb7 100644 --- a/verifying/CMakeLists.txt +++ b/verifying/CMakeLists.txt @@ -46,4 +46,4 @@ function(add_integration_test test_name label fail) endfunction() add_subdirectory(targets) -add_subdirectory(net) +add_subdirectory(blocking) From 76495a16181832fd74c209d981a1c39526cbb31f Mon Sep 17 00:00:00 2001 From: Nikolay Cherkashin Date: Mon, 9 Jun 2025 22:11:47 +0000 Subject: [PATCH 11/12] shift + f6 is not always a good solution --- verifying/targets/counique_args.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/verifying/targets/counique_args.cpp b/verifying/targets/counique_args.cpp index da89c341..e890135a 100644 --- a/verifying/targets/counique_args.cpp +++ b/verifying/targets/counique_args.cpp @@ -53,7 +53,7 @@ auto GenerateArgs(size_t thread_num) { target_method(GenerateArgs, int, NonLinearCommunicationTest, Get, size_t); using SpecT = - ltest::Spec; LTEST_ENTRYPOINT(SpecT); From 2d310e231e434027d25d22e93775b219b69db30c Mon Sep 17 00:00:00 2001 From: Nikolay Cherkashin Date: Tue, 10 Jun 2025 00:05:47 +0000 Subject: [PATCH 12/12] +mock --- test/runtime/stackfulltask_mock.h | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test/runtime/stackfulltask_mock.h b/test/runtime/stackfulltask_mock.h index 32fd28a2..3cf8de86 100644 --- a/test/runtime/stackfulltask_mock.h +++ b/test/runtime/stackfulltask_mock.h @@ -18,5 +18,9 @@ class MockTask : public CoroBase { MOCK_METHOD(bool, IsSuspended, (), (const)); MOCK_METHOD(void, Terminate, (), ()); MOCK_METHOD(void, SetToken, (std::shared_ptr), ()); + MOCK_METHOD(void, SetStrArgsAndName, + (std::string_view, + std::function(std::shared_ptr)>), + (override)); virtual ~MockTask() { is_returned = true; } };