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 integers
  • i8, i16, i32, i64 — signed integers
  • int — the largest signed integer available on the target architecture
  • uint — the largest unsigned integer available on the target architecture

Floats:

  • f32, f64 — floating-point numbers
  • float — 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.

Copyright (c) 2025 Ocean Softworks, Sharkk