Primitive and Compound Types
Our type system is built from a small set of core primitives that compose well. Everything else is built from these.
Core Primitives
Integers:
u8,u16,u32,u64— unsigned integersi8,i16,i32,i64— signed integersint— the largest signed integer available on the target architectureuint— the largest unsigned integer available on the target architecture
Floats:
f32,f64— floating-point numbersfloat— the largest float available on the target architecture
Boolean:
bool— true or false
Character:
char— Unicode scalar value
String:
string— UTF-8 encoded text with automatic reference counting
Null:
null— the null value, represents absence
Any Value:
mixed— can hold any type, checked at runtime
The mixed type is Coco's escape hatch when you need to accept any value. It follows Go's any semantics—you can assign anything to it, but you need type assertions to use it. See the dedicated mixed type page for details.
Compound Types
Primitive types combine into compound types:
Arrays
Fixed-size collections of the same type, stack-allocated:
[T; N] // N elements of type T
Example:
$numbers [i32; 5] = [1, 2, 3, 4, 5];
Slices
View into a sequence, size known at runtime:
[T] // sequence of T elements
Example:
$view [i32] = $numbers[1..3]; // slice of array
Tuples
Fixed-size heterogeneous collection:
(T1, T2, ...)
Example:
$point: (i32, i32) = (10, 20);
$x = $point.0;
$y = $point.1;
Maps
Key-value collections:
{K: V} // map from K to V
Example:
$scores {string: i32} = {"Alice": 95, "Bob": 87};
$alice_score = $scores["Alice"];
Nested types read naturally:
$data {string: [i32]} = {"nums": [1, 2, 3]};
$nested [{string: bool}] = [{"active": true}];
Types
Named product types with fields:
type Point {
x i32
y i32
}
Example:
$p Point = Point { x: 10, y: 20 };
$x_coord = $p.x;
Enums
Sum types with data — model alternatives explicitly:
enum Result<T, E> {
Ok(T),
Err(E)
}
Example:
$result Result<i32, Error> = Ok(42);
match $result {
Ok($value) => print("Success: {}", $value),
Err($e) => print("Error: {}", $e)
}
Why enums are powerful:
- Forces you to handle all cases explicitly
- The compiler ensures you don't forget a variant
- Makes illegal states unrepresentable
Reference Types
Different ways to refer to data:
Pointers
Raw memory address with no safety guarantees:
*T // raw pointer to T
Example:
$ptr i32 = / some address */;
References
Borrowed view, guaranteed valid by the compiler. There's just one syntax—&T—and the compiler automatically determines mutability based on how you use the reference:
$x i32 = 42;
$ref = &$x; // borrow $x
// The compiler tracks whether $ref is used to mutate $x
// If it is, the borrow becomes exclusive (no other references allowed)
// If not, multiple immutable borrows are fine
You don't declare &mut like in Rust. The compiler detects mutation and enforces exclusive access automatically. This keeps the syntax simple while maintaining full memory safety.
The String Type
Strings get special treatment in Coco. Unlike C (unsafe char arrays) or Rust (two types: String and &str), Coco has a single string type that's both safe and ergonomic.
How Strings Work
Strings use automatic reference counting (ARC) internally:
$s1 = "hello"; // String literal
$s2 = $s1; // Cheap: increment ref count (no data copy)
$s3 = "world"; // Another string
Memory representation:
string = {
ptr: *StringData // Pointer to heap-allocated string data
}
StringData = {
ref_count: atomic usize, // How many strings point here
len: usize, // Length in bytes
capacity: usize, // Allocated capacity
data: [u8] // UTF-8 encoded bytes
}
UTF-8 Encoding
All strings are UTF-8 encoded:
$emoji = "Hello 👋 World";
echo $emoji.len(); // Byte length (not character count!)
Important: String length is in bytes, not Unicode characters. To iterate by characters:
for $ch in $text.chars() {
echo $ch;
}
Copy-on-Write Semantics
When you modify a string with multiple references, Coco automatically makes a copy first:
$s1 = "hello";
$s2 = $s1; // Both point to same data (ref_count = 2)
$s2.push_str(" world"); // ref_count > 1, so copy first!
// Now:
// $s1 = "hello" (original data, ref_count = 1)
// $s2 = "hello world" (new data, ref_count = 1)
If the string has no other references, mutation happens in-place:
$s = "hello";
$s.push_str(" world"); // ref_count = 1, modify in-place (efficient!)
// $s = "hello world"
String Operations
Creation:
$literal = "hello"; // String literal
$empty = string::new(); // Empty string
$from_int = string::from(42); // "42"
Concatenation:
$greeting = "Hello" + " " + "World"; // Creates new string
// Or:
$msg = "Hello";
$msg.push_str(" World"); // Append in-place (if not shared)
Slicing:
$text = "hello world";
$slice = $text[0..5]; // "hello" (creates new string)
$ch = $text.char_at(0); // 'h'
Common methods:
$text.len() // Byte length
$text.is_empty() // Check if empty
$text.chars() // Iterator over Unicode characters
$text.contains("ll") // Substring search
$text.split(" ") // Split into array
$text.trim() // Remove whitespace
$text.to_uppercase() // Convert case
Performance Characteristics
| Operation | Cost | Notes |
|---|---|---|
| Create from literal | O(1) | Points to static data |
| Copy (assignment) | O(1) | Just increment ref count |
| Mutate (unique ref) | O(1) amortized | In-place modification |
| Mutate (shared) | O(n) | Copy-on-write, then modify |
| Concatenation | O(n) | Creates new string |
| Indexing by byte | O(1) | Direct array access |
| Indexing by char | O(n) | Must scan UTF-8 |
Why ARC for Strings?
Strings are different from other types:
- They're copied frequently
- They're often large
- Programmers expect them to "just work"
ARC gives us:
- Simple: One type, not two like Rust
- Efficient: Cheap to copy (no full string duplication)
- Safe: No use-after-free, no manual memory management
The cost is explicit:
- Ref count increment/decrement on copy/drop
- Copy-on-write cost when mutating shared strings
These costs are transparent and documented. You can reason about when they happen.
String Literals
String literals are special — they live in the data segment (not heap) and have an immortal ref count:
$s = "hello"; // Points to static data, no allocation
No allocation, no deallocation, no ref count manipulation. Fast and efficient.
Memory Layout is Predictable
You should always know what your types look like in memory:
type Point { x i32, y i32 }
// In memory: [x: 4 bytes][y: 4 bytes] = 8 bytes total
[i32; 5]
// In memory: [elem0][elem1][elem2][elem3][elem4] = 20 bytes contiguous
*i32
// In memory: 8 bytes (on 64-bit) containing an address
No Hidden Costs
The type system makes costs visible:
- Passing by value copies (visible in the type)
- Passing by reference doesn't copy (also visible in the type)
- Allocation is explicit: you call
alloc()or similar - Copying is explicit: if it happens, it's because you wrote it
Exception: Strings
The string type uses ARC (automatic reference counting). The costs are:
- Ref count increment/decrement when copying/dropping
- Copy-on-write allocation when mutating shared strings
These costs are documented and predictable. You know when they happen. It's a deliberate trade-off: simpler API, slightly less zero-cost than move semantics.
Operations Map to Hardware
Types compile to efficient machine code:
$a + $b // → ADD instruction
$arr[$i] // → pointer arithmetic + load
*$ptr // → dereference, one memory load
Simple, transparent, predictable.