Skip to main content

Control flow, expressions and functions

14. Control Flow​

If / Else If / Else​

if (condition) {
// then branch
}

if (x == 0) {
a = 1;
} else if (x < 10) {
a = 2;
} else {
a = 3;
}

Single-statement bodies are allowed without braces:

if (a)
total[0] <- total[0] + 1;

Conditions are evaluated in order, and an else if condition is evaluated only if every earlier condition in its chain was false. && and || in a condition evaluate both operands (see Logical Operators).

While Loops​

while (cnd < 5) {
i in [0, 4) a[i] <- a[i] + b;
cnd = cnd + 1;
}

while repeats its body while the condition is true.

The condition is evaluated before every iteration, including after a continue.

Forever Blocks​

The forever keyword creates an infinite execution loop:

forever {
// runs indefinitely
a[0] += 1;
}

Named forever blocks:

forever main {
// named infinite loop
...
}

Break and Continue​

forever {
if (done) break;
if (skip) continue;
process();
}

break; exits the nearest enclosing loop and continue; starts its next iteration. The nearest loop is the innermost lexically enclosing while, forever block, serial iterator loop (§12), or dispatch handler; if/else arms and plain { } blocks are transparent. In a dispatch handler, continue completes the handler like falling off its end and break leaves the dispatch (see Handler Completion). Loop control in a function cannot bind to a loop at the call site; Carbide rejects it. Loop control with no enclosing loop or handler (at top level, in main, or in a parallel block outside any loop) is rejected at its line.

A serial iterator loop encloses its body's loop control wherever it appears, so i in [0, 4) { r[i] = 1; if (i == 2) break; } is valid at top level.

A break or continue whose nearest loop would be a parallel iterator (§20) is a compile-time error. A break or continue whose target loop or handler lies outside a parallel block, however many arms it has and including one that executes only conditionally, is a compile-time error. return is not allowed in any parallel block.

break(0) executes the hardware breakpoint instruction, which stops the PE; its operand is ignored.

Return​

Return from a function:

function add(xp a, xp b) {
return a + b;
}

The return type may be declared explicitly:

function widen(xp value) -> sp { return value; }
function reset(xp value) -> void { xp copy = value; }

Primitive, typedef, and struct result types are supported; union and array result types are not (return a union through an array parameter). When -> type is present, it is authoritative and every return must be exact or a legal implicit primitive conversion. A legal conversion is accepted with a source warning; an illegal or unresolved conversion is an error. -> void forbids value returns. Without an annotation, all value returns must infer the same canonical type; no value return infers void. Value-returning functions must conservatively return on every path. Recursive functions, direct or mutual, compile to real calls. A recursive function that returns a value needs -> type, because its type cannot be inferred through the recursion, and a recursive call inside a parallel block is rejected.


15. Expressions and Operators​

30. Unicode Support lists the Unicode spelling of each operator.

These rules differ from C and Python. Here p, v, a and b are xp values read at run time, and a < b is true:

RuleExampleResult
Signed division and % of run-time values round toward minus infinity, as in Pythonp / 2, p % 2 with p = -7-4, 1
A division of literals or param values truncates, as in C-7 / 2, -7 % 2-3, -1
>> is a logical shift on every type, signed ones included; x>> is the arithmetic shiftp >> 1, p x>> 1 with p = -832764, -4
A 16-bit shift uses only the low 4 bits of its countv << 16 with v = 11
A comparison read as a value is -1 when truea < b, (a < b) * 3-1, -3
As an operand of + or -, a true comparison reads as 1(a < b) + 1011
So a comparison compared with 1 is never true(a < b) == 10
&&, || and ?: evaluate both operands; && and || give 0 or 1(a < b) && 11
** binds as tightly as *, and every level groups left to right2 * 3 ** 2, 2 ** 3 ** 236, 64
&, | and ^ share one level1 | 2 & 00
++ and -- are statements, not expressionsa = c++;syntax error
An array or struct argument is the caller's storage, not a copyx.a = 99; in the functionchanges the caller's struct

