C⏚ v3.0.0Updated 2026-08-23·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.

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

/ 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 operandWhat is generated
Power of twoA shift or mask. Free.
Any other constantA reciprocal multiply, (x * M) >> s. One cycle.
A variableRejected. 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

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
var[i]           // on a scalar integer: one BIT, as a bool

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.

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 idiom

There 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 nibble

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