Interfacing with C#

When you compile a Cell program, the compiler does not generate a binary executable. Instead, it generates a number of text files containing code in the chosen output language. We'll examine the C# code generator here. If you define a Main(..) procedure in you Cell code, the generated code can then be handed over to a C# compiler to generate an executable file. That's how for instance the Cell compiler itself is built, and also the simplest way to build a program that tests your automata. But if you don't define a Main(..) procedure, the compiler will just generate a set of classes, one for each type of automaton in your Cell code, that can be used to instantiate and manipulate the corresponding automata from your C# code. The compiler will produce four files, runtime.cs, generated.cs, automata.cs and typedefs.cs. The first one contains the runtime library and the second one the core generated code. The last two files are created only when you don't define a Main(..) procedure in the Cell code, and they contain the definition of the classes you'll be working with in your own C# code. automata.cs contains the definition of the classes that correspond to the automata defined in your Cell code. The interface of those classes is documented in pseudo-C# in another generated file, automata.txt. The purpose of the last file, typedefs.cs, will be discussed later.

It's often a good idea not to use the classes in automata.cs directly, but to derive your own classes from them, and add new methods (and/or member or class variables, if needed) there. Another slightly more laborious alternative is to write a wrapper class for them. Among other things, if a method of the generated classes requires some sort of manual data conversion, you really don't want to repeatedly perform those conversions all over your codebase: it's much better to have all of them in just one place. The best thing to do is probably to define an overloaded version of the same method, or a similar one, in the derived class, and have it take care of all data conversions before and/or after invoking the generated one.

Data conversion

The biggest issue one encounters when interfacing two languages as different as Cell and C# is converting data back and forth between the two native representations. Fortunately, the compiler does most of the heavy lifting for you. For starters, there's a number of simple Cell data types that are mapped directly to a corresponding C# type. They are shown in the following table:

Cell C#
Int long
Float double
Bool bool
String string
Date DateTime
Time DateTime
Symbol string
T* T[]
[T] T[]
[K -> V] (K, V)[]
[T1, T2] (T1, T2)[]
[T1, T2, T3] (T1, T2, T3)[]
(T1, T2, ..) (T1, T2, ..)
(field1: T1, field2: T2, ...) (T1 field1, T2 field2, ...)
any_tag(T) T
Maybe[T] T / null

The first six entries in the above table are self-explanatory. The mapping for Cell sequences and sets is also straightforward, as both are mapped to C# arrays. For example, the Cell type Int* is mapped to long[] in C#, and [Date] is mapped to DateTime[]. Similarly, maps and binary relations are mapped to arrays of 2-tuples, and ternary relations to arrays of 3-tuples. Cell tuples are mapped to tuples in C#, and Cell records to named tuples. The last two entries require a bit of an explanation.

  • Tagged types can be mapped directly to a C# type only if the tag is a known symbol. In this case, the tag is simply ignored and the mapping of the untagged value is used. A type like <user_id(Int)>, for example, will be mapped to a long in C#, and the generated code will take care of adding or removing the user_id tag as needed.
  • The Maybe[T] type is mapped directly to the type of its parameter, and the value :nothing is mapped to null. Maybe[String], for example, is mapped to String, and the value :just("Hello!") is returned as the C# string "Hello!". Primitive types like long or double are replaced with the corresponding nullable types: Maybe[Int] for example, is mapped to int?, and Maybe[Float] to double?.

Not all types can be handled using the above mapping and that includes important practical cases like polymorphic and recursive types. We'll see how to deal with those more complex types later.

Relational automata

Let's now take a look at the classes that are generated when a relational automaton is compiled. We'll make use of a very simple one we've seen before, Counter:

schema Counter {
  value:   Int = 0;
  updates: Int = 0;
}

Counter.incr {
  set value = value + 1;
  set updates = updates + 1;
}

Counter.decr {
  set value = value - 1;
  set updates = updates + 1;
}

Counter.reset {
  set value = 0;
  set updates = updates + 1;
}

Counter.reset(Int) {
  set value = untag(this);
  set updates = updates + 1;
}

using Counter {
  Int value   = value;
  Int updates = updates;

  Bool is_greater_than(Int a_value) = value > a_value;

  (value: Int, updates: Int) copy_state = (
    value:   value,
    updates: updates
  );
}

This is the interface of the corresponding C# class generated by the Cell compiler:

namespace Cell.Automata {
  class Counter {
    Counter();

    void Load(Stream);
    void Save(Stream);

    void Execute(string);

    Action<string> OnSuccess(string);
    Action<string> OnFailure(string);

    // Message handlers
    void Incr();
    void Decr();
    void Reset();
    void Reset(long);

