Examples

Here, we take a closer look at the examples in the demo package. Each section below describes the functionality of a file in the demo package, and what aspect of Storm it tries to illustrate.

Eval

The files eval.bs and eval.bnf illustrates the tight interaction between different languages by implementing a simple calculator that can evaluate simple expressions. Call demo:eval("1 + 3") to evaluate expressions, or demo:tree("1 + 3") to see the syntax tree for expressions.

delimiter = Whitespace;

void Whitespace();
Whitespace : "[ \n\r\t]*";

Int Expr();
Expr => +(a, b) : Expr a, "\+", Prod b;
Expr => -(a, b) : Expr a, "\-", Prod b;
Expr => v : Prod v;

Int Prod();
Prod => *(a, b) : Prod a, "\*", Atom b;
Prod => /(a, b) : Prod a, "/", Atom b;
Prod => v : Atom v;

Int Atom();
Atom => toInt(v) : "[0-9]+" v;
Atom => v : "(", Expr v, ")";
Atom => -(v) : "\-", Atom v;

The file eval.bnf contains the grammar describing the expressions supported by the calculator. Expr is the rule that describes entire expressions, and it is therefore the start rule. Aside from the language, the bnf-file also describes how to transform the syntax tree from a successful parse into a representation that is more convenient for the user of the grammar. In this particular case, we are only interested in evaluating the parsed expression, and as such we use the transformations to evaluate the expression rather than constructing another representation. See BNF Syntax for details on the syntax and semantics of the grammar language in Storm.

use core:lang;
use core:io;

// Evaluate things using the syntax!
Int eval(Str v) {
    tree(v).transform();
}

Expr tree(Str v) {
    Parser<Expr> p;
    p.parse(v, Url());
    if (p.hasError)
        p.throwError;
    p.tree;
}

The functions eval and tree are implemented in Basic Storm in the file eval.bs. The tree function creates Parser instance which parses strings starting with the supplied Expr rule, parses the string supplied and returns the syntax tree unless there is an error in the input. eval simply calls tree to get the syntax tree for the parse, but then continues to transform the syntax tree into the representation described in the bnf-file (which is implemented as evaluating the expression).

In this file, we are using Expr, which is defined as a rule in the syntax language, as if it was a regular type. Basic Storm has no special knowledge of rules and productions defined in the syntax language, but since the syntax language stores rules and productions as types in the name tree Storm provides, any language may access and use them. The types defined by rules and productions are used to store the syntax tree that results from parsing a string, which is why the tree function returns an Expr. Furthermore, Expr contains a function transform, which returns the type specified when we declared Expr in the syntax language. This function is generated by the syntax language, and it transforms the current syntax node according to the specification in the syntax language. If we had specified names for the productions, we could have inspected the syntax tree using constructs like if (x as AddExpr) and then inspecting the member variables in those types. This illustrates the kind of close cooperation possible within Storm, without even losing type safety.

Present

This file serves to illustrate the fact that the syntax highlighting provided by Storm properly supports language extensions. Open the file in Emacs in storm-mode, uncomment the present-block and see what happens. Since the present package, which contains the syntax needed, is not yet included, the syntax highlighting will be wrong. Now, uncomment the use present;-line in the top of the file and see that the syntax highlighting is now correct since the proper grammar is now included.

The grammar for the presentation language is available in the file root/presentation/syntax.bnf, which is shown below:

use core.lang;
use lang.bs;
use layout;

// Borrow the low-level syntax from Basic Storm.
optional delimiter = SDelimiter;
required delimiter = SRequiredDelimiter;

// Entry point to the grammar: declare a presentation.
SPlainFileItem => PresDecl(name, title, env, cont) : "presentation" #keyword ~ SName name #typeName, SDumbString title, "{" [, SPresCont @cont,]+ "}";

// Contents of a presentation block...
void SPresCont(ExprBlock me);
SPresCont : (SPresStmt(me) -> add,)*;

