Implementing simple no-frills coroutines is surprisingly easy, due to the way the Astrobe compiler creates code:
- before calling a (non-function) procedure, any computed values in registers have been safely stored, either onto the stack at the location of the corresponding local variables (including value parameters), or into the variables passed as VAR parameters, or into module variables, and
- the procedure prologue and epilogue can directly be used for the coroutine transfer logic.
Control between coroutines is passed using procedure
Coroutines.Transfer. Due to the above, no data registers or computational state need to be preserved upon such a context switch.1
Here’s the most basic implementation. All functionality related to monitoring (stack monitoring, procedure call traces) is omitted for clarity.
Each coroutine is described by a RECORD:
TYPE Coroutine* = POINTER TO CoroutineDesc; CoroutineDesc* = RECORD sp: INTEGER; (* ... *) END;
SP: stored stack pointer register value
CONST SP = 14; PROCEDURE Transfer*(f, t: Coroutine); BEGIN (* prologue *) f.sp := SYSTEM.REG(SP); SYSTEM.LDREG(SP, t.sp) (* epilogue *) END Transfer;
Neglecting for now how to initially set up the coroutine to make this work, let’s assume we have two running coroutines, both have done transfers before (“steady state”), and we want to transfer control from
t, that is, coroutine
Coroutine.Transfer(f, t): 2
- Upon entry, the stack of
fis in use. The prologue pushes the LNK register value, and parameters
tonto that stack. The stack pointer value is decreased by 12 (four bytes for LNK,
- The SP register value is saved as
- The SP register gets set to
t.sp. At this point,
tis still accessible on the stack of
t.sphas been set when coroutine
tdid its last transfer, ie. when it had the role of
f, see the preceding step.
- Now, the stack of
- The epilogue restores the LNK register value from the stack of
t, and adjusts the stack pointer value by +12. The LNK value has been set when coroutine
tdid its last transfer, ie. by the corresponding prologue.
- The epilogue branches to LNK, hence
tcontinues right after at the point when it last called
Note that any kind of coroutine status data could be pushed onto the stack before the switch if ever needed.
For the first invocation via
Transfer, the coroutine stack needs to be set up, to a state as if a
Coroutines.Transfer had been done before, in order for the first transfer to the new coroutine to work.
PROCEDURE Init*(cor: Coroutine; code: PROCEDURE; stackAdr, stackSize: INTEGER); BEGIN (* ... *) cor.sp := stackAdr + stackSize; DEC(cor.sp, 12); SYSTEM.PUT(cor.sp, SYSTEM.VAL(INTEGER, code)); END Init;
- Set the stored stack pointer value
cor.spto the top of the stack memory area.
cor.spto allow the “+12” adjustment in the epilogue of the initial
- Store the coroutine code address on the stack, ie. at the position of the LNK register value.
The stack of the new coroutine is now in a state to allow to be transferred-to from another coroutine.
The First Coroutine
All system and control processes are invoked by the scheduler coroutine,
Coroutines.Transfer, initially for new coroutines (processes), and then in steady state. Hence we need to set up this first coroutine at the end of the start-up sequence.
Processes creates the coroutine RECORD for the scheduler:
VAR loop: Coroutines.Coroutine. (* ... *) NEW(loop); ASSERT(loop # NIL); (* ... *)
Processes.Go at the end of the start-up sequence:3
PROCEDURE Go*; VAR jump: Coroutines.Coroutine; BEGIN SYSTEM.LDREG(SP, SchedulerStackTop - 32); NEW(jump); Coroutines.Init(loop, Loop, SchedulerStackBottom, SchedulerStackSize); Coroutines.Transfer(jump, loop) END Go;
jump is just a temporary data structure to allow this initial transfer. It does not need to be initialised, as we’ll never transfer back to
jump, in fact we could not, as it will be collected.
Furthermore, as described here, the stack used by the startup process, including the body of module
Oberon, is being reallocated to the scheduler coroutine and the commands handling process. Hence, the stack used by
Processes.Go will be overwritten by
Coroutines.Init. Therefore, the stack pointer is set to not use the top part of the scheduler stack.
With Astrobe for the Cortex processors, this would not be true if ‘Coroutines.Transfer’ were declared a leaf procedure. Hence in the Cortex version there is a NOTE!, saying “No, this cannot be a leaf procedure!”, since one could be tempted… :) ↩︎
Remember that coroutine transfers are always between explicitly known coroutines. For “anonymous” yielding, a scheduler is required. ↩︎
As does module
Errorsat the end of the error-recovery procedure. ↩︎