TSL Basics

Identifiers are case-sensitive in TSL. Any legal C# identifier is legal in TSL. Line comments starting with // and block comments surrounded by /* and */ are supported. The syntax of TSL is similar to that of the struct construct in C/C++ or C#.

The reserved keywords in TSL include: C# keywords, TSL keywords, and primitive data types.

Table 1. Reserved TSL Keywords

struct

cell

protocol

server

proxy

enum

include

extern

optional

invisible

List

Type

syn

asyn

http

Request

Response

stream

bit

byte

sbyte

bool

char

short

ushort

int

uint

long

ulong

float

double

decimal

DateTime

Guid

string

CellId

index

target

void

Table 2. Built-in Types
Name Alias Data Type Bits

bool

Boolean

8

char

Unicode character

16

int8

sbyte

signed 8-bit integer

8

uint8

byte

unsigned 8-bit integer

8

int16

short

signed 16-bit integer

16

uint16

ushort

unsigned 16-bit integer

16

int32

int

signed 32-bit integer

32

uint32

uint

unsigned 32-bit integer

32

CellId

signed 64-bit integer

64

int64

long

signed 64-bit integer

64

uint64

ulong

unsigned 64-bit integer

64

float

Single-precision float

32

double

Double-precision float

64

DateTime

DateTime structure

64

decimal

Decimal numbers

128

Guid

Globally unique id

128

TSL Script #

A TSL script consists of one or more of the following top-level constructs:

  • TSL script inclusion statement

  • Enum construct

  • Struct construct

  • Cell construct

  • Protocol construct

  • Server construct

  • Proxy construct

We can include other TSL files in the current TSL project by using the "include" key word. For example, include another.tsl. Note, if a TSL file is already added to the current TSL project, it is not necessary to include it explicitly.

Enumerations are declared using the enum keyword. They can be referenced in the current TSL project or the .NET projects referencing the TSL project.

Struct #

A struct specifies a user-defined type. It is composed of three types of fields: built-in data types, other user-defined structs, and container types. A container type stores a collection of data elements. GE currently supports three container types: Array, List, String/string.

TSL supports single-dimensional and multi-dimensional arrays. A single-dimensional array of type T can be declared in two ways: Array<T>(3) myArray; or T[3] myArray;.

For example:

Array<int32>(3) myArray;
int32[3] myArray;

Multi-dimensional arrays can be declared via: T[m,n] myMultiArray;

For example:

int32[3,5] mymultiArray;

Note that TSL array does not support variable-length elements. Thus the following declaration is not allowed:

string [10] strings; // Not allowed!

A list of T can be declared via: List<T> mylist;

For example:

List<int32> mylist;

TSL String/string represents a sequence of characters.

Note that a modifier optional can be applied to any field of a struct. Optional modifier indicates the current field can be absent.

Defining a struct in TSL is pretty much like defining a struct in C/C++ or C#. For example:

struct MyStructA
{
    int32 Value;
    Guid Id;
    DateTime timestamp;
}
struct MyStructB
{
    int64 Link;
    List<float> Weights;
    bool[16] flags;
    string Name;
    bit[10] bitflags;
    MyStructA substruct;
    List<MyStructA> substructs;
}

Cell #

A Cell is a user-defined type. It is composed of three types of fields: built-in types, other user-defined TSL structs, and container types.

The storage layer of GE is a key-value store. The schema of the value part of a key-value pair can be specified by a TSL cell. GE generates the key-value store interfaces (SaveCell, LoadCell, and UpdateCell, etc.) as well as the data access methods for each cell type. A corresponding cell 'accessor' will be generated for each TSL cell. A cell can be accessed and manipulated in-place through its cell accessor interfaces.

This is a simple TSL cell:

cell MyCell
{
  int32 Value;
  string Name;
}

After compilation, we can access and manipulate MyCell via its cell accessor:

long cellId = 1;
using(var cell = Global.LocalStorage.UseMyCell(cellId))
{
  Console.WriteLine("Value: {0}", cell.Value);
  cell.Name = "a new name";
}

The manipulations are thread-safe when a cell is accessed via its cell accessor.

At the storage level, a cell is just a blob of bytes in the main memory. From the point view of a developer, a cell can be considered as a flat structured data container. It looks very much like a struct in C/C++ or C#. A fundamental difference between a C/C++ struct and a GE cell is their way of organizing memory. All the data fields in a cell are stored in a continuous memory region, while a C/C++ struct may contain data field references pointing to other data fields stored at a different memory region.

Data Modeling in TSL #

