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:
| Rule | Example | Result |
|---|---|---|
Signed division and % of run-time values round toward minus infinity, as in Python | p / 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 shift | p >> 1, p x>> 1 with p = -8 | 32764, -4 |
| A 16-bit shift uses only the low 4 bits of its count | v << 16 with v = 1 | 1 |
| A comparison read as a value is -1 when true | a < b, (a < b) * 3 | -1, -3 |
As an operand of + or -, a true comparison reads as 1 | (a < b) + 10 | 11 |
| So a comparison compared with 1 is never true | (a < b) == 1 | 0 |
&&, || and ?: evaluate both operands; && and || give 0 or 1 | (a < b) && 1 | 1 |
** binds as tightly as *, and every level groups left to right | 2 * 3 ** 2, 2 ** 3 ** 2 | 36, 64 |
&, | and ^ share one level | 1 | 2 & 0 | 0 |
++ and -- are statements, not expressions | a = c++; | syntax error |
| An array or struct argument is the caller's storage, not a copy | x.a = 99; in the function | changes 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
| Operator | Description | Example |
|---|---|---|
+ | Addition | a + b |
- | Subtraction | a - b |
* | Multiplication | a * b |
/ | Division | a / b |
% | Modulo | a % b |
** | Power | a ** 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
| Operator | Description | Example |
|---|---|---|
& | Bitwise AND | a & b |
| | Bitwise OR | a | b |
^ | Bitwise XOR | a ^ b |
~ | Bitwise NOT | ~a |
<< | Left shift | a << b |
>>/0>> | Logical right shift | a >> b |
x>>/~>> | Arithmetic right shift | a 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
| Operator | Description | Example |
|---|---|---|
== | Equal | a == b |
!= | Not equal | a != b |
< | Less than | a < b |
> | Greater than | a > b |
<= | Less or equal | a <= b |
>= | Greater or equal | a >= 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
| Operator | Description | Example |
|---|---|---|
|| | Logical OR | a || b |
&& | Logical AND | a && 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)
- Unary operators (
-,!,~, casts, function calls) **(power),*,/,%+,-<<,>>/0>>,x>>/~>>==,!=,<,>,<=,>=&,|,^&&||?:(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
inlinefunction; - 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
parallelordispatchblock, 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.