Skip to main content

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 + 2 stores 1. With no context it is xp (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.7 is 3, (xp)2.5 is 2 and (xp)40000.0 is 32767. An implicit conversion, such as the return of a -> xp function, does the same. To truncate as C does, use round(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 an hp bit pattern, so an xp 3 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 = 1 and xp s = -1, u > s converts s to u16 and 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 "evaluate x * y in its natural type, then convert the result to sp". This may generate a runtime conversion instruction (e.g., hp → sp widening).

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:

  1. Same type → no conversion.
  2. One operand unknown → use the known type.
  3. Both float, different width → widen to the wider float (hp + sp → sp).
  4. 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).
  5. Both integer, same width → unsigned wins over signed (u16 + s16 → u16).
  6. 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-advance and pop-on-advance-or-nop are 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).

BitsField
24:22Command 0
27:25Command 1
29:28, 0Command 2 (bits 1:0 in 29:28, bit 2 in bit 0)
3:1, 6:4, 9:7, 12:10, 15:13Commands 3 to 7
21:16Task 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:

AddressNameDescription
0x7C44Fabric Misc ConfigMP bank enables, misc flags
0x7C45Row IDPE's fabric row (Paint y)
0x7C46Column IDPE'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):

BitsFieldDescription
31:20col_idDestination column (in w[1], upper bits)
19:8row_idDestination row (spans w[1] and w[0])
7northSet if destination is north of source
6southSet if destination is south of source
5ctrl_termIf 1, message terminated by control bit
4:0lenNumber 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.