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

Declarations

C⏚ entities are built from four kinds of declaration: variables, types, functions, and ports. This page is the reference for each.

Variables

Two kinds of variable, distinguished by where they're declared.

Local variables live inside a function body. Their scope ends with the function.

void loop() {
  u8 tmp = port.read();
  // tmp scoped to this loop() call
}

State variables are declared at entity level (inside a task, outside any function). They persist across cycles and become registers in synthesis. They are private to the task.

task Counter {
  u8 count = 0;        // state variable
  void loop() { count++; }
}

State variables in networks declare named connection points, not registers; see Networks.

Initialization

A scalar may be initialized at declaration:

u8 counter = 0;
bool started = false;

Arrays use brace-initialization:

u32 triple[3] = {1, 2, 3};

char arrays accept string literals:

const char message[12] = "Hello world!";

An uninitialized variable is zero. There is no undefined-value state in C⏚.

Types

Every C⏚ type has an exact bit width. There are no implicit promotions, no padding, and no sign-extension surprises. The full mental model is in Concepts.

Fixed-width integers

NameWidthRange
bool1false, true
i2i642–64Two's-complement signed
u2u642–64Natural-binary unsigned
char8Unsigned byte, semantic alias for codepoints

The minimum integer width is 2. A one-bit integer is bool; the signed equivalent has the disturbing range [-1, 0] and is disallowed.

C and OpenCL aliases

AliasWidthEquivalent
short16i16
int32i32
long64i64
signed32i32
unsigned32u32
ushort16u16
uint32u32
ulong64u64

signed int and unsigned int are the same as int and unsigned, included for C compatibility.

Custom-width integers

Any of the int variants accepts a compile-time width expression:

const u8 W = 64;
 
int<W>          a;   // 64-bit signed
unsigned<W * 2> b;   // 128-bit unsigned
uint<addrWidth> c;

Arrays

C-like syntax, constant dimensions:

u128 aesKey[11];     // 1408 bits
bool flags[3][16];   // 48 bits

Array storage size is the product of element width and every dimension.

typedef

typedef gives a domain-specific name to an existing type without introducing a new type:

typedef u8 pixel;
typedef int<addrWidth> addr_t;

typedef is allowed in networks, tasks, and bundles.

Type unification

When an expression combines two values of different types, the compiler computes a unified result type:

LeftRightResult
signed<X>signed<Y>signed<max(X, Y)>
unsigned<X>unsigned<Y>unsigned<max(X, Y)>
signed<X>unsigned<Y>signed<max(X, Y)>
unsigned<X>signed<Y>signed<max(X, Y)>
boolboolbool

Signed wins over unsigned. The result preserves arithmetic meaning: i3(−2) * u6(50) is i9(−100), not u9(300).

Earlier versions of the language used max(X, Y) + 1 for mixed signedness to guarantee no overflow. The wider result types surprised users; v2 trades the theoretical guarantee for predictability.

Structs and enums

Two user-defined composite types build on the scalars above. A struct groups fixed-width fields under one name; an enum names a set of integer constants. Both are covered in Structs & enums.

Functions

A function declaration has:

  • an optional const keyword,
  • a return type (or void),
  • a name and parameter list,
  • a body.

Two kinds of function, separated by what they can do.

Constant functions

A constant function is side-effect-free. It can read state variables but cannot access ports. Declare it with const:

const u9 increment(u8 x) {
  return x + 1;
}

Functions declared inside a bundle are implicitly constant; the const keyword is optional there.

Functions with side effects

A function with side effects can read state, write state, and access ports. It can only be declared inside a task and must return void. A function that returns a non-void value must be const.

void emit(u8 value) {
  data.write(value);
  count++;
}

A side effect is any read or write of a port, any write to a state variable, or any call to a function that has side effects.

Ports

A port is the interface through which a task or network communicates. A port declaration has a direction, an optional synchronization qualifier, a type, and a name.

in  u16 a, b, u48 c;     // a and b are u16; c is u48
out bool x;

When consecutive ports share a type, the type can be omitted.

Direction

KeywordMeaning
inInput - read by the entity, written by the outside
outOutput - written by the entity, read by the outside

Synchronization qualifier

Four qualifiers select the handshake. The qualifier appears between the direction and the type and applies only to the current port.

QualifierReplaces (v1)HandshakeVisibility
(bare)(bare)None - combinational wireSame cycle
pushsyncValid-strobed, no back-pressureNext cycle
streamsync readyValid + ready, back-pressureNext cycle
confirmsync ackValid + ack, no back-pressureNext cycle

The v1 names are deprecated. New code uses the v2 keywords.

When several ports share the same qualifier, use a group:

push {
  in  u16 a;
  out u8  data, bool valid;
}

Push

A push port carries one cycle of valid data per write. The producer asserts valid for exactly one cycle; the consumer must consume on that cycle or the data is dropped.

read() on a push port is blocking - the surrounding execution rule only fires when data is present. Reading several push ports in one expression blocks until all of them are valid in the same cycle.

in push u3 a, b;
out u6 product;
 
void loop() {
  product.write(a.read() * b.read());
}

To test for data without consuming, call available() on the port. It returns true for one cycle when fresh data is present:

if (in.available()) {
  u8 v = in.read();
  // …
}

Stream

A stream port adds a ready signal from consumer to producer. The consumer can stall the producer indefinitely. Use it for:

  • pipelines with back-pressure (e.g. Ethernet inter-frame gaps),
  • FIFO interfaces,
  • compatibility with external ready/valid hardware.

Confirm

A confirm port carries a valid from producer and an ack from consumer. The producer is not held back if the consumer fails to acknowledge in time. Use it when the consumer must acknowledge receipt but the producer cannot afford to stall.

Bare

A bare port (no qualifier) is a combinational wire. The written value is visible in the same cycle. Bare ports have no valid flag, no commit, no _next buffer. Use them for combinational outputs and persistent status signals.


Next: Bundles, Tasks, or Networks.