Skip to main content

Data Types

Structs

Flex has support for structs that resemble structs in C and C++. They are declared using the struct keyword.

Zero or more fields may be declared within a struct, and each field must have a data type declared after its name. Properties may be declared after field declarations.

Here is a struct named Space3D with three fields of type float64 named x, y, and z:

struct Space3D {
    x : float64;
    y : float64;
    z : float64;
}

Struct values can be created using struct-builder syntax, and the fields of a struct can be accessed using dot notation:

const mySpace3D: Space3D = Space3D{ x = 10.0; y = 20.0; z = 30.0; };
const xField: float64 = mySpace3D.x;

Inheritance

A struct can extend another struct using the extends keyword. The struct being extended is called the parent, while the other struct is a child. A child struct implicitly contains the fields of its parent in addition to its own fields.

In the example below, we define a parent struct Position and a child struct LabeledPosition.

extensible struct Position {
    x : int32;
    y : int32;
}

struct LabeledPosition extends Position {
    label: string;
}

By extending Position, the LabeledPosition struct also has x and y fields in addition to label. Additionally, values of type LabeledPosition can be implicitly cast to Position, which means that a LabeledPosition value can be used anywhere that a Position value is expected.

const myLP: LabeledPosition = LabeledPosition {    // constructing a LabeledPosition
  x = 10;
  y = 20;
  label = "myLP";
};

const xField: int32 = myLP.x;                      // field access

const myPosition: Position = myLP;                 // implicit casting to ancestor

Additional Rules

  • Only structs declared extensible can be extended.
  • A child can have at most one parent, but an extensible struct can have zero or more children.

Abstract structs

An extensible struct S can also be declared abstract which prevents the instantiation of S directly. A value of type S can still be created by instantiating a (non-abstract) descendent of S. Abstract structs are useful to define meta-information that must be included in many different messages in a message standard.

abstract extensible struct MessageWrapper {
    messageID: string;
    date: string;
}

Variants

