Skip to main content

Expressions

The body of transforms is specified using Flex's language of expressions that is syntactically similar to C++ and Java expressions. We will discuss Flex expressions informally here. For the complete grammar of Flex including its expression language, see Flex Complete Syntax.

Literals

Literals for built-in types are similar to literals in other languages. Integer literals in Flex can be used as values of any of the integral data types (int8, int16, ..., uint8, uint16, ...). Decimal literals can be used as values of float32 and float64.

flex
42 // integral literal
-42 // integral literal
1_000_000 // integral literal
3.14 // decimal literal
6.022e23 // decimal literal
0xC0A86401 // integral literal (hexadecimal)
0b0000111100001111 // integral literal (binary)
"foo bar" // string literal
true // bit literal
false // bit literal

Arithmetic and Logic

Flex has arithmetic, logical, and comparison operators similar to other languages.

The arithmetic operators + - * / % and comparison operators > >= < <= are defined over all numeric types.

flex
(42: int32) + 8 // ~> 50: int32
(42: int32) - 8 // ~> 34: int32
(42: int32) * 8 // ~> 316: int32
(42: int32) / 8 // ~> 5: int32
(42: int32) % 8 // ~> 2: int32
(40: int32) == 42 // ~> false
(40: int32) != 42 // ~> true
(40: int32) > 42 // ~> false
(40: int32) >= 42 // ~> false
(40: int32) < 42 // ~> true
(40: int32) <= 42 // ~> true

The binary logical operators && || are defined over bit as well as integral types (as bitwise operations). Unary negation ! is defined over bit only.

flex
true && false // ~> false
true || false // ~> true
(5: uint8) || 3 // ~> 7: uint8
(5: uint8) && 3 // ~> 1: uint8
!true // ~> false

Equality operators == != are defined over all types. Inequality is just the negation of equality (i.e., x != y is equivalent to !(x == y)).

flex
(42: int32) == 42 // ~> true
false == false // ~> true
"foo" == "foo" // ~> true
Pos{ x=0; y=0; } == Pos{ x=0; y=0; } // ~> true
Pos{ x=0; y=0; } == Pos{ y=0; x=0; } // ~> true
(true, "foo") == (true, "foo") // ~> true
(42: int32) == 43 // ~> false
true == false // ~> false
"foo" == "bar" // ~> false
Pos{ x=0; y=1; } == Pos{ x=0; y=0; } // ~> false
Pos{ x=0; y=1; } == Pos{ y=0; x=1; } // ~> false
(true, "foo") == (true, "bar") // ~> false

For all binary operators, Flex requires that the left- and right-hand sides have the same type, and there is no implicit type coersion.

flex
(42: int32) + (43: int16) // type-checking error
(42: int32) < (43: int16) // type-checking error
(42: int32) == (42.0: float64) // type-checking error
true == "true" // type-checking error
true != (0: int32) // type-checking error
(42: int8, "foo") == ("foo", 42: int8) // type-checking error

Although there is no implicit type conversion, Flex can infer the type of numeric literals as long as there is only a single type that would satisfy typing constraints. A type ascription (e.g., : int32) is usually enough information to satisfy the type checker when there would otherwise be ambiguity. For example, the types of the literals in 42 + 8 * 9 are ambiguous, making the expression invalid. However, (42: int32) + 8 * 9 as well as (42 + 8 * 9) : int32 are both acceptable.

Casting

Flex supports several ways to cast numeric values to other numeric types. value_cast is used when the type conversion from type A to B is guaranteed to succeed for all values of type A (e.g., uint16 to uint32). If the cast may fail for some A, then value_cast? is used instead which returns an Optional<B>. For converting float64 to float32, there is a special function float64to32 that performs a lossy conversion.

flex
value_cast<int32>(42 : int16) // ~> 42 : int32
value_cast<int32>(42: uint16) // ~> 42 : int32
value_cast<float64>(3.14 : float32) // ~> 3.14 : float32
value_cast?<int32>(42 : int16) // ~> some(42 : int32)
value_cast?<int8>(127 : int16) // ~> some(127 : int8)
value_cast?<int8>(128 : int16) // ~> none
value_cast?<int8>(128 : uint8) // ~> none
value_cast?<uint8>(-42 : int8) // ~> none
float64to32(3.14) // ~> 3.14 : float32

value_cast? is often used with the getOrElse function to provide an alterative value if the cast fails:

flex
getOrElse(value_cast?<uint8>(42: int8), 0) // ~> 42 : uint8
getOrElse(value_cast?<uint8>(-42: int8), 0) // ~> 0 : uint8

Flex does not include built-in functions for casting to and from bit and string types. Since there are choices to be made about precisely how such conversions would be implemented, we leave it to the user to define them as helper functions. As an example:

flex
function bit2uint8(b: bit) -> uint8 =
match (b) {
false => 0x00;
true => 0xFF;
// true => 0x01; // another possibility
};

Statements and Blocks

Flex takes after functional languages in that computation is specified primarily with expressions rather than statements. However, Flex does include a few statement forms that are syntactically reminiscent of imperative languages.

Statements must appear in a block, an expression form composed of a sequence of statements and a final expression:

flex
{
let pi: float64 = 3.14;
let radius: float64 = diameter / 2.0;
assert radius > 0;
3.14 * radius * radius;
}

