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 thoughtest
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.