Variants are used to express that a piece of data has multiple representations. (Formally, they are Flex's representation of discriminated unions.) For example, an angle could be represented as three integers for degrees, minutes, and seconds (DMS format) or as a single decimal representing the value in radians.

variant Angle {
    DMS(int32, int32, int32);    // degrees, minutes, seconds
    Radians(float64);
}

DMS and Radians are called the constructors of the variant; when they are called like functions, they build values of type Angle. A variant value can be destructured using a match expression (see Pattern Matching for more details).

const myAngle1: Angle = Angle.DMS(30, 15, 5);
const myAngle2: Angle = Angle.Radians(3.14159);

match (myAngle1) {
    Angle.DMS(d, m, s) => ...  // d, m, and s are accessible here
    Angle.Radians(r)   => ...  // r is accessible here
}

Enumerations

An enumeration is a type that is defined by explicitly listing its values. The example below defines an enumeration SimulationStatus that contains four values: Stopped, Running, Paused, and Reset.

enum SimulationStatus int32 {
  Stopped = 0;
  Running = 1;
  Paused = 2;
  Reset = 3;
}

To support effective transpilation to lower-level languages, each value of an enum must be associated with a value of some integral type. In the example above, the integral type is int32 and the associated int32 values are 0, 1, 2, and 3.

Constructing a value of type SimulationStatus is as simple as accessing on of the values in the enum definition.

const mySimulationStatus: SimulationStatus = SimulationStatus.Running;

Type Aliases and Newtypes

Sometimes we want to ascribe additional meaning to a type, such as whether an int32 value represents an altitude, angle, quantity, etc. Flex provides two mechanisms for assigning such additional meaning.

A type alias is an identifier that can be used as a synonym of another type. It does not define a new type, just a new name for an existing type. Thus, replacing a type expression with an alias has no effect other than improving readablilty. Type aliases are defined using the type keyword:

type Altitude = int32;

A newtype, in contrast, is distinct from its underlying type, and there is no implicit conversion to or from the newtype. You can think of a newtype as a struct with a single field. Building and deconstructing values of a newtype is similar to building structs and accessing struct fields:

newtype Altitude {
    value: int32;
}

const alt: Altitude = Altitude{ 42 };   // convert int32 to Altitude
const x: int32 = alt.value;             // convert Altitude to int32

Ontological Significance and Messages

An important concept in Tangram ProTM is that of ontological significance. We say that a data type has ontological significance if there is a universal understanding (across all uses) of its meaning.

We will illustrate the importance of ontological significance with an example. Suppose we define the following structs and functions. The getPosFromAVS function extracts a ship's position from a vehicle state value. The pos2loc function translates a Position into a Location in order to inferface with a component that only understands Location data and not Position data.

struct AirVehicleState {
    ownshipPosition: Position; // offset (in kilometers) from an origin point.
    // other details omitted
}

struct Position {
    x: float64;
    y: float64;
}

struct Location {
    x: float64;     // measured in miles
    y: float64;     // measured in miles
}

function getPosFromAVS(avs: AirVehicleState) -> Position {
    avs.ownshipPosition;
}

function pos2loc(pos: Position) -> Location {
    Location {
        x = pos.x;
        y = pos.y;
    };
}

Furthermore, suppose that all Position values contained within an AirVehicleState represent offsets measured in kilometers. In contrast, Location data is always in miles. The offsets in a Position value can be expressed in different units depending on the circumstance; getPosFromAVS assumes kilometers while pos2loc assumes miles.

Is there a universal understanding of what Position means? No! The two functions are not interoperable because they make different assumptions about the meaning of the data in a Position value. Tangram ProTM is designed to perform code synthesis tasks such as fusing the above two functions into a single function from AirVehicleState to Location. However, the contradictory interpretations of the meaning of Position prevent this fusion from happening safely.

The problem is that Position is not ontologically significant; different functions treat Position values differently. In contrast, messages of open standards like LMCP or Mavlink all have universally agreed-upon interpretations. Thus, it is safe to perform code synthesis tasks on functions that operate on them.

In Flex, the message keyword is used to indicate that a data type is ontologically significant and that performing code synthesis is safe. The message keyword is allowed to modify struct and variant declarations. Newtypes are always ontologically significant (without needing the message keyword).

message struct AirVehicleState {
    ownshipPosition: Position;
    // other details omitted
}

Annotations

Annotations are a way to attach meta data to fields of data types. Using an annotation declaration, a user can define new annotations to decorate their Flex specification. There are annotations defined in the built-in Flex package tangram::flex::helpers that inform Flex to do something different with the output.

// Field is the scope Length applies to.
annotation Length(x: int32) | Field | 

// No scope means the annotation applies in any scope. 
annotation Rename(name: string)

// Struct, Field, and TypeAlias are the scopes ExampleAnnotation applies to.
annotation ExampleAnnotation(bytes: int32, value: string) | Struct Field TypeAlias |

Annotation scopes are an optional space separated list of scope names surrounded by |. If the scope is omitted the annotation can apply in any valid location. Application of an annotation uses the name of the annotation prefixed by @ with arguments for each of the parameters passed in.

Valid Scopes with Examples

Scopes can be one of the following:

  • EnumValue
enum Ex int32 {
    @Rename("One");
    one;
}
  • Enum
@Rename("test")
enum Test int32 { 
  zero = 0; 
}
  • Struct
@Rename("test")
@ExampleAnnotation(4, "A struct with 4 bytes of data")
struct Test { 
  x: int32;
}
  • Field
message struct Ex { 
  @Rename("X")
  @ExampleAnnotation(4, "A 4 byte integer")
  x: int32;
  @Length(10)
  @ExampleAnnotation(40, "Size 10 array")
  @Rename(Values)
  values: int32[];
}
  • Variant
@Rename("test")
variant Test {
    DMS(int32, int32, int32);    // degrees, minutes, seconds
    Radians(float64);
}
  • VariantConstructor
variant Angle {
    @Rename("Degrees_Minutes_Seconds")
    DMS(int32, int32, int32);    // degrees, minutes, seconds
    Radians(float64);
}
  • NewType
@Rename("altitude"
newtype Altitude {
    @Rename("Value")
    value: int32;
}
  • TypeAlias
@Rename("altitude")
type Altitude = int32;

See DECL_ANNOTATION and ANNOTATION in the Complete Syntax Declaration for the complete description of annotation syntax for declaration and use.

Special Annotations

If a user uses the annotations Rename or Length this will result in a change to the transpiler and code generation output when using tangram generated output.

  • Rename is an annotation that applies to any scope and will rename the annotated syntax to the name specified. For example, test is a keyword in flex. Using @Rename("test") you can generate output using this name even though test as an identifier is invalid flex.
  • Length is an annotation that will applies to string or array fields and will contrain that field to be a fixed width.