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.