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()):
primitive_type_of(value) maps the receiver Value to a PrimitiveType.
- The interpreter looks up the name in that type’s table.
- For a
Getter, the function is called immediately with args = [receiver] — no call parentheses in source needed.
- 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 / Getter | Arguments | Returns | Description |
|---|
upper() | — | String | Converts all characters to uppercase |
lower() | — | String | Converts all characters to lowercase |
trim() | — | String | Strips leading and trailing whitespace |
contains(s) | s: String | Bool | Returns true if the string contains s |
startsWith(s) | s: String | Bool | Returns true if the string starts with s |
endsWith(s) | s: String | Bool | Returns true if the string ends with s |
isEmpty() | — | Bool | Returns true if the string has zero characters |
toString() | — | String | Returns the string itself (identity) |
length (getter) | — | Int | Number 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 / Getter | Arguments | Returns | Description |
|---|
push(x) | x: Value | Array | Appends x to the end; returns the mutated array |
pop() | — | Array | Removes the last element; returns the mutated array |
shift() | — | Array | Removes the first element; returns the mutated array |
unshift(x) | x: Value | Array | Prepends x to the front; returns the mutated array |
indexOf(x) | x: Value | Int | Index of the first occurrence of x, or -1 if not found |
includes(x) | x: Value | Bool | Returns true if x is present in the array |
join(sep) | sep: String | String | Joins all elements with sep as separator |
reverse() | — | Array | Reverses the array in place; returns the mutated array |
toString() | — | Array | Returns the array value itself (identity) |
length (getter) | — | Int | Number 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 / Getter | Arguments | Returns | Description |
|---|
join(sep) | sep: String | String | Joins all elements with sep as separator |
toString() | — | Tuple | Returns the tuple value itself (identity) |
length (getter) | — | Int | Number 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 / Getter | Arguments | Returns | Description |
|---|
keys() | — | Array<String> | Sorted list of all keys |
values() | — | Array<Value> | Values sorted by their corresponding key |
has(k) | k: String | Bool | Returns true if k is a key in the record |
toString() | — | Record | Returns the record value itself (identity) |
length (getter) | — | Int | Number of key-value pairs |
size (getter) | — | Int | Alias 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.
| Method | Arguments | Returns | Description |
|---|
toString() | — | String | Decimal string representation of the number |
abs() | — | Int or Float | Absolute 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
| Method | Arguments | Returns | Description |
|---|
toString() | — | String | "true" or "false" |
true.toString(); // "true"
false.toString(); // "false"
let flag = 1 > 0;
flag.toString(); // "true"
Char
| Method | Arguments | Returns | Description |
|---|
toString() | — | String | Single-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"