Cells are the basic building blocks of data modeling. Here we use a simple graph modeling example to demonstrate the basic data modeling techniques.

Note: any cell in GE is referenced by a 64-bit CellId, thus a list of cells can be represented by List<CellId> in TSL.

cell struct SimpleGraphNode
{
    List<CellId> Inlinks;
    List<CellId> Outlinks;
    string Name;
}

This cell type can be used to represent the nodes of a simple directed graph. Each node in this graph has a name and a list of inlinks and outlinks.

What if the graph edges have additional data associated? Here is an example:

struct MyEdge
{
    long Link;
    float Weight;
}

cell struct MyCell
{
    List<MyEdge> Inlinks;
    List<MyEdge> Outlinks;
    string Name;
}

This script contains a 'Cell' and a 'Struct'. This time, each edge contains a weight. Sample code for accessing MyCell:

Global.LocalStorage.SaveMyCell(132);

using (var ca = Global.LocalStorage.UseMyCell(132))
{
    ca.Inlinks.Add(new MyEdge(10, 11));
    ca.Outlinks.Add(new MyEdge(11, 12));
    ca.Name = "Hello, Cell Manipulator!";
}

using (var ca = Global.LocalStorage.UseMyCell(132))
{
    Console.WriteLine("Inlink.Count: {0}", ca.Inlinks.Count);
    Console.WriteLine("Outlink.Count: {0}", ca.Outlinks.Count);
    Console.WriteLine("Cell Name: {0}", ca.Name);
}

Protocol #

A protocol specifies a contract between a message sender and its receiver. It specifies three things: protocol type, request message, and response message. For example:

struct MyRequest
{
  string Value;
}

struct MyResponse
{
  int32 Result;
}

protocol myProtocol
{
    Type: Syn;
    Request: MyRequest;
    Response: MyResponse;
}

Possible protocol Type are: Syn, Asyn, and HTTP.

If the protocol is a Syn protocol:

  • Its Request can be: void or a user-defined struct.
  • Its Response can be: void or a user-defined struct.

If the protocol is an Asyn protocol:

  • Its Request can be: void or a user-defined struct.
  • Its Response MUST be void.

If the protocol is an HTTP protocol:

  • Its Request can be: void, a user-defined struct, or Stream.
  • Its Response can be: void, a user-defined struct, or Stream.

A Request or Response with Stream type can be manipulated via data stream interfaces.

Server/Proxy/Module #

We can bind a number of protocols to a server by defining a server struct in TSL. For example, if we have defined two protocols named protocolA and protocolB, then we can bind them to a server struct called myServer:

server my_server
{
    protocol protocolA;
    protocol protocolB;
}

After compilation, an abstract base server named myServerBase supporting both protocolA and protocolB will be generated. Two abstract message handlers protocolAHandler and protocolBHandler will be generated. We can implement our server logic by overriding the generated handlers.

A proxy can be defined similarly, except that the construct type "proxy" is used.

Note that a server/proxy cannot inherit protocols from another server/proxy instance. GE relies on protocol ids to dispatch messages and the protocol ids are numbered sequentially within a server/proxy instance. Inheriting protocols from other servers/proxies causes message dispatching problems. If we want to make a group of protocols reusable, we can group them into a module struct:

module my_module
{
    protocol protocolC;
    protocol protocolD;
}

Then, we can "plug-in" the module into a server/proxy instance:

    ...
    my_server server = new my_server();
    server.RegisterCommunicationModule<my_module>();

In this way, a client that understands the protocols defined in my_module can talk to the server.

Attributes #

An attribute is a textual tag associated with a construct in TSL. Attributes provide metadata about the construct and can be accessed at run time. An attribute can be a string or a pair of strings, where it is regarded as a key-value pair. A single-string attribute is regarded as a key-value pair with an empty value. Duplicated keys are not allowed on the same construct. A few attributes are reserved for built-in features, e.g. Inverted Index.

To tag a construct with an attribute, place the attribute above it. The syntax is akin to that of C# attributes:

/// Tagging the cell
[GraphNode, BaseType : Person]
cell Student
{
    /// Tagging the field
    [GraphEdge : Outlinks]
    List<CellId> friends;
}

The key and its value of an attribute are separated by a colon (:). Multiple attributes can reside within one pair of square brackets, separated by a comma (,). The attribute strings can be any literal strings with : and , escaped as \: and \,. Non-escaped spaces at the start and the end of a string are trimmed, so that [ a b : 123\ \ ] will be interpreted as [a b:123 ].