Language reference

A complete guide to code-lang syntax and semantics. All examples can be run in the REPL or saved to a .cl file.

Comments

# single-line comment

/* multi-line
   comment */

Variables

let declares a mutable variable. const declares a constant — reassignment is a runtime error. An uninitialized let defaults to null.

let age = 25;
const PI = 3.14159;
let pending;       # null until assigned

age = 26;      # ok
PI  = 3;       # error: cannot reassign constant

Destructuring

Unpack arrays and hashes directly into named bindings. Works with both let and const. Use _ to skip a position.

# array destructuring
let [a, b, c] = [1, 2, 3];
a;   # 1

let [first, _, third] = [10, 20, 30];
first;   # 10
third;   # 30

# hash destructuring
let { name, age } = { "name": "Walon", "age": 25 };
name;   # Walon

# rename on destructure
let { name: n, age: years } = { "name": "Walon", "age": 25 };
n;      # Walon
years;  # 25

Types

TypeLiteral exampleNotes
Integer42, -764-bit signed
Float3.14, -0.564-bit IEEE 754
String"hello", "hi ${name}"UTF-8, double-quoted; supports interpolation
Char'a'Single character, single-quoted
Booleantrue, false
NullnullAbsence of value
Array[1, "x", true]Mixed types allowed
Hash{ "k": 1 }Any type as key or value
Functionfn(x) { x * 2 }First-class value
StructPoint { x: 1 }Typed object with defaults

Operators

CategoryOperators
Arithmetic+ - * / % · ** power · // floor division
Comparison== != < > <= >=
Logical&& || ! — short-circuit evaluation
Compound assign+= -= *= /= %=
Increment / decrement++ -- prefix and postfix
String concat+ — works between strings
Null coalescing?? — returns right side when left side is null
2 ** 8;       # 256
17 // 5;      # 3  (floor division)
10 % 3;       # 1

let n = 5;
n++;          # n is now 6
n += 10;      # n is now 16

let x;           # x is null
let y = x ?? 0;  # y is 0 because x is null
let z = 5 ?? 0;  # z is 5

typeof

The typeof keyword returns the type of any expression as a lowercase string.

typeof 42;          # "integer"
typeof 3.14;        # "float"
typeof "hello";     # "string"
typeof true;        # "boolean"
typeof null;        # "null"
typeof [1, 2];      # "array"
typeof fn(x){x};    # "function"

String interpolation

Embed any expression inside a string with ${...}. The expression is evaluated and converted to a string automatically.

let name = "Walon";
let age  = 25;

"Hello, ${name}!";          # Hello, Walon!
"In 5 years you'll be ${age + 5}.";   # In 5 years you'll be 30.
"pi ≈ ${math.round(3.14159, 2)}";

Control flow

if / elseif / else

Branches are expressions — the last evaluated value is the result of the whole block.
let score = 85;

if (score >= 90) {
    "A"
} elseif (score >= 80) {
    "B"
} else {
    "C"
};

while

let i = 0;
while (i < 5) {
    i += 1;
};

for

for (let i = 0; i < 5; i++) {
    if (i == 2) { continue; };
    if (i == 4) { break; };
};

break and continue work in both while and for.

for-in

Iterate over arrays or hashes without a counter.

let nums = [10, 20, 30];

for (n in nums) {
    fmt.print(n);
};

let scores = { "Alice": 95, "Bob": 87 };

for (name, score in scores) {
    fmt.print("${name}: ${score}");
};

switch

Compare a subject against a series of patterns using ==. The first matching arm runs. Add a default arm to handle any value that does not match. If no arm matches and there is no default, the result is null.

let direction = Direction.North;

switch (direction) {
    Direction.North => fmt.print("going north"),
    Direction.South => fmt.print("going south"),
    Direction.East  => fmt.print("going east"),
    Direction.West  => fmt.print("going west"),
    default         => fmt.print("unknown direction"),
};

# switch on any value
switch (score // 10) {
    10 => "A+",
    9  => "A",
    8  => "B",
    default => "below B",
};

Functions

Functions are values. Assign them with let or const. Return early with return — the last expression in a block is also returned implicitly.

let add = fn(a, b) {
    return a + b;
};

let square = fn(x) { x * x };   # implicit return

add(3, 4);      # 7
square(9);      # 81

Default parameters

Parameters can have default values that are used when the caller omits the argument.

let greet = fn(name, msg = "Hello") {
    "${msg}, ${name}!"
};