// either regular Basic Storm statements, or our special ones.
Expr SPresStmt(Block block);
SPresStmt => e : SStmt(block) e;
SPresStmt => slideLayout(block, layout, name, intro)
    : (SName name, "=", )? "slide" #keyword ~ SIntro(block) intro, SLayoutRoot(block) layout;
SPresStmt => slidePlainLayout(block, layout, name, intro)
        : (SName name, "=", )? "borderless" #keyword ~ "slide" #keyword ~ SIntro(block) intro, SLayoutRoot(block) layout;
SPresStmt => slideBackground(block, layout)
    : "background" #keyword ~ SLayoutRoot(block) layout;

// Slide intro animation.
Maybe<Expr> SIntro(Block block);
SIntro => Maybe<Expr>() : ;
SIntro => slideIntro(block, name, params) : SType name, SParamList(block) params, "=>";
SIntro => slideIntro(block, name, params) : SType name, "(", SParamList(block) params, ")", "=>";

// Extend the syntax to allow skipping parens around parameter lists inside presentation blocks.
SPresStmt..SLayout => LayoutBlock(block, name, params) : SType name, SParamList(block.block) params, "{" [, SLayoutContent(me), ]+ "}";

// Specify animations for elements.
SLayoutItem => add(block, ani) : SElemAni(block.block) ani, ";";

// Create animations for elements.
AniDecl SElemAni(Block block);
SElemAni => AniDecl(block, step, name, params)
    : "@" #keyword, "[0-9]+" step #constant, (SAniOpts(me, block),)* ":", SType name, SParamList(block) params;

// Animation options.
void SAniOpts(AniDecl to, Block block);
SAniOpts => setOffset(to, time) : "+" #keyword, SExpr(block) time;
SAniOpts => setDuration(to, time) : ",", SExpr(block) time;

The presentation language also uses syntax from the layout language, which is implemented in root/layout/syntax.bnf as follows:

use core.lang;
use lang.bs;

// Borrow the low-level syntax from Basic Storm.
optional delimiter = SDelimiter;
required delimiter = SRequiredDelimiter;

// Backend-agnostic version of the syntax. There might be more specialized variants for use
// with specific backends for additional convenience.
lang.bs.SAtom => block(l) : "layout" #keyword ~ SLayoutRoot(block) l;

LayoutRoot SLayoutRoot(Block block);
SLayoutRoot => createRoot(block) : SLayout(me) -> add;

// Define an instance of a Layout object with associated properties.
LayoutBlock SLayout(LayoutRoot block);
SLayout => LayoutBlock(block, name, params) : SType name, ("(", SParamList(block.block) params, ")",)? "{" [, SLayoutContent(me),]+ "}";

// Contents of a Layout object.
void SLayoutContent(LayoutBlock block);
SLayoutContent : (SLayoutItem(block),)*;

// One property or a nested layout object.
void SLayoutItem(LayoutBlock block);

// A property, either in this layout or its parent.
SLayoutItem => add(block, name, params) : SName name #varName - (, ":", SParamList(block.block) params)?, ";";

// A nested layout item.
SLayoutItem => add(block, l) : SLayout(block) l;

// Parameter list.
Actuals SParamList(Block root);
SParamList => Actuals() : ;
SParamList => Actuals() : lang.bs.SExpr(root) -> add - (, ",", lang.bs.SExpr(root) -> add)*;

Finally, these can be used as illustrated in the example in root/presentation/test/simple.bs:

use presentation;
use layout;

// Declare the presentation. Uses an extension implemented in the package 'presentation'.
presentation Simple "My presentation" {
    // Generate a random caption for the first slide.
    Str caption = "Presentation number " + rand(1, 10).toS;

    // Create a slide.
    slide title caption, "By myself" {}

    // Another one, with an animation!
    slide FadeIn => content "Hello!" {
        list [ "Welcome to " + title, "In Storm!" ] {}
    }
}

void simple() {
    Simple.show();
}

Reload

This file shows that it is possible to reload code in an already running program in Storm (to certain degrees, at least).