    // User-defined methods
    long Value();
    long Updates();
    bool IsGreaterThan(long);
    (long value, long updates) CopyState();
  }
}

As you can see, the generated C# class has the same name of the Cell automaton it derives from, and is declared in the Cell.Automata namespace. The first three methods, Load(), Save(..) and Execute(..), and the two delegate fields, OnSuccess and OnFailure, are the same for all relational automata. All other methods are different for each automaton, and are used to send specific types of messages to it or to invoke its (read-only) methods.

The Save(..) method is the equivalent of the Save(..) procedure in Cell, in that it takes a snapshot of the state of the automaton, which is written to the provided System.IO.Stream. The state is saved in the standard text format used for all Cell values.

Load(..) is used to set the state of an automaton instance, which is read from a System.IO.Stream, and is the equivalent of the Load(..) procedure in Cell. It can be used at any time in the life of the automaton instance, any number of times. The new state has to be provided in the standard text format. If the provided state is not a valid one, Load(..) will throw an exception. In that case, the automaton instance will just retain the state it had before, and will still be perfectly functional.

Execute(..) is used to send the automaton a message, which has to be passed in text form. A few examples:

counter.Execute("incr");
counter.Execute("decr");
counter.Execute("reset");
counter.Execute("reset(-1)");

Errors handling works in the same way as with Load(..). If an error occurs an exception will be thrown, but the automaton will remain fully operational, and its state will be left untouched.

The next four methods, Incr(), Decr(), Reset() and Reset(long) provides another way to send messages to the automaton:

counter.Incr();    // Same as counter.Execute("incr");
counter.Reset(-1); // Same as counter.Execute("reset(-1)");

They are a lot faster than Execute(..), and usually they're more convenient too. There are cases though when the ability to generically send a message of any type to an automaton is crucial, so that's why the compiler provides two ways of doing the same thing.

When updating an automaton instance keep in mind that Cell does not (yet) provide a way to incrementally persist its state: every time you call the Save(..) method the entire state of the automaton is saved. That's an expensive operation so typically you'll be performing it only once in a while. That means that you would have unsaved data in memory most of the time, which is of course at risk of being lost in the event of a crash. One simple and efficient way to avoid that is to store the list of messages that were received since the last save. If the application crashes, all you need to do when you restart it is to load the last saved state and re-send all the messages it received after that. That will recreate the exact same state you lost in the crash.

The OnSuccess delegate field is meant to help with that. If it's not null (which is the initial value) the delegate it points to is called every time a message is processed successfully, and the message itself is passed to it in textual form. The other delegate, OnFailure, on the other hand is invoked whenever a message handler fails. Saving those failed messages is not necessary for persistence, but it's typically useful for debugging.

The last four methods, Value(), Updates(), IsGreaterThan(..) and CopyState(..), are just wrappers for the corresponding (read-only) methods of Counter.

You can see how in the signatures of the methods of the generated class the types of both arguments and return values are derived using the rules described in the previous paragraph. For example, is_greater_than(..) takes an argument of type Int and returns a value of type Bool, which become long and bool respectively in C#. Similarly, copy_state returns a record in Cell, which is mapped to a named tuple with the same fields in C#, and the types of those fields are in turn mapped from Int to long.

More on data conversions

What happens when the type of an argument or the return value of a method (or message handler) is too complex to be dealt with using the mapping described earlier? Here the default behavior of the compiler is to use the standard textual representation of Cell values as the data exchange format. Let's illustrate this with an example:

type Point = point(x: Int, y: Int);

type Shape = square(left: Int, bottom: Int, side: Nat),
             rectangle(left: Int, bottom: Int, width: Nat, height: Nat),
             circle(center: Point, radius: Int);

schema Canvas {
  ...
}

using Canvas {
  Shape* shapes_at(Point p) = ...
}

In this example, shapes_at(..) return a sequence of values of type Shape, which is a polymorphic type. By default the generated Canvas class will look like this:

namespace Cell.Automata {
  class Canvas {
    Canvas();

    void Load(Stream);
    void Save(Stream);

    string[] ShapesAt((long x, long y) p);
  }
}

As you can see, the return type of ShapesAt(..) is string[]. Each string in the array is the textual representation of the corresponding Cell value. That is, if shapes_at(..) returns the following Cell value:

( square(left: 0, bottom: 0, side: 10),
  circle(center: point(x: 8, y: 3), radius: 5),
  rectangle(left: -25, bottom: 14, width: 4, height: 2)
)

then the caller of ShapesAt(..) on the C# side will get back the following C# object:

string[] {
  "square(left: 0, bottom: 0, side: 10)",
  "circle(center: point(x: 8, y: 3), radius: 5)",
  "rectangle(left: -25, bottom: 14, width: 4, height: 2)"
}

