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 primitive types (String, Int, Float, Bool, Char, Array, Tuple, Record) support method calls and property getters, but the values themselves are never boxed into wrapper objects. Instead, cls-runtime/src/stdlib/primitive.rs builds a global set of static dispatch tables — one per primitive type — mapping method names to native Rust functions. When the interpreter evaluates "hello".upper(), it looks up PrimitiveType::String"upper" in the table and calls the corresponding function directly, with no object allocation on the heap.

How Dispatch Works

pub enum PrimitiveMethod {
    Method(MethodFn),   // callable: receiver.method(args...)
    Getter(MethodFn),   // property: receiver.name  (no parentheses)
}

pub type MethodFn = Arc<dyn Fn(&[Value]) -> ClsResult<Value>>;
The table structure is HashMap<PrimitiveType, HashMap<&'static str, PrimitiveMethod>>. At interpreter startup, build_method_tables() populates all tables once. When the interpreter resolves a member access (.name) or a method call (.name()):
  1. primitive_type_of(value) maps the receiver Value to a PrimitiveType.
  2. The interpreter looks up the name in that type’s table.
  3. For a Getter, the function is called immediately with args = [receiver] — no call parentheses in source needed.
  4. For a Method, the interpreter binds the receiver and returns a callable FunValue (internally named __method__.name), then invokes it with the provided arguments.
The receiver always travels as args[0] in the native function. Any additional arguments follow at args[1], args[2], etc.
Because the receiver type is statically known at the call site, a future native compiler can skip the table lookup entirely and emit a direct call to the Rust function — monomorphisation at compile time, zero overhead at runtime.

Mutation Semantics

There are two categories of primitive types with respect to mutation:
  • Immutable (String, Int, Float, Bool, Char): transformation methods return a new value. The original is not modified. Reassign explicitly if needed: s = s.upper();
  • Mutable (Array): mutation methods (push, pop, shift, unshift, reverse) return the modified array. The interpreter’s evaluate_call automatically writes the result back to the originating variable — you do not write arr = arr.push(4).
Tuple and Record are effectively immutable: there are no mutation methods defined for them in the dispatch table.

String Methods

Strings are immutable. All transformation methods return a new String value.
Method / GetterArgumentsReturnsDescription
upper()StringConverts all characters to uppercase
lower()StringConverts all characters to lowercase
trim()StringStrips leading and trailing whitespace
contains(s)s: StringBoolReturns true if the string contains s
startsWith(s)s: StringBoolReturns true if the string starts with s
endsWith(s)s: StringBoolReturns true if the string ends with s
isEmpty()BoolReturns true if the string has zero characters
toString()StringReturns the string itself (identity)
length (getter)IntNumber of Unicode scalar values (characters)
length counts Unicode scalar values (Rust chars), not bytes. A string containing a 4-byte emoji has length = 1.
let s = "  Hello, World!  ";

s.trim();              // "Hello, World!"
s.trim().upper();      // "HELLO, WORLD!"
s.trim().lower();      // "hello, world!"

"cls".contains("ls");         // true
"cls".startsWith("cl");       // true
"cls".endsWith("ls");         // true
"cls".isEmpty();              // false
"".isEmpty();                 // true

"hello".length;               // 5
"héllo".length;               // 5  (accented char = 1)

// Strings are immutable — reassign to keep the change
let name = "ada";
name = name.upper();   // "ADA"

Array Methods

Arrays are mutable. Mutating methods modify the array and return it; the interpreter writes the result back to the variable automatically.
Method / GetterArgumentsReturnsDescription
push(x)x: ValueArrayAppends x to the end; returns the mutated array
pop()ArrayRemoves the last element; returns the mutated array
shift()ArrayRemoves the first element; returns the mutated array
unshift(x)x: ValueArrayPrepends x to the front; returns the mutated array
indexOf(x)x: ValueIntIndex of the first occurrence of x, or -1 if not found
includes(x)x: ValueBoolReturns true if x is present in the array
join(sep)sep: StringStringJoins all elements with sep as separator
reverse()ArrayReverses the array in place; returns the mutated array
toString()ArrayReturns the array value itself (identity)
length (getter)IntNumber of elements
let nums = [1, 2, 3];

nums.push(4);       // nums is now [1, 2, 3, 4]  — write-back automatic
nums.pop();         // nums is now [1, 2, 3]
nums.unshift(0);    // nums is now [0, 1, 2, 3]
nums.shift();       // nums is now [1, 2, 3]

nums.indexOf(2);    // 1
nums.includes(99);  // false

