scScript Coroutines
There is a good deal of theoretical literature on coroutines and their use as light-weight threads. The most common examples tend to depict a server architecture of some kind with one or more persistent clients or sources of data. So perhaps a better name would be PermRoutines.
Technically, scScript provides stackfull asymmetrical (full) coroutines. In a nutshell, this mechanism allows a function to persist after its initial call and execution. Therefore, the persistent function can keep its local information (variables and execution state) after returning (initially with submit, subsequently with yield) then get re-invoked (repeatedly!) from where it left off. Such persistent functions are normally implemented in C as separate threads, but a reasonable facsimile can be emulated using static local variables to save state. scScript cannot declare static locals, although it can have private vars in closures. Then again, scScript can make any function persist!
Both the persistent routine (P) itself and its
caller/master (M) should be aware of the situation, so a
small initial handshake is required:
McallsPlike any other function, with the proper in/out arg list.Pinitializes and does aSubmit()to return itsRCback toM.Mstores theRCsubmitted byPin a variable (P_RC).Mperforms its own work...
From now on, M can re-invoke P
with Resume(P_RC):
Presumes execution where it left off (afterSubmit).Pcompletes its task/computation then does:Yield Val.Mtakes the yieldedVal(returned from its call toResume) and performs its own work…- Later,
Mre-invokesPwithResume(P_RC)again! Presumes execution where it left off (afterYield) and the cycle continues…
The cycle terminates when:
Mdoes aRelease(P_RC)orPdoes aReturninstead of aYield.
If P terminates with
a Return, M must know not to invoke it again
with another Resume (as it will fail). So it is generally
recommended that persistent functions Yield non-zero values
but Return 0 to signal termination. Of
course, P can establish a much richer exchange
with M using its out params. Yield (just
like Return) can also exit non-locally
using OutOf V where V contains
the RC passed down from P. (P
can gets its own RC using GetRC(), this will
be exactly the same RC that Submit passed
to M.)
Yield, except in specialized cases,
should always be called in a loop! It is a very common mistake to
forget the loop, thus getting only a single cycle out of a
persistent function. (Yes, I have fallen in this hole
myself... multiple times!)
The following is a somewhat traditional, if playful, example, first written without coroutines:
// Widget factory... no coroutines! (CR1.sc) Function OrderA()() {Var I = Sys.Rand(0, 10); Writeln(" Supplier: Sending " , I," part A to factory" );Return I; }Function OrderB()() { … }// Just like Order A Function OrderC()() { … }// Just like Order A Var A, B, C;Var Record;Function Factory()() {Var Made; Writeln("Factory: Part Shelf [" , A, " ", B, " ", C,"]" );// Order Widget parts If (!A) Writeln(" Order part A" ), A = OrderA();If (!B) Writeln(" Order part B" ), B = OrderB();If (!C) Writeln(" Order part C" ), C = OrderC(); Write("\n Assembling " );While (A && B && C) { Write("*" ); Made += 1; --A, --B, --C; }If (Made) Writeln(" Shipping " , Made," Widgets to HQ!" );Else Writeln(" Failed!! Nothing to ship..." );If (Made > Record) { Record = Made; Write(" The factory is celebrating a new record!\n\n" ); }return Made; }Function Sales()() {Var I = Sys.Rand(0, 20); Write("Sales: Sold " , I," Widgets!!\n\n" );return I; }Function WidgetHQ()() {Var Sold;Var Stored;Var Cycles; Writeln("ACME Widgets Inc." ); Write(" Starting Operation...\n\n" );Do { Sold = Sales();while (Sold > Stored) { Stored += Factory(); Write("HQ Inventory: " , Stored," Widgets.\n\n" ); } Stored -= Sold; Write("** Fulfilled to customer: " , Sold,"!!\n\n" ); }while ((Cycles++ < 10) || Stored); } WidgetHQ();
Now the same example but written with coroutines:
// Widget factory... WITH coroutines! (CR2.sc) Function OrderA()() {Var I = Sys.Rand(0, 10); Writeln(" Supplier: Sending " , I," part A to factory" );Return I; }Function OrderB()() { … }// Just like Order A Function OrderC()() { … }// Just like Order A Function Factory()() {Var Made;Var A, B, C;Var Record;Submit ();while (1) { Made = 0; Writeln("Factory:" );// Order Widget parts If (!A) Writeln(" Order part A" ), A = OrderA();If (!B) Writeln(" Order part B" ), B = OrderB();If (!C) Writeln(" Order part C" ), C = OrderC(); Write("\n Assembling " );While (A && B && C) { Write("*" ); Made += 1; --A, --B, --C; }If (Made) Writeln(" Shipping " , Made," Widgets to HQ!" );Else Writeln(" Failed!! Nothing to ship..." );If (Made > Record) { Record = Made; Write(" The factory is celebrating a new record!\n\n" ); }Yield Made; } }Function Sales()() {Var I = Sys.Rand(0, 20); Write("Sales: Sold " , I," Widgets!!\n\n" );Return I; }Function WidgetHQ()() {Var Sold;Var Stored;Var Cycles;Var FactoryRC; Writeln("ACME Widgets Inc." ); Write(" Starting Operation...\n\n" ); FactoryRC = Factory();Do { Sold = Sales();While (Sold > Stored) { Stored += Resume(FactoryRC); Write("HQ Inventory: " , Stored," Widgets.\n\n" ); } Stored -= Sold; Write("** Fulfilled to customer: " , Sold,"!!\n\n" ); }While ((Cycles++ < 10) || Stored);Release (FactoryRC); } WidgetHQ();
WidgetHQ is the main routine in both cases. It calls
the Sales department, which like a typical internet
business, sells widgets it does not have. Then the business scrambles
to get the widgets produced at the Factory and sent
to HQ for shipping to customers. But
the Factory itself has to order
parts A, B, and C from unreliable
suppliers who, perhaps suffering from a language barrier, seem to ship a
random number (if any) each time. So the Factory does its
best and builds as many widgets as it can each cycle, bringing joy to
this drudgery by celebrating each time a new production record is
achieved. All of this gets reported in the *Output* window
when the script runs.
There are a few slight differences in the two examples. In the
coroutine version, WidgetHQ nests the
main Do...While inner loop between a call
to Factory(), where it stashes the
returned RC, and the Release
of RC. In turn, the persistent Factory does
an initial Submit to kick things off, then performs
a Yield instead of a Return. It also runs
in a seemingly endless While(1) loop, where it resets
the Made count each time. Finally, WidgetHQ
(the master) calls Release to terminate the Factory.
Importantly, the persistent Factory routine can store all
its own variables and does not need to use globals. This would be a
great advantage if the code was more complex and had many more lines,
variables, and subroutines of its own. This
persistent Factory could itself be the master for
different persistent emulations of remote supplier. Coroutines (or
threads) are often used for interfacing to external mechanisms where
they maintain complex state while awaiting actual events.
As previously mentioned, a non-local Return
or Yield is just a matter of passing the RC
down a call chain and using the OutOf modifier. In
the following example (CR3.sc), Factory uses a
separate Build function to illustrate the point.
Function Factory()() {Var Made;Var A, B, C;Var Record;Var FRC = GetRC();Function Build()() { Made = 0; Writeln("Factory:" );// Order Widget parts If (!A) Writeln(" Order part A" ), A = OrderA();If (!B) Writeln(" Order part B" ), B = OrderB();If (!C) Writeln(" Order part C" ), C = OrderC(); Write("\n Assembling " );While (A && B && C) { Write("*" ); Made += 1; --A, --B, --C; }If (Made) Writeln(" Shipping " , Made," Widgets to HQ!" );Else Writeln(" Failed!! Nothing to ship..." );If (Made > Record) { Record = Made; Write(" The factory is celebrating a new record!\n\n" ); }Yield MadeOutOf FRC; }Submit ();while (1) Build(); }