C⏚ v2.0.0Updated 2026-05-12·Language

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:

  1. 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.
  2. 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.
  3. 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 : e2
conde1e2Result
boolT1T2unify(T1, T2)

The condition must be bool. The expression is invalid if unify(T1, T2) is undefined.

Boolean

e1 || e2
e1 && e2
e1e2Result
boolboolbool

Bitwise

e1 | e2     // OR
e1 ^ e2     // XOR
e1 & e2     // AND
e1e2Result
T1T2unify(T1, T2) for `
T1T2unify(T1, T2) with size min(size(T1), size(T2)) for &

Equality

e1 == e2
e1 != e2
e1e2Result
T1T2bool

Relational

e1 <  e2
e1 <= e2
e1 >  e2
e1 >= e2
e1e2Result
T1T2bool

Shift

e1 << e2
e1 >> e2

Right shift is arithmetic for signed operands and logical for unsigned ones.

Additive

e1 + e2
e1 - e2
e1e2Result
T1T2unify(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 % e2

The result is wide enough to hold the worst case. For *, that is size(T1) + size(T2) bits.

Unary

OperatorOperandResult
~eint<N>same type as operand
!eboolbool
-e (variable)int<N>signed<N + 1>
-e (literal)constantthe literal's natural type
sizeof(e)constantunsigned

~ 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) e

A 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

Array 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
false

Character

'a'

A character literal has type char (8-bit unsigned).

Integer

-1
42
0b10_10_10
0xC0FFEE
0x794389801297897498324987234098213

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