Menu

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:

From now on, M can re-invoke P with Resume(P_RC):

The cycle terminates when:

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 Made OutOf FRC;
    }

    Submit();
    while (1) Build();
}