Language overview

A quick tour of cx for programmers who already know a C-family language. Each section shows the basics of one area and links to the page that covers it in detail. For the reasoning behind the design, see Design principles; for how cx differs from C++, Rust, and others, see Comparison with related languages.

Variables and functions

Local variables are declared with an explicit type or with var to infer it, and const declares compile-time constants. Semicolons are optional, as are the parentheses around the conditions of if, for, while, and switch. Arguments can be labeled with their parameter names, in any order, which keeps calls like foo(true, false) readable:

void greet(string name, int times) {
    for _ in 0..times {
        println("hi ", name);
    }
}

void main() {
    const greeting = "hello";
    var count = 2; // inferred as int
    println(greeting);
    greet(times = count, name = "Bo"); // prints "hi Bo" twice
}

Functions can be overloaded on parameter types and names. See Variables and constants and Functions.

Control flow

for loops iterate over collections and ranges, and switch cases break automatically, so a missing break can never silently fall through:

void main() {
    for i in 1...3 {
        switch i {
            case 1: println("one");
            case 2: println("two");
            default: println("other");
        }
    }
}

See Control flow.

Structs

struct is the only composite value type; there is no separate class. Methods receive this as a non-null borrow, and member access always uses ., even through pointers. Constructors take positional or named arguments:

struct Point {
    int x;
    int y;

    int lengthSquared() {
        return x * x + y * y;
    }
}

void main() {
    var p = Point(y = 4, x = 3);
    var ptr = &p;
    println(ptr.lengthSquared()); // prints 25
}

Structs can have destructors, and defer runs cleanup when leaving the current scope, including early returns:

void process(bool fail) {
    defer println("releasing resource");
    if fail {
        println("failed, returning early");
        return;
    }
    println("working");
}

void main() {
    process(false);
    process(true);
}

See Structs, Anonymous structs, and References.

Arrays, lists, and strings

Fixed-size arrays are values that can be passed and returned like any other, List is the resizable array, and strings come as string views and owned StringBuf buffers:

void main() {
    int[3] sizes = [64, 128, 32];
    var list = List([1, 2, 3]);
    list.push(sizes[1]);
    println(list); // prints [1, 2, 3, 128]

    var csv = StringBuf("a,b,c");
    println(join(csv.split(','), ";")); // prints a;b;c
}

See Arrays, List, and Strings.

Nullable types

All types are non-nullable unless marked with ?. The compiler narrows a nullable value to its non-null type inside a null check, and warns when a possibly-null value is used without one:

int? parsePort(string scheme) {
    if scheme == "http" {
        return 80;
    }
    return null;
}

void main() {
    var port = parsePort("http");
    if port {
        println(port + 1); // ok, port is an int here
    }
}

See Nullable types.

Enums and tagged unions

Enum cases can carry payloads, making enum a type-safe tagged union. switch matches on the case and narrows the value, so each branch can access that case's fields:

enum Shape {
    Circle(float radius),
    Rect(float width, float height),
}

float area(Shape shape) {
    switch shape {
        case Circle: return 3.0 * shape.radius * shape.radius;
        case Rect: return shape.width * shape.height;
    }
}

void main() {
    println(area(Shape.Circle(1.0))); // prints 3
    println(area(Shape.Rect(width = 2.0, height = 3.0))); // prints 6
}

See Enum types.

Closures and iterators

Lambdas infer their parameter types and can capture local variables. Functions like filter and map return lazy views that chain without allocating:

void main() {
    var numbers = List([1, 2, 3, 4, 5, 6]);
    int limit = 4;
    var total = numbers.filter(n => n % 2 == 0 && n <= limit).map(n => n * n).sum();
    println(total); // prints 20
}

See Closures and Iterators.

Interfaces and generics

Interfaces declare required methods (and optionally default implementations), and serve as bounds on generic type parameters, so a mismatch reports which requirement failed:

interface HasArea {
    float area();
}

struct Square: HasArea {
    float side;

    float area() {
        return side * side;
    }
}

void printArea<T: HasArea>(T& shape) {
    println(shape.area());
}

void main() {
    var square = Square(side = 2.0);
    printArea(square); // prints 4
}

See Interfaces and Generics.

Error handling

Fallible functions return Result by convention, and callers handle both cases with switch, so every error path is visible in the code:

Result<int, string> parseDigit(char c) {
    if c >= '0' && c <= '9' {
        return Ok(int(c) - int('0'));
    }
    return Err("not a digit");
}

void main() {
    switch parseDigit('x') {
        case Ok value: println(value);
        case Err error: println(error); // prints "not a digit"
    }
}

See Error handling.

Safety checks

Debug builds check array bounds, integer overflow, and null dereferences, and report memory leaks at exit. Where measured performance requires it, checks can be disabled with attributes on a function, statement, or expression: @unchecked disables all checks in its scope, while @noOverflowCheck, @noBoundsCheck, and @noNullCheck disable one check each:

@noOverflowCheck
int wrapAdd(int a, int b) {
    return a + b; // wraps instead of aborting on overflow
}

void main() {
    var x = 2147483647;
    println(wrapAdd(x, 1)); // prints -2147483648
    var y = @noOverflowCheck x + 1; // expression-level
    println(y); // prints -2147483648
}

See Builtin types and Build modes.

Modules, builds, and C interop

All .cx files in a project compile as one module, with no header files, forward declarations, or imports between them, and the standard library needs no import either. cx run and cx build work without any configuration. C headers import directly, with no bindings to write:

import "stdlib.h";

void main() {
    println(atoi("42") + 1); // prints 43
}

See Modules and imports, Build system, and Advanced topics.

More topics

The remaining guide pages cover Builtin types, Type aliases, Unions, Private, Function pointers, Operator overloading, Casting, and Editor setup.