Exchanging data in text form is not particularly elegant nor efficient, but it's at least simple and straightforward, and in some cases it works just fine. It tends to work better when passing data from C# to Cell than in the other direction, since strings are easy to generate but difficult to parse.

As an alternative, you can ask the compiler to generate an equivalent C# class for some of the types defined in your Cell code base. What you need to do is create a text file (let's call it types.txt) containing the list of types you want to generate (one type per line). Let's say for instance that you want to generate C# classes for the Point and Shape types above. The content of types.txt would then look like this:

  Point
Shape

When you compile your code, you'll have to point the compiler to that file using the -g flag:

  cellc-cs -g types.txt <project file>

The interface of the generated Canvas class will now look like this:

namespace Cell.Automata {
  class Canvas {
    Canvas();

    void Load(Stream);
    void Save(Stream);

    Shape[] ShapesAt((long x, long y) p);
  }
}

As you can see, the return type of ShapesAt(..) has now become Shape[]. You'll find the definition of Shape in the generated typedefs.cs file:

namespace Cell.Typedefs {
  public partial class Point {
    public long x;
    public long y;

    public Point(long x, long y) {
      this.x = x;
      this.y = y;
    }
  }

  public interface Shape {

  }

  public partial class Rectangle : Shape {
    public long left;
    public long width;
    public long bottom;
    public long height;

    public Rectangle(long left, long width, long bottom, long height) {
      this.left = left;
      this.width = width;
      this.bottom = bottom;
      this.height = height;
    }
  }

  public partial class Circle : Shape {
    public Point center;
    public long  radius;

    public Circle(Point center, long radius) {
      this.center = center;
      this.radius = radius;
    }
  }

  public partial class Square : Shape {
    public long left;
    public long side;
    public long bottom;

    public Square(long left, long side, long bottom) {
      this.left = left;
      this.side = side;
      this.bottom = bottom;
    }
  }
}

The definition of the Point class at the top is straightforward: the type Point, which in the Cell codebase is defined as a tagged record with two fields, x and y, of type Int, is mapped to a C# class by the same name with two member variables x and y of type long.

The mapping for Shape is a bit more complicated, since that's a polymorphic type. The compiler here has created three classes, Square, Rectangle and Circle each of which corresponds to one of the three alternatives in the Shape type definition. Shape in C# is defined as an empty interface, which is then implemented by the three concrete classes, so that they can be manipulated polymorphically.

All the classes in typedefs.cs are defined as partial, which means that you're free to add new methods or member variables to them if need be. You can also directly edit the generated class definitions, as long as you don't change the signature of the generated constructor, and of course the name, namespace and inheritance hierarchy of the generated classes.

Note that at the moment these generated classes are used only when moving data from Cell to C#, but not in the other direction (that is, they're used in the return types of the generated methods, but not in the types of their arguments). For example, the argument of shapes_at(..) is of type Point in Cell, but its type in the generated ShapesAt(..) is (long x, long y) rather than Point.

In order for the compiler to be able to generate a corresponding C# class for it, a Cell type definition has to obey a number of restrictions. Non-polymorphic types have to be defined as (possibly tagged) records with no optional fields or (possibly tagged) tuples. Any of the following definitions for Point, for example, would allow the generation of a corresponding C# class:

type Point = (x: Int, y: Int);
type Point = point(x: Int, y: Int);
type Point = (Int, Int);
type Point = point(Int, Int);

but the following ones would not:

type Point = (x: Int, y: Int, z: Int?);
type Point = point(x: Int, y: Int, z: Int?);

since the field z is optional (in this case though you can achieve the same result by making z mandatory and changing its type to Maybe[Int]). The rules are even more restrictive for polymorphic types: each alternative in a type union must be a tagged record or tuple, and the tags have to be different. You're free to define each alternative as a separate type though. Shape for example could have been defined as follow, without affecting the ability of the compiler to generated a corresponding C# class for it:

type Square = square(left: Int, bottom: Int, side: Nat);
type Rect   = rectangle(left: Int, bottom: Int, width: Nat, height: Nat);
type Circle = circle(center: Point, radius: Int);

type Shape = Square, Rect, Circle;

Reactive automata

We'll use Switch as our first example. We defined it in a previous chapter as follows:

reactive Switch {
  input:
    switch_on  : Bool;
    switch_off : Bool;

  output:
    is_on : Bool;

  state:
    is_on : Bool = switch_on;

  rules:
    is_on = true  when switch_on;
    is_on = false when switch_off;
}

This is the interface of the corresponding C# class:

namespace Cell.Facades {
  class Switch {
    enum Input {SWITCH_ON, SWITCH_OFF};

    enum Output {IS_ON};

    Switch();

    void SetInput(Input input, string value);
    string ReadOutput(Output output);