greet("Walon");            # Hello, Walon!
greet("Walon", "Hi");      # Hi, Walon!

Closures

Functions close over the enclosing scope and capture variables by reference.

let make_adder = fn(n) {
    return fn(x) { x + n };
};

let add5 = make_adder(5);
add5(10);   # 15
add5(20);   # 25

Recursion

let fib = fn(n) {
    if (n <= 1) { return n; };
    return fib(n - 1) + fib(n - 2);
};

fib(10);   # 55

Higher-order functions

let apply = fn(f, x) { f(x) };
apply(fn(n) { n * 2 }, 7);   # 14

import "arrays";
let doubled = arrays.map([1, 2, 3], fn(x) { x * 2 });   # [2, 4, 6]
let evens   = arrays.filter([1,2,3,4], fn(x) { x % 2 == 0 });
let sum     = arrays.reduce([1,2,3,4], fn(acc, x) { acc + x }, 0);

Arrays

let nums = [1, 2, 3, 4, 5];

nums[0];          # 1
nums[2] = 99;     # mutate in place
nums[-1];         # last element (if supported)

let mixed = [1, "hello", true, [2, 3]];

See the arrays module for slice, sort, zip, flatten, and 15 more operations.

Hashes

let person = { "name": "Alice", "age": 30 };

person["name"];        # Alice
person.name;           # same — dot access works too
person["role"] = "admin";   # add or update key

Keys can be any type. See the hash module for keys, values, merge, and more.

Structs

Structs define a named type with default field values. Instantiate with TypeName { fields } — any field not provided gets its default.

struct User {
    name: "Guest",
    role: "user",
    active: true,
}

let admin = User { name: "Walon", role: "admin" };
let guest = User {};

admin.name;    # Walon
guest.name;    # Guest
guest.active;  # true

Self-methods

Give a struct a method by storing a function in a field. Name the first parameter self — the interpreter injects the receiver automatically when the method is called with dot notation. You never pass self explicitly.

struct Point {
    x: 0,
    y: 0,
    distance: fn(self) {
        import "math";
        math.sqrt(self.x ** 2 + self.y ** 2)
    },
    translate: fn(self, dx, dy) {
        Point { x: self.x + dx, y: self.y + dy }
    },
}

let p = Point { x: 3, y: 4 };
p.distance();           # 5.0
p.translate(1, 1).x;   # 4
Default parameters work in self-methods too: fn(self, factor = 2) { self.x * factor }. Arity errors skip the implicit self in the message so it reads naturally.

Enums

Enums define a named set of variants. Access variants with dot notation. Variants compare equal only to themselves, making them safe to use in switch arms.

enum Direction { North, South, East, West }
enum Status    { Ok, Err, Pending }

let d = Direction.North;

d == Direction.North;   # true
d == Direction.South;   # false

switch (d) {
    Direction.North => "up",
    Direction.South => "down",
};

Error handling

Errors in code-lang are values, not exceptions. When a stdlib function fails it returns an error object — it does not crash the program. Use is_error(val) to check it.

import "fmt";
import "fs";

let content = fs.read_file("maybe.txt");

if (is_error(content)) {
    fmt.print("could not read file");
} else {
    fmt.print(content);
};
is_error() is a global builtin — no import needed. An error value stored in a variable is safe to inspect. A bare error expression that is not assigned or checked propagates up and halts the current block.

Use ?? to supply a fallback when a value is null:

let val = might_be_null() ?? "default";

Modules

Import stdlib

import "math";
import "strings";

math.sqrt(16);              # 4.0
math.clamp(150, 0, 100);    # 100

strings.split("a,b,c", ",");   # ["a", "b", "c"]
strings.to_upper("hello");     # HELLO

Import a .cl file

Import any .cl file by its path (without the extension). Everything declared at the top level in that file becomes a field on the resulting module object.

# utils.cl
const VERSION = "1.0";
let double = fn(x) { x * 2 };

# main.cl
import "utils";
utils.double(5);    # 10
utils.VERSION;      # 1.0

Use pub to control what is exported. When any pub declaration exists, only those names are accessible from outside the module.

# utils.cl
pub let greet = fn(name) { "Hello, ${name}!" };
let _secret   = 42;   # not exported

# main.cl
import "utils";
utils.greet("world");   # Hello, world!
utils._secret;          # error: utils has no public member '_secret'

See all 12 built-in modules in the standard library reference.