Language overview

Simple and expressive language

cx keeps the language small so there is less to learn and less that can surprise you. There is only one composite value type (struct) instead of separate struct and class concepts, member functions receive this as a non-null T& borrow, and member access always uses ., never ->. Fewer overlapping concepts means a C++ programmer is productive immediately, without relearning which of several similar tools fits each situation.

Control flow follows the same principle. Switch cases break automatically, so a missing break can never silently fall through the way it does in C and C++:

void main() {
    var x = 2;
    switch x {
        case 1: println("one");
        case 2: println("two");
        default: println("other");
    }
}

The language is also simpler to implement tooling for: parsing is straightforward, which makes syntax highlighting, auto-formatting, and linting easier to write and keep correct.

Safer by default

Bugs that C and C++ leave to testing and luck are caught automatically. Array accesses are bounds-checked, integer arithmetic is overflow-checked, and dereferencing null is checked, so corrupted output and security holes from reading past a buffer turn into loud failures at the exact line that caused them. The checks can be disabled individually or globally when measured performance requires it.

Values that may be absent are marked with ?, which forces callers to handle both cases instead of forgetting a null check:

int? readPort() {
    return 8080;
}

void main() {
    if readPort() != null {
        println(readPort()! + 1);
    }
}

The compiler also warns when a variable might be read before initialization, for example when passing it as an out parameter to a C function. Assigning the keyword undefined to the variable suppresses the warning when the programmer knows it is safe. With --Wunused-result, ignored return values produce a warning as well; assigning to _ marks a result as deliberately unused instead of casting to void.

Improved type system

Stronger typing catches bugs at compile time and makes refactoring safer. There are no bug-prone implicit conversions between built-in types; safe ones are still allowed but can be forbidden with a compiler flag. All types are non-nullable by default, with ? marking the ones that admit null, so an unexpected null becomes a compile error rather than a crash.

Fallible operations return Result instead of out parameters or sentinel values, and callers handle each case explicitly with switch:

Result<int, string> fetch(bool ok) {
    if ok {
        return Result.Ok(value = 42);
    }
    return Result.Err(error = "boom");
}

void main() {
    switch fetch(true) {
        case Ok value: println(value);
        case Err error: println(error);
    }
}

Generic code constrains its parameters with interfaces (cx's equivalent of C++ concepts), so instantiating a template with an unsuitable type reports which requirement failed instead of pages of substitution errors. Unconstrained parameters stay available for flexibility. Arrays are first-class values with a known size that can be returned and passed by value, and anonymous structs provide lightweight syntax for grouping values. Tagged unions allow runtime polymorphism without dynamic allocation and virtual function calls.

Standard library covers common use cases better

String handling works on views and owned buffers with practical helpers. Splitting user input and rejoining it needs no manual loop:

void main() {
    var csv = StringBuf("a,b,c");
    println(join(csv.split(','), ";"));
}

Collections offer the transformations used in everyday data processing. Computing a total or reshaping a list reads as a pipeline instead of nested loops and temporary buffers:

void main() {
    var nums = List([1, 2, 3, 4]);
    println(sum(nums));
    println(nums.map(n => n * 2).toList());
}

Math support includes constants such as Pi:

void main() {
    println(pi);
}

Beyond that, the standard library covers file system access and process control, and its algorithms take range objects instead of iterator pairs for ease of use.

Improved syntax

cx's syntax stays close to the C family while removing verbose or cryptic spellings. There is no C-style cast syntax, only explicit conversions such as int(x). Array sizes sit next to the element type, semicolons are optional, and lambdas infer their argument types, which keeps callbacks readable:

void main() {
    var nums = List([1, 2, 3, 4]);
    var doubled = nums.map(n => n * 2).toList();
    println(doubled);
}

Better compilation model

No header files and no forward declarations: a program is compiled as a whole instead of one translation unit at a time, which leaves more room for optimization and avoids link-time surprises. Libraries are imported as a whole, so users never hunt for which file declares a feature, and library authors can reorganize files without breaking compatibility. Every library lives in its own namespace automatically, with no per-file declarations to maintain. Together this means faster builds and less time wrestling the build when a project grows past a handful of files.

Standard build system and package manager

Building a cx project is done with a single command, cx build, which works out of the box without any build configuration, and dependencies are fetched from Git repositories. See the Build system page for details.

Long-term goal: include a built-in linting tool that runs during compilation to enforce a specific coding style, or to disallow uses of certain language features.

Faster than C++

More optimization opportunities:

cx uses the open-source LLVM library as a code generation back-end, benefiting from all current and future optimizations implemented in LLVM.

No hidden expensive operations, such as implicit calls to copy constructors and copy assignment operators like in C++.

Transparent interoperation with existing C APIs

Existing C libraries stay usable: headers import directly, so calling into battle-tested code such as parsers, codecs, or operating system APIs needs no bindings layer. A command-line tool that needs one C helper does not have to drop down to C for the whole program:

import "stdlib.h";

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

Support for some level of interoperability with C++ APIs is a longer-term goal.

Additional language features

Destructuring binds enum payloads and multiple values to separate variables at the point of use, so dispatching on a result reads linearly instead of nesting accessors:

void main() {
    Result<int, string> r = Result.Ok(value = 42);
    switch r {
        case Ok value: println(value);
        case Err error: println(error);
    }
}

Named arguments label call sites that would otherwise be cryptic sequences like foo(true, false). Labels are checked against parameter names and may come in any order, which keeps calls readable when a function takes several same-typed parameters:

struct Point: Copyable {
    int x;
    int y;
}

void main() {
    var p = Point(y = 2, x = 1);
    println(p.x + p.y);
}

Defer statement runs cleanup when leaving the current scope, including early returns. Resource handling no longer needs an RAII wrapper class per resource just to guarantee release:

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

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

Simple type inference for local and global variables removes redundant annotations, which keeps code shorter and makes type changes during refactoring touch fewer lines. The strong type system ensures inference never silently picks an unexpected type.

Compile-time reflection (for example iterating over enum cases or rendering an enum case as text) is a longer-term goal. It should follow the "pay only for what you use" principle.

...and all the good parts from C and C++