Casting and wavelet access
27. Type Casting and Coercion
Typed Literals (:type suffix)
Numeric literals can be annotated with an explicit type using a colon suffix. This controls how the compiler represents the literal during compilation — it does not generate a runtime conversion instruction.
100.0:sp // single-precision float literal
3.14:hp // half-precision float literal
0:u32 // unsigned 32-bit zero
0xDEAD:xp // hex literal as 16-bit signed integer
42:s16 // signed 16-bit integer literal
The :type suffix is only valid on literals (numeric constants).
It cannot be applied to variables, expressions, or function calls.
Exponent markers are lowercase (e, p), and an exponent-notation literal is
always sp; it takes no :type suffix.
Default literal types:
- An unsuffixed integer literal below 2^32 is untyped: it folds with its exact
value and then takes its context's type, wrapping there, so
u32 r = 0xFFFFFFFF + 2stores 1. With no context it isxp(16-bit signed). - An unsuffixed integer literal of 2^32 or more is
u64. - Float literals without a suffix default to
sp(32-bit float). - An operation of two integer literals, at least one of them typed, folds in the typed operand's type (the wider one when both are typed), wrapping in it.
When to use typed literals:
Use :type when you need a literal of a specific type in a context
where the default would cause a type mismatch or where you want to
be explicit about precision:
hp x = 1.0:hp; // literal is hp, no implicit cast
sp y = 0:sp; // integer literal treated as sp float zero
u16 mask = 0xFF:u16; // 16-bit unsigned literal
C-Style Casts ((type))
Cast expressions convert the runtime value of an expression to a different type. The syntax follows C-style casts:
(type)expression
Examples:
sp x = (sp)integer_value; // convert integer to sp float
xp n = (xp)float_value; // convert float to 16-bit integer
u32 wide = (u32)narrow_val; // widen integer
hp h = (hp)rx[]; // read the next 16 bits from rx as hp
// Cast a complex expression (use parentheses around the expression):
tx[] <- (sp)(col_a[i] * row_b[j]); // hp product cast to sp
// Chained casts:
h = (hp)(xp)rx[]; // read an xp from rx, then convert it to hp
tx[] <- (hp)rx[];
Three conversions differ from C:
- A float-to-integer conversion rounds in the current FP mode, by default to
nearest even, and saturates:
(xp)2.7is 3,(xp)2.5is 2 and(xp)40000.0is 32767. An implicit conversion, such as the return of a-> xpfunction, does the same. To truncate as C does, useround(x, 1). - A cast on a socket read sets the type the incoming bits are read as, and
converts nothing.
(hp)rx[]reads the next 16 bits as anhpbit pattern, so anxp3 from the sender arrives as a tiny subnormal, not 3.0. To convert, read the value with the type it was sent with, then cast:(hp)(xp)rx[]is 3.0. - An integer is not promoted to a wider type before an operation (see
Implicit Type Coercion): with
u16 u = 1andxp s = -1,u > sconvertsstou16and is false.
Key difference from :type:
100.0:sp— tells the compiler "this literal is sp". No conversion code is generated; the literal is stored as sp at compile time.(sp)(x * y)— tells the compiler "evaluatex * yin its natural type, then convert the result to sp". This may generate a runtime conversion instruction (e.g.,hp → spwidening).
Unsigned Integer to Float Conversions
SDR has no instruction that converts an unsigned integer to a float. Carbide
rejects every conversion from u16 or u32 to hp or sp with
Tungsten SDR Codegen: Couldn't match the instruction!: a cast such as
(sp)w[i], an assignment, a return, and an operand of a mixed expression such
as u + h with hp h. Convert a u16 through s32 first: (sp)(s32)u is
exact. Casting a u32 through s32 interprets values of 2^31 or more as
negative. A bitcast reinterprets bits rather than converting the unsigned
value.
Bit Reinterpretation (bitcast)
bitcast(type, expression) (also spelled reinterpret(type, expression))
reinterprets the bits of expression as type without conversion. The
destination and source must have equal bit width; a width-changing
bitcast is rejected at compile time. Use a cast
(type) to widen or narrow, or a union or address(&array) alias to view
storage at a different width.
A 64-bit bitcast is a 64-bit copy, so it is subject to the target's 64-bit support: Carbide has no 64-bit copy for SDR and rejects it.
Hardware FP Mode
The FP control word (FPCW) selects the rounding mode, flush-to-zero and the
FP16 (hp) format of the floating-point instructions. An instruction that
carries its own rounding mode overrides it; round(x, m) compiles to one. By
default the FPCW selects IEEE FP16 and round to nearest even.
The FSCALE configuration word (FSCL) sets the FP16 formats of the fscaleh
instruction only. The compiler has no option that selects this mode and emits no
control-word initializer: a program runs in the mode its host or its own code
establishes.
Implicit Type Coercion
When operands of different types appear in a binary expression, the compiler applies implicit coercion following these rules:
- Same type → no conversion.
- One operand unknown → use the known type.
- Both float, different width → widen to the wider float
(
hp+sp→sp). - Float + integer → convert the integer to the float type (float always wins, regardless of integer width). On SDR the integer must be signed (see Unsigned Integer to Float Conversions).
- Both integer, same width → unsigned wins over signed
(
u16+s16→u16). - Both integer, different width → widen to the wider type
(
u16+u32→u32).
An unsuffixed literal below 2^32 adopts the other operand's type; a wider one
is u64 (see Typed Literals).
hp a = 1.0;
sp b = 2.0;
sp c = a + b; // a is implicitly widened from hp to sp
xp i = 3;
hp d = i + a; // i is implicitly converted from xp to hp
Special Constants
∞ (also INF) is the end of an infinite iterator, i in [0, ∞). NAN
can also initialize a float array element (sp a[n=1] = {NAN}; stores a quiet
NaN). Carbide does not support ∞, INF or NAN as run-time values.
28. Wavelet Access Modes
Standard Socket Access
tx[] <- value; // send
value <- rx[]; // receive
Raw Wavelet Send/Receive via Union
To send or receive a raw 32-bit wavelet (e.g. a message-passing
header), define a union of u32 and xp[2], construct the value
as two 16-bit halves, and send/receive the u32 member:
union mp_header {
u32 u; // 32-bit view for socket I/O
xp w[2]; // two 16-bit halves: w[0]=lower, w[1]=upper
};
union mp_header hdr;
hdr.w[0] = 0x44; // lower 16 bits
hdr.w[1] = 0x00; // upper 16 bits
sock[] <- hdr.u; // send as 32-bit wavelet
union mp_header reply;
reply.u <- sock[]; // receive 32-bit wavelet
xp low = reply.w[0]; // extract lower 16 bits
Data and Index Access
a[].data // the data (lower) half of the next wavelet
a[].index // its index (upper) half (also: a[].idx)
The index half can only address the write of the loop statement that reads the data half: each wavelet's data is stored at its own index.
socket a;
xp x[n=8];
i in [0, 4) x[a[].index] <- a[].data;
Any other use of the index half, such as x[0] = a[].index;, is rejected, and
so is a loop body with other statements beside this one. The index is not
checked: an index past the end of x writes the memory after it. The sender
puts the index in the upper half of each 32-bit wavelet and the data in the
lower half, for example with the union above.
Flip (Local Switch Advance)
flip tx[]; marks the send just before it on tx: the last word of
that send carries the local-switch-flip bit. It must directly follow
a send on the same socket; a statement in between is rejected
(unimplemented: inst_type INST_FLIP). When the PE transmits that
last data word, the fabric's input switch on the sending tile
advances to the next position.
{
let i in [0, N);
∀i tx[] <- data[i];
flip tx[]; // set flip bit — the last word advances the local input switch
}
The flip bit is piggybacked onto the final data wavelet — no extra wavelet is sent. It only advances the local (sending tile's own) input switch; downstream switches are not affected.
Constraints:
- Only data wavelets or control wavelets with Command 0 = NOP can carry the flip bit.
- The input select may differ across switch positions, but the output enable mask and the advance-on-reset setting must be the same in every position.
- Pop control modes
pop-on-advanceandpop-on-advance-or-nopare not supported with local switch advance. - The flip sideband is NOT buffered in the fabric and will NOT be observed by the output switch. Input and output switches can get out of sync; only a subset of fabric features is supported.
Typical usage — serial drain:
/* Each tile injects data [C>R], then flips to passthrough [L>R].
* Tiles closest to the receiver drain first. */
{
i in [0, N) tx[] <- data[i];
flip tx[];
}
Lesson 10 of the tutorial, Switches: PEs that take turns on one color, is a complete working example.
Control Wavelets
tx[] <- control(w); sends the 32-bit word w as a control wavelet. A router
reads the word as eight 3-bit commands and acts on command 0. A route with a
pop setting can remove command 0 as the wavelet leaves the router, so the
next command moves into its place for the next router (see
Routing in the Paint
reference).
| Bits | Field |
|---|---|
| 24:22 | Command 0 |
| 27:25 | Command 1 |
| 29:28, 0 | Command 2 (bits 1:0 in 29:28, bit 2 in bit 0) |
| 3:1, 6:4, 9:7, 12:10, 15:13 | Commands 3 to 7 |
| 21:16 | Task ID, for the receiving CE |
A command's bits 1:0 are its opcode: 00 NOP, 01 switch advance, 10
switch reset, 11 teardown. Its bit 2 is the CE filter: a control wavelet
whose command 0 has it set is not delivered to the CE.
socket tx;
u32 ADV = 0x09409249; // command 0: advance; commands 1-7: NOP; all CE-filtered
tx[] <- control(ADV);
0x01800000 is a switch reset with the CE filter on command 0 only. Commands
1 to 7 are NOPs without the filter, so once a route pops command 0, the next
router can deliver the wavelet to its CE. 0x09809249 is the same reset with
the CE filter on every command, as ADV does.
Hardware Register Access (address())
The address() declaration maps a variable directly onto a
hardware register at a fixed memory address. This is used to
read PE coordinates and other hardware state:
// Map variables onto hardware register addresses
xp col_id[1] address(0x7C46); // Column ID Register
xp row_id[1] address(0x7C45); // Row ID Register
// Read the PE's fabric coordinates
xp my_col = col_id[0];
xp my_row = row_id[0];
Common hardware registers:
| Address | Name | Description |
|---|---|---|
0x7C44 | Fabric Misc Config | MP bank enables, misc flags |
0x7C45 | Row ID | PE's fabric row (Paint y) |
0x7C46 | Column ID | PE's fabric column (Paint x) |
Paint writes the Row ID and Column ID registers of every tile it lays out, with or without message passing, so any kernel can read its position this way.
Caution: Direct register access is a low-level mechanism. The
address()flag permanently binds a variable to a specific hardware address.
Message Passing
Message passing allows a PE to send a multi-wavelet message to any other PE using coordinate-based routing. Unlike normal color-based routing (which follows statically configured paths), message-passing wavelets carry a destination header and the fabric hardware routes them automatically.
Sending a Message
A message consists of a header wavelet followed by data
wavelets. Use a union to construct the 32-bit header as two
16-bit halves and send it as u32:
socket mp_sock; // bound to a bank color in paint
// Hardware registers mapped via address()
xp col_id[1] address(0x7C46); // Column ID Register
xp row_id[1] address(0x7C45); // Row ID Register
union mp_header {
u32 u; // 32-bit view for socket I/O
xp w[2]; // w[0]=lower 16 bits, w[1]=upper 16 bits
};
union payload_word {
u32 u;
xp w[2];
};
xp payload[4];
{
// Read own coordinates
xp my_col = col_id[0];
xp my_row = row_id[0];
payload[0] = my_col;
payload[1] = my_row;
payload[2] = 0xCAFE;
payload[3] = 0xBEEF;
// Build header: destination (col=0, row=0), len=4.
// Row 0 is north of every other row; on row 0 itself the
// destination is on the same row, so both direction bits stay clear.
union mp_header hdr;
xp dir = 0;
if (my_row > 0) dir = 0x80; // north
hdr.w[0] = 4 | dir; // lower: direction + length
hdr.w[1] = 0; // upper: dest col/row = 0
// Send: header (as u32) then data
mp_sock[] <- hdr.u;
union payload_word word;
i in [0, 4) {
word.w[0] = payload[i];
word.w[1] = 0;
mp_sock[] <- word.u;
}
}
Paint writes a tile's y coordinate into its Row ID, and Paint's +Y is
the router's south, so row 0 is the northernmost row. Set north
(0x80) only when the destination row ID is smaller than the sender's
and south (0x40) only when it is larger; a message to a PE on the
sender's own row has both bits clear.
Receiving a Message
socket mp_sock;
xp data[4];
union mp_header {
u32 u;
xp w[2];
};
union payload_word {
u32 u;
xp w[2];
};
// Receive header as u32, decode as two xp halves
union mp_header hdr;
hdr.u <- mp_sock[];
xp count = hdr.w[0] & 0x1F; // bits 4:0 = wavelet count
// Receive the full-wavelet payload and unpack its lower half
union payload_word word;
i in [0, count) {
word.u <- mp_sock[];
data[i] = word.w[0];
}
Send every word of a message as a full 32-bit wavelet, as the example
does with word.u. A 16-bit send is half a wavelet, and the hardware
does not define a half-wavelet on a message-passing color. Once a PE
sends a header it must send the whole message: until it does, no other
message can use that path.
Message Header Format
The 32-bit header wavelet encodes the destination and message
length. Using a union { u32 u; xp w[2]; }, the fields are
distributed across w[1] (upper) and w[0] (lower):
| Bits | Field | Description |
|---|---|---|
| 31:20 | col_id | Destination column (in w[1], upper bits) |
| 19:8 | row_id | Destination row (spans w[1] and w[0]) |
| 7 | north | Set if destination is north of source |
| 6 | south | Set if destination is south of source |
| 5 | ctrl_term | If 1, message terminated by control bit |
| 4:0 | len | Number of data wavelets that follow (if bit 5 = 0): 0 to 31 |
The fabric routes the message X-first (east/west to match the column), then Y (north/south to match the row), and finally delivers to the destination PE's compute element.
Note: Every tile on the routing path between sender and receiver must have message passing enabled via
mp()in paint. Nothing reports a tile that lacks it; the message is lost. See the Paint Language Reference for configuration details.
The tutorial topics Message passing: routing by address and Collectives over messages: tree reduction and prefix scan are complete working examples.