The two shift rows hold for run-time values only. Carbide folds a shift of literals and param values on the exact value, with a different result: -25 >> 2 gives -7, while p >> 2 with p holding -25 gives 16377, and 1 << 16 gives 0. A variable or a function's parameter follows the run-time rule even when its value is known: after xp c = -25;, c >> 2 gives 16377, and so does a >> 2 in a function called as f(-25).

Arithmetic Operators​

OperatorDescriptionExample
+Additiona + b
-Subtractiona - b
*Multiplicationa * b
/Divisiona / b
%Moduloa % b
**Powera ** b

A power has no runtime lowering, except the power-of-two form 2 ** n; it must fold at compile time. It folds when both operands are integer constants: untyped literals or param values. A typed literal such as 3:u32, a run-time value such as a[0], and a floating-point operand are rejected. The folded value must fit a signed 32-bit integer (3 ** 20 is the error constant pow result overflows integer type) and then wraps to the destination's type: with xp param N = 200, N ** 2 stores -25536. A negative exponent truncates the real power toward zero: 3 ** -1 is 0.

Runtime integer division supports scalar 16-bit operands in addition to the floating forms; the table above gives the signed rounding. u16 division is unsigned. A 32-bit integer division is not supported, and Carbide stops with an internal error (IRToMachine has no target-opcode mapping). SDR has no divide instruction, so a divisor that is not a constant power of two expands to an inline long division. A constant zero divisor is a compile-time error.

% takes integer operands. Carbide accepts % on hp and sp operands, but the result is wrong (7.0 % 3.0 gives 7.0), so do not use it.

Bitwise Operators​

OperatorDescriptionExample
&Bitwise ANDa & b
|Bitwise ORa | b
^Bitwise XORa ^ b
~Bitwise NOT~a
<<Left shifta << b
>>/0>>Logical right shifta >> b
x>>/~>>Arithmetic right shifta x>> b

Shift operands undergo the usual arithmetic conversion, and the result has that uniform integer type. For a u32 operation, Carbide accepts only a constant count below 32; a larger or run-time count is rejected. 0>>/>> shifts in zeros; x>> treats the u32 value bits as signed two's-complement bits and replicates the sign bit. This contract is identical whether the target uses one native instruction or an inlined software implementation.

For &, |, ^, and ~, the resolved operands and result must have one exact uniform integer type. SDR supports xp/s16, u16, s32, and u32. Scalar and lane-wise forms are supported. &, | and ^ also accept u1 operands; ~ on a u1 is not supported by Carbide. Floating-point, pointer, and aggregate values are not bitwise integer operands. An optional instruction predicate remains only an execution guard; these operators write an ordinary integer result and do not produce a predicate or flag destination.

An address takes part only in + and - with an integer offset; a bitwise operator on an address, such as &table | offset, is rejected.

Comparison Operators​

OperatorDescriptionExample
==Equala == b
!=Not equala != b
<Less thana < b
>Greater thana > b
<=Less or equala <= b
>=Greater or equala >= b

Comparisons as values. A runtime comparison read as a value — stored to a destination, used as an operand of an arithmetic, bitwise, == or != operator, or passed as an argument — is the hardware compare mask: all ones (0xffff for a 16-bit comparison, 0xffffffff for a 32-bit one) when true and 0 when false. Stored to a float destination, it is -1.0. As an operand of + or - it instead reads as 1 when true. &&, || and ! yield 0 or 1:

t = a < b; // -1 (0xffff) when a < b, 0 otherwise
r = (z == 0) + 10; // 11 when z is zero, 10 otherwise
u = (a < b) && 1; // 1 when a < b, 0 otherwise

A comparison stored in a u1 first reads as 0 or 1 wherever it is used: after u1 q = a < b;, t = q; is 1 when a < b (see Booleans and Predicates).

