Skip to main content

Squirrel Generator Functions

How To Add Parallel Processing Capabilities To Your Code

Squirrel programs, whether they are running on the device or in the impCloud™ as agents, have to be ‘event driven’. In this methodology, the system runs the main program loop on your behalf, and you write code that attaches to the main loop as handlers for the specific individual events you are interested in. This is because there are other, system-maintained event handlers that also hang off the same loop, for example to deal with network messages.

  • If you’re new to the event-driven approach, we suggest you take a look at the Developer Guide Event-driven Programming, which explores in detail how imp application code uses this technique.

While this event-driven approach is a well-known way of writing desktop programs, mobile apps and server software, it is less common in the embedded world, where programmers are more used to writing their own main loop. But if you attempt to do that on an imp, you’ll find that other important things, including network messages, don’t get handled.

Fortunately, there is a way to write what looks very much like a main loop yet still allows the system to do what it needs to do; it allows the system event handlers to continue running. This technique relies on an little-known feature of the Squirrel language: generators.

Yield

A generator is a special kind of function. When a generator is first called, it doesn’t execute immediately and in its entirety in the the way an ordinary function would. Instead it is loaded into memory and its execution suspended until it is required.

It is the presence of Squirrel’s yield keyword in one or more statements within the code that marks the function as a generator. When the generator function is called in the customary way, Squirrel establishes the function’s data structure in memory and then checks for the presence of yield. Having found the keyword, Squirrel returns a reference to the generator. If there is no yield, the function executes there and then.

To activate the generator, the calling code must include a resume statement with the generator’s reference. When resume is encountered, Squirrel runs the generator code. When the Squirrel encounters yield, it completes the current line and then once again suspends the execution of the function. It preserves the function’s state, including all of its local variables.

If the generator is brought back into action by a further resume statement, the generator will continue to execute its code with the line that followed the suspending yield statement.

Think of a generator, then, as a set of instructions which can be executed in stages at arbitrary points in time without losing any of its state.

Emulating main()

To go back to our original example, here is some sample code — a typical ‘Hello World’ program that flashes an LED — which might be used to simulate an embedded device’s main loop:

function mainloop() {
    while (true) {
        hardware.pin1.write(1);
        yield setTimer(1.0);
        hardware.pin1.write(0);
        yield setTimer(1.0);
    }
}

function setTimer(duration) {
    imp.wakeup(duration, function() {
        resume looper;
    });
}

// Set up a pin connected to an LED
// NOTE You may need to change 'pin1' 
// if you are not using an impExplorer Kit
hardware.pin1.configure(DIGITAL_OUT, 0);
looper <- mainloop();
resume looper;

The function mainloop() loops infinitely. At each pass, it writes a 1 to the imp’s pin 1, waits a second, writes a zero, waits another second, and then starts all over again. To do so, it contains a while(true) loop which you would normally expect to block the CPU and thus something to be avoided in an event-driven environment such as impOS. However, by including yield statements after each pin write, we establish points where we can suspend the execution of mainloop() and step out of it to run other code.

The yield statement cedes runtime to the evaluation of the expression that follows it. In this case, we yield to the second function, setTimer(), which sets a timer to wake after the passed period and resume mainloop(), accessed through the global variable, looper.

The next three lines are run first. After configuring the output pin, we call mainloop() and store the result in looper. The presence of at least one yield statement in mainloop() causes Squirrel to create a generator from mainloop() and return a reference to this generator. This reference is stored in looper. Note that we don’t simply load looper with a reference to mainloop() as we might if we intended mainloop() to be a callback function, for example.

At this point the generator is suspended, so the final line uses the resume command to activate it. The generator begins its operation by instigating the seemingly closed loop, writes a 1 to pin 1 to turn on the LED, and then yields its execution to setTimer().

When this timer fires, the generator resumes execution right after the yield that caused the suspension. It writes a 0 to pin 1, turning the LED off again. The next line once again causes the generator to yield its runtime, again to setTimer().

This time when the timer queued by setTimer() fires, the generator’s while loop checks its condition and re-iterates the loop: pin 1 goes to 1, and the first yield statement is run once again, etc., etc.

Although we have what appears to be an infinitely blocking loop structure — replicating, say, an Arduino loop() function, which is an implicit while(true) loop — we repeatedly yield the loop’s runtime to the other code. After setTimer() has set up the timer, Squirrel idles. This allows the system’s own event loop to be processed. As far as the rest of the code inside the generator is concerned, however, it is running on the CPU exclusively, with a couple of one-second delay loops built in.

Every generator comes to an end when it hits a return statement or returns implicitly by reaching the end of the sequence of instructions it contains. At this point, the generator is considered dead and can no longer be resumed. In our sample code, the generator never dies, and the loop runs forever, just like an Arduino loop(). Squirrel runtime errors will automatically cause generators to die, as will exceptions that are not caught and handled.

