Type System Philosophy
Types are where the rubber meets the road. They're how you tell the compiler what your data looks like, how you enforce correctness at compile time, and how you communicate intent to other programmers. Get the type system right, and you've got a language that's both powerful and pleasant to use. Get it wrong, and you're fighting the compiler all day.
We're going for something specific here: powerful type safety with the simplicity and transparency of C. That's a tough balance, but it's worth striving for.
Philosophy: Simple, Transparent, Safe
Before we dive into the details, let's talk about what we're trying to achieve with types.
Simple
Types should be easy to understand. When you see int or *u8 or [float], you should immediately know what you're dealing with. Complex type expressions should be rare, used only when the problem is genuinely complex.
We're not gonna have types where you need a PhD to parse them. We want types that feel like you're having a straightforward conversation.
What this means:
- Small number of core type primitives that compose well
- Type syntax that reads naturally
- Minimal magic — what you see is what you get
- Type inference to reduce boilerplate, but not to hide what's actually happening
Transparent
You should always know what your types mean in terms of memory and performance. When you see a type, you should be able to reason about:
- How much memory it takes up
- Where that memory lives (stack? heap? register?)
- What operations are cheap vs. expensive
- How it maps to the underlying hardware
This is where we take inspiration from C. In C, int is an integer that fits in a register. int* is a pointer — a memory address. int[10] is 10 consecutive integers in memory. Simple, transparent, predictable.
What this means:
- Types have obvious memory layouts
- No hidden allocations or indirections
- Clear distinction between value types and reference types
- Predictable performance characteristics
Safe
Types should catch bugs at compile time. Memory safety, type safety, thread safety — as much as we can catch before the code ever runs, the better. But safety shouldn't come at the cost of simplicity or transparency.
The type system should help you write correct code. Ownership tracking, lifetime analysis, exhaustive pattern matching — these catch real bugs.
What this means:
- Catch use-after-free and double-free at compile time
- Prevent data races and iterator invalidation
- No null pointer exceptions (or at least, make them opt-in)
- Type-driven correctness without ceremony
The Balance
Simple: A handful of primitive types, clear composition rules, minimal magic.
Transparent: Predictable memory layout, explicit costs, hardware mapping.
Safe: Ownership tracking, no null, exhaustive matching, compiler-verified correctness.
That's the goal. Simple, transparent, safe. You get modern safety features without sacrificing control or performance. The type system is your friend, not your adversary.
Design Decisions
Mutable by Default, No Ceremony
Coco takes the simplest approach: variables are mutable by default, and you don't need a keyword to declare them.
$x = 5;
$x = 10; // Just works
No let, no var, no mut — just assign and use. The compiler optimizes variables that never change automatically.
Want immutability? Use const when you need it:
const PI = 3.14159;
PI = 3.0; // ERROR: can't modify const
Why this way?
- Simpler: Most variables change. Don't mark the common case.
- Less ceremony: No extra keywords cluttering your code.
- Compiler handles optimization: It knows which variables never change.
Simple References
References in Coco use just & — no &mut, no ceremony. The compiler detects mutation and enforces safety automatically.
$x = 42;
$r1 = &$x; // Just a reference
$r2 = &$x; // Another reference
echo *$r1; // Reading: fine, multiple readers OK
*$r1 = 100; // Mutation: compiler checks exclusive access
How it works:
- Compiler detects mutation: Analyzes what you do through the reference
- Enforces exclusivity: If you mutate, no other borrows can be active
- Same safety as Rust: Prevents data races, use-after-free, all at compile time
- Simpler syntax: One way to create a reference, not two
Example:
fn read_data($data &BigStruct) {
echo $data.field; // Just reading OK
}
fn modify_data($data &BigStruct) {
$data.field = 10; // Mutation detected: exclusive access enforced
}
$s = BigStruct { field: 5 };
read_data(&$s); // Fine
modify_data(&$s); // Fine: compiler ensures exclusive access
The compiler's flow analysis knows whether a reference is used for reading or writing, and enforces the rules accordingly. You write simple code, the compiler ensures safety.
Smart Parameter Passing
In C, everything passes by value (you copy the data). In Rust, you explicitly mark references with &. In Coco, the compiler does the right thing automatically:
- Small values (primitives, small structs ≤16 bytes): Passed by value
- Large values (big structs, strings): Passed by reference automatically
- Ownership still tracked: The borrow checker ensures safety
fn increment($x i32): i32 {
return $x + 1; // i32 is small, passed by value
}
fn process($text string) {
echo $text; // string is large, passed by reference automatically
}
Why? No extra syntax (&, *) cluttering your code. Performance is automatic. The ownership system still prevents use-after-free and data races — you just don't have to think about the plumbing.
The transparency: The rules are simple and documented. Small = value, large = reference. You can always reason about performance.
Strings: ARC for Simplicity
Most languages either have unsafe strings (C) or complex dual types (Rust's String vs &str). Coco has one string type that uses automatic reference counting (ARC).
$s1 = "hello";
$s2 = $s1; // Cheap: just increment ref count
$s2.push_str(" world"); // Copy-on-write: copies if shared
Why ARC? It's the sweet spot between simplicity and efficiency:
- One type to learn (not two like Rust)
- Cheap to copy (ref counting, not full string copy)
- Safe (no use-after-free, no manual memory management)
- Predictable cost (documented, transparent behavior)
The transparency: Yes, there's a reference count increment/decrement cost. But it's explicit in the documentation, and it's way cheaper than copying entire strings. The copy-on-write behavior is also documented — you know when you'll pay for a copy.
Why only strings? Most types work great with move semantics. Strings are special — they're copied frequently, often large, and programmers expect them to "just work." ARC for strings, moves for everything else. Simple rule.
Object-Oriented Programming
Coco treats OOP as a first-class citizen. Classes, inheritance, encapsulation — the tools you expect are there.
Why? Because OOP is a proven way to organize complex systems. Not everything needs to be OOP (that's why we also have functions and structs), but when you need it, it should be natural and ergonomic.
type Animal {
name string
fn new($name string): Animal {
return Animal { name: $name };
}
fn speak {
echo "Some generic animal sound";
}
}
type Dog {
Animal
fn new($name string): Dog {
return Dog { Animal: Animal::new($name) };
}
fn speak { // Override
echo "Woof!";
}
}
> Note: Defining a .new method is not required. If you don't need any extra initialization logic, you can construct types directly: $floofy = Animal{ name: "Floofy" };
What we support:
- Composition (embed types within types)
- Traits for shared behavior
- Methods inside type definitions
What we avoid:
- Deep hierarchies (composition over inheritance)
- Implicit conversions
- Complex class syntax
The philosophy: OOP where it makes sense, functional where it makes sense, procedural where it makes sense. Use the right tool for the job.
Traits: Composition Over Inheritance
Traits provide shared behavior without inheritance. Types implement trait methods using the @TraitName annotation:
trait Drawable {
fn draw;
}
type Circle {
radius f64
fn new($radius f64): Circle {
return Circle { radius: $radius };
}
// Trait method implemented with @TraitName annotation
@Drawable
fn draw {
echo "Drawing circle with radius {$this.radius}";
}
}
Why this syntax?
- Simple: Methods stay inside the type definition
- Clear:
@Drawableshows exactly what trait is being implemented - Flexible: Multiple traits without the complexity of multiple inheritance
Traits can have default implementations, require other traits, and resolve naming conflicts. This gives you composition without the diamond problem.