The let statement binds the result of an expression to an identifier. Bound identifiers are in scope for subsequent statements and the final expression. They are not in scope outside the block.

flex
let radius: float64 = diameter / 2.0;

If the result of the expression is a tuple, then the contents of the tuple can be unpacked and bound to two or more identifiers.

flex
let (x, y) = rotatePoint2D((3.0, 4.0), 45.0);

Note that since Flex does not have mutable state, binding an expression to an identifier is not the same as assigning a value to a variable in C++. The value bound to an identifier cannot be "changed" during execution. Flex also does not support shadowing: an identifier x cannot be introduced if another identifier named x is already in scope.

The assert statement causes the surrounding block expression to fail if a condition is not satisfied.

flex
assert radius > 0;

The result of evaluating a block expression is the result of the final expression, taking into account identifiers bound by let statements and failures prescribed by assert statements.

The bodies of functions and transforms are usually block expressions because let bindings provide a convenient way to break down complex calculations. If the body is a block expression, the = sign that would otherwise precede the body may be omitted.

flex
function eulerToQuaternion(e: Euler) -> Quaternion {
let t0 = cos(e.Yaw * 0.5);
let t1 = sin(e.Yaw * 0.5);
let t2 = cos(e.Roll * 0.5);
let t3 = sin(e.Roll * 0.5);
let t4 = cos(e.Pitch * 0.5);
let t5 = sin(e.Pitch * 0.5);
Quaternion {
w = t2 * t4 * t0 + t3 * t5 * t1;
x = t3 * t4 * t0 - t2 * t5 * t1;
y = t2 * t5 * t0 + t3 * t4 * t1;
z = t2 * t4 * t1 - t3 * t5 * t0;
};
}

Conditionals

Flex supports two kinds of conditional evaluation: if-else expressions and match expressions.

The if-else expression is comparable to the ternary operator ? : in C++ and Java, though it syntactically resembles the if-else blocks from those languages. Note that in Flex, if-else is not a block that contains statements. It is an expression that returns either the value of the then branch or the value of the else branch. Because of this the types of the then and else branches must be compatible.

The syntax if-else can be written using then as a separating token or with the condition wrapped in parentheses:

flex
const x: int32 = 42;
if x < 100 then x else x - 100; // ~> 42: int32
if x < 20 then x else x - 20; // ~> 22: int32
if x > 100 then x - 100
else if x > 10 then x - 10
else x // ~> 32: int32
if (x > 100) {
x - 100;
} else if (x > 10) {
x - 10;
} else {
x;
} // ~> 22: int32

Flex also supports conditional evaluation based on pattern matching (see Pattern Matching).

Tuples and Arrays

Aside from user-defined types, Flex has built-in support for two kinds of aggregate data structure: tuples and arrays (e.g., heterogeneous and homogeneous lists).

A tuple is a list of two or more values, possibly of different types. A tuple is introduced by listing its elements inside parentheses, for example (42: int8, "foo", true). Tuples are usually used by unpacking the contents using a let statement:

flex
let (x, y, z) = (1: int32, 2: int32, 3: int32);

An array is a list of zero or more values of the same type. An array is introduced by listing the elements inside square brackets (e.g., ["one", "two", "three"]). The primitive operations on arrays are access [], concatenation ++, length, and last.

flex
const squareNums: uint8[] = [1, 4, 9, 16];
squareNums[0 : int32] // ~> 1 : uint8
squareNums[4 : int32] // fails
squareNums[0 : int32 .. 2 : int32] // ~> [1, 4, 9] : uint8[]
squareNums ++ [25, 36] // ~> [1, 4, 9, 16, 25, 36]: uint8[]
length(squareNums) // ~> 4: uint32
last(squareNums) // ~> 16: uint8
last([]: uint8[]) // fails

Arrays can also be iterated over using an array comprehension. For example, here is an array comprehension that adds one to each element of the array squareNums:

flex
[x + 1 for x in squareNums] // ~> [2, 5, 10, 17]: uint8[]

The values can be filtered using an if clause:

flex
[x + 1 for x in squareNums if x%2==0] // ~> [5, 17]: uint8[]

Sometimes the element-wise operation needs to depend on the results of previous operations. Information can be carried from index to index using scan such as in this example to compute an array of cumulative sums:

flex
const evens: uint8[] = [2, 4, 6, 8];
[cumulSum for x in evens scan cumulSum: uint8 = 0 in cumulSum + x]
// ~> [2, 6, 12, 20]: uint8[]

You can also zip arrays together by separating multiple for clauses with the zip keyword:

flex
[x+y for x in squareNums zip for y in evens] // ~> [3, 8, 15, 24]: uint8[]

Optionals

Flex also has built in support for optional values. A value of type Optional<A> has one of two forms: some(a) where a is a value of type A, or none. An Optional<A> value is like a box that might contain a value of type A, or it might contain nothing. Optionals in Flex are often used with pattern matching.

flex
function safeDivide(num : float32, den : float32) -> Optional<float32> {
if den == 0.0 then none else some(num / den);
}
const safeDivideResult : Optional<float32> = safeDivide(2.0,3.0);
match(safeDivideResult) {
some(result) => result;
none => 0.0;
}
// ~> 0.6666667: float32