diff options
| author | Benoit Germain <benoit.germain@ubisoft.com> | 2025-06-26 09:18:54 +0200 |
|---|---|---|
| committer | Benoit Germain <benoit.germain@ubisoft.com> | 2025-06-26 09:18:54 +0200 |
| commit | d7d756e30680bcff036118b47ac45b740e020ea2 (patch) | |
| tree | 3e2a26409154760d66092e6e04a9fcb4ad4ed02a | |
| parent | 4f5fa626c0279f5aefac477a29702c43a6754c5a (diff) | |
| download | lanes-d7d756e30680bcff036118b47ac45b740e020ea2.tar.gz lanes-d7d756e30680bcff036118b47ac45b740e020ea2.tar.bz2 lanes-d7d756e30680bcff036118b47ac45b740e020ea2.zip | |
Preparation for lane:close() and correct to-be-closed variables
* lane:join() can no longer be used to read yielded values
* same with lane indexing
* stub for lane:close(), similar to coroutine.close() (not implemented yet)
* preparing tests for to-be-closed variables in yielded coroutine lanes
* yielded lanes unlock and terminate properly at Lanes shutdown
| -rw-r--r-- | src/lane.cpp | 79 | ||||
| -rw-r--r-- | unit_tests/UnitTests.vcxproj | 4 | ||||
| -rw-r--r-- | unit_tests/UnitTests.vcxproj.filters | 8 | ||||
| -rw-r--r-- | unit_tests/lane_tests.cpp | 9 | ||||
| -rw-r--r-- | unit_tests/scripts/coro/basics.lua | 99 | ||||
| -rw-r--r-- | unit_tests/scripts/coro/collect_yielded_lane.lua | 72 | ||||
| -rw-r--r-- | unit_tests/scripts/coro/error_handling.lua | 6 | ||||
| -rw-r--r-- | unit_tests/scripts/coro/regular_function.lua | 38 | ||||
| -rw-r--r-- | unit_tests/scripts/coro/yielding_function.lua | 107 |
9 files changed, 299 insertions, 123 deletions
diff --git a/src/lane.cpp b/src/lane.cpp index c6ea358..f8685e9 100644 --- a/src/lane.cpp +++ b/src/lane.cpp | |||
| @@ -129,8 +129,13 @@ static LUAG_FUNC(lane_join) | |||
| 129 | raise_luaL_argerror(L_, StackIndex{ 2 }, "incorrect duration type"); | 129 | raise_luaL_argerror(L_, StackIndex{ 2 }, "incorrect duration type"); |
| 130 | } | 130 | } |
| 131 | 131 | ||
| 132 | // it is forbidden to join a suspended coroutine | ||
| 133 | if (_lane->status.load(std::memory_order_acquire) == Lane::Suspended) { | ||
| 134 | raise_luaL_error(L_, "cannot join a suspended coroutine"); | ||
| 135 | } | ||
| 136 | |||
| 132 | lua_settop(L_, 1); // L_: lane | 137 | lua_settop(L_, 1); // L_: lane |
| 133 | bool const _done{ !_lane->thread.joinable() || _lane->waitForCompletion(_until, true) }; | 138 | bool const _done{ !_lane->thread.joinable() || _lane->waitForCompletion(_until, false) }; |
| 134 | 139 | ||
| 135 | if (!_done) { | 140 | if (!_done) { |
| 136 | lua_pushnil(L_); // L_: lane nil | 141 | lua_pushnil(L_); // L_: lane nil |
| @@ -145,7 +150,11 @@ static LUAG_FUNC(lane_join) | |||
| 145 | int const _stored{ _lane->storeResults(L_) }; | 150 | int const _stored{ _lane->storeResults(L_) }; |
| 146 | STACK_GROW(L_, std::max(3, _stored + 1)); | 151 | STACK_GROW(L_, std::max(3, _stored + 1)); |
| 147 | switch (_lane->status.load(std::memory_order_acquire)) { | 152 | switch (_lane->status.load(std::memory_order_acquire)) { |
| 148 | case Lane::Suspended: // got yielded values | 153 | case Lane::Suspended: |
| 154 | // TODO: maybe this can happen if we have a to-be-closed handle on a suspended lane? TO BE TESTED! | ||
| 155 | raise_luaL_error(L_, "INTERNAL ERROR: should not join a suspended coroutine"); | ||
| 156 | [[fallthrough]]; | ||
| 157 | |||
| 149 | case Lane::Done: // got regular return values | 158 | case Lane::Done: // got regular return values |
| 150 | if (_stored > 0) { | 159 | if (_stored > 0) { |
| 151 | lua_getiuservalue(L_, StackIndex{ 1 }, UserValueIndex{ 1 }); // L_: lane {uv} | 160 | lua_getiuservalue(L_, StackIndex{ 1 }, UserValueIndex{ 1 }); // L_: lane {uv} |
| @@ -210,7 +219,7 @@ LUAG_FUNC(lane_resume) | |||
| 210 | Lane* const _lane{ ToLane(L_, kIdxSelf) }; | 219 | Lane* const _lane{ ToLane(L_, kIdxSelf) }; |
| 211 | lua_State* const _L2{ _lane->L }; | 220 | lua_State* const _L2{ _lane->L }; |
| 212 | 221 | ||
| 213 | // wait until the lane yields or returns | 222 | // wait until the lane yields or returns |
| 214 | std::ignore = _lane->waitForCompletion(std::chrono::time_point<std::chrono::steady_clock>::max(), true); | 223 | std::ignore = _lane->waitForCompletion(std::chrono::time_point<std::chrono::steady_clock>::max(), true); |
| 215 | 224 | ||
| 216 | if (_lane->status.load(std::memory_order_acquire) != Lane::Suspended) { | 225 | if (_lane->status.load(std::memory_order_acquire) != Lane::Suspended) { |
| @@ -259,11 +268,17 @@ static int lane_index_number(lua_State* L_) | |||
| 259 | int const _key{ static_cast<int>(lua_tointeger(L_, 2)) }; | 268 | int const _key{ static_cast<int>(lua_tointeger(L_, 2)) }; |
| 260 | lua_pop(L_, 1); // L_: lane | 269 | lua_pop(L_, 1); // L_: lane |
| 261 | 270 | ||
| 271 | // wait until the lane finishes or is suspended | ||
| 262 | std::chrono::time_point<std::chrono::steady_clock> _until{ std::chrono::time_point<std::chrono::steady_clock>::max() }; | 272 | std::chrono::time_point<std::chrono::steady_clock> _until{ std::chrono::time_point<std::chrono::steady_clock>::max() }; |
| 263 | if (!_lane->waitForCompletion(_until, true)) { | 273 | if (!_lane->waitForCompletion(_until, true)) { |
| 264 | raise_luaL_error(L_, "INTERNAL ERROR: Failed to join"); | 274 | raise_luaL_error(L_, "INTERNAL ERROR: Failed to join"); |
| 265 | } | 275 | } |
| 266 | 276 | ||
| 277 | // it is forbidden to index a suspended coroutine. if you want to read yielded values, use lane:resume() | ||
| 278 | if (_lane->status.load(std::memory_order_acquire) == Lane::Suspended) { | ||
| 279 | raise_luaL_error(L_, "cannot index a suspended coroutine"); | ||
| 280 | } | ||
| 281 | |||
| 267 | // make sure results are stored | 282 | // make sure results are stored |
| 268 | int const _stored{ _lane->storeResults(L_) }; | 283 | int const _stored{ _lane->storeResults(L_) }; |
| 269 | if (_key > _stored) { | 284 | if (_key > _stored) { |
| @@ -485,6 +500,7 @@ static int PushStackTrace(lua_State* const L_, Lane::ErrorTraceLevel const error | |||
| 485 | StackIndex const _top{ lua_gettop(L_) }; | 500 | StackIndex const _top{ lua_gettop(L_) }; |
| 486 | switch (rc_) { | 501 | switch (rc_) { |
| 487 | case LuaError::OK: // no error, body return values are on the stack | 502 | case LuaError::OK: // no error, body return values are on the stack |
| 503 | case LuaError::YIELD: | ||
| 488 | break; | 504 | break; |
| 489 | 505 | ||
| 490 | case LuaError::ERRRUN: // cancellation or a runtime error | 506 | case LuaError::ERRRUN: // cancellation or a runtime error |
| @@ -707,7 +723,7 @@ static void lane_main(Lane* const lane_) | |||
| 707 | LuaError _rc{ LuaError::ERRRUN }; | 723 | LuaError _rc{ LuaError::ERRRUN }; |
| 708 | if (lane_->status.load(std::memory_order_acquire) == Lane::Pending) { // nothing wrong happened during preparation, we can work | 724 | if (lane_->status.load(std::memory_order_acquire) == Lane::Pending) { // nothing wrong happened during preparation, we can work |
| 709 | // At this point, the lane function and arguments are on the stack, possibly preceded by the error handler | 725 | // At this point, the lane function and arguments are on the stack, possibly preceded by the error handler |
| 710 | int const _errorHandlerCount{ lane_->errorHandlerCount() }; | 726 | int const _errorHandlerCount{ lane_->errorHandlerCount() }; // no error handler for coroutines, ever. |
| 711 | int _nargs{ lua_gettop(_L) - 1 - _errorHandlerCount }; | 727 | int _nargs{ lua_gettop(_L) - 1 - _errorHandlerCount }; |
| 712 | { | 728 | { |
| 713 | std::unique_lock _guard{ lane_->doneMutex }; | 729 | std::unique_lock _guard{ lane_->doneMutex }; |
| @@ -720,27 +736,40 @@ static void lane_main(Lane* const lane_) | |||
| 720 | lane_->nresults = lua_gettop(_L) - _errorHandlerCount; | 736 | lane_->nresults = lua_gettop(_L) - _errorHandlerCount; |
| 721 | } else { | 737 | } else { |
| 722 | // S and L are different: we run as a coroutine in Lua thread L created in state S | 738 | // S and L are different: we run as a coroutine in Lua thread L created in state S |
| 739 | bool _again{ true }; | ||
| 723 | do { | 740 | do { |
| 724 | // starting with Lua 5.4, lua_resume can leave more stuff on the stack below the actual yielded values. | 741 | // starting with Lua 5.4, lua_resume can leave more stuff on the stack below the actual yielded values. |
| 725 | // that's why we have lane_->nresults | 742 | // that's why we have lane_->nresults |
| 726 | _rc = luaG_resume(_L, nullptr, _nargs, &lane_->nresults); // L: eh? ... retvals|err... | 743 | _rc = luaG_resume(_L, nullptr, _nargs, &lane_->nresults); // L: ... retvals|err... |
| 727 | if (_rc == LuaError::YIELD) { | 744 | if (_rc == LuaError::YIELD) { |
| 745 | // on the stack we find the values pushed by lane:resume() | ||
| 746 | _nargs = lua_gettop(_L); | ||
| 747 | if (std::unique_lock _guard{ lane_->doneMutex }; true) | ||
| 748 | { | ||
| 728 | // change our status to suspended, and wait until someone wants us to resume | 749 | // change our status to suspended, and wait until someone wants us to resume |
| 729 | std::unique_lock _guard{ lane_->doneMutex }; | ||
| 730 | lane_->status.store(Lane::Suspended, std::memory_order_release); // Running -> Suspended | 750 | lane_->status.store(Lane::Suspended, std::memory_order_release); // Running -> Suspended |
| 731 | lane_->doneCondVar.notify_one(); | 751 | lane_->doneCondVar.notify_one(); |
| 732 | // wait until the user wants us to resume | 752 | // wait until the user wants us to resume |
| 733 | // TODO: do I update waiting_on or not, so that the lane can be woken by cancellation requests here? | 753 | // update waiting_on, so that the lane can be woken by cancellation requests here? |
| 734 | // lane_->waiting_on = &lane_->doneCondVar; | 754 | lane_->waiting_on = &lane_->doneCondVar; |
| 735 | lane_->doneCondVar.wait(_guard, [lane_]() { return lane_->status.load(std::memory_order_acquire) == Lane::Resuming; }); | 755 | lane_->doneCondVar.wait(_guard, [lane_]() { return lane_->status.load(std::memory_order_acquire) == Lane::Resuming || lane_->cancelRequest.load(std::memory_order_relaxed) != CancelRequest::None; }); |
| 736 | // here lane_->doneMutex is locked again | 756 | // here lane_->doneMutex is locked again |
| 737 | // lane_->waiting_on = nullptr; | 757 | lane_->waiting_on = nullptr; |
| 738 | lane_->status.store(Lane::Running, std::memory_order_release); // Resuming -> Running | 758 | lane_->status.store(Lane::Running, std::memory_order_release); // Resuming -> Running |
| 739 | // on the stack we find the values pushed by lane:resume() | ||
| 740 | _nargs = lua_gettop(_L); | ||
| 741 | } | 759 | } |
| 742 | } while (_rc == LuaError::YIELD); | 760 | // wait was interrupted because of a cancellation, finish the lane |
| 743 | if (_rc != LuaError::OK) { // : err... | 761 | _again = (lane_->cancelRequest.load(std::memory_order_relaxed) == CancelRequest::None); |
| 762 | } else { | ||
| 763 | _again = false; | ||
| 764 | } | ||
| 765 | } while (_again); | ||
| 766 | #if LUA_VERSION_NUM >= 504 | ||
| 767 | if (_rc == LuaError::YIELD) { | ||
| 768 | // lane is cancelled before completion (for example at Lanes shutdown), close everything | ||
| 769 | _rc = static_cast<LuaError>(lua_closethread(_L, nullptr)); | ||
| 770 | } | ||
| 771 | #endif // LUA_VERSION_NUM | ||
| 772 | if (_rc != LuaError::OK) { // an error occurred // L: err... | ||
| 744 | // for some reason, in my tests with Lua 5.4, when the coroutine raises an error, I have 3 copies of it on the stack | 773 | // for some reason, in my tests with Lua 5.4, when the coroutine raises an error, I have 3 copies of it on the stack |
| 745 | // or false + the error message when running Lua 5.1 | 774 | // or false + the error message when running Lua 5.1 |
| 746 | // since the rest of our code wants only the error message, let us keep only the latter. | 775 | // since the rest of our code wants only the error message, let us keep only the latter. |
| @@ -805,7 +834,9 @@ static void lane_main(Lane* const lane_) | |||
| 805 | // ################################################################################################# | 834 | // ################################################################################################# |
| 806 | 835 | ||
| 807 | #if LUA_VERSION_NUM >= 504 | 836 | #if LUA_VERSION_NUM >= 504 |
| 808 | static LUAG_FUNC(lane_close) | 837 | |
| 838 | // __close(lane_ud, <err>) | ||
| 839 | static LUAG_FUNC(lane___close) | ||
| 809 | { | 840 | { |
| 810 | [[maybe_unused]] Lane* const _lane{ ToLane(L_, StackIndex{ 1 }) }; // L_: lane err|nil | 841 | [[maybe_unused]] Lane* const _lane{ ToLane(L_, StackIndex{ 1 }) }; // L_: lane err|nil |
| 811 | // drop the error if any | 842 | // drop the error if any |
| @@ -818,6 +849,16 @@ static LUAG_FUNC(lane_close) | |||
| 818 | lua_call(L_, 1, LUA_MULTRET); // L_: join() results | 849 | lua_call(L_, 1, LUA_MULTRET); // L_: join() results |
| 819 | return lua_gettop(L_); | 850 | return lua_gettop(L_); |
| 820 | } | 851 | } |
| 852 | |||
| 853 | // ################################################################################################# | ||
| 854 | |||
| 855 | // close(lane_ud) | ||
| 856 | static LUAG_FUNC(lane_close) | ||
| 857 | { | ||
| 858 | [[maybe_unused]] Lane* const _lane{ ToLane(L_, StackIndex{ 1 }) }; // L_: lane err|nil | ||
| 859 | raise_luaL_error(L_, "not implemented"); // TODO! | ||
| 860 | return 0; | ||
| 861 | } | ||
| 821 | #endif // LUA_VERSION_NUM >= 504 | 862 | #endif // LUA_VERSION_NUM >= 504 |
| 822 | 863 | ||
| 823 | // ################################################################################################# | 864 | // ################################################################################################# |
| @@ -966,7 +1007,8 @@ CancelResult Lane::internalCancel(CancelRequest const rq_, std::chrono::time_poi | |||
| 966 | // lane_->thread.get_stop_source().request_stop(); | 1007 | // lane_->thread.get_stop_source().request_stop(); |
| 967 | } | 1008 | } |
| 968 | if (wakeLane_ == WakeLane::Yes) { // wake the thread so that execution returns from any pending linda operation if desired | 1009 | if (wakeLane_ == WakeLane::Yes) { // wake the thread so that execution returns from any pending linda operation if desired |
| 969 | if (status.load(std::memory_order_acquire) == Lane::Waiting) { // waiting_on is updated under control of status acquire/release semantics | 1010 | auto const _status{ status.load(std::memory_order_acquire) }; |
| 1011 | if (_status == Lane::Waiting || _status == Lane::Suspended) { // waiting_on is updated under control of status acquire/release semantics | ||
| 970 | if (std::condition_variable* const _waiting_on{ waiting_on }) { | 1012 | if (std::condition_variable* const _waiting_on{ waiting_on }) { |
| 971 | _waiting_on->notify_all(); | 1013 | _waiting_on->notify_all(); |
| 972 | } | 1014 | } |
| @@ -1007,11 +1049,14 @@ namespace { | |||
| 1007 | namespace local { | 1049 | namespace local { |
| 1008 | static struct luaL_Reg const sLaneFunctions[] = { | 1050 | static struct luaL_Reg const sLaneFunctions[] = { |
| 1009 | #if LUA_VERSION_NUM >= 504 | 1051 | #if LUA_VERSION_NUM >= 504 |
| 1010 | { "__close", LG_lane_close }, | 1052 | { "__close", LG_lane___close }, |
| 1011 | #endif // LUA_VERSION_NUM >= 504 | 1053 | #endif // LUA_VERSION_NUM >= 504 |
| 1012 | { "__gc", LG_lane_gc }, | 1054 | { "__gc", LG_lane_gc }, |
| 1013 | { "__index", LG_lane_index }, | 1055 | { "__index", LG_lane_index }, |
| 1014 | { "cancel", LG_lane_cancel }, | 1056 | { "cancel", LG_lane_cancel }, |
| 1057 | #if LUA_VERSION_NUM >= 504 | ||
| 1058 | { "close", LG_lane_close }, | ||
| 1059 | #endif // LUA_VERSION_NUM >= 504 | ||
| 1015 | { "get_threadname", LG_lane_get_threadname }, | 1060 | { "get_threadname", LG_lane_get_threadname }, |
| 1016 | { "join", LG_lane_join }, | 1061 | { "join", LG_lane_join }, |
| 1017 | { "resume", LG_lane_resume }, | 1062 | { "resume", LG_lane_resume }, |
diff --git a/unit_tests/UnitTests.vcxproj b/unit_tests/UnitTests.vcxproj index 79361c5..d6bfd5a 100644 --- a/unit_tests/UnitTests.vcxproj +++ b/unit_tests/UnitTests.vcxproj | |||
| @@ -967,11 +967,13 @@ | |||
| 967 | </ItemGroup> | 967 | </ItemGroup> |
| 968 | <ItemGroup> | 968 | <ItemGroup> |
| 969 | <None Include="..\.runsettings" /> | 969 | <None Include="..\.runsettings" /> |
| 970 | <None Include="scripts\coro\collect_yielded_lane.lua" /> | ||
| 971 | <None Include="scripts\coro\regular_function.lua" /> | ||
| 970 | <None Include="scripts\lane\body_is_a_c_function.lua" /> | 972 | <None Include="scripts\lane\body_is_a_c_function.lua" /> |
| 971 | <None Include="scripts\lane\tasking_cancelling_with_hook.lua" /> | 973 | <None Include="scripts\lane\tasking_cancelling_with_hook.lua" /> |
| 972 | <None Include="scripts\linda\wake_period.lua" /> | 974 | <None Include="scripts\linda\wake_period.lua" /> |
| 973 | <None Include="UnitTests.makefile" /> | 975 | <None Include="UnitTests.makefile" /> |
| 974 | <None Include="scripts\coro\basics.lua" /> | 976 | <None Include="scripts\coro\yielding_function.lua" /> |
| 975 | <None Include="scripts\coro\error_handling.lua" /> | 977 | <None Include="scripts\coro\error_handling.lua" /> |
| 976 | <None Include="scripts\lane\cooperative_shutdown.lua" /> | 978 | <None Include="scripts\lane\cooperative_shutdown.lua" /> |
| 977 | <None Include="scripts\lane\stdlib_naming.lua" /> | 979 | <None Include="scripts\lane\stdlib_naming.lua" /> |
diff --git a/unit_tests/UnitTests.vcxproj.filters b/unit_tests/UnitTests.vcxproj.filters index dfa642b..c5a2761 100644 --- a/unit_tests/UnitTests.vcxproj.filters +++ b/unit_tests/UnitTests.vcxproj.filters | |||
| @@ -95,7 +95,7 @@ | |||
| 95 | <None Include="scripts\lane\tasking_join_test.lua"> | 95 | <None Include="scripts\lane\tasking_join_test.lua"> |
| 96 | <Filter>Scripts\lane</Filter> | 96 | <Filter>Scripts\lane</Filter> |
| 97 | </None> | 97 | </None> |
| 98 | <None Include="scripts\coro\basics.lua"> | 98 | <None Include="scripts\coro\yielding_function.lua"> |
| 99 | <Filter>Scripts\coro</Filter> | 99 | <Filter>Scripts\coro</Filter> |
| 100 | </None> | 100 | </None> |
| 101 | <None Include="scripts\coro\error_handling.lua"> | 101 | <None Include="scripts\coro\error_handling.lua"> |
| @@ -119,8 +119,14 @@ | |||
| 119 | <None Include="scripts\linda\wake_period.lua"> | 119 | <None Include="scripts\linda\wake_period.lua"> |
| 120 | <Filter>Scripts\linda</Filter> | 120 | <Filter>Scripts\linda</Filter> |
| 121 | </None> | 121 | </None> |
| 122 | <None Include="scripts\coro\collect_yielded_lane.lua"> | ||
| 123 | <Filter>Scripts\coro</Filter> | ||
| 124 | </None> | ||
| 122 | <None Include="scripts\lane\body_is_a_c_function.lua"> | 125 | <None Include="scripts\lane\body_is_a_c_function.lua"> |
| 123 | <Filter>Scripts\lane</Filter> | 126 | <Filter>Scripts\lane</Filter> |
| 124 | </None> | 127 | </None> |
| 128 | <None Include="scripts\coro\regular_function.lua"> | ||
| 129 | <Filter>Scripts\coro</Filter> | ||
| 130 | </None> | ||
| 125 | </ItemGroup> | 131 | </ItemGroup> |
| 126 | </Project> \ No newline at end of file | 132 | </Project> \ No newline at end of file |
diff --git a/unit_tests/lane_tests.cpp b/unit_tests/lane_tests.cpp index 82ca1ad..c4d3c95 100644 --- a/unit_tests/lane_tests.cpp +++ b/unit_tests/lane_tests.cpp | |||
| @@ -421,7 +421,7 @@ TEST_CASE("scripted_tests." #DIR "." #FILE) \ | |||
| 421 | 421 | ||
| 422 | MAKE_TEST_CASE(lane, body_is_a_c_function, AssertNoLuaError) | 422 | MAKE_TEST_CASE(lane, body_is_a_c_function, AssertNoLuaError) |
| 423 | MAKE_TEST_CASE(lane, cooperative_shutdown, AssertNoLuaError) | 423 | MAKE_TEST_CASE(lane, cooperative_shutdown, AssertNoLuaError) |
| 424 | #if LUA_VERSION_NUM >= 504 // // warnings are a Lua 5.4 feature | 424 | #if LUA_VERSION_NUM >= 504 // warnings are a Lua 5.4 feature |
| 425 | // NOTE: when this test ends, there are resource leaks and a dangling thread | 425 | // NOTE: when this test ends, there are resource leaks and a dangling thread |
| 426 | MAKE_TEST_CASE(lane, uncooperative_shutdown, AssertWarns) | 426 | MAKE_TEST_CASE(lane, uncooperative_shutdown, AssertWarns) |
| 427 | #endif // LUA_VERSION_NUM | 427 | #endif // LUA_VERSION_NUM |
| @@ -434,11 +434,16 @@ MAKE_TEST_CASE(lane, tasking_error, AssertNoLuaError) | |||
| 434 | MAKE_TEST_CASE(lane, tasking_join_test, AssertNoLuaError) | 434 | MAKE_TEST_CASE(lane, tasking_join_test, AssertNoLuaError) |
| 435 | MAKE_TEST_CASE(lane, tasking_send_receive_code, AssertNoLuaError) | 435 | MAKE_TEST_CASE(lane, tasking_send_receive_code, AssertNoLuaError) |
| 436 | MAKE_TEST_CASE(lane, stdlib_naming, AssertNoLuaError) | 436 | MAKE_TEST_CASE(lane, stdlib_naming, AssertNoLuaError) |
| 437 | MAKE_TEST_CASE(coro, basics, AssertNoLuaError) | 437 | |
| 438 | #if LUA_VERSION_NUM >= 504 // this makes use of to-be-closed variables, a Lua 5.4 feature | ||
| 439 | MAKE_TEST_CASE(coro, collect_yielded_lane, AssertNoLuaError) | ||
| 440 | #endif // LUA_VERSION_NUM | ||
| 438 | #if LUAJIT_FLAVOR() == 0 | 441 | #if LUAJIT_FLAVOR() == 0 |
| 439 | // TODO: for some reason, the test fails with LuaJIT. To be investigated | 442 | // TODO: for some reason, the test fails with LuaJIT. To be investigated |
| 440 | MAKE_TEST_CASE(coro, error_handling, AssertNoLuaError) | 443 | MAKE_TEST_CASE(coro, error_handling, AssertNoLuaError) |
| 441 | #endif // LUAJIT_FLAVOR() | 444 | #endif // LUAJIT_FLAVOR() |
| 445 | MAKE_TEST_CASE(coro, regular_function, AssertNoLuaError) | ||
| 446 | MAKE_TEST_CASE(coro, yielding_function, AssertNoLuaError) | ||
| 442 | 447 | ||
| 443 | /* | 448 | /* |
| 444 | TEST_CASE("lanes.scripted_tests") | 449 | TEST_CASE("lanes.scripted_tests") |
diff --git a/unit_tests/scripts/coro/basics.lua b/unit_tests/scripts/coro/basics.lua deleted file mode 100644 index c0b7a36..0000000 --- a/unit_tests/scripts/coro/basics.lua +++ /dev/null | |||
| @@ -1,99 +0,0 @@ | |||
| 1 | local lanes = require "lanes" | ||
| 2 | |||
| 3 | local fixture = require "fixture" | ||
| 4 | lanes.finally(fixture.throwing_finalizer) | ||
| 5 | |||
| 6 | local utils = lanes.require "_utils" | ||
| 7 | local PRINT = utils.MAKE_PRINT() | ||
| 8 | |||
| 9 | if true then | ||
| 10 | -- a lane body that just returns some value | ||
| 11 | local lane = function(msg_) | ||
| 12 | local utils = lanes.require "_utils" | ||
| 13 | local PRINT = utils.MAKE_PRINT() | ||
| 14 | PRINT "In lane" | ||
| 15 | assert(msg_ == "hi") | ||
| 16 | return "bye" | ||
| 17 | end | ||
| 18 | |||
| 19 | -- the generator | ||
| 20 | local g1 = lanes.coro("*", {name = "auto"}, lane) | ||
| 21 | |||
| 22 | -- launch lane | ||
| 23 | local h1 = g1("hi") | ||
| 24 | |||
| 25 | local r = h1[1] | ||
| 26 | assert(r == "bye") | ||
| 27 | end | ||
| 28 | |||
| 29 | -- a lane coroutine that yields back what got in, one element at a time | ||
| 30 | local yielder = function(...) | ||
| 31 | local utils = lanes.require "_utils" | ||
| 32 | local PRINT = utils.MAKE_PRINT() | ||
| 33 | PRINT "In lane" | ||
| 34 | for _i = 1, select('#', ...) do | ||
| 35 | local _val = select(_i, ...) | ||
| 36 | PRINT("yielding #", _i, _val) | ||
| 37 | local _ack = coroutine.yield(_val) | ||
| 38 | assert(_ack == _i) | ||
| 39 | end | ||
| 40 | return "done!" | ||
| 41 | end | ||
| 42 | |||
| 43 | if true then | ||
| 44 | -- if we start a non-coroutine lane with a yielding function, we should get an error, right? | ||
| 45 | local fun_g = lanes.gen("*", { name = 'auto' }, yielder) | ||
| 46 | local h = fun_g("hello", "world", "!") | ||
| 47 | local err, status, stack = h:join() | ||
| 48 | PRINT(err, status, stack) | ||
| 49 | -- the actual error message is not the same for Lua 5.1 | ||
| 50 | -- of course, it also has to be different for LuaJIT as well | ||
| 51 | -- also, LuaJIT prepends a file:line to the actual error message, which Lua5.1 does not. | ||
| 52 | local msgs = { | ||
| 53 | ["Lua 5.1"] = jit and "attempt to yield across C-call boundary" or "attempt to yield across metamethod/C-call boundary", | ||
| 54 | ["Lua 5.2"] = "attempt to yield from outside a coroutine", | ||
| 55 | ["Lua 5.3"] = "attempt to yield from outside a coroutine", | ||
| 56 | ["Lua 5.4"] = "attempt to yield from outside a coroutine" | ||
| 57 | } | ||
| 58 | local expected_msg = msgs[_VERSION] | ||
| 59 | PRINT("expected_msg = " .. expected_msg) | ||
| 60 | assert(err == nil and string.find(status, expected_msg, 1, true) and stack == nil, "status = " .. status) | ||
| 61 | end | ||
| 62 | |||
| 63 | -- the generator | ||
| 64 | local coro_g = lanes.coro("*", {name = "auto"}, yielder) | ||
| 65 | |||
| 66 | if true then | ||
| 67 | -- launch coroutine lane | ||
| 68 | local h2 = coro_g("hello", "world", "!") | ||
| 69 | -- read the yielded values, sending back the expected index | ||
| 70 | assert(h2:resume(1) == "hello") | ||
| 71 | assert(h2:resume(2) == "world") | ||
| 72 | assert(h2:resume(3) == "!") | ||
| 73 | -- the lane return value is available as usual | ||
| 74 | local r = h2[1] | ||
| 75 | assert(r == "done!") | ||
| 76 | end | ||
| 77 | |||
| 78 | if true then | ||
| 79 | -- another coroutine lane | ||
| 80 | local h3 = coro_g("hello", "world", "!") | ||
| 81 | |||
| 82 | -- yielded values are available as regular return values | ||
| 83 | assert(h3[1] == "hello" and h3.status == "suspended") | ||
| 84 | -- since we consumed the returned values, they should not be here when we resume | ||
| 85 | assert(h3:resume(1) == nil) | ||
| 86 | |||
| 87 | -- similarly, we can get them with join() | ||
| 88 | local r3, ret3 = h3:join() | ||
| 89 | assert(r3 == true and ret3 == "world" and h3.status == "suspended") | ||
| 90 | -- since we consumed the returned values, they should not be here when we resume | ||
| 91 | assert(h3:resume(2) == nil) | ||
| 92 | |||
| 93 | -- the rest should work as usual | ||
| 94 | assert(h3:resume(3) == "!") | ||
| 95 | |||
| 96 | -- the final return value of the lane body remains to be read | ||
| 97 | local r3, ret3 = h3:join() | ||
| 98 | assert(r3 == true and ret3 == "done!" and h3.status == "done") | ||
| 99 | end | ||
diff --git a/unit_tests/scripts/coro/collect_yielded_lane.lua b/unit_tests/scripts/coro/collect_yielded_lane.lua new file mode 100644 index 0000000..0459698 --- /dev/null +++ b/unit_tests/scripts/coro/collect_yielded_lane.lua | |||
| @@ -0,0 +1,72 @@ | |||
| 1 | local lanes = require "lanes" | ||
| 2 | |||
| 3 | local fixture = require "fixture" | ||
| 4 | lanes.finally(fixture.throwing_finalizer) | ||
| 5 | |||
| 6 | local utils = lanes.require "_utils" | ||
| 7 | local PRINT = utils.MAKE_PRINT() | ||
| 8 | |||
| 9 | -- a lane body that yields stuff | ||
| 10 | local yielder = function(out_linda_) | ||
| 11 | -- here is a to-be-closed variable that, when closed, sends "Closed!" in the "out" slot of the provided linda | ||
| 12 | local t <close> = setmetatable( | ||
| 13 | { text = "Closed!" }, { | ||
| 14 | __close = function(self, err) | ||
| 15 | out_linda_:send("out", self.text) | ||
| 16 | end | ||
| 17 | } | ||
| 18 | ) | ||
| 19 | -- yield forever | ||
| 20 | while true do | ||
| 21 | coroutine.yield("I yield!") | ||
| 22 | end | ||
| 23 | end | ||
| 24 | |||
| 25 | local out_linda = lanes.linda() | ||
| 26 | |||
| 27 | local test_close = function(what_, f_) | ||
| 28 | local c = coroutine.create(f_) | ||
| 29 | for i = 1, 10 do | ||
| 30 | local t, r = coroutine.resume(c, out_linda) -- returns true + <yielded values> | ||
| 31 | assert(t == true and r == "I yield!", "got " .. tostring(t) .. " " .. tostring(r)) | ||
| 32 | local s = coroutine.status(c) | ||
| 33 | assert(s == "suspended") | ||
| 34 | end | ||
| 35 | local r, s = coroutine.close(c) | ||
| 36 | assert(r == true and s == nil) | ||
| 37 | -- the local variable inside the yielder body should be closed | ||
| 38 | local s, r = out_linda:receive(0, "out") | ||
| 39 | assert(s == "out" and r == "Closed!", what_ .. " got " .. tostring(s) .. " " .. tostring(r)) | ||
| 40 | end | ||
| 41 | |||
| 42 | -- first, try the close mechanism outside of a lane | ||
| 43 | test_close("base", yielder) | ||
| 44 | |||
| 45 | -- try again with a function obtained through dump/undump | ||
| 46 | -- note this means our yielder implementation can't have upvalues, as they are lost in the process | ||
| 47 | test_close("dumped", load(string.dump(yielder))) | ||
| 48 | |||
| 49 | ------------------------------------------------------------------------------ | ||
| 50 | -- TEST: to-be-closed variables are properly closed when the lane is collected | ||
| 51 | ------------------------------------------------------------------------------ | ||
| 52 | if false then -- NOT IMPLEMENTED YET! | ||
| 53 | |||
| 54 | -- the generator | ||
| 55 | local coro_g = lanes.coro("*", yielder) | ||
| 56 | |||
| 57 | -- start the lane | ||
| 58 | local h = coro_g(out_linda) | ||
| 59 | |||
| 60 | -- join it so that it reaches suspended state | ||
| 61 | local r, v = h:join(0.5) | ||
| 62 | assert(r == nil and v == "timeout", "got " .. tostring(r) .. " " .. tostring(v)) | ||
| 63 | assert(h.status == "suspended") | ||
| 64 | |||
| 65 | -- force collection of the lane | ||
| 66 | h = nil | ||
| 67 | collectgarbage() | ||
| 68 | |||
| 69 | -- I want the to-be-closed variable of the coroutine linda to be properly closed | ||
| 70 | local s, r = out_linda:receive(0, "out") | ||
| 71 | assert(s == "out" and r == "Closed!", "coro got " .. tostring(s) .. " " .. tostring(r)) | ||
| 72 | end | ||
diff --git a/unit_tests/scripts/coro/error_handling.lua b/unit_tests/scripts/coro/error_handling.lua index ba6cff6..1cfb8c8 100644 --- a/unit_tests/scripts/coro/error_handling.lua +++ b/unit_tests/scripts/coro/error_handling.lua | |||
| @@ -38,15 +38,15 @@ local force_error_test = function(error_trace_level_) | |||
| 38 | utils.dump_error_stack(error_trace_level_, c) | 38 | utils.dump_error_stack(error_trace_level_, c) |
| 39 | end | 39 | end |
| 40 | 40 | ||
| 41 | if false then | 41 | if true then |
| 42 | force_error_test("minimal") | 42 | force_error_test("minimal") |
| 43 | end | 43 | end |
| 44 | 44 | ||
| 45 | if false then | 45 | if true then |
| 46 | force_error_test("basic") | 46 | force_error_test("basic") |
| 47 | end | 47 | end |
| 48 | 48 | ||
| 49 | if false then | 49 | if true then |
| 50 | force_error_test("extended") | 50 | force_error_test("extended") |
| 51 | end | 51 | end |
| 52 | 52 | ||
diff --git a/unit_tests/scripts/coro/regular_function.lua b/unit_tests/scripts/coro/regular_function.lua new file mode 100644 index 0000000..09aa3b7 --- /dev/null +++ b/unit_tests/scripts/coro/regular_function.lua | |||
| @@ -0,0 +1,38 @@ | |||
| 1 | local lanes = require "lanes".configure() | ||
| 2 | |||
| 3 | local utils = lanes.require "_utils" | ||
| 4 | local PRINT = utils.MAKE_PRINT() | ||
| 5 | |||
| 6 | -- a lane body that just returns some value | ||
| 7 | local returner = function(msg_) | ||
| 8 | local utils = lanes.require "_utils" | ||
| 9 | local PRINT = utils.MAKE_PRINT() | ||
| 10 | PRINT "In lane" | ||
| 11 | assert(msg_ == "hi") | ||
| 12 | return "bye" | ||
| 13 | end | ||
| 14 | |||
| 15 | -- a function that returns some value can run in a coroutine | ||
| 16 | if true then | ||
| 17 | -- the generator | ||
| 18 | local g = lanes.coro("*", {name = "auto"}, returner) | ||
| 19 | |||
| 20 | -- launch lane | ||
| 21 | local h = g("hi") | ||
| 22 | |||
| 23 | local r = h[1] | ||
| 24 | assert(r == "bye") | ||
| 25 | end | ||
| 26 | |||
| 27 | -- can't resume a coro after the lane body has returned | ||
| 28 | if true then | ||
| 29 | -- the generator | ||
| 30 | local g = lanes.coro("*", {name = "auto"}, returner) | ||
| 31 | |||
| 32 | -- launch lane | ||
| 33 | local h = g("hi") | ||
| 34 | |||
| 35 | -- resuming a lane that terminated execution should raise an error | ||
| 36 | local b, e = pcall(h.resume, h) | ||
| 37 | assert(b == false and type(e) == "string") | ||
| 38 | end | ||
diff --git a/unit_tests/scripts/coro/yielding_function.lua b/unit_tests/scripts/coro/yielding_function.lua new file mode 100644 index 0000000..e7367ea --- /dev/null +++ b/unit_tests/scripts/coro/yielding_function.lua | |||
| @@ -0,0 +1,107 @@ | |||
| 1 | local lanes = require "lanes" | ||
| 2 | |||
| 3 | local fixture = require "fixture" | ||
| 4 | lanes.finally(fixture.throwing_finalizer) | ||
| 5 | |||
| 6 | local utils = lanes.require "_utils" | ||
| 7 | local PRINT = utils.MAKE_PRINT() | ||
| 8 | |||
| 9 | -- a lane coroutine that yields back what got in, one element at a time | ||
| 10 | local yielder = function(...) | ||
| 11 | local utils = lanes.require "_utils" | ||
| 12 | local PRINT = utils.MAKE_PRINT() | ||
| 13 | PRINT "In lane" | ||
| 14 | for _i = 1, select('#', ...) do | ||
| 15 | local _val = select(_i, ...) | ||
| 16 | PRINT("yielding #", _i, _val) | ||
| 17 | local _ack = coroutine.yield(_val) | ||
| 18 | assert(_ack == _i) | ||
| 19 | end | ||
| 20 | return "done!" | ||
| 21 | end | ||
| 22 | |||
| 23 | -------------------------------------------------------------------------------------------------- | ||
| 24 | -- TEST: if we start a non-coroutine lane with a yielding function, we should get an error, right? | ||
| 25 | -------------------------------------------------------------------------------------------------- | ||
| 26 | if true then | ||
| 27 | local fun_g = lanes.gen("*", { name = 'auto' }, yielder) | ||
| 28 | local h = fun_g("hello", "world", "!") | ||
| 29 | local err, status, stack = h:join() | ||
| 30 | PRINT(err, status, stack) | ||
| 31 | -- the actual error message is not the same for Lua 5.1 | ||
| 32 | -- of course, it also has to be different for LuaJIT as well | ||
| 33 | -- also, LuaJIT prepends a file:line to the actual error message, which Lua5.1 does not. | ||
| 34 | local msgs = { | ||
| 35 | ["Lua 5.1"] = jit and "attempt to yield across C-call boundary" or "attempt to yield across metamethod/C-call boundary", | ||
| 36 | ["Lua 5.2"] = "attempt to yield from outside a coroutine", | ||
| 37 | ["Lua 5.3"] = "attempt to yield from outside a coroutine", | ||
| 38 | ["Lua 5.4"] = "attempt to yield from outside a coroutine" | ||
| 39 | } | ||
| 40 | local expected_msg = msgs[_VERSION] | ||
| 41 | PRINT("expected_msg = " .. expected_msg) | ||
| 42 | assert(err == nil and string.find(status, expected_msg, 1, true) and stack == nil, "status = " .. status) | ||
| 43 | end | ||
| 44 | |||
| 45 | -- the coroutine generator | ||
| 46 | local coro_g = lanes.coro("*", {name = "auto"}, yielder) | ||
| 47 | |||
| 48 | ------------------------------------------------------------------------------------------------- | ||
| 49 | -- TEST: we can resume as many times as the lane yields, then read the returned value on indexing | ||
| 50 | ------------------------------------------------------------------------------------------------- | ||
| 51 | if true then | ||
| 52 | -- launch coroutine lane | ||
| 53 | local h = coro_g("hello", "world", "!") | ||
| 54 | -- read the yielded values, sending back the expected index | ||
| 55 | assert(h:resume(1) == "hello") | ||
| 56 | assert(h:resume(2) == "world") | ||
| 57 | assert(h:resume(3) == "!") | ||
| 58 | -- the lane return value is available as usual | ||
| 59 | local r = h[1] | ||
| 60 | assert(r == "done!") | ||
| 61 | end | ||
| 62 | |||
| 63 | --------------------------------------------------------------------------------------------- | ||
| 64 | -- TEST: we can resume as many times as the lane yields, then read the returned value on join | ||
| 65 | --------------------------------------------------------------------------------------------- | ||
| 66 | if true then | ||
| 67 | -- launch coroutine lane | ||
| 68 | local h = coro_g("hello", "world", "!") | ||
| 69 | -- read the yielded values, sending back the expected index | ||
| 70 | assert(h:resume(1) == "hello") | ||
| 71 | assert(h:resume(2) == "world") | ||
| 72 | assert(h:resume(3) == "!") | ||
| 73 | -- the lane return value is available as usual | ||
| 74 | local s, r = h:join() | ||
| 75 | assert(h.status == "done" and s == true and r == "done!") | ||
| 76 | end | ||
| 77 | |||
| 78 | --------------------------------------------------------------------------------------------------- | ||
| 79 | -- TEST: if we join a yielded lane, we get a timeout, and we can resume as if we didn't try to join | ||
| 80 | --------------------------------------------------------------------------------------------------- | ||
| 81 | if true then | ||
| 82 | -- launch coroutine lane | ||
| 83 | local h = coro_g("hello", "world", "!") | ||
| 84 | -- read the first yielded value, sending back the expected index | ||
| 85 | assert(h:resume(1) == "hello") | ||
| 86 | -- join the lane. since it will reach a yield point, it remains suspended, and we should get a timeout | ||
| 87 | local b, r = h:join(0.5) | ||
| 88 | local s = h.status | ||
| 89 | assert(s == "suspended" and b == nil and r == "timeout", "got " .. s .. " " .. tostring(b) .. " " .. r) | ||
| 90 | -- trying to resume again should proceed normally, since nothing changed | ||
| 91 | assert(h:resume(2) == "world") | ||
| 92 | assert(h:resume(3) == "!") | ||
| 93 | -- the lane return value is available as usual | ||
| 94 | local s, r = h:join() | ||
| 95 | assert(h.status == "done" and s == true and r == "done!") | ||
| 96 | end | ||
| 97 | |||
| 98 | --------------------------------------------------------- | ||
| 99 | -- TEST: if we index yielded lane, we should get an error | ||
| 100 | --------------------------------------------------------- | ||
| 101 | -- TODO: implement this test! | ||
| 102 | |||
| 103 | |||
| 104 | ------------------------------------------------------------------------------------- | ||
| 105 | -- TEST: if we close yielded lane, we can join it and get the last yielded values out | ||
| 106 | ------------------------------------------------------------------------------------- | ||
| 107 | -- TODO: we need to implement lane:close() for that! | ||
