aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBenoit Germain <benoit.germain@ubisoft.com>2025-06-26 09:18:54 +0200
committerBenoit Germain <benoit.germain@ubisoft.com>2025-06-26 09:18:54 +0200
commitd7d756e30680bcff036118b47ac45b740e020ea2 (patch)
tree3e2a26409154760d66092e6e04a9fcb4ad4ed02a
parent4f5fa626c0279f5aefac477a29702c43a6754c5a (diff)
downloadlanes-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.cpp79
-rw-r--r--unit_tests/UnitTests.vcxproj4
-rw-r--r--unit_tests/UnitTests.vcxproj.filters8
-rw-r--r--unit_tests/lane_tests.cpp9
-rw-r--r--unit_tests/scripts/coro/basics.lua99
-rw-r--r--unit_tests/scripts/coro/collect_yielded_lane.lua72
-rw-r--r--unit_tests/scripts/coro/error_handling.lua6
-rw-r--r--unit_tests/scripts/coro/regular_function.lua38
-rw-r--r--unit_tests/scripts/coro/yielding_function.lua107
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
808static LUAG_FUNC(lane_close) 837
838// __close(lane_ud, <err>)
839static 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)
856static 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
422MAKE_TEST_CASE(lane, body_is_a_c_function, AssertNoLuaError) 422MAKE_TEST_CASE(lane, body_is_a_c_function, AssertNoLuaError)
423MAKE_TEST_CASE(lane, cooperative_shutdown, AssertNoLuaError) 423MAKE_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
426MAKE_TEST_CASE(lane, uncooperative_shutdown, AssertWarns) 426MAKE_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)
434MAKE_TEST_CASE(lane, tasking_join_test, AssertNoLuaError) 434MAKE_TEST_CASE(lane, tasking_join_test, AssertNoLuaError)
435MAKE_TEST_CASE(lane, tasking_send_receive_code, AssertNoLuaError) 435MAKE_TEST_CASE(lane, tasking_send_receive_code, AssertNoLuaError)
436MAKE_TEST_CASE(lane, stdlib_naming, AssertNoLuaError) 436MAKE_TEST_CASE(lane, stdlib_naming, AssertNoLuaError)
437MAKE_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
439MAKE_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
440MAKE_TEST_CASE(coro, error_handling, AssertNoLuaError) 443MAKE_TEST_CASE(coro, error_handling, AssertNoLuaError)
441#endif // LUAJIT_FLAVOR() 444#endif // LUAJIT_FLAVOR()
445MAKE_TEST_CASE(coro, regular_function, AssertNoLuaError)
446MAKE_TEST_CASE(coro, yielding_function, AssertNoLuaError)
442 447
443/* 448/*
444TEST_CASE("lanes.scripted_tests") 449TEST_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 @@
1local lanes = require "lanes"
2
3local fixture = require "fixture"
4lanes.finally(fixture.throwing_finalizer)
5
6local utils = lanes.require "_utils"
7local PRINT = utils.MAKE_PRINT()
8
9if 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")
27end
28
29-- a lane coroutine that yields back what got in, one element at a time
30local 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!"
41end
42
43if 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)
61end
62
63-- the generator
64local coro_g = lanes.coro("*", {name = "auto"}, yielder)
65
66if 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!")
76end
77
78if 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")
99end
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 @@
1local lanes = require "lanes"
2
3local fixture = require "fixture"
4lanes.finally(fixture.throwing_finalizer)
5
6local utils = lanes.require "_utils"
7local PRINT = utils.MAKE_PRINT()
8
9-- a lane body that yields stuff
10local 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
23end
24
25local out_linda = lanes.linda()
26
27local 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))
40end
41
42-- first, try the close mechanism outside of a lane
43test_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
47test_close("dumped", load(string.dump(yielder)))
48
49------------------------------------------------------------------------------
50-- TEST: to-be-closed variables are properly closed when the lane is collected
51------------------------------------------------------------------------------
52if 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))
72end
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)
39end 39end
40 40
41if false then 41if true then
42 force_error_test("minimal") 42 force_error_test("minimal")
43end 43end
44 44
45if false then 45if true then
46 force_error_test("basic") 46 force_error_test("basic")
47end 47end
48 48
49if false then 49if true then
50 force_error_test("extended") 50 force_error_test("extended")
51end 51end
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 @@
1local lanes = require "lanes".configure()
2
3local utils = lanes.require "_utils"
4local PRINT = utils.MAKE_PRINT()
5
6-- a lane body that just returns some value
7local 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"
13end
14
15-- a function that returns some value can run in a coroutine
16if 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")
25end
26
27-- can't resume a coro after the lane body has returned
28if 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")
38end
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 @@
1local lanes = require "lanes"
2
3local fixture = require "fixture"
4lanes.finally(fixture.throwing_finalizer)
5
6local utils = lanes.require "_utils"
7local PRINT = utils.MAKE_PRINT()
8
9-- a lane coroutine that yields back what got in, one element at a time
10local 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!"
21end
22
23--------------------------------------------------------------------------------------------------
24-- TEST: if we start a non-coroutine lane with a yielding function, we should get an error, right?
25--------------------------------------------------------------------------------------------------
26if 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)
43end
44
45-- the coroutine generator
46local 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-------------------------------------------------------------------------------------------------
51if 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!")
61end
62
63---------------------------------------------------------------------------------------------
64-- TEST: we can resume as many times as the lane yields, then read the returned value on join
65---------------------------------------------------------------------------------------------
66if 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!")
76end
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---------------------------------------------------------------------------------------------------
81if 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!")
96end
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!