Skip to main content
C△ extends C11 syntax with new constructs while staying familiar to C programmers. Source files use the .ctri extension, and library files use .plib. Because C△ inherits the full C11 grammar, any valid C11 code you already know — including pointers, enums, unions, and typedef — continues to work exactly as expected.

File Extensions

Every file you write in C△ falls into one of two roles, indicated by its extension:
ExtensionPurpose
.ctriC△ source file (executable or translation unit)
.plibC△ library file (self-contained, no separate header needed)
The compiler determines whether a .ctri file is an executable or a library unit by checking for the presence of a main function. Library files (.plib) are written in ordinary C△ — they support dynam arrays, typed struct inheritance, and all other language features. Unlike C, there is no separate header file or preprocessor step.

Imports and Modules

C△ uses using and expose to bring library symbols into scope. The compiler performs auto-import: it inspects which functions and variables your file actually references and inserts only the necessary imports automatically. If nothing from a library is used, the compiler will not insert it.
// Import everything from a system library (searched in PLIBS/)
using <plstd>

// Import a specific function from a system library.
// When you import a single symbol this way, the library name in the current
// file is rewritten to that symbol — so `plstd` is no longer in scope here,
// only `printd` is.
using printd from <plstd>

// Import a specific function from a local library file
using helper from "utils"

// Globalize an entire library — all its symbols enter the current scope
// so you can call them unqualified (e.g. `printd(...)`).
// `expose` takes the *current* library name. After `using printd from <plstd>`
// the library name is `printd`, so the correct form is `expose printd`.
expose printd

// Globalize only a specific namespace from a library
expose io@lib;

// Explicit namespace access without globalizing — function@namespace syntax.
// Use this when you haven't exposed the library. After a selective
// `using printd from <plstd>`, the access form is `printd@printd(...)`.
printd@printd("value: %d\n", 42);

// Intra-file variable import — share a variable across functions in the same file
using scopeName&myVar
Prefer using specificFunction from <lib> over using <lib> when you only need one or two symbols — it keeps your namespace clean and makes dependencies explicit.

Variables

C△ supports every C11 variable declaration unchanged, and adds three new forms: the first-class string type, the auto inferred type, and multi-variable packing.
// Standard C types — unchanged from C11
int x = 5;
float pi = 3.14;
char c = 'A';

// C△ first-class string type
string name = "Hypotenuse";

// Inferred type — the compiler deduces the type from the initializer
auto value = 42;
auto label = "hello";

// Multiple variable packing — all three variables are initialized to 10
auto x, z, w = 10;
auto in C△ means inferred/dynamic type — it does not carry the C11 auto storage-class meaning, which has been removed. Using C11 auto will raise a SyntaxError.
auto (inferred type) and multi-variable packing are not yet implemented in the current compiler. string and standard C11 declarations work. See Compiler Status.

Control Flow

C△ inherits all C11 control-flow statements without modification. Use if/else, for, while, do/while, switch, break, continue, return, and goto exactly as you would in C11.
// Conditional branching
if (x > 0) {
    printd(x);
} else {
    printd(0);
}

// Counted loop
for (int i = 0; i < 10; i++) {
    printd(i);
}

// Condition-based loop
while (x > 0) {
    x--;
}

Functions

Declare functions using the same syntax as C11. C△ adds one new form: the variadic argument stream, which collects all call-site arguments into a tuple pointer.
// Standard function — identical to C11
int add(int a, int b) {
    return a + b;
}

// Variadic argument stream — args is a pointer to a tuple of all arguments
int sum(tuple args*) {
    // iterate over args to accumulate the sum
}
The tuple args* variadic form gives you a typed, iterable view of arguments at runtime — more ergonomic than C’s <stdarg.h> approach.
Variadic tuple args* is not yet implementedtuple is not supported in the current compiler. Standard fixed-arity functions work. See Compiler Status.

Lambdas (lamb)

lamb is not yet implemented in the current compiler. See Compiler Status.
Not implemented yet. lamb (named lambdas) is part of the language design but is not supported by the current compiler. See Compiler Status.
Named lambdas let you define single-expression functions in one line. The return type is inferred automatically, so you never need to annotate it.
lamb double(int num) = num * 2;
lamb add(int a, int b) = a + b;

