Cella Documentation 0.1.0-dev Home Source

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")}
Output
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 name

The other two cases report:

All files must begin with a module nameModule name must precede all other declarations or imports

A 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 source

Comments

// 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 */)}
Output
4

A block comment that is never closed is an error, reported at its /*:

Unterminated block comment

Names

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)}
Output
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     while
main: 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    use
Range: rec{	get: i32	set: i32} main: fun{	val copy = Range(1, 2)	printf("%d %d\n", copy.get, copy.set)}
Output
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)}
Output
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))}
Output
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)}
Output
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)}
Output
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)}
Output
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:

LiteralExamplesChapter
Integers42, 1_000_000, 0xFF, 0b1010, 0o17, 7u8Numbers and arithmetic
Floats3.14, 1.5e3, 2.0f32Numbers and arithmetic
Booleanstrue, falseBool, 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 pointersnullPointers and raw memory
Uninitialized storageundefVariables and constants

A character that can't start any token is an error:

main: fun{	val price = 5 $Unexpected character '$'}