Type system

The type system in Storm is inspired from languages like C++, but modified to suit the threading model in the compiler while also trying to make things simple. Both when declaring and using types. There are three distinct kind of types in Storm: values, classes and actors. Each of these are explained below.

These three kind of types provides steps from pure values (value) to pure references (actor), where classes are something in between. To explain the reasoning behind these three kind of types, we have to take a quick look at the threading model. The goal of the threading model in Storm is to ensure that no data is shared between different threads (and to help the programmer keep track of what runs where, we will see this later on). To accomplish this, Storm takes an approach similar to the actor model. Each thread is an actor, and when calling a function on another thread a message is sent to the other thread. To ensure that no data is shared, anything that is sent in a message has to be copied. Values and classes fall into this category, but actors are an exception to this rule, as actors are only accessed through messages, which are thread safe anyway.

Based on this information, we can see the type system from two different perspectives: when we are working from within a single thread, we can get value semantics by using values, and reference semantics using classes (or actors). When we observe the behavior when types are sent through a message, things are slightly different. In this case, both values and classes seem like they have by-value semantics (any changes from the other thread are not visible) while actors still have by-reference semantics. This is also the reason why classes in Storm behave more like values than in Java.

Function overriding

Storm supports function overriding in classes and actors. Overriding functions causes vtable-based dispatch to be used, but unlike e.g. C++, Storm will keep track of where the vtable is required and only use it if it is really necessary. Thus, you will not pay the cost of overriding functions unless you use it.

A function in a derived class does not have to match a function in the parent class exactly. It is possible to accept wider types (i.e. less specific) in the derived class. However, this means a function could override two functions in a parent class, which is not allowed. This happens if, for example, the parent class contains the functions add(Str) and add(Url), and the derived class contains the function add(Object). However, if the parent class would also contain an exact match, (add(Object) in this case) as well, the exact match is preferred over inexact matches. This restrictive behaviour is to reduce the unintentional surprises in the overriding behaviour.

Intentions regarding function overriding can be asserted by Storm, much like in other languages. This is done by setting flags on the relevant functions (using Function.make(FnFlags)). The following flags are available in this regard:

Cloning

As mentioned above, Storm has to be able to clone values and classes whenever they are sent as a message. Cloning comes in two types: deep and shallow cloning. Shallow cloning is implemented, just like in C++, by the copy constructor of the type. In Basic Storm, a copy constructor is automatically generated for you if you do not declare it yourself.

Deep cloning clones an entire object graph, and manages arbitrary graphs, even graphs containing cycles and shared objects. The deep cloning is implemented using the compiler-generated clone method in core. The compiler generated clone looks like this (in C++-like syntax):

T clone(T v) {
    T tmp(v);
    CloneEnv env;
    tmp.deepCopy(env);
    return tmp;
}

It first calls the copy-constructor, then creates a built-in object called CloneEnv and finally calls the deepCopy member. deepCopy is also automatically generated if you are using Basic Storm, and this function makes sure to either clone or call deepCopy on any member variables of that type. The CloneEnv remembers any objects that have been cloned, so that we can correctly handle any cycles or shared parts of the object graph.

The clone methods are implemented like this:

void deepCopy(CloneEnv env) {
    super.deepCopy(env);
    a = clone(a, env);
    // ...
}

T clone(T object, CloneEnv env) {
    T result = env.cloned(object);
    if (!result) {
        result = new T(object);
        result.deepCopy(object, env);
    }
    return result;
}

Note that cloning an actor simply returns the actor itself, since actors are supposed to reference a single instance.

Type requirements

Any members that are required by Storm will be generated automatically by most languages (such as Basic Storm), but this must be done manually when working in C++.

Values and classes need the following members:

The following members are recommended for all types:

Global variables

Global variables are supported by the system. Global variables are always associated with a named thread, which means that only the specified thread may access the variable. This is so that programs using multiple global variables to represent a single global state will be less likely to be subject to data races due to multiple threads accessing the variables. Disallowing access from all threads but one makes sure programs provide accessors that enforce proper synchronization (due to them being threaded).

Packages

The type system is organized into packages, much like Java. Each package is represented by a directory in the file system, except in the case of virtual packages. Virtual packages are created for built in types in the case no directory is found in the file system.