    void Apply();
    string ReadState();
    void SetState(string);

    Output[] ChangedOutputs();

    // Inputs
    bool SwitchOn;
    bool SwitchOff;

    // Outputs
    bool IsOn;
  }
}

The first thing to note here is the two enumerations Input and Output, whose elements are the uppercase version of the names of the inputs and outputs of Switch. These are used in conjunction with the methods SetInput() and ReadOutput() as shown here:

// Setting the value of the two inputs
switch.SetInput(Input.SWITCH_ON, "true");
switch.SetInput(Input.SWITCH_OFF, "false");

// Propagating the changes to the inputs
// throughout the automaton instance
switch.Apply();

// Reading and printing the value of the only output
string isOn = switch.ReadOutput(Output.IS_ON);
Console.WriteLine("is_on = {0}", isOn);

As an alternative to SetInput(..) and ReadOutput(..), which can operate on any input or output and use the textual representation of a value as an exchange format, the generated class also provides another set of methods each of which can manipulate a single input or output, but that are more convenient to use in most cases. The above code snippet can be rewritten as follow:

// Setting the value of the two inputs
switch.SwitchOn = true;
switch.SwitchOff = false;

// Propagating the changes to the inputs
// throughout the automaton instance
switch.Apply();

// Reading and printing the value of the only output
bool isOn = switch.IsOn;
Console.WriteLine("is_on = {0}", isOn);

ReadState() takes a snapshot of the state of the automaton and returns it in textual form. SetState(..) does the opposite: it sets the state of the automaton to whatever state is passed to it. Here too the new state has to be provided in textual form. When working with time-aware automata both methods are subjects to the limitations that we've already discussed in a previous chapter. The method ChangedOutputs() provides a list of outputs that have changed (or have been active, in the case of discrete outputs) as a result of the last call to Apply():

// Changing inputs here
...

// Propagating those changes
switch.Apply();

// Iterating through the outputs that have changed
// if continuous or have been activated if discrete
foreach (var outputId in switch.ChangedOutputs) {
  // Reading the value of the changed output
  string outputValue = switch.ReadOutput(outputId);

  // Now time to do something with the value of the output
  ...
}

The last thing we need to see is how to deal with time-aware automata. We'll use WaterSensor, whose definition is copied here:

type WaterSensorState = initializing, unknown, submerged(Bool);

reactive WaterSensor raw_reading -> sensor_state {
  input:
    raw_reading* : Maybe[Bool];

  output:
    sensor_state : WaterSensorState;

  state:
    sensor_state : WaterSensorState = :initializing;

  rules:
    good_reading := value(raw_reading) if raw_reading != :nothing;
    too_long_without_readings = 30s sans good_reading;
    sensor_state = :submerged(good_reading);
    sensor_state = :unknown when too_long_without_readings;
}

This is the interface of the generated C# class:

namespace Cell.Facades {
  interface WaterSensorState {

  }

  class Unknown : WaterSensorState {
    static readonly Unknown singleton;
  }

  class Initializing : WaterSensorState {
    static readonly Initializing singleton;
  }

  class Submerged : WaterSensorState {
    bool value;
  }


  class WaterSensor {
    enum Input {RAW_READING};

    enum Output {SENSOR_STATE};

    WaterSensor();

    void SetInput(Input input, string value);
    string ReadOutput(Output output);

    void SetElapsedMillisecs(int);
    void SetElapsedSecs(int);

    bool Apply();
    string ReadState();
    void SetState(string);

    Output[] ChangedOutputs();

    // Inputs
    bool? RawReading;

    // Outputs
    WaterSensorState SensorState;
  }
}

The only differences here, apart from the input setters and output getters which are obviously specific to each automaton type, are the two extra methods SetElapsedSecs(..) and SetElapsedMillisecs(..) and the fact that Apply() now returns a boolean value. The former are the equivalent of the elapsed instruction in Cell, and the value now returned by Apply() has the same meaning as the one returned by the apply instruction in a Cell procedure. Here's an example of how to update an instance of WaterSensor:

// Updating the values of the inputs here
...

// Setting the amount of time that has elapsed
// since the last call to waterSensor.Apply()
waterSensor.SetElapsedMillisecs(100);

do {
  // Repeatedly calling Apply() until it returns true
  // That happens only once all pending timers have
  // been processed and the changes in the values of
  // the inputs propagated throughout the automaton
  bool done = waterSensor.Apply();

  // Iterating through the outputs that have changed
  // if countinuous or have been activated if discrete
  foreach (var outputId in waterSensor.ChangedOutputs) {
    // Reading the value of the changed output
    Value outputValue = waterSensor.ReadOutput(outputId);

    // Now time to do something with the value of the output
    ...
  }
} while (!done);