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 constantDestructuring
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; # 25Types
| Type | Literal example | Notes |
|---|---|---|
| Integer | 42, -7 | 64-bit signed |
| Float | 3.14, -0.5 | 64-bit IEEE 754 |
| String | "hello", "hi ${name}" | UTF-8, double-quoted; supports interpolation |
| Char | 'a' | Single character, single-quoted |
| Boolean | true, false | |
| Null | null | Absence of value |
| Array | [1, "x", true] | Mixed types allowed |
| Hash | { "k": 1 } | Any type as key or value |
| Function | fn(x) { x * 2 } | First-class value |
| Struct | Point { x: 1 } | Typed object with defaults |
Operators
| Category | Operators |
|---|---|
| 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 5typeof
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
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); # 81Default 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); # 25Recursion
let fib = fn(n) {
if (n <= 1) { return n; };
return fib(n - 1) + fib(n - 2);
};
fib(10); # 55Higher-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 keyKeys 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; # trueSelf-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; # 4fn(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"); # HELLOImport 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.0Use 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.