Built-ins, Unicode and compiler usage
29. Built-in Functions and Intrinsics
Unary Functions
| Function | Description |
|---|---|
abs(x) | Absolute value |
neg(x) / -x | Negate |
not(x) | Logical NOT (like !x) |
sqr(x) | Square |
pop(x) | Population count (count set bits) |
clz(x) | Count leading zeros of a 16-bit integer |
ctz(x) | Count trailing zeros of a 16-bit integer |
log2(x) | Logarithm of a compile-time operand |
isnan(x) | -1 if x is a NaN, 0 otherwise |
__builtin_norm(x) | x with its exponent set to 0: the significand, in [1, 2), with the sign of x (__builtin_norm(3.0) is 1.5) |
rand() | Pseudo-random bits from the hardware generator, as wide as the destination |
control(x) | Create control wavelet |
transform(x) | Transform wavelet |
To get 1 for true from isnan or isunordered, store the result in a u1,
or write isnan(x) ? 1 : 0.
rand() gives 16 bits into an xp and 32 bits into a u32; with no
destination it is xp. Assigned to an sp, the bits are read as a float, so
the value can have any exponent and can be a NaN. Draw an integer and convert
it to build a numeric distribution.
pop, clz and ctz count the bits of a 16-bit integer (xp, s16 or
u16); clz(0) and ctz(0) are 16. A run-time u32 or s32 operand fails
to compile with a code-generator message such as
Couldn't match the instruction!, not a type error.
log2(x) folds at compile time: an integer operand gives the floor of its
base-2 logarithm (log2(9) is 3), a float operand its logarithm
(log2(0.5) is -1.0). An operand that is not above zero is rejected, and so is
a run-time operand (log2 takes a compile-time operand).
NEG, ABS, SQR and NORM accept only the operand types in the table below, on
a scalar or on one element per lane. There is no implicit cast, widening, or
narrowing. The one mixed form is sqr((ep)s) of an sp value s, stored to
an sp: the square is meant to execute in ep (FSQRE), but Carbide
currently emits the sp square. norm(x) is not an alias for
__builtin_norm(x). The types accepted on SDR are:
| Operation | Types |
|---|---|
-x, neg(x) | s16/xp, u16, s32, u32, hp, sp |
abs(x) | s16/xp, u16, hp, sp |
sqr(x) floating | hp, ep, sp |
__builtin_norm(x) | hp, sp |
Every other type or shape is rejected.
Carbide does not support integer sqr(x). Integer NEG
is modular in the destination width. Floating NEG flips the sign bit,
ABS clears it, and floating SQR performs one native square at the declared
precision. NORM uses the target instruction's specified special-value and
subnormal behavior. math.fast never changes this matrix.
abs reads an integer as signed: abs of the u16 value 40000 is 25536.
Binary Functions
| Function | Description |
|---|---|
max(a, b) | Maximum |
min(a, b) | Minimum |
round(x, m) | x converted to an xp with rounding mode m |
builtin_dot(a[i], b[i]) | Inside i in [0, N), the dot product of a and b: the same as ∑ i in [0, N) a[i] * b[i] |
isunordered(a, b) | -1 if a or b is a NaN, 0 otherwise |
round(x, m) takes a float x and a constant mode m: 0 rounds to nearest
even, 1 toward zero, 2 toward −∞ and 3 toward +∞. round(2.5, 0) is 2 and
round(-2.5, 2) is -3. A value out of range saturates to 32767 or -32768. The
result is an xp; storing it into a float is rejected. A mode read at run time
is accepted but ignored: the conversion rounds to nearest even.
min and max first apply Tungsten's
Implicit Type Coercion, not C's promotion: mixing
xp and u16 compares unsigned, so max(x, u) with xp x = -8 and
u16 u = 3 is -8.
Floating-point forms use the target's native operation. Integer forms are
lowered to a compare-and-select library implementation when that width is
available: xp, s16, u16, s32 and u32 on SDR.
The __builtin_-prefixed forms are listed under
Hardware Intrinsics below. The prefix is part of the
name: copysign and norm on their own are not accepted.
Other Built-ins
| Expression | Description |
|---|---|
break(0) | Raise a hardware breakpoint |
__builtin_nop() | No operation |
2 ** x | Power of two |
a.base | Base address of array |
Hardware Intrinsics
The __builtin_ family exposes hardware operations that have no operator
spelling. They are expressions, except __builtin_nop(), which is a statement. The
prefix is part of the name.
| Intrinsic | Args | Description |
|---|---|---|
__builtin_copysign(a, b) | 2 | a with the sign of b |
__builtin_norm(x) | 1 | Normalize x |
__builtin_merge(hi, lo) | 2 | Put hi in bits 31:16 and lo in bits 15:0 of a 32-bit word — used to build a wavelet before a socket send; for an indexed wavelet, hi is the index |
__builtin_divstarter(src0, src1, dst) | 3 | Divide starter — see below |
__builtin_fcpxstarter(src0, table, dst) | 3 | Complex-function starter — see below |
__builtin_nop() | 0 | Emit a no-op |
__builtin_merge accepts exact width contracts. SDR supports an ordinary
16+16-to-32 concatenation.
SDR's 64-bit form is MERGES32: a 16-bit
sparse index plus 32-bit datum written to a non-GPR 64-bit DSR destination,
with layout {index,data[31:16],index,data[15:0]}. SDR rejects ordinary 32+32-to-64 merge.
All other width combinations are unsupported.
Starter Intrinsics
__builtin_divstarter and __builtin_fcpxstarter correspond to the hardware's
divide and complex-function starter instructions. Carbide supports only their
sp forms (FDIVSS and FCPXSS). They exist to get the corner cases out of the
way before the iterative body runs, and they are unusual in producing two
results:
- the return value is a
u1predicate. For__builtin_divstarterit is 1 only on a special case (NaN, infinity, divide-by-zero, a zero operand) and 0 on the normal path; for__builtin_fcpxstarterit is set as its table of actions directs for each class of operand; - the third argument is a 32-bit integer destination that receives the
payload — on the normal path a seed for the iterative body, and on the
special path the finished qNaN / infinity / zero result. For
__builtin_fcpxstarterit must be ans32.
For __builtin_fcpxstarter, src0 is the function operand and the second
argument is a 32-bit integer table of actions selecting what to do for each
class of operand. The table holds eight 4-bit actions, one per class of
src0, from bits 3:0 to bits 31:28: signalling NaN, quiet NaN, +∞, −∞, +0,
−0, a positive number and a negative number. Action 3 selects the normal path.
Actions 8, 0xA and 0xE return a NaN, +∞ and −∞, and 9 and 0xD return +0 and
−0; each sets the predicate. With u32 ACTION_TABLE = 0x83EE8A88;, the table
of a logarithm's starter, 4.0 takes the normal path with payload 2, 0.0 gives
−∞ and -1.0 gives a NaN.
When the predicate marks the special cases, the normal body is guarded by its negation:
[[vectorize.pref_len(N), vectorize.iter(j)]]
∀i in [0, N) {
if (!__builtin_fcpxstarter(src[i], ACTION_TABLE, payload[i])) {
dst[i] = src[i] * SCALE; // normal path only
}
}
Inside a vectorized loop the predicate is per-lane, so it cannot be held in a
scalar — several lanes would write the same variable. Carbide does not reject
a scalar u1 here, but the lanes then do not see their own predicates, and
some take the wrong branch without a warning. Save the predicate to a u1 array indexed by
the auxiliary iterator when it is needed after the call:
u1 flag[n=N];
[[vectorize.pref_len(N), vectorize.iter(j)]]
∀i in [0, N) {
flag[j] = __builtin_divstarter(src0[i], src1[i], out[i]);
if (flag[j]) { special[i] = 1; }
if (!flag[j]) { normal[i] = 1; }
}
30. Unicode Support
Tungsten uses Unicode for mathematical readability. Most forms have an ASCII
spelling; a descending iterator, for example, is i↓ in [0, N) or
i in (N, 0].
Mathematical Symbols
| Unicode | ASCII Equivalent | Meaning |
|---|---|---|
← | <- | Assignment |
←̟/⬲/⥆ | <+- / += | Accumulate assignment |
∀ | (none) | For-all (loop prefix) |
∈/∊ | in | Set membership (loop) |
∞ | INF | Infinite iterator end |
↓ | (end, start] range | Decreasing iterator |
× | * | Multiplication |
⨁/⊕ | ^ | XOR |
⬶ | << | Shift left |
⤖ | x>> / ~>> | Arithmetic shift right |
⤅ | 0>> / >> | Logical shift right |
∧ | && | Logical AND |
∨ | || | Logical OR |
¬ | ! | Logical NOT |
≠ | != | Not equal |
≤/≦ | <= | Less or equal |
≥/≧ | >= | Greater or equal |
∑ | (prefix) | Dot product sum |
Only the symbols in the table are accepted. A let iterator is applied with ∀, which has no ASCII
spelling; an iterator declared inline, as in i in [0, N) a[i] = 0;, needs no
∀ (see Unicode For-All).
Greek Letters in Identifiers
Tungsten supports Greek letters as identifiers:
hp α; // alpha
sp β = 2.0; // beta
xp Δx = 1; // delta-x
Combining Marks and Superscripts
Identifiers can include diacritical marks and superscript or subscript digits:
xp x²; // x-squared (superscript)
sp v⃗; // v-vector (combining arrow)
hp x′; // x-prime (U+2032 PRIME)
31. Compiler Usage
Basic Compilation
carbide source=mycode.w
This writes the generated Yeti, the debug information and the address map to
mycode_artefacts/, and no image; add image= for one (see
Output Files).
carbide help lists every option, and carbide version prints the
compiler's version. Option Reference describes each
option.
Architecture Target
carbide source=mycode.w arch=sdr # WSE-3/CS-3
Setting Parameters
carbide source=mycode.w set=N=16 set=M=32
set= gives a declared param a number; a name the kernel does not declare
is ignored. A kernel with sockets, compiled on its own, also needs a color and
a bus for each socket (see Sockets):
| Form | Effect |
|---|---|
set=rx=3 | Socket rx gets color 3 on bus 3 |
set=rx={.color=3,.bus=1} | Color 3 on bus 1. The fields may come in either order, and both are needed |
set=s={{.color=2,.bus=1},{.color=4,.bus=3}} | The first pair is the socket's input, the second its output |
set=t=[{.color=1,.bus=2},{.color=3,.bus=4}] | One pair for each element of a socket array |
Quote a form with braces in a shell. Paint passes these forms itself. A kernel
with a dispatch block also needs the message definitions that Paint passes
as rpco=, so compile it through Paint.
Output Files
carbide source=mycode.w image=output.img symbols=output.sym
carbide source=mycode.w listing=output.lst
The generated Yeti goes to mycode_artefacts/mycode0.yeti; wasm=output.yeti
renames it to mycode_artefacts/output0.yeti.
Multiple Source Files
Each invocation compiles exactly one source file; a second source= is
rejected. A bare mycode.w as the first argument is the same as
source=mycode.w, and a .yeti source is assembled rather than compiled.
Code in other files is brought in with import (see Imports).
libpath=<dir> adds a directory to search for imported packages:
carbide source=mycode.w libpath=pkgs # import mylib.util finds pkgs/mylib/util.w
Debugging
carbide source=mycode.w verbosity=stats # the register allocator's summary and each stage's time
carbide source=mycode.w verbose # those, each pass's time, and the stage messages
carbide source=mycode.w debug_passes=all # trace every pass to stdout
carbide source=mycode.w wall # the optional warnings
A compile that succeeds prints nothing unless verbosity=stats, verbose or
a debugging option asks for output.
Optimization Flags
carbide source=mycode.w Os # cap loop_versioning_size_threshold at 0
carbide source=mycode.w Oz # cap it at -5
loop_versioning_size_threshold=<n> is the number of instructions a loop
version may add; its default is 0, so at the default Os changes nothing.
Array Bounds Verification
By default Carbide rejects one kind of out-of-bounds access: a ∑ whose
affine subscripts, over constant iterator ranges, would read outside an operand
array (∑ reads outside 'a'). It neither rejects nor warns about any other
out-of-bounds index, even a constant one and even with wall: on xp a[n=4],
a[7] = 9; compiles and writes whatever follows a. Compile with
fsanitize=bounds while developing.
It inserts a runtime assertion on every array access the
compiler cannot prove in bounds, outside parallel arms; a failing assertion
stops at a breakpoint:
carbide source=mycode.w fsanitize=bounds
no_verify_array_bounds lets an access reach past its array's declared extent
into the rest of its storage; under it, fsanitize=bounds checks nothing.
carbide source=mycode.w no_verify_array_bounds
This option does not make an out-of-bounds access safe or reserve backing memory for that access.
Option Reference
Every option carbide help lists, in its groups. Paint passes arch, set,
source, libpath, rpc, rpco, image, symbols, listing,
wasmlisting, debuginfo and ucid itself; the rest are for compiling or
debugging a kernel by hand.
Compilation modes. Paint compiles a kernel in two steps with these:
| Option | Effect |
|---|---|
rpc=<file> | Write the RPC interface file that Paint reads, then stop |
rpco=<file> | Read Paint's RPC file and rewrite each RPC call from it |
rpc_buffer_threshold=<n> | With rpco: an RPC call with at least n lambda wavelets sends them from a buffer. Default 2 |
Source input:
| Option | Effect |
|---|---|
source=<file> | The input: a .w file, or a .yeti file to assemble. One per compile; a bare <file>.w as the first argument is the same |
libpath=<dir> | A package search root for import. Repeatable; searched in command-line order, after the source's directory and before the installed packages |
Architecture:
| Option | Effect |
|---|---|
arch=sdr | Target Schrödinger (WSE-3/CS-3). The default |
Output files:
| Option | Effect |
|---|---|
image=<file> | The executable image |
symbols=<file> | The symbol table |
listing=<file> | The assembly listing with addresses; with wasmlisting, the source listing instead |
wasmlisting=<file> | With listing: the assembly listing with addresses |
debuginfo=<file> | A copy of the debug information (JSON, see Debug information) that <source>_artefacts always gets |
wasm=<file> | Name the Yeti file in <source>_artefacts after this file's stem instead of the source's |
ast_json=<file> | The final AST as JSON, or the AST that a failed verification rejected |
ucid=<string> | The suffix of each <source>_artefacts file name, also recorded in the debug information. Default 0 |
Register allocation:
| Option | Effect |
|---|---|
alloc_gprs=<n,...> | The general-purpose registers (0-15) the allocator may use. Registers the call convention places are used regardless. Default: all 16 |
alloc_dsra=<n,...> | The DSR slots (0-15) of bank a (T0X) the allocator may use. Default: all 16 |
alloc_dsrb=<n,...> | The same for bank b (T1X) |
alloc_dsrc=<n,...> | The same for bank c (T2X) |
Optimization:
| Option | Effect |
|---|---|
Os | Optimize for size: cap loop_versioning_size_threshold at 0 |
Oz | Minimize size: cap loop_versioning_size_threshold at -5 |
loop_versioning_size_threshold=<n> | The number of instructions a loop version may add. Every version adds some, so at the default, 0, no loop is versioned |
ir_functions | Keep function calls as calls, then copy the small ones and those called from only one place. Calls to inline functions, the compiler's routines behind operators, and calls that a call cannot carry are always copied; see Inline Functions. On by default |
no_ir_functions | Copy every function call; a program with recursion is then rejected. ir_functions wins when both are given |
skip_ir_pass=<pass,...> | Leave the named IR passes and machine optimizations out of the pipeline |
Checking:
| Option | Effect |
|---|---|
fsanitize=bounds | Assert each array access not proven in bounds, outside parallel arms (see Array Bounds Verification) |
no_verify_array_bounds | Let an access run past its array into the rest of its object; turns fsanitize=bounds off |
no_verify_ast | Skip verifying the AST after parsing and after the AST passes |
verify_ast_unchanged | Fail an AST pass that reports no change but changes the AST |
eval_ir | Evaluate the AST, its IR, and the IR after each IR pass, and fail where a global's value changes |
wall | Enable the optional warnings |
Debugging:
| Option | Effect |
|---|---|
debug | Trace the AST and Yeti pass managers to stdout |
debug_passes=<pass,...> | Trace only the named AST and Yeti passes to stdout, or every pass with all |
debug_only=<pass,...> | Like debug_passes; the first name also turns on that pass's own output, for example const-folding |
print_ir=<pass,...> | Dump the IR to stderr after the named IR passes, or after every one with all |
print_mir=<pass,...> | Dump the machine function after the named machine passes, for example phi-elim, or after every one with all |
print_ast=<pass,...> | Dump the AST to stderr after the named AST passes; parse gives the parser's AST and all every pass's |
dump_pass_graph | Print the AST pass pipeline to stdout as a DOT graph, and exit |
debug_context | Start every task with as many NOPs as a task-table entry holds |
verbosity=quiet, =stats, =progress | What a successful compile prints to stderr: nothing (the default); the register allocator's summary and each stage's time; or those plus each pass's time and the stage messages |
verbose | The same as verbosity=progress |
pass_pipeline_verbose | Print the name of each AST and Yeti pass to stdout as it runs |
vlowerprim | Print the AST before and after each lowering to primitives |
version | Print carbide <commit[-dirty]> (<branch>, <timestamp>) and exit; carbide version does the same |
Parameters: the set= forms in Setting Parameters.
Flags that carbide help marks as ignored have no effect.