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.
The shift amount must be a compile-time constant. A constant shift is just wiring and costs nothing; a variable one needs a barrel shifter, which the compiler will not infer:
the right operand of '>>' must be a compile-time constant
(no variable shifter is generated)If you need a runtime shift amount, build the barrel shifter
explicitly as a for over the bits of the shift amount, each stage
shifting by a constant power of two.
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.
/ and % require a compile-time constant on the right.
Hardware division is expensive, so the compiler will not infer a
divider from an operator. What a constant divisor costs depends on
the constant:
| Right operand | What is generated |
|---|---|
| Power of two | A shift or mask. Free. |
| Any other constant | A reciprocal multiply, (x * M) >> s. One cycle. |
| A variable | Rejected. Use std.math.Divide. |
Both forms are correct for signed and unsigned operands, and truncate toward zero.
A variable divisor gives:
the right operand of '/' must be a compile-time constant
(use the std.math.Divide built-in for a variable divisor)That built-in is a real sequential divider you instantiate and wire, not an operator. See Standard library → std.math.Divide.
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 access
var[i] // on a scalar integer: one BIT, as a boolArray 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.
Bit access
Indexing a scalar integer selects a single bit and yields a bool,
counting from 0 at the least significant end:
u8 v = 0xAB;
bool b = v[3]; // true
if (sh[0]) { x = x << 1; } // the barrel-shifter idiomThere is no range slice. v[7:4] does not parse, and the error
does not mention ranges — you get mismatched input '['. Shift and
mask instead, then cast to the width you want:
u4 lo = (u4)(v & 0xF); // low nibble
u4 hi = (u4)((v >> 4) & 0xF); // high nibbleThe shift distance must be a constant, as everywhere else.
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.