Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/elfrask/cls/llms.txt

Use this file to discover all available pages before exploring further.

CLS has a static type system that operates entirely at compile time. The type checker, invoked with clx check, validates your program before it runs — catching type mismatches, invalid assignments, and unresolved names. The runtime tree-walking interpreter does not use type information; types have no effect on execution, only on static analysis. This gives you the safety of static typing with the flexibility of a dynamic interpreter during development.
The type system is purely compile-time. Running clx run skips type checking. Use clx check explicitly to validate your types before shipping.

Primitive Types

CLS has eight built-in primitive types. Both long-form and short-form aliases are accepted in type annotations.
TypeAliasesDescription
Intint, i32, i64, Integer64-bit signed integer
Floatfloat, f32, f6464-bit floating-point number
StringstrUTF-8 character string
Boolbool, BooleanBoolean value (true or false)
Charchar, CharacterA single Unicode character
AnyanyDynamic type — assignable to and from everything
NullThe null value
VoidvoidNo value; used for procedures with no return

Arrays

Arrays are ordered, homogeneous, mutable collections. The type is written as ElementType[], or equivalently Array<ElementType>. The type checker infers Array<T> from the type of the first element in a literal.
var numbers: Int[] = [1, 2, 3, 4, 5];
var names: String[] = ["Alice", "Bob", "Charlie"];
var matrix: Int[][] = [[1, 2], [3, 4]];   # 2-D array

# Inference from literal
var inferred = [10, 20, 30];              # inferred as Int[]

numbers[0];                               # → 1
len(numbers);                             # → 5
numbers.push(6);                          # mutation in place

Tuples

Tuples are heterogeneous, immutable, fixed-length sequences. The type is written as (T1, T2, ...) listing each slot’s type. Positional access with a literal index yields the exact type of that slot; access with a dynamic (variable) index yields the union of all slot types.
var pair: (Int, String) = (1, "hello");
var triple: (Int, Bool, Float) = (42, true, 3.14);

pair[0];               # type: Int    — literal index
pair[1];               # type: String — literal index

var i = 0;
pair[i];               # type: Int | String — dynamic index → union of all slots
Tuples are immutable. Attempting to assign to a tuple slot (pair[0] = 9) is a type error caught by clx check.

Records

Records are typed key-value dictionaries, declared as Record<KeyType, ValueType>. The type checker infers Record<String, T> from an object literal, where T is the common value type.
alias Dict = Record<String, Int>;
var scores: Dict = { alice: 95, bob: 87 };

var config: Record<String, Any> = { debug: true, port: 8080 };

scores["alice"];         # → 95
scores.bob;              # → 87  (dot access also works)

Union Types

A union type A | B | C accepts values whose type matches any of its members. Unions of string, number, or boolean literals are particularly useful for constraining values to a finite set — similar to enums in other languages.
alias Color = "red" | "green" | "blue";
alias Status = "ok" | "error" | "pending";
alias SmallInt = 1 | 2 | 3;

var c: Color = "red";       # ok
var s: Status = "ok";       # ok

# In strict mode, the following is a type error:
# var bad: Color = "purple";   # "purple" is not a member of Color
Union types compose well with switch/case — exhaustive pattern matching over a union gives the type checker enough information to narrow types within each branch.

Literal Types

A literal type represents a single, exact value. const declarations infer literal types; var and let declarations infer the base type.
const LANG = "CLS";       # type: "CLS"  (string literal type)
var   lang = "CLS";       # type: String (base type)

const VERSION = 2;        # type: 2      (integer literal type)
var   version = 2;        # type: Int    (base type)
Literal types are assignable to their base type ("CLS" is assignable to String), but the reverse is not true — String is not assignable to "CLS" without an exact match. This makes const ideal for building discriminated union members.

Type Aliases

The alias keyword creates a named type synonym. Aliases only exist at compile time and have no runtime cost.
alias Vec3      = (Int, Int, Int);         # tuple
alias Color     = "red" | "green";        # union of literals
alias FnInt     = (Int) -> Int;            # function type
alias Dict      = Record<String, Int>;    # dictionary
alias NumOrStr  = Int | String;           # union of base types
alias Vec3 = (Int, Int, Int);

var position: Vec3 = (10, 20, 30);
var x: Int = position[0];

Interfaces

Interfaces declare the shape of an object: its fields (with types) and its methods (with parameter and return types). They are purely compile-time constructs and support generic type parameters with optional defaults.
interface Hello<T=Int> {
    num: T,
    greet(name: String): String,
};

interface Serializable {
    serialize(): String,
    deserialize(data: String): Void,
};
Fields are written as name: Type and methods as name(params): ReturnType. Comma-separated members are terminated by };.

Type Extraction

You can extract the type of a named field or positional slot from an interface or tuple using the subscript syntax T["field"] or T[index]. Generic arguments are substituted before extraction.
interface Hello<T=Int> {
    num: T,
    greet(name: String): String,
};

var n: Hello["num"] = 1;              # Int (uses default T=Int)
var s: Hello<String>["num"] = "hi";   # String (T substituted)

var t: (Int, String)[1];              # String — slot 1 of the tuple
This is especially useful for deriving types from existing interfaces without repeating yourself.

Generics

Functions, classes, and interfaces can all be parameterized with type variables written inside <> after the name.
The type checker infers type arguments from the call-site values, so you rarely need to supply them explicitly.
function id<T>(x: T) -> T {
    return x;
};

var n: Int    = id(5);        # T inferred as Int
var s: String = id("hello");  # T inferred as String

Phantom Types

A phantom type parameter !T marks a generic parameter that does not participate in the types of any members. It is not substituted and not unified during type checking. Phantom parameters are used to attach additional type-level identity to a structure without affecting its runtime shape.
interface Marcador<T> {
    real: T,
    fantasma: !T,   # phantom — not substituted for T
};

var r: Marcador<String>["real"]     = "ok";  # String
var f: Marcador<String>["fantasma"];         # !T — not a concrete type
Phantom parameters are an advanced feature for compile-time type tagging. They have no runtime representation and do not affect how values are stored or accessed by the interpreter.

Assignment Rules

The type checker uses these rules to decide when an assignment x: A = expr (where expr has type B) is valid:
RuleDescription
Any compatibilityAny is assignable to every type, and every type is assignable to Any
IdentityIdentical types are always assignable
Numeric wideningInt is assignable to Float
Literal → baseA literal type (e.g. "red") is assignable to its base type (String)
Literal → literalOnly if the literal values match exactly
Union membershipA union type A | B is assignable to T if at least one member is assignable to T
Tuple positionalTuples are compared position-by-position
var a: Float  = 42;          # ok — Int is assignable to Float
var b: String = "hello";     # ok — "hello" literal → String base

alias Color = "red" | "green";
var c: Color  = "red";       # ok — "red" is a member
var d: String = c;           # ok — Color members are assignable to String

var e: Any    = true;        # ok — everything → Any
var f: Int    = e;           # ok — Any → everything

Build docs developers (and LLMs) love