nums.join(", ");    // "1, 2, 3"
nums.join("-");     // "1-2-3"

nums.reverse();     // nums is now [3, 2, 1]
nums.length;        // 3
pop() and shift() silently do nothing if the array is already empty — they do not throw. Check length first if you need to guard against an empty array.
// Write-back example — you do NOT need to reassign
let fruits = ["apple", "banana"];
fruits.push("cherry");
print(fruits);   // ["apple", "banana", "cherry"]

Tuple Methods

Tuples are immutable ordered sequences. There are no mutation methods — attempting to use push or pop on a tuple will fail because those entries do not exist in the tuple dispatch table.
Method / GetterArgumentsReturnsDescription
join(sep)sep: StringStringJoins all elements with sep as separator
toString()TupleReturns the tuple value itself (identity)
length (getter)IntNumber of elements
let coords = (10, 20, 30);

coords.length;          // 3
coords.join(", ");      // "10, 20, 30"
coords.join(" / ");     // "10 / 20 / 30"

// Tuples are immutable — index assignment is a runtime error
// coords[0] = 99;   ← ERROR

Record Methods

Records are string-keyed maps. keys() and values() return results sorted alphabetically by key, ensuring deterministic output.
Method / GetterArgumentsReturnsDescription
keys()Array<String>Sorted list of all keys
values()Array<Value>Values sorted by their corresponding key
has(k)k: StringBoolReturns true if k is a key in the record
toString()RecordReturns the record value itself (identity)
length (getter)IntNumber of key-value pairs
size (getter)IntAlias for length
let config = { host: "localhost", port: 8080, debug: true };

config.length;             // 3
config.size;               // 3  (same as length)
config.has("host");        // true
config.has("timeout");     // false

config.keys();             // ["debug", "host", "port"]   — alphabetical
config.values();           // [true, "localhost", 8080]   — matching order

// Accessing a field directly
config.host;               // "localhost"
keys() and values() return their results in alphabetical order by key. This is deterministic regardless of the internal HashMap iteration order.

Int and Float Methods

Both Int and Float share a single dispatch table (number_table). The methods handle both types via pattern matching internally.
MethodArgumentsReturnsDescription
toString()StringDecimal string representation of the number
abs()Int or FloatAbsolute value; preserves the original type
let n = -42;
let f = -3.14;

n.toString();   // "42"  — wait, abs first:
n.abs();        // 42    (Int)
f.abs();        // 3.14  (Float)

(-99).toString();     // "-99"
(-0.5).abs();         // 0.5
abs() is type-preserving: calling it on an Int returns an Int; calling it on a Float returns a Float. No implicit coercion occurs.

Bool and Char Methods

Bool and Char each have a single method.

Bool

MethodArgumentsReturnsDescription
toString()String"true" or "false"
true.toString();    // "true"
false.toString();   // "false"

let flag = 1 > 0;
flag.toString();    // "true"

Char

MethodArgumentsReturnsDescription
toString()StringSingle-character string
let c = 'Z';
c.toString();   // "Z"

'€'.toString(); // "€"

How to Add a New Method

Adding a method to an existing primitive type is a one-step change in cls-runtime/src/stdlib/primitive.rs. Find the table function for the type (string_table, array_table, number_table, etc.) and insert a new entry:
// Method: callable with parentheses
t.insert("myMethod", method(|args| {
    let s = expect_string(args)?;   // validate receiver type
    // args[1], args[2], ... are additional arguments
    Ok(Value::String(format!("processed: {}", s)))
}));

// Getter: accessed without parentheses
t.insert("myGetter", getter(|args| {
    let s = expect_string(args)?;
    Ok(Value::Int(s.len() as i64))
}));
The method(f) and getter(f) helper functions wrap a plain fn(&[Value]) -> ClsResult<Value> into the appropriate PrimitiveMethod variant. Use expect_string, expect_array, or expect_tuple to validate and extract the receiver from args[0]. For Int, Float, Bool, Char, and Record, pattern-match args.first() directly (following the style of number_table and record_table).
// Example: add a `repeat(n)` method to String
t.insert("repeat", method(|args| {
    let s = expect_string(args)?;
    let n = match args.get(1) {
        Some(Value::Int(n)) => *n as usize,
        _ => return Err(ClsError::RuntimeError("repeat: expected Int".into())),
    };
    Ok(Value::String(s.repeat(n)))
}));
After adding the entry, no other file needs to change — build_method_tables() is called once at interpreter startup and the new method is immediately available in CLS source.
// After adding repeat:
"ha".repeat(3);   // "hahaha"

Build docs developers (and LLMs) love