Expressions and Operators¶
An expression computes a value from simpler ones. This chapter describes the operators that Termina provides to build expressions, the rules that govern how their operands must be typed, and the way a literal acquires its type. All of Termina's operators are binary and left-associative, and each one maps to its counterpart in the generated C code, together with the run-time checks that the last section of the chapter describes.
Arithmetic operators¶
The five arithmetic operators are +, -, *, /, and %. The first four
act on operands of any numeric type, integer or floating-point, and produce a
result of that same type. The remainder operator % yields the remainder of
the integer division of its operands and is defined for integers only.
Both operands of an arithmetic operator must have the same type, and that type
is also the type of the result. Termina performs no implicit conversions
between integer types, so an expression that mixes widths or signedness is
rejected. The following fragment, where a is a u32 and c is a u16, does
not compile:
To combine values of different types, one of them must be converted explicitly, as described under casting below.
A minus sign written before a decimal literal is part of the
literal, so -100 is a single negative constant, which the generated C writes
as -(100L) when its type is i32. Termina has no unary minus operator, and
the negation of a variable is written as a subtraction, 0 - a.
Comparison operators¶
The comparison operators relate two operands of the same type and produce a
bool. They are == and != for equality and inequality, and <, <=, >,
and >= for ordering. The ordering operators accept both integer and
floating-point operands; the equality operators are not defined for
floating-point types, for the reasons given in the chapter on types.
Logical operators¶
The logical operators combine boolean values: && is logical conjunction and
|| is logical disjunction. Both evaluate their right operand only when
necessary, that is, && does not evaluate the right operand if the left one is
already false, and || does not evaluate it if the left one is already
true. The right operand may or may not run, so it cannot contain an operation
with an effect of its own. Such operations are a call to a function that
receives a mutable reference, a call to a method that takes &mut self, an
access to a located field, and an operation checked at run time. A run-time
check raises an exception when it fails. The generated code checks signed
addition, subtraction and multiplication, and any array access, slice, shift,
division or remainder whose index, bounds, shift amount or divisor is not a
constant. A call to a
function whose body contains any of these carries the same effect. The
transpiler rejects them in the right operand.
A comparison in the left operand bounds a variable in the right one: after
i < 8 : usize &&, the right operand runs only when i is below 8, and after
count == 0 : u32 ||, only when count is not zero. The transpiler uses these
bounds to leave out a check that cannot fail: an index check when the index is
kept inside the array, a division check when the divisor is kept away from
zero, and a shift check when the amount is kept below the width of the shifted
value. The right operand then has no effect and is accepted:
The check of a signed division or remainder also covers the quotient of the
minimum value of the type by -1. A comparison of the divisor with zero does
not exclude that case, so a signed division stays checked after such a guard
and is rejected in the right operand. The guard is written instead as an if
that encloses the division:
Termina does not provide a unary logical-negation operator. A boolean value is
negated by comparing it against false:
Bitwise operators¶
The bitwise operators act on the individual bits of unsigned integer operands:
&, |, and ^ are bitwise and, or, and exclusive or, while << and
>> shift the bits of the left operand left or right by the amount given on the
right. The amount may be of any unsigned type, independently of the type of the
value being shifted, and the result has the type of the left operand.
let masked : u32 = flags & mask;
let combined : u32 = high | low;
let shifted : u32 = value << 2 : usize;
In C, the result of these operations depends on how negative numbers are
represented, and shifting a negative value left, or shifting a one into the sign
bit, is undefined behavior, so the transpiler rejects signed operands. A signed value whose
bits have to be manipulated is first converted to the unsigned type of the same
width with as.
There is no bitwise-complement operator. A complement is obtained, where
needed, through ^ with an all-ones mask.
Casting with as¶
Because Termina never converts between types on its own, a value is converted
from one type to another with an explicit cast, written with the as operator
followed by the target type. A cast generates the corresponding C cast:
A cast is the only way to use a value of one integer type where another is expected. It also keeps the conversion visible in the source, so that a possible truncation is apparent at the point where it happens instead of being hidden behind an implicit promotion.
Typed literals¶
A numeric literal such as 0 or 42 does not carry a type of its own; it
takes the type required by the context in which it appears. When counter is a
u32, the literal in counter + 1 is understood to be a u32. In most
expressions the type can be determined this way, but there are positions where
it cannot, and the transpiler then requires the literal to be annotated
explicitly with the syntax literal : type. The amount of a shift is one such
position, since it need not share the type of the value being shifted:
The same syntax is used to write a literal of a specific type wherever that is needed. Expressions composed entirely of typed literals are evaluated by the transpiler at compile time, so the following declaration produces a single constant in the generated code rather than a runtime shift:
Operator precedence¶
When an expression combines several operators without parentheses, the order in which they apply is governed by their precedence. From highest to lowest, the groups are:
| Precedence | Operators |
|---|---|
| highest | * / % |
+ - |
|
<< >> |
|
< <= > >= |
|
== != |
|
& |
|
\| |
|
^ |
|
&& |
|
| lowest | \|\| |
All operators associate to the left. The relative order of the bitwise
operators differs from that of C, where ^ binds more tightly than |, so
parentheses are recommended whenever an expression mixes operators from
different groups.
Integer promotion and explicit casts¶
The arithmetic of C is governed by a rule known as integer promotion: operands
narrower than int, such as values of an 8- or 16-bit type, are converted to
int before an operation is carried out, and the operation is then performed at
the width of int. An expression written in terms of small integer types is
therefore not evaluated at the width its types suggest, which can alter the
result of a computation, for example when the sum of two uint8_t values
exceeds 255 and is compared against another uint8_t. Integer promotion is
intrinsic to C and cannot be switched off; coding standards such as MISRA C address it indirectly, through rules that constrain the implicit
conversions surrounding it and call for explicit casts.
Termina evaluates arithmetic at the width of the operand types, with no
promotion: the result of every operation has the type of its operands. To
preserve this meaning in the generated C, where the promotion rule still
applies, the transpiler wraps each intermediate result in an explicit cast back
to its type and, when that type is an unsigned type narrower than int, masks
the result to the width of the type before the cast. The effect is visible in a
function that adds three 8-bit values:
The sub-expression a + b is masked to eight bits and cast back to uint8_t
before c is added, so the intermediate sum is computed at the width of the
operand type, as the Termina source prescribes, rather than being carried in the
wider int that C's promotion rule would otherwise use. The outer sum carries
no mask, since the return already converts it to uint8_t; used as the
operand of a comparison, as in a + b + c > 3, it is masked and cast as well.
The transpiler emits the same mask and cast for every intermediate result of an
unsigned type narrower than int. The mask changes the value only when the
result does not fit in that width, the case of overflow treated in the next
section.
Arithmetic overflow¶
When a computation produces a result that does not fit in the type of its operands, the two integer families behave differently, and in both cases the outcome is well-defined.
Unsigned arithmetic is modular. A result that exceeds the range of an unsigned
type wraps around modulo 2ⁿ, where n is the width of the type, so that adding one
to the maximum value of a u8 yields 0. The mask and the explicit cast
described in the previous section reduce each intermediate value to its type
before the computation proceeds, which produces the wraparound at the declared
width. Modular arithmetic of this kind is
a legitimate tool in tasks such as bit manipulation and the handling of counters
that are meant to roll over.
Signed arithmetic does not wrap, and signed overflow has an undefined result in
C. For a signed operation, the generated code calls a function of the OSAL that
checks the operands before carrying out the operation and raises the exception
EArithmeticOverflow when the result would not be representable in its type.
A division or a remainder whose divisor is zero raises
EDivisionByZero in the same way, for signed and unsigned types alike, and a
divisor that is the constant zero is rejected by the transpiler before any code
is generated. A runtime exception restarts the system. In the following
function, the addition and the division by count are checked, while the
division by the constant 2 is not, since its divisor can be neither zero nor
-1, the only values for which a signed quotient can fail:
The transpiler also leaves out the check of an operation whose operands the
code bounds before it runs. In the following function the condition keeps a
between -99 and 99, the sum stays between 901 and 1099, and the addition is
carried out directly:
With the same bounds, the transpiler rejects an operation whose check fails for every value its operands may take, such as a division by a variable that a condition has just fixed at zero.
In the current version, a division of a floating-point value by zero, and a
conversion with as from a floating-point type to an integer type, or from
f64 to f32, whose value does not fit the target type, are not checked.