Expressions
Two invariants hold for every C⏚ expression:
- No overflow. The result type is wide enough to hold the result. Evaluating an expression cannot lose bits.
- No state changes. An expression may read ports (which can introduce an execution cycle), but it cannot modify a variable. Assignment and increment are statements.
Evaluation order
Each binary expression is evaluated in three steps:
- Resize each operand. Comparison operators resize both
operands to
unify(T1, T2). Arithmetic operators resize to the result type to prevent overflow. Truncation drops high-order bits. Extension adds high-order bits: signed numbers are sign-extended, unsigned numbers are zero-extended. - Convert signedness if mixed. Unsigned operands are reinterpreted as signed. This produces correct results for operators where sign matters, most notably multiplication and arithmetic right shift.
- Evaluate.
The resize-then-convert order matters. Consider i7 x = −50
times u3 y = 5. The result type is i10. Resizing first gives
i10 x = −50 and i10 y = 5 (zero-extended from 3 bits);
multiplying yields −250. Converting first would reinterpret
y as a 3-bit signed (−3), then extend to i10 y = −3, and
the product becomes 150. Wrong.
Ternary
cond ? e1 : e2cond | e1 | e2 | Result |
|---|---|---|---|
bool | T1 | T2 | unify(T1, T2) |
The condition must be bool. The expression is invalid if
unify(T1, T2) is undefined.
Boolean
e1 || e2
e1 && e2e1 | e2 | Result |
|---|---|---|
bool | bool | bool |
Bitwise
e1 | e2 // OR
e1 ^ e2 // XOR
e1 & e2 // ANDe1 | e2 | Result |
|---|---|---|
T1 | T2 | unify(T1, T2) for ` |
T1 | T2 | unify(T1, T2) with size min(size(T1), size(T2)) for & |
Equality
e1 == e2
e1 != e2e1 | e2 | Result |
|---|---|---|
T1 | T2 | bool |
Relational
e1 < e2
e1 <= e2
e1 > e2
e1 >= e2e1 | e2 | Result |
|---|---|---|
T1 | T2 | bool |
Shift
e1 << e2
e1 >> e2Right shift is arithmetic for signed operands and logical for unsigned ones.
Additive
e1 + e2
e1 - e2e1 | e2 | Result |
|---|---|---|
T1 | T2 | unify(T1, T2) with size max(size(T1), size(T2)) + 1 |
The extra bit prevents overflow. Adding u3 x = 6 and u2 y = 2
yields 8, which needs u4.
Multiplicative
e1 * e2
e1 / e2
e1 % e2The result is wide enough to hold the worst case. For *, that
is size(T1) + size(T2) bits.
Unary
| Operator | Operand | Result |
|---|---|---|
~e | int<N> | same type as operand |
!e | bool | bool |
-e (variable) | int<N> | signed<N + 1> |
-e (literal) | constant | the literal's natural type |
sizeof(e) | constant | unsigned |
~ is bitwise complement; the operand type is preserved because
no bit position changes meaning.
- always produces a signed result with one extra bit. The
worst case for an unsigned operand is −(2^N − 1), which
requires N + 1 bits signed; the worst case for a signed
operand is −(−2^(N−1)), same requirement. If you want an
unsigned result, rearrange the expression so the leading
operator is not unary minus: write a - b * c, not - b * c + a.
sizeof(x) returns the number of bits needed to represent x
when x is a compile-time constant. sizeof(256) is 9.
Cast
(type) eA cast truncates or extends as needed. Reinterpreting bits at the boundary follows the same rules as resize-then-convert above.
Variable access
var
var[i1]...[iN] // array accessArray indices must be expressions of integer type. Out-of-bounds access is a compile-time error when the index is constant; at runtime it wraps around the natural width of the index type.
Port access
port.read()
port.read // parentheses optional
port.available()read() returns the value on the port and consumes it.
available() returns true when a synchronized port has valid
data this cycle, without consuming.
A port may be read at most once per cycle. Reading the same port twice creates an implicit cycle break between the reads.
void loop() {
result.write(op1.read + op2.read); // cycle 1
result.write(bigOp.read); // cycle 2
}The compiler also forbids reading on an output port and writing on an input port; ports are unidirectional.
Function call
func(e1, ..., eN)Argument types must be compatible with the parameter types after
unification. See Declarations
for the difference between const functions and functions with
side effects.
Literals
Boolean
true
falseCharacter
'a'A character literal has type char (8-bit unsigned).
Integer
-1
42
0b10_10_10
0xC0FFEE
0x794389801297897498324987234098213Integer literals can be written in base 2 (0b…), 10, or 16
(0x…). Underscores between digits are ignored.
A positive literal is unsigned and as wide as needed. A negative literal is the unary-minus operator applied to a positive literal; its type is signed with one extra bit.
The literal-as-unary-minus distinction matters in grammar:
a-1 parses as a - 1, not as a followed by the literal
-1. This is the same disambiguation rule C uses.
String
"clock"String literals can initialize char arrays of matching size.
Otherwise they are accepted only as values in properties.