C⏚ v3.2.0Updated 2026-10-29·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.

A const does not have to live inside an entity. Constants, typedefs, structs and enums can also be declared at file scope, above the first entity, where they are in scope for the whole file. See Bundles → Declarations at file scope.

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
i2–i642–64Two's-complement signed
u2–u642–64Natural-binary unsigned
char8Unsigned byte, semantic alias for codepoints

The minimum integer width is 2. A one-bit value is a bool. Since 3.5.1 any integer with a literal width of 1 - u1, i1, uint<1>, int<1> - is an error, in every position: port, variable, struct field, cast.

Integer types must be at least two bits large, use bool to declare a single-bit variable

A width set by a parameter is still accepted, even when it is 1, so a generic FIFO can be instantiated one bit wide.

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
framed—A stream plus a frame boundary (last)Next cycle

The v1 names are deprecated. New code uses the v2 keywords. confirm (v1 sync ack) is no longer accepted - see Confirm.

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 (inp.available()) {
  u8 v = inp.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.

Framed

A framed port is a stream that also carries the end of a frame: a last flag that is true with the final value of each frame, like AXI-Stream TLAST. Use it whenever a value belongs to a packet, line or message, so the reader learns where it ends from the data, not from a pause.

in  framed u8 din;
out framed u8 dout;
 
void loop() {
  u8 v = din.read();
  dout.write(v, din.last);   // pass each value on with its boundary
}
  • Write with two arguments: dout.write(value, last). Writing a framed output without its boundary is an error.

  • Read din.last in the same cycle as din.read(): it belongs to the value read in that cycle. Read in a cycle that does not read din, it is whatever is on the wire, and the compiler warns.

  • One frame at a time:

    bool more = true;
    while (more) {
      u8 v = din.read();
      more = !din.last;
      // … use v
    }

    Do not write while (!din.last) { v = din.read(); }: the condition is tested before the read it should describe.

  • Connections: a framed input must be fed by a framed output; a plain stream has no boundary to give it, and the connection is an error. A framed output may feed a plain stream input, which simply ignores last.

  • A struct port cannot be framed yet.

In a task's test vectors, each inner list is one frame.

Confirm

Removed in 3.5.1. A confirm (or sync ack) port is a compile error:

`confirm` is not supported: its acknowledge was never read, so a value
the consumer was not ready for was lost. Use `stream` (valid/ready: the
value is held until it is taken) or `push` (valid for one cycle, no
back-pressure)

The generated HDL declared the _ack input and never read it, so a consumer that was not ready lost the value. Replace it:

  • stream if the consumer must be able to make the producer wait.
  • push if it never needs to.
out confirm u8 y;   // error since 3.5.1
out stream  u8 y;   // the value is held until it is taken

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.

Reserved words

These 54 words are C⏚ keywords and cannot be used as the name of a port, variable, function, instance, or entity:

ack            assert         bool           break          bundle
char           combinational  confirm        const          continue
else           enum           extends        false          fence
float          for            framed         idle           if
import         in             int            long           network
new            null           out            package        print
properties     push           reads          ready          return
short          signed         sizeof         stream         struct
sync           task           this           true           typedef
uchar          uint           ulong          unsigned       ushort
void           while          write          writes

Several are easy to reach for by accident. ack, ready, data and sync all read like signal names, and reads, writes, print and idle read like ordinary identifiers. Of those, only data is actually free.

The error will not mention the reserved word. The declaration fails to parse, so what you see is a complaint about the token after it:

Queue.cg:17: mismatched input 'ack' expecting RULE_ID

or, for an instance named sync, a message about the = that follows it. If a declaration you believe is correct produces a parse error naming a token inside it, check this list first.