Lexical structure
This chapter covers how Cella source text is put together: source files, comments, names, keywords, where lines may break, and how lists of members are separated.
Source files
Cella source files have the .ce extension and hold UTF-8 text. Lines may end in \n, \r\n or \r. Spaces and tabs separate tokens and mean nothing else: indentation is never significant.
Every source file starts with a mod line naming the module the file belongs to:
mod game ext{ puts: fun(text: cstr) -> i32} main: fun{ puts("ready")}ready
Only comments and blank lines may come before it. A file without a mod line, a file with two of them, and a file with anything else before it are errors:
main: fun{} mod secondCannot declare more than one module nameThe other two cases report:
All files must begin with a module nameModule name must precede all other declarations or importsA module name can have several parts separated by dots, such as mod game.physics. Modules and visibility covers modules, and how files share them.
Characters that change how text is displayed without being visible themselves, such as the Unicode controls that reverse the direction of text, are errors anywhere in a file, even in comments and strings:
Cannot use U+202E in sourceComments
// starts a comment that runs to the end of the line. /* starts a block comment that runs to the next */, across lines if needed. Block comments nest, so a block comment can comment out code that has block comments of its own:
// The program starts here.main: fun{ val width = 4 // in tiles /* val height = 2 /* the old size */ */ printf("%d\n", width /* not height */)}4
A block comment that is never closed is an error, reported at its /*:
Unterminated block commentNames
A name, also called an identifier, starts with a letter or _ and continues with letters, digits and _. Letters include those of every script, not only ASCII, and digits are only allowed after the first character. Names are case-sensitive: count and Count are different names.
Count: rec{ value: i32} main: fun{ val count = Count(1) val player2 = 2 printf("%d %d\n", count.value, player2)}1 2
The compiler accepts any of these names, but by default it gives a hint for names that don't follow the usual style, such as a variable named Count or _offset. Project settings covers the rules and how to change them.
Names are compared after Unicode normalization (NFC), so a letter written as one character and the same letter written as a base letter and a combining mark are the same name.
_ on its own isn't a name. It marks a value or parameter that is never used, such as val _ = make() or a parameter written _: i32, and it can't be read:
main: fun{ val _ = 3 val copy = _Cannot use '_' as a value}Names are usually written in camelCase for values and functions and PascalCase for types. The compiler doesn't enforce either.
Keywords
Keywords are words with a meaning of their own. Reserved keywords can never be names:
alignOf atomic break const cont default do dropdyn else false for fun if imm inis loop match mut nameOf new null ownpanic ref ret self sizeOf true undef valvar when whilemain: fun{ val loop = 1'loop' is a keyword val default = 2'default' is a keyword}Contextual keywords mean something only in certain places, such as the start of a declaration or after a declaration's colon. Everywhere else they are ordinary names, such as the fields of this record (Records covers records):
as copy enum ext get impl mod norefop prop pub pvt rec req set traitunion useRange: rec{ get: i32 set: i32} main: fun{ val copy = Range(1, 2) printf("%d %d\n", copy.get, copy.set)}1 2
Keywords lists every keyword with what it does.
Lines and statements
A new line ends a statement. There are no semicolons. Most statements fit on one line:
main: fun{ val width = 3 val height = 4 printf("%d\n", width * height)}12
Wrapping long lines
A statement continues on the next line when its line ends in a way that needs more: after a binary operator, an assignment operator, is or =>. Wrap a long expression after an operator. volume below has an expression body, => followed by its result, which Functions covers:
volume: fun(width: i64, height: i64, depth: i64) -> i64 => width * height * depth main: fun{ val total = 1 + 2 * 3 val inRange = total > 2 && total < 10 printf("%d %d %lld\n", total, inRange, volume(2, 3, 4))}7 1 24
A line can't start with a binary operator, an assignment operator, is or =>:
main: fun{ val total = 3 val inRange = total > 2 && total < 10Cannot start a line with '&&' val small = total is 3Cannot start a line with 'is'}+, - and * are different, because they can also start an expression: + 2, - 2 and * p are complete expressions on their own. A line that starts with one of them starts a new statement, and a value on its own isn't a statement:
main: fun{ val total = 1 + 2Cannot use values as statements}The same goes for declarations. The = before a value and the => before an expression body must stay on the line before:
limit: const i32 = 5Cannot start a line with '=' twice: fun(x: i32) -> i32 => x * 2Cannot start a line with '=>' main: fun{}Inside parentheses and brackets
Inside ( ) and [ ], new lines don't end anything, so arguments, array elements and parenthesized expressions can be split anywhere:
add: fun(a: i32, b: i32) -> i32 => a + b main: fun{ val sum = add( 10, 20 ) val values = [ 1, 2, 3 ] val scaled = (sum * 2) printf("%d %d %d\n", sum, values[2], scaled)}30 3 60
The ( of a call and the [ of an index must still be on the same line as what they apply to. On the next line, they start a new statement:
twice: fun(x: i32) -> i32 => x * 2 main: fun{ val result = twice (4)Cannot use values as statements}Member access
. may start a line, so a chain of member accesses can be written one per line:
Size: rec{ width: i32 height: i32} Window: rec{ size: Size} main: fun{ val window = Window(Size(640, 480)) val width = window .size .width printf("%d\n", width)}640
Prefix operators
A prefix operator, such as -, !, ~, @ or *, must have its operand on the same line:
main: fun{ val negative = -Expected an expression 5}Separating members
Lists of members in braces, such as the fields of a record, the cases of an enum and the arms of a match, follow one rule: neighbouring items are separated either by a , on the same line or by a new line. One item per line needs no commas, and several items on one line need them. Records, Enums and Pattern matching cover the forms in this example:
Color: enum { red, green, blue } Size: rec{ width: i32 height: i32} main: fun{ val size = Size(3, 4) val code = match Color.green { red => 1, green => 2, blue => 3 } printf("%d %d\n", size.width * size.height, code)}12 2
A , at the end of a line, a , after the last item, and two items on one line without a , are errors:
Size: rec{ width: i32,Cannot separate members on separate lines with ',' height: i32} Pair: rec { first: i32, second: i32, }Cannot end lists with ',' Point: rec { x: i32 y: i32 }Expected ',' or a new line before the next member main: fun{}Lists in parentheses and brackets, such as arguments, parameters and array elements, always use commas, and they can't end with one either.
Top-level declarations are separated by new lines only:
one: fun -> i32 => 1 two: fun -> i32 => 2Expected a new line before the next declaration main: fun{}Literals
Literals write values directly in the source. Each kind has its own chapter:
| Literal | Examples | Chapter |
|---|---|---|
| Integers | 42, 1_000_000, 0xFF, 0b1010, 0o17, 7u8 | Numbers and arithmetic |
| Floats | 3.14, 1.5e3, 2.0f32 | Numbers and arithmetic |
| Booleans | true, false | Bool, char and strings |
| Characters | 'a', '\n' | Bool, char and strings |
| Strings | "hello", "tab\there" | Bool, char and strings |
| Interpolated strings | "{count} items" | Bool, char and strings, String templates |
| Arrays | [1, 2, 3] | Arrays |
| Null pointers | null | Pointers and raw memory |
| Uninitialized storage | undef | Variables and constants |
A character that can't start any token is an error:
main: fun{ val price = 5 $Unexpected character '$'}
Cella Documentation