Logical Operators​

OperatorDescriptionExample
||Logical ORa || b
&&Logical ANDa && b
!Logical NOT!a

&& and || do not short-circuit: both operands are always evaluated. The result is 0 or 1.

Each operand's truth is operand != 0 at its own type and width: with u32 x = 0x10000, x || 0 is 1, and a float operand is true iff it compares unequal to 0.0, so 1 && 0.5 is 1, NaN is true and -0.0 is false. Folded operands follow the same rule in code, in an index and in static data (0x10000 || 0 is 1). An operand whose type its destination would give takes its type with no destination: round(f, m) converts to xp, a stream read is one xp word unless cast ((u32)rx[] reads a full 32-bit wavelet), and sizeof and rand() are xp. 2 ** x cannot be such an operand, even cast. A read that pops or advances is read once, whichever side of the operator it is on. A pointer or aggregate operand is rejected.

Ternary Operator​

result = (condition) ? value_if_true : value_if_false;

Both arms are evaluated.

Example:

a = (a == N) ? b + c : c - b;

Dot Product​

The summation operator ∑ computes a dot product:

k in [0, N) c[0] <- ∑ a[k] * b[k];

A ∑ that names its iterators binds them over its operands only. Its destination belongs to the enclosing scope, so a destination naming one of them reads the enclosing iterator or variable of that name:

let n in [0, N);
let k in [0, K);
∀n c[n] <- ∑ n,k x[n][k] * y[n][k]; // every c[n] is the sum over all n and k

Floating-Point Evaluation​

The compiler reassociates floating-point arithmetic freely, so that it can use the fused operations of the hardware. It may combine a product and a sum into one fused multiply-add, rounded once, and turn a sum of products over a loop into the dot-product operation that ∑ uses. That operation has a wider exponent range than sp, but it still rounds each product and each addition to its accumulator, so the order of the terms matters: over sp values 1e8, 1, -1e8 and 1, an ascending ∑ gives 1 and a descending one gives 0. A floating-point result can therefore differ in its last bits from one computed an operation at a time in source order. To check such a result exactly, the reference must round the same way; otherwise compare it within a tolerance.

Operator Precedence (highest to lowest)​

  1. Unary operators (-, !, ~, casts, function calls)
  2. ** (power), *, /, %
  3. +, -
  4. <<, >>/0>>, x>>/~>>
  5. ==, !=, <, >, <=, >=
  6. &, |, ^
  7. &&
  8. ||
  9. ?: (ternary)

16. Assignment​

Regular Assignment​

s = b + c; // simple assignment
a[i] = b * c; // array element

Arrow Assignment (<-)​

The arrow operator is the primary assignment in Tungsten, and the form the examples use for data movement. = does the same, for sockets too: tx[] = a[i]; sends. On SDR every transfer, to memory or to the fabric, is an instruction that the PE executes; there is no separate copy engine.

a[i] <- b[i] + 1;
tx[] <- data;
result <- rx[];

Accumulate Assignment (<+- / +=)​

Add-and-assign (accumulate) into the destination:

a[i] <+- b[i]; // a[i] = a[i] + b[i]
a[i] += b[i]; // same

When the right-hand side is a product of two values of the destination's floating-point type, acc <+- a * b is one fused multiply-add: the exact product is added and the sum is rounded once. A loop such as i in [0, N) c[0] <+- a[i] * b[i] does one fused multiply-add per element. The same loop into a scalar variable may instead compile to the dot-product operation that ∑ uses (Floating-Point Evaluation).

An hp product accumulated into an sp destination is one mixed fused multiply-add: the exact product is added to the sp value and the sum is rounded once. Carbide evaluates any other right-hand side and rounds it before the addition: acc += sqr(x) rounds sqr(x) before the addition and does not request the DOT/FSSQR extended accumulator operation.

Compound Assignments​

