aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBenoit Germain <benoit.germain@ubisoft.com>2024-06-10 16:21:31 +0200
committerBenoit Germain <benoit.germain@ubisoft.com>2024-06-10 16:21:31 +0200
commit3f5c16116a3a7740ac4ac62b663661d772543c2e (patch)
tree884df73600bd680929f7ad0ebdfadd087b4841e4
parent3763be94cdd1a5cf26fec0f09784b18188fd3054 (diff)
downloadlanes-3f5c16116a3a7740ac4ac62b663661d772543c2e.tar.gz
lanes-3f5c16116a3a7740ac4ac62b663661d772543c2e.tar.bz2
lanes-3f5c16116a3a7740ac4ac62b663661d772543c2e.zip
Replaced __lanesignore with __lanesconvert
-rw-r--r--docs/index.html91
-rw-r--r--src/intercopycontext.cpp87
-rw-r--r--src/intercopycontext.h3
-rw-r--r--tests/basic.lua4
4 files changed, 121 insertions, 64 deletions
diff --git a/docs/index.html b/docs/index.html
index 60f4970..24c7c52 100644
--- a/docs/index.html
+++ b/docs/index.html
@@ -546,7 +546,7 @@
546 </td> 546 </td>
547 <td /> 547 <td />
548 <td> 548 <td>
549 root level names, <tt>print</tt>, <tt>assert</tt>, <tt>unpack</tt> etc. 549 <tt>_G</tt> namespace (the default function environment): <tt>print</tt>, <tt>assert</tt>, <tt>dofile</tt>, etc.
550 </td> 550 </td>
551 </tr> 551 </tr>
552 <tr> 552 <tr>
@@ -1150,13 +1150,13 @@
1150<h2 id="lindas">Lindas</h2> 1150<h2 id="lindas">Lindas</h2>
1151 1151
1152<p> 1152<p>
1153 Communications between lanes is completely detached from the lane handles themselves. By itself, a lane can only provide return values once it's finished, or throw an error. 1153 Communications between lanes is completely detached from the lane handles themselves. By itself, a lane can only provide return values once it is finished, or throw an error.
1154 Needs to communicate during runtime are handled by <a href="http://en.wikipedia.org/wiki/Linda_%28coordination_language%29" target="_blank">Linda objects</a>, which are 1154 Needs to communicate during runtime are handled by <a href="http://en.wikipedia.org/wiki/Linda_%28coordination_language%29" target="_blank">Linda objects</a>, which are
1155 <a href="#deep_userdata">deep userdata</a> instances. They can be provided to a lane as startup parameters, upvalues or in some other Linda's message. 1155 <a href="#deep_userdata">deep userdata</a> instances. They can be provided to a lane as startup parameters, upvalues or in some other Linda's message.
1156</p> 1156</p>
1157 1157
1158<p> 1158<p>
1159 Access to a Linda object means a lane can read or write to any of its data slots. Multiple lanes can be accessing the same Linda in parallel. No application level locking is required; each Linda operation is atomic. 1159 Access to a Linda object means a lane can read or write to any of its data slots. Multiple lanes can be accessing the same Linda simultaneously. Seen from Lua, no application level locking is required; each Linda operation is atomic.
1160</p> 1160</p>
1161 1161
1162<table border="1" bgcolor="#FFFFE0" cellpadding="10" style="width:50%"><tr><td><pre> 1162<table border="1" bgcolor="#FFFFE0" cellpadding="10" style="width:50%"><tr><td><pre>
@@ -1212,35 +1212,53 @@
1212 1212
1213<table border="1" bgcolor="#E0E0FF" cellpadding="10" style="width:50%"><tr><td><pre> 1213<table border="1" bgcolor="#E0E0FF" cellpadding="10" style="width:50%"><tr><td><pre>
1214 h = lanes.linda([opt_name, [opt_group]]) 1214 h = lanes.linda([opt_name, [opt_group]])
1215</pre></td></tr></table>
1215 1216
1216 true|lanes.cancel_error = h:send([timeout_secs,] key, ...) 1217<p>
1218 Converting the Linda to a string will yield the provided name prefixed by <tt>"Linda: "</tt>.<br/>
1219 If <tt>opt_name</tt> is omitted, it will evaluate to an hexadecimal number uniquely representing that Linda when the Linda is converted to a string. The value is the same as returned by <tt>linda:deep()</tt>.<br/>
1220 If <tt>opt_name</tt> is <tt>"auto"</tt>, Lanes will try to construct a name from the source location that called <tt>lanes.linda()</tt>. If that fails, the Linda name will be <tt>"&lt;unresolved&gt;"</tt>.
1221</p>
1217 1222
1218 key, val = h:receive([timeout_secs,] key [, key...]) 1223<table border="1" bgcolor="#E0E0FF" cellpadding="10" style="width:50%"><tr><td><pre>
1224 true|lanes.cancel_error = h:limit(key, n_uint)
1225</pre></td></tr></table>
1219 1226
1220 key, val [, val...] = h:receive(timeout, h.batched, key, n_uint_min[, n_uint_max]) 1227<p>
1228 By default, queue sizes are unlimited but limits can be enforced using the <tt>limit()</tt> method. This can be useful to balance execution speeds in a producer/consumer scenario. <tt>nil</tt> removes the limit.<br/>
1229 A limit of 0 is allowed to block everything.<br/>
1230 If the key was full but the limit change added some room, <tt>limit()</tt> returns <tt>true</tt> and the Linda is signalled so that <tt>send()</tt>-blocked threads are awakened.<br/>
1231</p>
1221 1232
1222 true|lanes.cancel_error = h:limit(key, n_uint) 1233<table border="1" bgcolor="#E0E0FF" cellpadding="10" style="width:50%"><tr><td><pre>
1234 true|lanes.cancel_error = h:send([timeout_secs,] key, ...)
1223</pre></td></tr></table> 1235</pre></td></tr></table>
1224 1236
1225<p> 1237<p>
1226 Converting the Linda to a string will yield the provided name prefixed by <tt>"Linda: "</tt>.<br/> 1238 Timeouts are given in seconds (&gt= 0, millisecond accuracy) or <tt>nil</tt>. Timeout can be omitted only if the first key is not a number (then it is equivalent to an infinite duration).<br/>
1227 If <tt>opt_name</tt> is omitted, it will evaluate to an hexadecimal number uniquely representing that Linda when the Linda is converted to a string. The value is the same as returned by <tt>linda:deep()</tt>.<br/> 1239 Each key acts as a FIFO queue. There is no limit to the number of keys a Linda may contain. Different Lindas can have identical keys, which are totally unrelated.
1228 If <tt>opt_name</tt> is <tt>"auto"</tt>, Lanes will try to construct a name from the source location that called <tt>lanes.linda()</tt>. If that fails, the Linda name will be <tt>"&lt;unresolved&gt;"</tt>.
1229</p> 1240</p>
1230<p> 1241<p>
1231 Timeouts are given in seconds (&gt= 0, millisecond accuracy) or <tt>nil</tt>. Timeout can be omitted only if the first key is not a number (then it's equivalent to an infinite duration). 1242 If no data is provided after the key, <tt>send()</tt> raises an error.<br/>
1243 Also, if <tt>linda.null</tt> or <tt>lanes.null</tt> is sent as data in a Linda, it will be read as a <tt>nil</tt>.<br/>
1244 <tt>send()</tt> returns <tt>true</tt> if the sending succeeded, and <tt>false</tt> if the queue limit was met, and the queue did not empty enough during the given timeout.<br/>
1245 <tt>send()</tt> returns <tt>lanes.cancel_error</tt> if interrupted by a soft cancel request.<br/>
1232</p> 1246</p>
1233 1247
1248<table border="1" bgcolor="#E0E0FF" cellpadding="10" style="width:50%"><tr><td><pre>
1249 key, val = h:receive([timeout_secs,] key [, key...])
1250
1251 key, val [, val...] = h:receive(timeout, h.batched, key, n_uint_min[, n_uint_max])
1252</pre></td></tr></table>
1253
1254
1234<p> 1255<p>
1235 The <tt>send()</tt> and <tt>receive()</tt> methods use Linda keys as FIFO stacks (first in, first out).<br/> 1256 The <tt>send()</tt> and <tt>receive()</tt> methods use Linda keys as FIFO stacks (first in, first out).<br/>
1236 By default, stack sizes are unlimited but limits can be enforced using the <tt>limit()</tt> method. This can be useful to balance execution speeds in a producer/consumer scenario. <tt>nil</tt> removes the limit.<br/>
1237 A limit of 0 is allowed to block everything.<br/>
1238 If the key was full but the limit change added some room, <tt>limit()</tt> returns <tt>true</tt> and the Linda is signalled so that <tt>send()</tt>-blocked threads are awakened.<br/>
1239 In batched mode, <tt>linda:receive()</tt> will raise an error if <tt>min_count < 1</tt> or <tt>max_count < min_count</tt>. 1257 In batched mode, <tt>linda:receive()</tt> will raise an error if <tt>min_count < 1</tt> or <tt>max_count < min_count</tt>.
1240</p> 1258</p>
1241 1259
1242<p> 1260<p>
1243 Note that any number of lanes can be reading or writing a Linda. There can be many producers, and many consumers. It's up to you. 1261 Note that any number of lanes can be reading or writing a Linda. There can be many producers, and many consumers. It is up to you.
1244</p> 1262</p>
1245 1263
1246<p> 1264<p>
@@ -1248,16 +1266,6 @@
1248</p> 1266</p>
1249 1267
1250<p> 1268<p>
1251 <tt>send()</tt> returns <tt>true</tt> if the sending succeeded, and <tt>false</tt> if the queue limit was met, and the queue did not empty enough during the given timeout.
1252 <br/>
1253 <tt>send()</tt> returns <tt>lanes.cancel_error</tt> if interrupted by a soft cancel request.
1254 <br/>
1255 If no data is provided after the key, <tt>send()</tt> raises an error.
1256 <br/>
1257 Also, if <tt>linda.null</tt> or <tt>lanes.null</tt> is sent as data in a Linda, it will be read as a <tt>nil</tt>.
1258</p>
1259
1260<p>
1261 Equally, <tt>receive()</tt> returns a key and the value extracted from it. Note that <tt>nil</tt>s can be sent and received; the <tt>key</tt> value will tell it apart from a timeout.<br/> 1269 Equally, <tt>receive()</tt> returns a key and the value extracted from it. Note that <tt>nil</tt>s can be sent and received; the <tt>key</tt> value will tell it apart from a timeout.<br/>
1262 <tt>receive()</tt> returns <tt>nil, lanes.cancel_error</tt> if interrupted by a hard cancel request.<br/> 1270 <tt>receive()</tt> returns <tt>nil, lanes.cancel_error</tt> if interrupted by a hard cancel request.<br/>
1263 <tt>receive()</tt> returns <tt>nil, "timeout"</tt> if nothing was available. 1271 <tt>receive()</tt> returns <tt>nil, "timeout"</tt> if nothing was available.
@@ -1558,14 +1566,22 @@ On the other side, you need to use a common Linda for waiting for multiple keys.
1558 to the global table in the destination state. Note that this also applies when Lanes is built for Lua 5.1, as it doesn't hurt. 1566 to the global table in the destination state. Note that this also applies when Lanes is built for Lua 5.1, as it doesn't hurt.
1559 </li> 1567 </li>
1560 <li> 1568 <li>
1561 Full userdata can be passed only if it's prepared using the <a href="#deep_userdata">deep userdata</a> system, which handles its lifespan management 1569 Full userdata can be passed only if it is prepared using the <a href="#deep_userdata">deep userdata</a> system, which handles its lifespan management
1562 <ul> 1570 <ul>
1563 <li>In particular, lane handles cannot be passed between lanes.</li> 1571 <li>In particular, lane handles cannot be passed between lanes.</li>
1564 <li>Lanes can either throw an error or attempt a <a href="#demote_full_userdata">light userdata demotion</a>.</li> 1572 <li>Lanes can either throw an error or attempt a <a href="#demote_full_userdata">light userdata demotion</a>.</li>
1565 </ul> 1573 </ul>
1566 </li> 1574 </li>
1567 <li>Coroutines cannot be passed. A coroutine's Lua state is tied to the Lua state that created it, and there is no way the mixed C/Lua stack of a coroutine can be transfered from one Lua state to another.</li> 1575 <li>Coroutines cannot be passed. A coroutine's Lua state is tied to the Lua state that created it, and there is no way the mixed C/Lua stack of a coroutine can be transfered from one Lua state to another.</li>
1568 <li>If the metatable contains <tt>__lanesignore</tt>, the object is skipped and <tt>nil</tt> is transfered instead.</li> 1576 <li>
1577 If the metatable contains <tt>__lanesconvert</tt>, the object is converted as follows depending on the value:
1578 <ul>
1579 <li><tt>lanes.null</tt>: The value is converted to <tt>nil</tt>.</li>
1580 <li><tt>"decay"</tt>: The value is converted to a light userdata obtained from <tt>lua_topointer()</tt>.</li>
1581 <li>A function: The function is called as <tt>o:__lanesconvert(string)</tt>, where the argument is either <tt>"keeper"</tt> or <tt>"regular"</tt>, depending on the type of destination. Its (single) return value is the result of the conversion.</li>
1582 <li>Any other value raises an error.</li>
1583 </ul>
1584 </li>
1569 </ul> 1585 </ul>
1570</p> 1586</p>
1571 1587
@@ -1573,26 +1589,7 @@ On the other side, you need to use a common Linda for waiting for multiple keys.
1573<h3 id="function_notes">Notes about passing C functions</h3> 1589<h3 id="function_notes">Notes about passing C functions</h3>
1574 1590
1575<p> 1591<p>
1576 Originally, a C function was copied from one Lua state to another as follows: 1592 Functions are transfered as follows (more or less):
1577</p>
1578
1579<table border="1" bgcolor="#FFFFE0" cellpadding="10" style="width:50%"><tr><td><pre>
1580 // expects a C function on top of the source Lua stack
1581 copy_func(lua_State *dest, lua_State* source)
1582 {
1583 // extract C function pointer from source
1584 lua_CFunction func = lua_tocfunction(source, -1);
1585 // transfer upvalues
1586 int nup = transfer_upvalues(dest, source);
1587 // dest Lua stack contains a copy of all upvalues
1588 lua_pushcfunction(dest, func, nup);
1589 }
1590</pre></td></tr></table>
1591
1592<p>
1593 This has the main drawback of not being LuaJIT-compatible, because some functions registered by LuaJIT are not regular C functions, but specially optimized implementations. As a result, <tt>lua_tocfunction()</tt> returns <tt>nullptr</tt> for them.
1594 <br/>
1595 Therefore, Lanes no longer transfers functions that way. Instead, functions are transfered as follows (more or less):
1596</p> 1593</p>
1597 1594
1598<table border="1" bgcolor="#FFFFE0" cellpadding="10" style="width:50%"><tr><td><pre> 1595<table border="1" bgcolor="#FFFFE0" cellpadding="10" style="width:50%"><tr><td><pre>
diff --git a/src/intercopycontext.cpp b/src/intercopycontext.cpp
index c7fcf14..a5fd400 100644
--- a/src/intercopycontext.cpp
+++ b/src/intercopycontext.cpp
@@ -532,6 +532,76 @@ void InterCopyContext::inter_copy_keyvaluepair() const
532 532
533// ################################################################################################# 533// #################################################################################################
534 534
535LuaType InterCopyContext::processConversion() const
536{
537 static constexpr int kPODmask = (1 << LUA_TNIL) | (1 << LUA_TBOOLEAN) | (1 << LUA_TLIGHTUSERDATA) | (1 << LUA_TNUMBER) | (1 << LUA_TSTRING);
538
539 LuaType _val_type{ luaG_type(L1, L1_i) };
540
541 STACK_CHECK_START_REL(L1, 0);
542
543 // it's a POD: nothing to do
544 if (((1 << static_cast<int>(_val_type)) & kPODmask) != 0) {
545 return _val_type;
546 }
547
548 // no metatable: nothing to do
549 if (!lua_getmetatable(L1, L1_i)) { // L1: ...
550 STACK_CHECK(L1, 0);
551 return _val_type;
552 }
553 // we have a metatable // L1: ... mt
554 static constexpr std::string_view kConvertField{ "__lanesconvert" };
555 LuaType const _converterType{ luaG_getfield(L1, -1, kConvertField) }; // L1: ... mt kConvertField
556 switch (_converterType) {
557 case LuaType::NIL:
558 // no __lanesconvert, nothing to do
559 lua_pop(L1, 2); // L1: ...
560 break;
561
562 case LuaType::LIGHTUSERDATA:
563 if (kNilSentinel.equals(L1, -1)) {
564 DEBUGSPEW_CODE(DebugSpew(U) << "converted " << luaG_typename(L1, _val_type) << " to nil" << std::endl);
565 lua_replace(L1, L1_i); // L1: ... mt
566 lua_pop(L1, 1); // L1: ...
567 _val_type = _converterType;
568 } else {
569 raise_luaL_error(getErrL(), "Invalid %s type %s", kConvertField.data(), luaG_typename(L1, _converterType).data());
570 }
571 break;
572
573 case LuaType::STRING:
574 // kConvertField == "decay" -> replace source value with it's pointer
575 if (std::string_view const _mode{ luaG_tostring(L1, -1) }; _mode == "decay") {
576 lua_pop(L1, 1); // L1: ... mt
577 lua_pushlightuserdata(L1, const_cast<void*>(lua_topointer(L1, L1_i))); // L1: ... mt decayed
578 lua_replace(L1, L1_i); // L1: ... mt
579 lua_pop(L1, 1); // L1: ...
580 _val_type = LuaType::LIGHTUSERDATA;
581 } else {
582 raise_luaL_error(getErrL(), "Invalid %s mode '%s'", kConvertField.data(), _mode.data());
583 }
584 break;
585
586 case LuaType::FUNCTION:
587 lua_pushvalue(L1, L1_i); // L1: ... mt kConvertField val
588 std::ignore = luaG_pushstring(L1, mode == LookupMode::ToKeeper ? "keeper" : "regular"); // L1: ... mt kConvertField val string
589 lua_call(L1, 2, 1); // val:kConvertField(str) -> result // L1: ... mt kConvertField converted
590 lua_replace(L1, L1_i); // L1: ... mt
591 lua_pop(L1, 1); // L1: ... mt
592 _val_type = luaG_type(L1, L1_i);
593 break;
594
595 default:
596 raise_luaL_error(getErrL(), "Invalid %s type %s", kConvertField.data(), luaG_typename(L1, _converterType).data());
597 }
598 STACK_CHECK(L1, 0);
599 LUA_ASSERT(getErrL(), luaG_type(L1, L1_i) == _val_type);
600 return _val_type;
601}
602
603// #################################################################################################
604
535[[nodiscard]] bool InterCopyContext::push_cached_metatable() const 605[[nodiscard]] bool InterCopyContext::push_cached_metatable() const
536{ 606{
537 STACK_CHECK_START_REL(L1, 0); 607 STACK_CHECK_START_REL(L1, 0);
@@ -1085,21 +1155,10 @@ namespace {
1085 DEBUGSPEW_CODE(DebugSpew(U) << "inter_copy_one()" << std::endl); 1155 DEBUGSPEW_CODE(DebugSpew(U) << "inter_copy_one()" << std::endl);
1086 DEBUGSPEW_CODE(DebugSpewIndentScope _scope{ U }); 1156 DEBUGSPEW_CODE(DebugSpewIndentScope _scope{ U });
1087 1157
1088 LuaType _val_type{ luaG_type(L1, L1_i) }; 1158 // replace the value at L1_i with the result of a conversion if required
1089 DEBUGSPEW_CODE(DebugSpew(U) << local::sLuaTypeNames[static_cast<int>(_val_type)] << " " << local::sValueTypeNames[static_cast<int>(vt)] << ": "); 1159 LuaType const _val_type{ processConversion() };
1090
1091 // Non-POD can be skipped if its metatable contains { __lanesignore = true }
1092 if (((1 << static_cast<int>(_val_type)) & kPODmask) == 0) {
1093 if (lua_getmetatable(L1, L1_i)) { // L1: ... mt
1094 LuaType const _type{ luaG_getfield(L1, -1, "__lanesignore") }; // L1: ... mt ignore?
1095 if (_type == LuaType::BOOLEAN && lua_toboolean(L1, -1)) {
1096 DEBUGSPEW_CODE(DebugSpew(U) << "__lanesignore -> LUA_TNIL" << std::endl);
1097 _val_type = LuaType::NIL;
1098 }
1099 lua_pop(L1, 2); // L1: ...
1100 }
1101 }
1102 STACK_CHECK(L1, 0); 1160 STACK_CHECK(L1, 0);
1161 DEBUGSPEW_CODE(DebugSpew(U) << local::sLuaTypeNames[static_cast<int>(_val_type)] << " " << local::sValueTypeNames[static_cast<int>(vt)] << ": ");
1103 1162
1104 // Lets push nil to L2 if the object should be ignored 1163 // Lets push nil to L2 if the object should be ignored
1105 bool _ret{ true }; 1164 bool _ret{ true };
diff --git a/src/intercopycontext.h b/src/intercopycontext.h
index ffa825f..459551e 100644
--- a/src/intercopycontext.h
+++ b/src/intercopycontext.h
@@ -38,10 +38,11 @@ class InterCopyContext
38 char const* name; // that one can change when we reuse the context 38 char const* name; // that one can change when we reuse the context
39 39
40 private: 40 private:
41 [[nodiscard]] std::string_view findLookupName() const;
41 // when mode == LookupMode::FromKeeper, L1 is a keeper state and L2 is not, therefore L2 is the state where we want to raise the error 42 // when mode == LookupMode::FromKeeper, L1 is a keeper state and L2 is not, therefore L2 is the state where we want to raise the error
42 // whon mode != LookupMode::FromKeeper, L1 is not a keeper state, therefore L1 is the state where we want to raise the error 43 // whon mode != LookupMode::FromKeeper, L1 is not a keeper state, therefore L1 is the state where we want to raise the error
43 lua_State* getErrL() const { return (mode == LookupMode::FromKeeper) ? L2 : L1; } 44 lua_State* getErrL() const { return (mode == LookupMode::FromKeeper) ? L2 : L1; }
44 [[nodiscard]] std::string_view findLookupName() const; 45 [[nodiscard]] LuaType processConversion() const;
45 46
46 // for use in copy_cached_func 47 // for use in copy_cached_func
47 void copy_func() const; 48 void copy_func() const;
diff --git a/tests/basic.lua b/tests/basic.lua
index 20865cf..28f0334 100644
--- a/tests/basic.lua
+++ b/tests/basic.lua
@@ -226,7 +226,7 @@ local chunk= function(linda)
226 k,v=receive(); WR("chunk ", v.." received (expecting 1)\n"); assert(v==1) 226 k,v=receive(); WR("chunk ", v.." received (expecting 1)\n"); assert(v==1)
227 k,v=receive(); WR("chunk ", v.." received (expecting 2)\n"); assert(v==2) 227 k,v=receive(); WR("chunk ", v.." received (expecting 2)\n"); assert(v==2)
228 k,v=receive(); WR("chunk ", v.." received (expecting 3)\n"); assert(v==3) 228 k,v=receive(); WR("chunk ", v.." received (expecting 3)\n"); assert(v==3)
229 k,v=receive(); WR("chunk ", tostring(v).." received (expecting nil from __lanesignore)\n"); assert(v==nil) -- a table with __lanesignore was sent 229 k,v=receive(); WR("chunk ", tostring(v).." received (expecting nil from __lanesconvert)\n"); assert(v==nil, "table with __lanesconvert==lanes.null should be received as nil, got " .. tostring(v)) -- a table with __lanesconvert was sent
230 k,v=receive(); WR("chunk ", tostring(v).." received (expecting nil)\n"); assert(v==nil) 230 k,v=receive(); WR("chunk ", tostring(v).." received (expecting nil)\n"); assert(v==nil)
231 231
232 send(4,5,6); WR("chunk ", "4,5,6 sent\n") 232 send(4,5,6); WR("chunk ", "4,5,6 sent\n")
@@ -277,7 +277,7 @@ local comms_lane = lanes_gen("io", {gc_cb = gc_cb, name = "auto"}, chunk)(linda)
277SEND(1); WR("main ", "1 sent\n") 277SEND(1); WR("main ", "1 sent\n")
278SEND(2); WR("main ", "2 sent\n") 278SEND(2); WR("main ", "2 sent\n")
279SEND(3); WR("main ", "3 sent\n") 279SEND(3); WR("main ", "3 sent\n")
280SEND(setmetatable({"should be ignored"},{__lanesignore=true})); WR("main ", "__lanesignore table sent\n") 280SEND(setmetatable({"should be ignored"},{__lanesconvert=lanes.null})); WR("main ", "__lanesconvert table sent\n")
281for i=1,40 do 281for i=1,40 do
282 WR "." 282 WR "."
283 SLEEP(0.0001) 283 SLEEP(0.0001)