Language Overview
Introduction
This tutorial assumes you are familiar with basic programming concepts and have used another programming language before.
Hello, world!
We'll write a simple program that prints Hello, world! to the screen. Create a
new file named main.cn with the following content:
with std::io; fn main() { io::println("Hello, world!"); }
Now, run these commands to compile and execute the program:
$ centc main.cn $ ./main Hello, world!
You can also use the --run option to automatically run the compiled
executable:
$ centc main.cn --run
Hello, world!Variables
The let statement creates an immutable variable.
let language = "Cent";
To create a mutable variable, use the mut keyword.
mut score = 0; score = 10;
A variable's type can be specified explicitly:
mut score: i32 = 0;
If no initial value is given, the variable is zero-initialized.
mut score: i32;
Cent is statically typed, so you cannot change the type of a variable.
mut score = 10; score = 4.5; // invalid!
You can shadow a variable by declaring a new one with the same name:
mut score = 10; mut score = 4.5; score = 7.3;
mut score = 10; let score = score; // score is no longer mutable score = 7; // invalid!
Comments
You can use comments to explain certain parts of your code. Comments start with
//.
let variable = 10; // this is a variable
Constants
To create a constant, use the const keyword. Constants are computed at compile
time.
const PI = 3.14; const GOLDEN_RATIO = 1.618; const SECONDS_IN_A_DAY = 60 * 60 * 24;
Data types
Integer types
Integer type names start with i (signed) or u (unsigned), followed by the
size in bits:
i8 i16 i32 i64 // signed u8 u16 u32 u64 // unsigned
let a: u64 = 3; let b: i8 = -128;
There are special usize and isize types. They have the size of the pointer
type and are usually used for indexing.
Floating-point types
Floating-point types are used to store numbers with decimal points. In Cent,
there are two such types: f32 and f64.
let a: f32 = 3.5; let b: f64 = 1.2345678;
The bool type
A bool value is either true or false:
mut raining: bool = false; raining = true;
The rune type
The rune type represents a Unicode code point and is 4 bytes long.
let fire: rune = 'π₯';
Array types
Arrays use the [N]T syntax and hold multiple values of the same type:
let data = [4]u8{0xff, 0xff, 0xff, 0x0}; let data = [_]u8{0xff, 0xff, 0xff, 0x0}; // array length can be deduced
Arrays can be of variable length.
mut n: usize = 16; n = 1024; mut data: [n]u8;
Slice types
Slices use the []T syntax and represent a view into a sequence of elements.
Slices have a pointer and a length.
mut data: [1024]u8; let slice: []u8 = data; let len = slice.len; let ptr = slice.ptr;
Slices can be mutable:
mut data: [1024]u8; let slice: []mut u8 = data; slice[10] = 42;
Strings
In Cent, strings are just arrays of bytes. By default, strings are not null-terminated.
let language: [4]u8 = "Cent"; let language = [_]u8{'C' as u8, 'e' as u8, 'n' as u8, 't' as u8}; let null_terminated = "Hello, world!\0";
Optional types
Optional values can either be null or contain a value. To create an optional
type, use the ?T syntax:
mut optional: ?i32 = 32; // optional != null optional = 42; optional = null; // optional == null
To access the contained value without any checks, use the .! syntax:
mut optional: ?i32 = 32; let value = optional.!;
To provide a default value when the optional is null, use the ?? operator:
let x: ?i32 = null; let y = x ?? 42; // y = 42 let a: ?i32 = 10; let b = a ?? 42; // b = 10
Pointer types
A pointer references a value in memory. Pointer types use the *T syntax.
mut x = 42; let ptr: *i32 = &x; // *ptr = 42 x = 422; // *ptr = 422
Pointers can be mutable:
mut x = 42; let ptr: *mut i32 = &x; *ptr = 422; // x = 422
null. If you need a nullable
pointer, use an optional pointer type. An optional pointer has the
same size as a regular pointer.
Tuple types
Tuples use the (T1, T2, T3, ...) syntax and hold multiple values of different
types:
mut data: (i32, f32, bool, [6]u8) = (10, 42.42, true, "Hello!"); data.0 += 32; // data.0 = 42 data.1 = data.0; // data.1 = 42
with statements
Use with to import an external module.
with std::io; with std::fs; with std::posix as os; // import under a different name
You can also only import the things you use:
with buf::{Vector as Vec}; with io::{printf, eprintf};
Functions
Functions are defined using the fn keyword. The main function is the entry
point of the program. Functions can be used before they're defined.
with std::io; fn main() { hello_world(); } fn hello_world() { io::println("Hello, world!"); }
The return type goes after the parentheses. If omitted, the function returns
nothing. Use return to send a value back:
fn get_magic_number() i32 { return 42; }
Functions can take parameters.
fn main() { let a = add(3, 4); // a = 7 } fn add(a: i32, b: i32) i32 { return a + b; }
Default parameters
Functions can have default parameters. When arguments are omitted, the default values are used.
fn main() { let ten = add(3, 7); let nine = add(3, 3, 3); let one = add(1, -1, 1, 0); } fn add(a: i32, b: i32, c: i32 = 0, d: i32 = 0) i32 { return a + b + c + d; }
Control flow
if statements
Use if to run different code depending on a condition.
with std::io; fn main() { print_is_even(3); // x is odd print_is_even(4); // x is even } fn print_is_even(x: i32) { if x % 2 == 0 { io::println("x is even!"); } else { io::println("x is odd!"); } }
You can use else if to check additional conditions.
with std::io; fn greet(hour: u8) { if hour < 12 { io::println("Good morning!"); } else if hour < 18 { io::println("Hello!"); } else { io::println("Good evening!"); } }
switch statements
The switch statement allows you to compare a value against several possible
cases:
with std::io; fn day_of_week(day: u8) { switch day { 1 { io::println("Monday"); } 2 { io::println("Tuesday"); } 3 { io::println("Wednesday"); } 4 { io::println("Thursday"); } 5 { io::println("Friday"); } 6 { io::println("Saturday"); } 7 { io::println("Sunday"); } else { io::println("Invalid day of week!"); } } }
You can match multiple values in a single case.
with std::io; fn is_weekend(day: u8) { switch day { 1, 2, 3, 4, 5 { io::println("Weekday"); } 6, 7 { io::println("Weekend!"); } } }
while loops
A while loop runs as long as the condition is true.
with std::io; fn main() { mut i = 0; while i < 10 { i += 1; } io::print_int(i); // 10 io::print_rune('\n'); }
You can use while true to create an infinite loop. To exit a loop, use the
break keyword.
with std::io; fn main() { mut i = 0; while true { if i == 100 { break; } i += 2; } io::print_int(i); // 100 io::print_rune('\n'); }
To skip an iteration, use the continue keyword:
with std::io; fn main() { mut i = 0; mut sum = 0; while i < 10 { i += 1; if i % 2 == 0 { continue; } sum += i; } io::print_int(sum); // 25 io::print_rune('\n'); }
for loops
for loops allow you to iterate through a range or a sequence.
Exclusive ranges are created by using the x..y syntax:
with std::io; fn main() { for i in 1..10 { io::print_int(i); io::print_rune('\n'); } }
To create an inclusive range, use the x..=y syntax:
with std::io; fn main() { for i in 1..=10 { io::print_int(i); io::print_rune('\n'); } }
You can also iterate over arrays and slices:
with std::io; fn main() { let data = [4]i32{10, 20, 30, 40}; for x in data { io::print_int(x); io::print_rune('\n'); } }
You can mutate elements:
with std::io; fn main() { let data = [4]i32{10, 20, 30, 40}; for mut x in data { x = 0; } }
Literals
Numeric literals
The 0x prefix creates a hexadecimal literal. Use 0o for octal and 0b for
binary.
let hex = 0xff; // hex = 255 let oct = 0o777; // oct = 511 let bin = 0b101010; // bin = 42
You can insert underscores for better readability:
let big_number = 1_000_000_000; // big_number = 1000000000
Scientific notation is supported:
let one_million = 1e6;
Character literals
You can use escape sequences for special characters:
let apostrophe = '\''; let newline = '\n'; let carriage_return = '\r'; let tab = '\t'; let nullbyte = '\0';
Unicode characters can also be represented using escape sequences:
let smiling_face = '\U0001f604'; let omega = '\u03a9'; // '\u' for short codepoints
Character literals are of type rune.
String literals
String literals are UTF-8 encoded sequences of bytes.
let string = "π°ππΎπΈβ΄πΉβ― π";
You can use escape sequences in strings.
let message = "Hello\tworld\n";
Long string literals can be broken down to smaller ones:
let long_string = "this is a very " "loooooooooooooooooooooooooooooooooooooooooooooooooooooooooong " "striiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiing";
String literals can span multiple lines.
let shader = "#version 330 core void main() {}";
Expressions
Binary expressions
| Operator | Precedence | Meaning |
|---|---|---|
* / % |
1 | * - multiplication, / - division, % - modulo |
+ - |
2 | + - addition, - - subtraction |
<< >> |
3 | << - left bitwise shift, >> - right bitwise shift |
< > == != <= >= |
4 | < - less than, > - greater than, == - equal, != - not equal, <= - less or equal, >= - greater or equal |
& |
5 | bitwise and |
^ |
6 | exclusive or (XOR) |
| |
7 | bitwise or |
&& |
8 | logical and |
|| |
9 | logical or |
?? |
10 | null-coalescing |
let a = 2 + 3 * 6; // a = 20
Unary expressions
| Operator | Meaning |
|---|---|
- |
negation |
! |
logical not |
* |
dereference |
& |
address-of |
~ |
bitwise not |
let a = 5; let b = !true; // b = false mut c = -a; // c = -5 let p = &c; *p = -c; // c = 5
as expressions
The as operator converts a value to a different type:
let x = 42; let y = x as f64; // y: f64 = 42.0 let z = 3.9 as u8; // z: u8 = 3 let c = 'A' as u8; // c: u8 = 65
Module system
Each source file is a module. Files in the same directory share a translation unit and can access each other's private items.
To make an item publicly accessible, use the pub keyword:
// src/module.cn fn private() i32 { return 42; } pub fn public() i32 { return private() + 42; }
// src/etc/other.cn fn private() i32 { return 42; } pub fn public() i32 { return private() + 42; }
// src/main.cn with module; with etc::other; fn main() { let a = module::public(); let b = other::public(); let a = module::private(); // valid, same translation unit let b = other::private(); // invalid! }
Structs
You can use structs to create custom types. To create a struct, use the type
keyword.
type Vec3 { x: f32, y: f32, z: f32, } fn main() { let position = Vec3 { x: 10, y: 20, z: 30 }; let z = position.z; // z == 30 mut v = position; v.x = 42.5; // position.x == 10, v.x == 42.5 }
All struct fields are public. If some fields are not meant to be accessed,
prefix them with _:
type Timer { _seconds_left: f64, }
Nested structs
type Color { r: u8, g: u8, b: u8, a: u8, } type Button { text: []u8, color: Color, }
Unions
Unions are tagged by default. Tagged unions allow a value to be one of several types.
union Value { int: i32, float: f32, string: []u8, }
You can use switch on tagged unions.
fn main() { let v = Value { float: 42 }; switch v { Value::int { io::print_int(v.int); } Value::float { io::print_float(v.float); } Value::string { io::print(v.string); } } }
If you need a C-style union, mark it as #(untagged):
type Rgb { r: u8, g: u8, b: u8, a: u8, } #(untagged) union Pixel { colors: Rgb, raw: u32, }
Enums
Enums represent a type with a fixed set of possible values:
enum Color { red, green, blue, } enum Numbers { one = 1, two, // = 2 three, // = 3 }
You can use switch on enum types.
fn main() { let c = Color::red; switch c { Color::red { io::println("red"); } Color::green { io::println("green"); } Color::blue { io::println("blue"); } } }
You can explicitly specify the underlying type.
enum Color u8 { red, green, blue, }
Attributes
Attributes use the #(...) syntax:
#(extern, posix) fn fork() pid_t;
You can apply attributes to multiple declarations at once:
#(extern, posix) { fn fork() pid_t; fn getpid() pid_t; }
Type aliases
Use the type keyword to create a type alias:
type Age = i32; fn main() { mut age: Age = 25; age += 2; }
If you don't want an alias type to be treated exactly like the original type,
mark it as #(distinct):
#(distinct) type Id = i32; fn main() { mut id = 123456 as Id; id += 2; // invalid! }
for blocks
Methods and associated functions are defined inside for blocks.
type Vec2 { x: f32, y: f32, } for Vec2 { fn right(length: f32) Self { return Self { x: length, y: 0 }; } fn length_squared(self: Self) f32 { return self.x * self.x + self.y * self.y; } } fn main() { let v = Vec2::right(); let l = v.length_squared(); }
Modifying methods take a mutable pointer to self.
for Vec2 { fn reset(self: *mut Self) { self.x = 0; self.y = 0; } }
Function pointers
Function pointer types use the *fn(a: T, b: U, ...) R syntax:
fn add(a: i32, b: i32) i32 { return a + b; } fn main() { let fn_ptr: *fn(a: i32, b: i32) i32 = &add; let seven = fn_ptr(3, 4); }
defer statements
The defer statement schedules a block of code to run when the current scope is
exited.
with std::fs; fn main() { let file = fs::open("text.txt", fs::Mode::read) ?? core::panic("failed to open file"); defer file.close(); }
unreachable statements
unreachable indicates that a code path should never be reached at runtime:
fn divide(a: i32, b: i32) i32 { if b != 0 { return a / b; } unreachable; }
Reaching unreachable is undefined behavior.
Global variables
Variables can be declared at module level:
mut counter: i32 = 0; fn increment() { counter += 1; }