This document describes the basic structures of the CSL language.Documentation Index
Fetch the complete documentation index at: https://sdk.cerebras.ai/llms.txt
Use this file to discover all available pages before exploring further.
Type system overview
The basic types of CSL are:- void type (
void) - signed integers (
i8,i16,i32,i64) - unsigned integers (
u8,u16,u32,u64) - floating point numbers (
f16,f32,bf16,cb16)
[num_elements] base_type, for example: [3] i16. Array literals are specified by an array type followed by a list of
values, for example: [3]i16 {1, 2, 3}
For a detailed introduction to the type system of CSL,
see Type System in CSL.
Variables
Variable declarations are composed of a mutability specifier, a name, a type and an initializer:const or param variable cannot have its value changed after it has
been initialized, whereas a var variable has no such restriction.
The initializer expression is:
- Mandatory for
constvariables. - Optional for
varvariables. - Optional for
paramvariables. If one is not provided, theparammust be initialized through the module import system. See Modules.
.mySection, the global variable gets
placed into a separate object file section named .mySection, instead of
being placed into the object file section with the rest of the global variables.
The linksection attribute can be used together with the compiler flag
--link-section-start-address-bytes to place global variables at particular
memory addresses:
section1 is placed at the memory address
40960 (bytes), and section2 is placed at 40980.
Global variable declarations may also optionally specify the name of the ELF
symbol corresponding to the variable:
global_var within CSL gets
assigned the name different_name in the compiled object file. This can be
useful to control the name of symbols that are intended to be referenced by
other object files as external data. Any comptime expression evaluating to
a value of type comptime_string may be used for linkname.
Global variable declarations may optionally specify a storage class (either
export or extern). If a variable is declared export, it is made
accessible to other separately-compiled objects, and is guaranteed not to be
eliminated from the compiled object. If a variable is declared extern,
it is assumed that its definition will be supplied by another object that
will later be linked with the object we are compiling. An extern
declaration must not initialize the variable.
Variables with the export or extern storage classes must have
an export-compatible type. See Storage Classes for
details.
Pointers
To obtain a pointer to a variable, the address-of operator& is used:
.* is used:
Functions
Function definitions require afn or task keyword, a name, an optional
sequence of parameters, a return type and a function body:
task keyword are called tasks.
All function parameters are implicitly const variables.
It is unspecified whether function parameters are passed by value or by
reference. If it is necessary to modify a function argument, the function
parameter must be declared with a pointer type:
anytype.
In this case, the compiler will create a specialized copy of the function based
on the type of the corresponding argument used at the call site. This is
similar to typename templates in C++.
comptime keyword (see
Comptime). In this case, the compiler will create a
specialized copy of the function based on the value of the corresponding
argument at the call site. The argument must be comptime-known. This is similar
to non-type template parameters in C++.
.mySection, the function gets placed
into a separate object file section named .mySection, instead of being
placed into the object file section with the rest of the functions.
Tasks may not specify a link section name.
Function definitions may also optionally specify the name of the ELF symbol
corresponding to the function:
foo within CSL gets assigned the
name bar in the compiled object file. This can be useful to control the
name of functions that are intended to be called by other object files as
extern functions. Any comptime expression evaluating to a value of type
comptime_string may be used for linkname.
Function declarations may optionally specify a storage class (either
export or extern). If a function is declared export, it is made
accessible to other separately-compiled objects, and its definition is
guaranteed not to be eliminated from the compiled object. If a function is
declared extern, it is assumed that its definition will be supplied by
another object that will later be linked with the object we are compiling.
An extern function declaration must not contain a function body.
Functions with the export or extern storage classes must have
an export-compatible type. See Storage Classes for
details.
inline fn
Adding theinline keyword to a function definition makes that function
become semantically inlined at the callsite. This is not a hint to be possibly
observed by optimizations; rather, the body of the inline function is
expanded at callsites during semantic analysis. This means that unlike normal
function calls, comptime-known arguments of an inline function call become
comptime-known inside the expanded body. This comptime-ness can potentially
propagate all the way to the return value:
foo(1200, 34) evaluates to 1234 at comptime, so the
if condition evaluates to false and the @comptime_assert is ignored.
If inline is removed, foo(1200, 34) is no longer comptime-known, so the
@comptime_assert would fail.
Since inline functions are expanded at callsites, they only exist in
non-inlined form at comptime. As such, inline functions may not be used in
ways that require functions to be valid at runtime; for example, inline
functions cannot have a linkname and it is not allowed to take the address
of an inline function.
inline functions cannot have a storage class.
It is generally better to let the compiler decide when to inline a function,
except for these scenarios:
- To cause comptime-ness of the arguments to propagate to the return value of the function, as in the above example
- Real world performance measurements demand it
inline actually restricts how the compiler is allowed to compile
a function. This can harm binary size, compilation speed, and even runtime
performance.
noinline
Adding thenoinline keyword to a function definition prohibits that function
from being inlined at callsites. It cannot be combined with the inline
keyword or storage classes.
Direct and Indirect Function Calls
Functions can be called directly by name or indirectly through function pointers. For example:foo in the example above is implicitly coerced to the
requested function pointer type. Note however that function values can only be
coerced to const function pointers as shown in the example above.
It is also possible to take the address of a function symbol using the
address-of operator & as shown in the example below:
& operator is semantically
equivalent to the implicit coercion of a function value to a const
function pointer type. This means that the resulting address will always
be a const pointer as well.
Tasks cannot be called directly like regular functions, for example:
Statements
If-expression
If-expressions have the following syntax:if as a conditional statement, as opposed to an expression
evaluated for its value:
condition is known at compile-time, the branch not-taken is not
semantically checked by the compiler, but it must still be syntactically valid.
Otherwise, expr_then and expr_else must have compatible types.
The else clause is optional. If the else clause is omitted, the
if-expression evaluates to a value of type void when its condition is false.
This implies expr_then must have type void in if-expressions without an
else clause.
It is possible to combine an else clause with another if-expression:
For-statement
A for-statement iterates over the elements of an array or range:element acts as a const declaration
whose value is the element that is currently being iterated on.
For-statements may specify a const declaration for the index of the element
being iterated on:
break statement may be used to end the loop:
continue statement may be used to end the current iteration of the loop:
for loop is labeled, it can be referenced from a break nested
within its body. This makes it possible to break a loop from inner loops
nested within its body:
: occurs after the name, while
: occurs before the name when referring to a label in break.
Like identifiers, redefinition of a label is not allowed. However, labels
belong to a separate namespace from identifiers. In other words, it is legal for
a label to have the same name as a variable, function, or task that is in scope.
Also, since labels are only visible within their corresponding loop, it is
possible to reuse labels for loops that are not nested within each other.
While-statement
While-statements have the following syntax:continue or break statements may be used inside the body of a
while-statement.
When a while loop is labeled, it can be referenced from a break nested
within its body, including from inner loops nested within, in the same manner
as a for loop:
continue statement.
Blocks
Blocks are used to limit the scope of variable declarations:break
nested inside, which exits the block:
break to exit a block. In other words,
break without a label always acts on the closest loop and a block without a
label cannot be exited with break:
breaks that refer to blocks can be
used to return a value from the block:
breaks refer to a block, all of their values must have
compatible type:
break without a value is equivalent to a break whose value is
void. If control flow may reach the end of a block without breaking a value,
the block’s type is void and any values broken from the block must be
void. By this reasoning, unlabeled blocks always have type void since it
is not possible to break them:
void or if all of
the following hold:
- The block is referred to by exactly 1
breakwith a value - The block is guaranteed to terminate by executing this
break - The
break’s value is comptime-known
Switch-statement
Switch-statements have the following syntax:input can be an expression of a fixed-width integer type (i.e.,
comptime_int is not allowed) or of any enum type.
The body of the switch statement consists of 1 or more comma-separated branches
where each branch consists of 2 parts: the case_values and the corresponding
branch_expr. A branch may combine multiple case_value expressions via a
comma:
input with one of the provided
case_value expressions. If a match is found the corresponding branch will be
selected and the respective branch_expr will be executed. If no match is
possible, the else branch will be selected as the default and the
corresponding else_expr will be executed.
case_value expressions must be comptime-known and coercible to the type
of the input expression. They must also be unique.
All branch_expr expressions (including the else_expr expression, if
present) must have compatible types.
If input is known at compile-time, the branch_exprs corresponding to the
branches not-taken are not semantically checked by the compiler, but they must
still be syntactically valid.
A switch can also be used as an expression. In this scenario all branch_expr
expressions (including the else_expr expression, if present) must be able to
be coerced to the common requested type:
case_value expressions can be combined and if-statements can be used as
follows:
input
expression type either explicitly by specifying a case_value for each
possibility or implicitly through the else branch:
Inline assembly expressions
Inline assembly expressions have the following syntax:assembly_instructions is an expression of type comptime_string which is
a templated string of the instructions to be assembled. It supports the
following substitutions:
%[name]: Substitute the input or output operand whose constraint is namedname.%[name:modifier]: Substitute the input or output operand whose constraint is namedname, with the argument modifiermodifier.%%: Substitute a literal%character.%=: Substitute a decimal integer unique to this asm expression at assembly time. The compiler may duplicate asm expressions, but%=will be unique to this asm expression even in the presence of such duplication.
a, which indicates that the argument should be
formatted as an address, such as the address of a global variable.
The item output_constraints is an optional list of comma-separated items
of the form:
identifier1is a name used to refer to this output within the assembly instructions,constraint_stringis a specifier for the form of operand that should be used for this output within the assembly instructions, andidentifier2is the name of a CSL variable to which this output will be written.
input_constraints is an optional comma-separated list of the form:
identifieris a name used to refer to this input within the assembly instructions,constraint_stringis a specifier for the form of operand that should be used for this input within the assembly instructions, andexpris an expression that will supply the initial value for this register when the inline assembly is executed.
constraint_string for an output constraint consists of =,
followed optionally by &, and then a constraint code. The presence of &
denotes an early-clobber output.
The constraint_string for an input constraint may consist of a single
constraint code.
Alternatively, input constraints may be a tied constraint. Tied constraints
have the form ==[NAME] (square brackets included). A tied constraint
requires that an input be given the same register as the output named NAME
within the assembly string. It is an error to tie multiple inputs to the same
output. An input with a tied constraint must have the same type as the output it
is tied to.
A constraint code may be:
* of the form {R} (curly braces included), where R names a specific
register that is supported on the target. The corresponding argument’s bit width
must match that of the named register.
* r, specifying that the compiler should automatically select a
general-purpose register operand. The corresponding argument’s bit width must
match that of some general-purpose register.
* m, specifying that the compiler should select a memory address operand.
The corresponding argument may not have a comptime-only type (see
Comptime).
* i, allowing comptime-known integer or comptime-known pointer values. Not
valid for outputs. Integer values must be coercible to i64.
* n, allowing comptime-known integer values that are coercible to i64.
Not valid for outputs.
* X, allowing the compiler to select any kind of operand, no constraint
whatsoever. Not valid for outputs. The corresponding argument may not have a
comptime-only type (see Comptime).
A constraint code may be prefixed with *, which goes after the = in an
output constraint string, to denote an indirect constraint. This indicates that
the assembly writes to or reads from memory at a provided address. This can be
used for memory constraints, such as =*m or *m, to pass the address of a
variable into the assembly. It is also possible to use indirect register
constraints for outputs, such as =*r, but not inputs. Other constraint codes
may not be indirect. Arguments corresponding to indirect constraints must have
pointer type.
Supported registers are the
general-purpose registers and the carry/condition flag cflag. On all current
Cerebras architectures, the
general-purpose registers are the 16-bit registers r0 through r15, as
well as the 32-bit double-registers d0, d2, d4, …, d14. Note
that each dN register is essentially aliased with rN and rN+1.
The item clobbers is an optional comma-separated list of clobbers, where
each clobber is the name of a general-purpose register or the string memory.
Specifying that
a register is clobbered indicates to the compiler that it may be modified by
the inline assembly code as a side effect, so the compiler will need to save
it before entering the inline assembly block and restore it after exit.
Specifying a clobber of memory indicates that the inline assembly may write
to arbitrary memory locations.
Writing to a register without specifying it as an output or clobber register
is undefined behavior. Reading from a register that is not specified in an
input constraint is undefined behavior. Writing to an output register before
all reads of inputs have occurred is undefined behavior unless that output
register is specified as early-clobber.
The volatile keyword indicates that the assembly code has side effects not
expressed in the constraints or clobbers. This means the compiler may not
reorder volatile assembly expressions nor may it alter the amount of times a
volatile assembly expression is executed. For example, assembly expressions
marked volatile will not be eliminated even if nothing uses their output
values or if there are no outputs.
Example
The following function uses inline assembly to return twice the value ofx+y. (The code here is slightly inefficient, for the purposes of
demonstrating inputs, outputs, and clobbers all in one go.)
Global Assembly
Assembly expressions that occur in top-level comptime blocks are considered global assembly. These differ from other assembly expressions in a few ways. First, thevolatile keyword is not valid because all global assembly
expressions are unconditionally included into the program. Second, there are no
constraints or clobbers. Third, there are no template substitution rules. All
global assembly strings are concatenated verbatim into one long string and
assembled together.
The following code uses global assembly to define a function similar to the
above example of inline assembly:
Operators
| Name | Syntax | Types | Remarks | Example |
|---|---|---|---|---|
| Addition | a + ba += b | Integers, floats | 2 + 5 == 7 | |
| Subtraction | a - ba -= b | Integers, floats | 2 - 5 == -3 | |
| Negation | -a | Integers, floats | -1 == 0 - 1 | |
| Multiplication | a * ba *= b | Integers, floats | 2 * 5 == 10 | |
| Division | a / ba /= b | Integers, floats | 10 / 5 == 2 | |
| Remainder of division | a % ba %= b | Integers | 10 % 3 == 1 | |
| Bit shift left | a << ba <<= b | Integers | b must be unsigned. | 0b1 << 8 == 0b100000000 |
| Bit shift right | a >> ba >>= b | Integers | Arithmetic shift right if a is signed, otherwise logical shift right. b must be unsigned. | 0b1010 >> 1 == 0b101 |
| Bitwise AND | a & ba &= b | Integers | 0b011 & 0b101 == 0b001 | |
| Bitwise OR | a | ba |= b | Integers | 0b010 | 0b100 == 0b110 | |
| Bitwise XOR | a ^ ba ^= b | Integers | 0b011 ^ 0b101 == 0b110 | |
| Bitwise NOT | ~a | Fixed-width integers | ~@as(u8, 0b10101111) == 0b01010000 | |
| Logical AND | a and b | bool | If a is false, returns false without evaluating b. Otherwise, returns b. | (false and true) == false |
| Logical OR | a or b | bool | If a is true, returns true without evaluating b. Otherwise, returns b. | (false or true) == true |
| Logical NOT | !a | bool | !false == true | |
| Equality | a == b | Integers, floats, bool, enum, direction, comptime_string, color, control_task_id, data_task_id, input_queue, local_task_id, output_queue, ut_id, type | (1 == 1) == true | |
| Inequality | a != b | Integers, floats, bool, enum, direction, comptime_string, color, control_task_id, data_task_id, input_queue, local_task_id, output_queue, ut_id, type | (1 != 1) == false | |
| Greater than | a > b | Integers, floats | (2 > 1) == true | |
| Greater than or equal | a >= b | Integers, floats | (2 >= 1) == true | |
| Less than | a < b | Integers, floats | (1 < 2) == true | |
| Less than or equal | a <= b | Integers, floats | (1 <= 2) == true |
comptime_int (see Comptime).
Comments
// begins a single-line comment. Comments beginning with /// or //!
are doc comments, which are only allowed in certain positions. There are no
multi-line comments in CSL.
Doc comments
Doc comments come in two forms. A regular doc comment begins with exactly three/ characters, so it must begin with /// and cannot begin with ////.
A top-level doc comment begins with //!.
Regular doc comments may occur immediately before top-level functions and
variable declarations, and before members of a struct, enum, and union
type definition. Top-level doc comments may occur at the very top of a source
file, and at the very top of the body of a struct, enum, or union
declaration, just after the opening curly brace.
Doc comments are currently unused, but support for documentation generation is
planned.