// Call a lambda exactly like a regular function
auto result = double(5);   // result = 10
Lambdas defined with lamb are named — they are callable by name anywhere in the same file after their definition. They are not anonymous closures; use regular functions if you need recursion or multiple statements.

Structs

Struct constructors/destructors (init/end), member functions, and typed struct inheritance are not yet implemented. Plain C-style structs (fields only) work via the C11 baseline. See Compiler Status.
C△ provides two kinds of struct: plain structs and typed structs. Both support an init constructor lifecycle and an end destructor lifecycle, as well as member functions.
Plain structs have constructors and member functions but no inheritance. Use them for lightweight data types where you don’t need polymorphism.
struct Point(int x, int y) {
    init {
        int self.x = x;
        int self.y = y;
    }

    int distanceTo(Point other) {
        // compute and return distance
    }

    end {
        // cleanup runs when the struct goes out of scope
    }
}
When two parent types define the same method name, calling that method directly on a typed struct with multiple inheritance is ambiguous. Always qualify the call with the parent name — for example, obj.Dog.speak() — to resolve the conflict explicitly.

Dynamic Arrays (dynam)

A dynam array grows and shrinks at runtime with no fixed capacity. Declare one with a bracketed initializer list, then use the built-in methods to modify it.
dynam int numbers = [1, 2, 3, 4, 5];

// Append a value to the end
numbers.push(6);

// Remove the element at index 0
numbers.remove(0);

// Get the current number of elements
int count = len(numbers);
len() is a base function built into the language itself — it is not part of any library and requires no import.

Tuples

tuple is not yet implemented in the current compiler. The variadic tuple args* form depends on it and is also unavailable. See Compiler Status.
A tuple is a heterogeneous, dynamically-sized list that can hold values of mixed types. Index into it with [] and grow it with .push().
tuple t = [1, "hello", 3.14];

// Access elements by index
auto first  = t[0];    // 1
auto second = t[1];    // "hello"

// Append a new element (any type)
t.push("added");

// Get the number of elements
int count = len(t);    // count = 4
Tuples are ideal for collecting heterogeneous return values or passing mixed-type argument streams to variadic functions (tuple args*).

Inline Assembly (asm)

C△ lets you embed assembly directly in a source file using the asm keyword. There are two forms: named asm functions (callable from C△ code) and anonymous asm {} blocks (inline). Declare the target architecture with syntax, open the appropriate text section, write your instructions, and exit cleanly — non-void asm functions must end with return; anonymous blocks must end with an explicit sys_exit syscall.
// Named asm function — `return` sends back whatever is in rax (x86_64) or x0 (ARM64).
// A return-type annotation is optional when your assembly already populates that register.
asm addInts(int a, int b) {
    syntax x86_64_linux
    section .text
    mov rax, a
    mov rbx, b
    add rax, rbx
    return
}

// Anonymous asm block — must exit explicitly (here, sys_exit on Linux)
asm {
    syntax x86_64_linux
    section .text
    mov rax, 60      // sys_exit
    mov rdi, 0
    syscall
}
Each asm block is compiled to a separate .asm file, assembled with NASM, and linked into the final binary by GCC. You do not manage this pipeline yourself. See the Inline Assembly page for the full reference — including all syntax targets, anonymous asm blocks, and the rules for data declarations.

Namespaces

Use space to declare a named namespace, @ to access its members without globalizing it, and expose to bring all its symbols directly into scope.
// Declare a namespace
space mySpace {
    int myspacevar = 0;
}

// Access a member with the @ operator (member@namespace)
random@mySpace();

// Globalize a namespace — members become directly accessible
expose mySpace;
random();   // now accessible without @ qualifier
Use the @ operator when you want to reference a single symbol from a namespace without polluting your current scope. Use expose when you need frequent access to many symbols from the same namespace and name collisions are not a concern.

Built-in Functions

The following functions are built into the language itself — they require no import and are always available.

len(collection) — Length

len() returns the number of elements in a string, dynam array, or tuple. It works uniformly across all three collection types.
len() on string and dynam works in the current compiler. tuple is not yet implemented — see Compiler Status.
string s = "hello";
int l = len(s);           // l = 5

dynam int arr = [1, 2, 3];
int n = len(arr);         // n = 3

tuple t = [1, "hi", 3.14];
int t_len = len(t);       // t_len = 3