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
| Name | Width | Range |
|---|---|---|
bool | 1 | false, true |
i2–i64 | 2–64 | Two's-complement signed |
u2–u64 | 2–64 | Natural-binary unsigned |
char | 8 | Unsigned 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
| Alias | Width | Equivalent |
|---|---|---|
short | 16 | i16 |
int | 32 | i32 |
long | 64 | i64 |
signed | 32 | i32 |
unsigned | 32 | u32 |
ushort | 16 | u16 |
uint | 32 | u32 |
ulong | 64 | u64 |
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 bitsArray 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:
| Left | Right | Result |
|---|---|---|
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)> |
bool | bool | bool |
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
constkeyword, - 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
| Keyword | Meaning |
|---|---|
in | Input - read by the entity, written by the outside |
out | Output - 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.
| Qualifier | Replaces (v1) | Handshake | Visibility |
|---|---|---|---|
| (bare) | (bare) | None - combinational wire | Same cycle |
push | sync | Valid-strobed, no back-pressure | Next cycle |
stream | sync ready | Valid + ready, back-pressure | Next cycle |
confirm | sync ack | Valid + ack, no back-pressure | Next 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.