use core:debug;

void myPrint(Nat v) {
    print(v.toS);
    // print("*" * v);
}

void slowFn(Int times) on Demo {
    for (Int i = 0; i < times; i++) {
        myPrint((i + 1).nat);
        sleep(1 s);
    }
    print("Done!");
}

void reloadMain() {
    spawn slowFn(10);
}

From the REPL, call demo:reloadMain, and you shall see that you are returned to the prompt, but the numbers 1 to 10 are displayed in sequence in the background. While this is happening, change the myPrint function by commenting the first print statement and replace it with the second one and type reload{demo} into the REPL. Now, you shall see stars being displayed instead of numbers, even when you reload the code in the middle of the running code!

Note that any changes made to the slowFn function while it is being executed will not be visible. This is because code reloads replace entire functions, and since the call stack will still contain a pointer to the old version of slowFn, the old version will be used until slowFn is complete.

Thread

The file thread.bs illustrates how the threading system in Storm works.

use core:debug;

thread Demo;

Int threadDemo(Str data, Int times) on Demo {
    for (Int i = 0; i < times; i++) {
        print(data * (i + 1).nat);
        dbgSleep(100);
        dbgYield;
        // sleep(500 ms);
    }
    times * 2;
}

Int seq() on Compiler {
    var a = threadDemo("A", 10);
    print("1");
    var b = threadDemo("B", 10);
    print("2");
    a + b;
}

Int spawn() on Compiler {
    var a = spawn threadDemo("A", 10);
    print("1");
    var b = spawn threadDemo("B", 10);
    print("2");
    a.result + b.result;
}

The seq function calls the function threadDemo twice. Since threadDemo is declared to be executed on the Demo thread, this causes Storm to post a message to the Demo thread, asking for the function to be executed there. This all happens behind the scenes, and the function call behaves (almost) as if it was being a regular function.

The function spawn, on the other hand calls threadDemo using the spawn keyword. This causes execution in Spawn to progress until a.result + b.result is being evaluated (a and b are Future<Int> objects here). This time, we can see that the A and B outputs are interleaved. However, the execution is still entirely deterministic. As both calls to threadDemo are being executed on the same OS thread, no thread switching is performed until one of the calls explicitly yields. dbgSleep is a version of sleep that blocks the entire OS thread while sleep does not block the thread if there is other work to do. dbgYield performs an explicit yield. This is not generally necessary, as any primitive in Storm that could block the current thread ensures to perform a yield before attempting to block the thread.

See Threads for details on the semantics of the threading system in Storm.

Actor

The file actor.bs is another version of the example in thread.bs, using actors instead of plain functions.

use core:debug;

thread Actor1;
thread Actor2;

class Output on ? {
    Str text;
    Int times;

    init(Thread, Str text, Int times) {
        init() {
            text = text;
            times = times;
        }
    }

    Int run() {
        for (Int i = 0; i < times; i++) {
            print(text * (i + 1).nat);
            sleep(150 ms);
        }
        times * 2;
    }
}

Int actorSeq() on Compiler {
    Output a(Actor1, "A", 10);
    Output b(Actor2, "B", 10);
    var x = a.run();
    print("1");
    var y = b.run();
    print("2");
    x + y;
}

Int actorSpawn() on Compiler {
    Output a(Actor1, "A", 10);
    Output b(Actor2, "B", 10);
    var x = spawn a.run();
    print("1");
    var y = spawn b.run();
    print("2");
    x.result + y.result;
}

The actorSeq function creates two actors on different OS threads and calls run on both of them. As in the previous example, Storm posts a message to the proper thread behind the scenes, so calling the run function appears as a regular function call. As such, the output is deterministic and should match the output of seq in the previous example.

The function actorSpawn is similar to spawn in the previous example. However, this example is not deterministic as the actors are associated with different threads that are scheduled independently by the operating system. Try running this function multiple times and see if you can see different interleavings!

See Threads for details on the semantics of the threading system in Storm.