Skip to content

Lexical structure

xcc uses C-family comment syntax:

// single-line — runs to end of line
/* block — runs until the matching closer */
  • Case-sensitive (Foo and foo are distinct).
  • Start with a letter; subsequent characters may be letters, digits, or underscore.
  • Reserved words may not be used as variable, class, or struct names.

Three radix prefixes are recognised, and _ is silently ignored anywhere inside a numeric literal so you can group digits for readability.

u16 a = 1234; // decimal
u16 b = $1234; // hex
u16 c = 0x1234; // hex, the C spelling — identical to the line above
u8 d = %1010_0101; // binary, with grouping underscore
u32 big = 16_777_216;
u32 mask = 0xFFFF_0000; // underscores work in either hex spelling

$ comes from 6502 assembler tradition and 0x (or 0X) from C; they mean exactly the same thing and either can be used anywhere. Binary keeps %.

Strings are double-quoted, null-terminated, but the trailing \0 is not counted in length. The recognised escape sequences are:

EscapeMeans
\nnewline (CR + LF)
\rcarriage return
\ttab
\0end-of-string marker
\\a literal backslash
\"a literal " inside a string
\'a literal ' inside a character literal
string greeting = "hello\n";

A character literal is a single character (or two characters where the first is \) inside single quotes, evaluated as a u8:

u8 tab = '\t';
u8 a = 'A';

A single string literal never splits across source lines — but adjacent string literals concatenate, as in C, and a newline between them makes no difference:

Stdio.print("one" "two\n"); // onetwo
Stdio.print("a long message that would "
"otherwise be one unbreakable "
"source line as wide as itself\n");

The join happens in the parser, so the pieces are one literal by the time anything else sees them — there is no run-time concatenation and no cost.

These are the words the lexer turns into keyword tokens. They are never identifiers, anywhere.

asm, auto, bool, break, case, catch, class, continue, default, defer, delete, double, else, enum, extern, false, final, float, for, global, i8, i16, i32, if, in, inline, new, optional, pointer, protocol, register, release, retain, return, sizeof, static, string, struct, switch, throw, throws, true, try, typedef, u8, u16, u32, use, void, volatile, while.

Separately, the parser rejects the C reserved words as variable names even where xcc gives them no meaning of its own, so that C-shaped source doesn’t quietly acquire a different meaning:

char, const, do, goto, int, long, restrict, short, signed, union, unsigned — plus those above that C also reserves.

A third group is meaningful only in a particular position, and is an ordinary identifier everywhere else: self and super inside a method body; init and dealloc as method names; weak:, banked:, main: and shadow: as declaration qualifiers; va_start / va_arg / va_end inside a variadic; clobbers after an asm block; and the function annotations (:naked, :hwStack, :irq, :vbi, …) documented on the Functions page. Using one of these as a variable name is legal but a reliable way to confuse the next reader.

A block is { ... }.

void greet(void) {
Stdio.print("hi\n");
}

Statements terminate with ;. The terminator is not optional — function declarations without a body, variable declarations, and expression statements all end in ;.