An arithmetic, bitwise or shift operator followed by = assigns its result to the left operand:

a += b; // a = a + b
a -= b; // a = a - b
a *= b; // a = a * b
a &= b; // a = a & b
a <<= 1; // a = a << 1

The compound forms are +=, -=, *=, /=, &=, |=, ^=, <<=, >>=/0>>= and x>>=/~>>=, and the Unicode spellings ×=, ⨁=/⊕=, ⬶=, ⤅= and ⤖=. There is no %=.

Increment and Decrement​

counter++;
counter--;

17. Functions​

Function Definition​

function add(xp a, xp b) {
return a + b;
}

function reset() {
count = 0;
}

A function may omit its return annotation and let the compiler infer one canonical type from its value returns. It may instead declare the contract with -> type; an annotated result is authoritative, and -> void forbids value returns. There is no bare return: return; fails with symbol [return] is not defined.

function widen(xp value) -> sp {
return value;
}

function reset(xp value) -> void {
xp copy = value;
}

Parameters​

A parameter is a type followed by a name. Arrays may be given their extents or left unsized, and structs, unions and typedef'd names may be passed as well as base types:

function scale(hp x) // scalar
function process(xp arr[2]) // array with a known extent
function smooth(hp data[N]) // extent from a parameter
function total(xp values[]) // unsized: any length
function detect(sp r[], xp idx[]) // several unsized arrays
function ff_add(struct float_float a, struct float_float b)
function pack(union bits b)
function add_one(float_32 a) // a typedef'd name

A scalar parameter is a copy: assigning to it does not change the caller's variable. An array or struct parameter names the caller's storage: writing values[0] or x.a in the function writes the caller's array or struct.

An unsized parameter has rank 1: sp d[][] declares the same parameter as sp d[], so d[0][1] is rejected. An unsized parameter accepts an array of any extent, so one function can serve callers with different sizes. The function has no way to ask how long the array is, so pass the length alongside it when the body needs to know:

function fill_pairs(xp len, struct Pair pairs[]) {
i in [0, len) pairs[i].a = i;
}

Calling Functions​

result = add(x, y);
process(my_array);
reset();

Inline Functions​

inline asks for the body to be expanded at the call site rather than called:

inline function fast_add(xp a, xp b) {
return a + b;
}

By default Carbide keeps a call to an unmarked function as a real call, which jumps to one shared copy of the body. It then copies the body into the caller when the function is called from only one place, or when the body is small: about 45 operations or fewer, after the call's constant arguments fold.

Some calls are always copied:

  • calls to an inline function;
  • the compiler's own routines behind operators such as /;
  • calls to a function that cannot run as a call: one that sends, receives or waits, holds a parallel or dispatch block, or whose arguments need more than the four 16-bit argument registers R8 to R11 (an SP value takes two, an array its address in one).

A function imported from a module follows the same rules as your own. Each copy adds the body to the PE's memory.

Recursion is the exception. A function that calls itself, directly or through other functions, always compiles to real calls. Marking it inline, or compiling with no_ir_functions, which copies every call, is rejected with recursive function call chain [fact -> fact] is not supported.

Functions as RPC targets​

Functions can be called remotely via sockets within dispatch blocks:

socket sock;

// Sender side
i in [0, 4) sock[] <- f(a[i]);

// Receiver side
forever dispatch(sock) {
f(xp a) {
total[0] <- add(total[0], a);
}
}

The destination declaration determines the meaning of the unqualified form stream[] <- f(...): a socket sends an RPC, while a FIFO, ring, or other local stream evaluates f locally and writes its result. Parentheses around the call do not change this distinction. A socket destination sends an RPC even when f is a local function: sock[] <- g(x); fails with RPC function declaration for [g] not found in scope when the receiver has no g handler. To send the value a local function returns, store it first: xp t = g(x); sock[] <- t;. The qualified form names the socket: sock[] <- sock::f(x); is always an RPC on sock.