Return Values

When called yield returns the result of the expression to its right. For example:

function loopsteps(total) {
    for (local i = 1 ; i < total + 1 ; i++) {
        yield i;
    }

    return null;
}

local looper = loopsteps(10);
local yieldResult;

while (yieldResult = resume looper) {
    server.log("Step through the loop: " + format("%u", yieldResult));
}

This will display ten entries in the impCentral log, with the numbers 1 through 10, each value return by yield i and relayed by resume looper.

Retained State

It’s the generator’s ability to retain its state even though its execution is suspended that gives the generator power that alternative constructions lack. For instance, we might use an imp.wakeup() call to restart the function. We might have written:

function mainloop() {
    hardware.pin1.write(flag ? 1 : 0);
    flag = !flag;
    if (flashes++ == 30) return;
    imp.wakeup(0.5, mainloop);
}


hardware.pin1.configure(DIGITAL_OUT, 0);
flag <- true;
flashes <- 0;
mainloop();

This switches pin 1 on and off every half-second, while also allowing the system access to processing resources in the meantime. However, this approach doesn’t preserve variables within its scope. Adding a loop counter shows how values persist across iterations of a generator. We could, for instance, modify mainloop() this way:

function mainloop() {
    local flashes = 0;
    local flag = true;

    while (true) {
        hardware.pin1.write(flag ? 1 : 0);
        flag = !flag;
        if (flashes++ == 30) return;
        yield imp.wakeup(0.5, function() {
            resume looper;
        });
    }
}

hardware.pin1.configure(DIGITAL_OUT, 0);
looper <- mainloop();
resume looper;

The outcome is the same, but now all the variables relevant to mainloop() are encapsulated within it and maintained until it returns.

Automatic Looping

Squirrel’s foreach command provides a convenient way of managing generator loop stages until the function has run its course:

function intGenerator() {
    server.log("Yielding 1");
    yield 1;
    server.log("Yielding 2");
    yield 2;
    server.log("Yielding 4");
    yield 4;
    server.log("RETURNing 8");
    return 8;
}

local looper = intGenerator();
foreach (stage in looper) {
    server.log(stage);
}

The foreach statement detects that looper is a reference to a generator and so automatically issues resume commands: once to start it up and then again following each of the generator’s yield statements. The generator’s return statement triggers the exit from the foreach loop; stage will never take the value 8, only 1, 2 and 4.

Other Features

Calling a single generator function multiple times will create multiple, independent instances of the generator:

function intGenerator() {
    server.log("Yielding 1");
    yield 1;
    server.log("Yielding 2");
    yield 2;
    server.log("Yielding 4");
    yield 4;
    server.log("RETURNing 8");
    return 8;
}

local looper1 = intGenerator();
local looper2 = intGenerator();
local looper3 = intGenerator();
local loopers = [looper1, looper2, looper3];
foreach (looper in loopers) {
    foreach (stage in looper) {
        server.log(stage);
    }
}

The three variables looper1, looper2 and looper3 are entirely separate entities, though of course they all perform exactly the same tasks. Passing a parameter can be used to distinguish one from the other:

function intGenerator(value) {
    server.log("Yielding 1...");
    yield value + 1;
    server.log("Yielding 2...");
    yield value + 2;
    server.log("Yielding 4...");
    yield value + 4;
    server.log("RETURNing 8...");
    return value + 8;
}

local looper1 = intGenerator(10);
local looper2 = intGenerator(20);
local looper3 = intGenerator(30);
local loopers = [looper1, looper2, looper3];
local i = 0;
local returnValue;
while (returnValue = resume loopers[i]) {
    server.log("   ...Yielded " + returnValue);
    if (returnValue == 38) break;
    i = i < 2 ? i + 1 : 0;
}

Generators are first-class objects in Squirrel so they can be stored in arrays and tables:

local generators = [looper1, looper2, looper3];

and operate as the properties of other objects. However, because they are functions they can’t be serialized, neither can arrays or table containing them.

Storing generators in arrays and tables is particularly useful nonetheless: it allows you to maintain multiple, parallel loops. This provides a convenient way of storing the references to each generator, allowing you to iterate through all of them, triggering each with a resume command.

Multiple generators can therefore mimic a multitasking environment without any need to write complex code to manage threads. In this case, because all other generators are suspended when one is running — ie. the code is not truly parallel — there is no need to lock resources to prevent, say, global variables being changed by one generator mid-way through a calculation being performed on those variables by another.

Memory Considerations

There is no limit on the number of generators you can employ, either in device or agent code, except for the memory they take up. That memory consists of the stack frame of the generator function, which includes all the generator function’s local variables. However, you can easily keep track of available memory by using the imp API method imp.getmemoryfree().