**Status:** Design Specification
**Language family:** C-like systems programming language
**Primary goals:** memory safety, predictable performance, C interoperability, explicit low-level escape hatches
Cobalt's syntax is intentionally familiar to programmers coming from C, C++, and Rust. Where appropriate, the specification uses notation similar to Rust for concepts such as references and lifetimes. This is intended to make the underlying relationships immediately recognisable; it does not imply that Cobalt's semantics are identical to Rust's.
Cobalt is a statically typed, compiled, C-like systems programming language.
Cobalt is designed to prevent the following classes of memory errors in safe code:
* integer overflow leading to invalid memory sizes
* integer truncation affecting memory operations
* allocation-size mismatches
* out-of-bounds indexing
* out-of-bounds pointer arithmetic
* invalid memory reads
* invalid memory writes
* stale-pointer access
* use-after-free
* double free
* invalid free
* freeing through the wrong allocator
* ownership confusion
* lifetime violations
* data races
The fundamental safety model is:
OBJECT
+
LOCATION
+
AMOUNT
+
LIFETIME
+
OWNERSHIP
=
VALID MEMORY OPERATION
This directly reflects the central model developed throughout *The Wrong Memory*.
Cobalt deliberately resembles C in syntax.
A C programmer should recognize:
if (condition)
{
}
for (...)
{
}
while (...)
{
}
struct Point
{
int x;
int y;
};
Point p;
p.x = 10;
However, Cobalt changes the semantic defaults.
In particular:
Cobalt safe code:
memory validity is the default.
Cobalt unsafe code:
explicit programmer responsibility.
C interoperability:
explicitly isolated boundary.
The language does not require programmers to manually reconstruct memory-safety proofs for ordinary operations.
Cobalt has two semantic modes.
All ordinary Cobalt code is safe.
Safe code cannot:
* dereference arbitrary raw pointers
* perform unchecked pointer arithmetic
* access memory outside an object's bounds
* access an object after its lifetime
* free memory it does not own
* free memory twice
* perform unchecked integer operations that could invalidate memory-safety assumptions
* create data races through ordinary references
Unsafe operations require:
unsafe
{
...
}
An unsafe block is an explicit boundary.
Unsafe code may perform operations unavailable to safe code, including:
raw pointer dereference
raw pointer arithmetic
manual allocation
manual deallocation
FFI calls
representation-level access
uninitialized-memory operations
unchecked numeric operations
Unsafe code remains subject to the language's defined semantics. It does not mean "anything the CPU happens to do is valid."
Cobalt safe code has no language-level undefined behavior resulting from ordinary memory misuse.
In particular, the following are never silently undefined in safe code:
integer overflow
out-of-bounds indexing
use-after-free
double free
invalid free
null dereference
dangling reference
invalid pointer arithmetic
data race
An operation either:
1. is proven valid,
2. produces a defined failure such as an error or panic,
3. or is rejected by the compiler.
Undefined behavior is permitted only within explicitly unsafe operations and only where the specification explicitly identifies an operation as having unsafe preconditions.
A conforming implementation conceptually performs:
source
↓
lexing
↓
parsing
↓
AST
↓
name resolution
↓
type checking
↓
constant evaluation
↓
ownership analysis
↓
borrow checking
↓
lifetime checking
↓
bounds checking analysis
↓
unsafe verification
↓
lowering
↓
code generation
Optimizations must preserve the observable semantics of valid programs.
Identifiers follow:
identifier:
letter
_
identifier letter
identifier digit
Examples:
buffer
packet_length
Point
MAX_SIZE
Keywords include:
as
break
const
continue
else
enum
extern
false
fn
for
if
impl
import
in
let
match
module
move
mut
public
return
static
struct
trait
true
type
unsafe
union
use
while
Implementations may reserve additional keywords beginning with `__`.
Cobalt defines fixed-width integer types:
i8 i16 i32 i64 i128
u8 u16 u32 u64 u128
It also defines:
usize
isize
usize is an unsigned integer type whose width is sufficient to represent every valid object size on the target.
isize is the signed counterpart of usize, with the same width and a range suitable for representing signed offsets within the target's addressable object model.
Other primitive types:
bool
char
f32
f64
Integer operations are checked by default.
For example:
u32 x = UINT32_MAX;
u32 y = x + 1;
does not silently wrap.
The operation produces a defined overflow failure.
For operations where wrapping is explicitly intended, the programmer must request it:
u32 y = x.wrapping_add(1);
or:
u32 y = wrapping(x + 1);
depending on the library/API style.
The standard integer API provides:
checked_add()
checked_sub()
checked_mul()
checked_div()
checked_shl()
checked_shr()
Conceptually:
Result<u64, ArithmeticError> checked_mul(u64 a, u64 b);
The language also provides checked arithmetic operators where required by context.
Saturating operations are explicit:
x.saturating_add(y);
x.saturating_sub(y);
x.saturating_mul(y);
Wrapping operations are explicit:
x.wrapping_add(y);
x.wrapping_sub(y);
x.wrapping_mul(y);
This prevents accidental wrapping from silently changing an allocation size, offset, or access range.
The need for this distinction follows directly from the integer-to-allocation failure chain described in Chapter 1.
Numeric conversions are explicit when they can lose information.
For example:
u64 x = ...;
u32 y = x;
is rejected if the conversion cannot be proven safe.
Instead:
u32 y = try_cast<u32>(x)?;
or:
u32 y = checked_cast<u32>(x);
may be used.
Lossy conversion is never implicit.
A conversion that is mathematically lossless may be implicit where the target type can represent every source value.
Example:
u32 x = 10;
u64 y = x;
is permitted.
Signed and unsigned conversions are never performed implicitly when they can change the value.
This specifically prevents the class of bugs where a negative signed value becomes a very large unsigned size.
`usize` represents memory sizes and indexes.
The following APIs use `usize`:
allocation sizes
array indexes
slice lengths
slice offsets
object sizes
alignment values
A memory size cannot silently change meaning through a narrowing conversion.
The compiler provides:
sizeof(T)
alignof(T)
`sizeof(T)` is a compile-time constant for statically sized types.
Size calculations used by allocation operations must themselves be checked.
The standard library provides:
checked_size_mul(count, sizeof(T))
checked_size_add(a, b)
However, ordinary typed containers should make these APIs unnecessary in most code.
For example:
Vec<Point> points = Vec<Point>::with_capacity(count);
is preferable to:
malloc(count * sizeof(Point));
because the container owns the relationship between element count and byte capacity.
Every stored value belongs to an object.
An object has:
type
storage
size
alignment
lifetime
ownership state
A pointer or reference does not itself create an object.
An address existing in the machine address space does not imply that a Cobalt object exists there.
This explicitly preserves the distinction between:
address exists
and:
object exists
highlighted in the source material.
Local values normally have automatic storage:
fn process()
{
Point p;
}
`p` remains alive until its scope ends unless moved earlier.
Heap ownership is represented by an owning type.
Primary owning pointer:
Box<T>
Example:
Box<Point> p = Box<Point>::new(Point{});
The owner is responsible for destroying the object.
Safe Cobalt code does not expose:
free(pointer);
for ordinary owned objects.
Instead:
Box<Point> p = ...;
automatically destroys the object when its owner leaves scope.
This makes allocation and destruction a matched type-level relationship rather than an unrelated pair of function calls.
The source chapters identify wrong allocator, double free, premature free, interior-pointer free, and wrong-owner free as distinct failure modes.
Every non-copyable value has exactly one logical owner.
Example:
Box<Data> a = create();
Box<Data> b = move a;
After the move:
b owns Data
a is moved-from
Using `a` afterward is rejected.
A type is copyable only if its definition explicitly permits copying.
Primitive scalar values are normally copyable.
Resource-owning types are normally move-only.
For example:
Box<File> a = ...;
Box<File> b = a;
is invalid.
Instead:
Box<File> b = move a;
transfers ownership.
Every owning value has a deterministic destruction point.
Conceptually:
construct
↓
owned
↓
move / borrow
↓
last owner leaves scope
↓
destructor
↓
lifetime ends
Destructors run exactly once for each owned object.
Every object has a lifetime:
creation ---------------- destruction
A reference must not outlive the object it refers to.
This turns temporal memory safety into a language rule rather than a programmer convention. The source material explicitly describes lifetime validity as a temporal analogue of spatial bounds.
Cobalt has:
&T
&mut T
Example:
fn print_point(&Point p)
{
...
}
A shared reference:
& T
permits reading but not mutation.
A mutable reference:
&mut T
permits mutation.
Borrowing does not transfer ownership.
fn inspect(&Data data)
{
...
}
Data d = create();
inspect(&d);
// d still belongs to caller
fn update(&mut Data data)
{
data.value += 1;
}
At most one active mutable reference may exist for a given object region.
A mutable reference cannot coexist with incompatible shared references.
The compiler verifies:
references remain within their source lifetime
mutable aliases do not conflict
moved values are not subsequently used
destroyed objects have no live references
The programmer therefore does not need manual lifetime bookkeeping for normal code.
Common lifetimes need not be written explicitly.
Example:
fn first(&Slice<int> values) -> ∫
The compiler infers that the returned reference is tied to `values`.
When inference is ambiguous, an explicit lifetime may be required.
Cobalt may use C-like lifetime syntax:
fn first<'a>(&'a Slice<int> values) -> &'a int;
Lifetimes are compile-time entities.
They do not necessarily exist at runtime.
A reference cannot be converted into an integer in safe code.
This prevents the language from reducing an object identity to an arbitrary address.
Cobalt supports:
*const T
*mut T
Raw pointers exist primarily for:
FFI
device memory
custom allocators
OS interfaces
hardware access
low-level runtime code
They are unsafe to dereference.
A reference may be converted to a raw pointer:
*const T p = raw(&value);
This does not extend the lifetime of `value`.
Raw pointer dereference requires unsafe:
unsafe
{
value = *p;
}
The programmer is responsible for proving:
pointer is valid
object is live
location is aligned
object type is correct
access is permitted
access size is valid
Safe Cobalt does not permit arbitrary raw pointer arithmetic.
Instead of:
p + offset
safe code uses slices or indexed views.
For example:
Slice<int> values = ...;
int x = values[index];
The compiler owns the relationship between:
base
element size
length
index
This directly addresses the source chapter's observation that an address being calculable does not imply that the corresponding object belongs to the allocation.
Unsafe code may perform:
p.add(offset)
but the resulting pointer is only valid under explicit conditions.
The implementation must define pointer arithmetic in terms of the allocation/object from which the pointer originated.
Integer-to-pointer arithmetic is not permitted to manufacture a valid Cobalt reference.
Fixed-size arrays use:
T[N]
Example:
int[10] values;
The compiler knows the exact element count.
values[index]
is bounds checked.
The compiler may eliminate a check only when it can prove:
0 <= index < N
If the condition cannot be proven statically, a runtime bounds check is inserted.
A slice is a non-owning view:
Slice<T>
It represents:
object
starting position
element count
lifetime
access permissions
A slice does not own its elements.
MutSlice<T>
is a mutable view.
Example:
fn clear(MutSlice<u8> data)
{
for (usize i = 0; i < data.len(); i++)
{
data[i] = 0;
}
}
The slice itself establishes the valid range.
For a slice:
0 <= index < length
and:
length <= available object elements
must hold.
The programmer cannot construct an ordinary safe slice with a larger logical range than the underlying object.
Slice<T> tail = values.subslice(start, count)?;
The operation verifies:
start <= length
count <= length - start
The subtraction-first form avoids overflow in:
start + count
and mirrors the defensive invariant emphasized by the source material.
Dynamic containers distinguish:
length
capacity
`length` is the number of initialized logical elements.
`capacity` is the number of elements for which storage has been reserved.
They are never interchangeable.
The source material explicitly identifies confusion between logical length and physical capacity as a recurring source of errors.
The standard dynamic array is:
Vec<T>
Example:
Vec<int> values;
values.push(10);
values.push(20);
The vector maintains:
pointer
length
capacity
element type
allocation ownership
as one abstraction.
A vector's growth algorithm must establish:
new_capacity >= required_length
and:
new_capacity * sizeof(T)
must be representable.
Growth arithmetic cannot silently wrap.
values.reserve(additional)?;
establishes enough capacity for `additional` new elements before insertion.
The conceptual invariant is:
additional <= capacity - length
after successful reservation.
This centralizes the arithmetic and allocation relationship rather than scattering checks throughout callers.
A vector may move its underlying storage when it grows.
Any references or slices into the vector are therefore invalidated unless the operation's API explicitly guarantees otherwise.
The borrow checker prevents use of invalidated references.
Cobalt does not expose raw `memcpy` as the normal copying primitive.
For typed objects:
copy(destination, source);
requires compatible types and ranges.
For byte ranges:
copy_bytes(destination, source);
requires both source and destination byte ranges to be valid.
A memory copy is defined as:
source range
↓
destination range
not:
pointer A
↓
pointer B
Both ranges must be established.
This follows the central lesson of Chapter 5.
For:
copy(dst, src);
the compiler verifies:
source contains enough initialized T objects
destination contains enough writable T slots
source and destination lifetimes are valid
types are compatible
Byte-level copying uses:
ByteSlice
MutByteSlice
rather than arbitrary pointer-plus-length combinations.
Example:
copy_bytes(dst.bytes(), src.bytes());
The ranges themselves carry the available size.
A normal safe copy does not accept an independent arbitrary length:
memcpy(dst, src, attacker_length);
Instead:
copy(dst, src);
derives the amount from the validated ranges.
If copying a deliberately smaller amount:
copy_prefix(dst, src, count)?;
the operation checks both:
count <= source.length
count <= destination.length
The source material identifies both source and destination bounds as necessary.
For potentially overlapping ranges:
move_range(dst, src);
is used.
For non-overlapping ranges:
copy(dst, src);
may provide stronger optimization guarantees.
A read requires a valid source range.
For example:
u32 value = read_u32(input)?;
requires enough bytes in `input`.
The parser cannot merely trust a length field contained in the input.
A length field is treated as a claim until validated against the actual available range.
Reading a typed object from bytes requires:
sufficient size
alignment if required
valid representation
correct type
initialized storage
A buffer containing enough bytes does not automatically become a valid object of every type.
This distinction is explicitly highlighted in Chapter 6.
Cobalt distinguishes:
allocated
initialized
live
storage.
An allocation may contain storage that has not yet been initialized as a `T`.
Safe code cannot read an uninitialized `T`.
Low-level code may use:
MaybeUninit<T>
to represent allocated but uninitialized storage.
Example:
MaybeUninit<T> slot;
A value can only be converted to `T` after initialization has been established.
A typed write requires a valid writable destination.
buffer[index] = value;
is valid only if:
buffer is live
index is in bounds
destination is writable
value has compatible type
For a write beginning at offset `o` with size `n`, validity requires:
o <= capacity
n <= capacity - o
rather than calculating:
o + n
first and hoping it does not overflow.
This captures the complete-range requirement described in Chapters 1 and 7.
Every safe memory operation has an object identity.
The language does not define safety merely in terms of numerical addresses.
A valid operation must establish:
intended object
+
location within object
+
valid range
A correct size at an incorrect location is still invalid.
Chapter 8 explicitly identifies wrong base, wrong offset, wrong unit, wrong index, overflow, stale pointer, and wrong object as independent ways to obtain a wrong location.
Memory-related quantities have semantic units.
Examples:
elements
bytes
bits
characters
records
The type system should prevent accidental interchange where practical.
For example:
ElementCount
ByteCount
may be distinct standard-library types for APIs where unit confusion would be dangerous.
Every allocation has a unique logical identity.
An address does not identify an allocation independently of its lifetime.
After destruction:
old pointer
does not become a pointer to a new object merely because an allocator later reuses the same numerical address.
Safe Cobalt cannot produce:
Box<T> p = ...;
&T ref = p.borrow();
drop(p);
use(ref);
because `ref` prevents destruction while it remains live.
An object may be destroyed only when:
its owner is being destroyed
or through an explicitly defined ownership-consuming operation.
No ordinary function may arbitrarily destroy an object through a borrowed reference.
Shared ownership is represented by an explicit type:
Rc<T>
Arc<T>
`Rc<T>` is for single-threaded shared ownership.
`Arc<T>` is for thread-safe shared ownership.
The reference-count implementation guarantees that the object remains alive while valid owners exist.
Reference counting does not automatically solve cycles; cyclic structures require weak references or another strategy.
The source material identifies explicit lifetime management as the purpose of reference counting.
Weak<T>
does not keep an object alive.
Upgrading a weak reference produces:
Option<StrongReference>
rather than an invalid reference.
Ordinary references cannot be null.
Optional references use:
Option<&T>
Option<&mut T>
Pointers that may be absent use:
Option<*const T>
Option<*mut T>
or a dedicated nullable pointer representation where ABI requires it.
Because ordinary references are non-null, this is impossible in safe code:
*T
where `T` is absent.
C-like structures:
struct Point
{
int x;
int y;
}
Fields are initialized according to the type's initialization rules.
Uninitialized fields are not silently permitted.
Enums provide tagged alternatives:
enum Result<T, E>
{
Ok(T),
Err(E)
}
The compiler ensures that only valid variants are constructed.
Cobalt may provide:
union Value
{
int i;
float f;
}
but safe access requires a valid active representation.
For unrestricted representation-level union access, unsafe code may be required.
Every type has:
size
alignment
layout
valid bit patterns
The compiler may add padding according to target ABI rules.
The language does not permit arbitrary reinterpretation of a value as another type merely because their sizes match.
Explicit byte/bit operations are provided through:
Byte
ByteSlice
bit_cast<T>()
`bit_cast` is legal only when both types have compatible representation requirements.
Strings are not raw C character arrays.
Primary types:
String
StringView
A `String` owns its contents.
A `StringView` borrows them.
UTF-8 validity is maintained by the string abstraction.
Raw bytes use:
ByteSlice
rather than `StringView`.
Functions use C-like syntax:
fn add(int a, int b) -> int
{
return a + b;
}
Function types are first-class where supported.
Recoverable failures use:
Result<T, E>
Optional values use:
Option<T>
The `?` operator propagates errors:
fn load() -> Result<Data, Error>
{
Data d = read_data()?;
return Ok(d);
}
A panic is a controlled runtime failure.
Examples include:
unrecoverable bounds failure
explicit panic
assertion failure
Implementations may configure panic behavior as:
abort
unwind
subject to ABI/runtime restrictions.
A dynamic bounds violation in safe code produces a defined failure.
It cannot become arbitrary memory corruption.
The compiler may remove a runtime bounds check when static analysis proves the index valid.
For example:
for (usize i = 0; i < values.len(); i++)
{
process(values[i]);
}
may compile without repeated bounds checks.
Optimization must preserve semantics.
Cobalt supports parametric generics:
struct Box<T>
{
T value;
}
Generic constraints use traits:
fn print<T: Display>(T value)
{
...
}
Traits define behavior:
trait Display
{
fn display(&self) -> String;
}
Implementations:
impl Display for Point
{
fn display(&self) -> String
{
...
}
}
Generic code receives the same memory guarantees as concrete code.
A generic function cannot assume that:
T is Copy
T has trivial destruction
T has a fixed runtime size
T may be aliased
unless its constraints establish those properties.
Types may be:
Sized
Dynamically sized
A generic parameter is `Sized` by default unless explicitly declared otherwise.
A trait may declare operations requiring an unsafe implementation.
Example:
unsafe trait RawAllocator
{
...
}
The implementation is responsible for satisfying the documented invariants.
Modules use:
module graphics
{
...
}
Imports:
import graphics::Point;
Aliases:
import graphics as gfx;
Visibility is explicit:
public struct Point
{
...
}
Name resolution occurs before type checking.
Each local binding has a unique compiler identity.
Thus:
let value = 10;
{
let value = 20;
use(value);
}
use(value);
contains two distinct bindings.
Declarations are private by default.
Public declarations require:
public
A public API cannot expose inaccessible private implementation types.
Cobalt supports:
match result
{
Ok(value) => process(value),
Err(error) => report(error)
}
Pattern bindings are scoped to their match arm.
The compiler verifies exhaustiveness where required.
Local declarations:
let value = expression;
let mut value = expression;
A mutable binding permits reassignment.
Mutability is distinct from ownership.
Assignment:
value = expression;
is permitted only when `value` is mutable.
Assignment to a borrowed shared reference is forbidden.
Explicit moves use:
move value
Some contexts may infer moves automatically when the type is move-only.
Closures capture variables according to ownership rules.
A closure may capture by:
shared borrow
mutable borrow
move
The compiler selects or requires the appropriate capture mode.
A closure cannot outlive a borrowed variable it captures.
Iteration uses:
for (item in collection)
{
...
}
The iterator API defines whether iteration:
moves elements
borrows elements
mutably borrows elements
Cobalt's concurrency model is ownership-aware.
A value may be transferred between threads only when its type satisfies the required thread-safety traits.
Safe Cobalt cannot contain a data race.
The compiler prevents unsynchronized concurrent mutable access.
Shared mutable state requires explicit synchronization.
The standard library provides:
Atomic<T>
AtomicBool
AtomicInt
AtomicPtr
where supported by the target.
Memory ordering is explicit:
relaxed
acquire
release
acq_rel
seq_cst
Synchronization primitives provide scoped guards:
lock(mutex)
{
shared_state.update();
}
The guard automatically releases the lock when leaving scope.
Cobalt supports C interoperability through:
extern "C"
{
fn c_function(...);
}
FFI calls are unsafe unless wrapped by a safe abstraction whose invariants have been established.
C pointer parameters are represented explicitly:
extern "C" fn process(*mut CData);
The compiler does not assume that an arbitrary C pointer is safe.
C strings use explicit types:
CStr
CString
rather than treating arbitrary `char*` as safe strings.
A C string wrapper must establish:
valid address
valid lifetime
terminating NUL
maximum search range
FFI declarations must specify ownership where necessary.
For example, APIs may distinguish:
borrowed pointer
owned pointer
callee-owned result
caller-owned result
Safe wrappers must encode those rules.
Memory allocated by a foreign allocator cannot be released using an unrelated Cobalt allocator.
The corresponding release function must be used.
This prevents allocator-mismatch bugs identified in the source material.
Advanced code may define:
trait Allocator
{
fn allocate(Layout layout) -> Result<Allocation, AllocError>;
fn deallocate(Allocation allocation);
}
The `Allocation` object records the allocation identity and layout.
An interior pointer cannot be supplied where an `Allocation` is required.
An allocation consists conceptually of:
allocation_id
base
size
alignment
allocator
lifetime
The allocator may use any physical representation.
The logical identity is maintained by the language/runtime model.
An allocation may be deallocated only when:
caller owns allocation
allocation is live
allocator matches
pointer/handle identifies the allocation itself
no required references remain
Double deallocation is rejected by ownership rules in safe abstractions.
Destroying an object does not imply that its physical bytes are erased.
Explicit secure erasure is a separate operation:
secure_zero(bytes);
The source material explicitly distinguishes lifetime termination from physical erasure.
Device and memory-mapped regions are represented by explicit unsafe abstractions.
The implementation may provide:
Volatile<T>
Mmio<T>
to prevent ordinary compiler transformations from violating hardware semantics.
`volatile` access is not a general memory-safety mechanism.
It specifies observable access behavior for special memory.
It does not disable:
bounds checking
ownership
lifetimes
type checking
Compiler intrinsics may exist for:
SIMD
atomics
CPU instructions
memory barriers
ABI operations
Unsafe intrinsics require explicit unsafe context unless wrapped in a safe abstraction.
Constants:
const usize MAX_PACKET = 4096;
must be initialized with compile-time evaluable expressions.
Compile-time arithmetic is checked.
An overflowing constant expression is a compile-time error unless explicitly using a wrapping operation.
Array dimensions must be representable and non-negative.
For:
T[N]
the compiler must ensure:
N * sizeof(T)
is representable.
A type whose layout cannot be represented on the target is rejected.
This includes:
array size overflow
struct layout overflow
alignment overflow
padding overflow
By default, the compiler controls field layout.
An explicit C-compatible layout may be requested:
@repr(C)
struct Header
{
u32 length;
u16 type;
}
Packed structures require explicit annotation:
@repr(packed)
Access to misaligned fields may require special operations or unsafe code.
The compiler must not silently create invalid aligned references.
Safe serialization APIs operate on explicit byte ranges.
A serializer establishes:
output capacity
encoded size
representation validity
before writing.
Deserialization treats external lengths and counts as untrusted.
For:
u32 length = read_u32(input)?;
`length` is initially merely an input claim.
The parser must establish its relationship to:
remaining input
available output
maximum permitted size
before using it as a memory boundary.
This is a direct application of the source's "length fields are claims, not facts" principle.
Whenever a quantity influences memory, the compiler and/or API must preserve the relationship:
external value
↓
interpretation
↓
conversion
↓
arithmetic
↓
size / offset
↓
object
↓
range
↓
memory operation
An earlier validation does not automatically validate a later transformed quantity.
The following are treated as memory-relevant:
count
length
size
capacity
offset
stride
index
width
height
element_size
alignment
Their types and transformations must remain explicit.
The source material repeatedly emphasizes following such quantities through the complete chain rather than inspecting individual operations in isolation.
Every safe memory operation must establish five properties:
1. OBJECT
Which object is involved?
2. LOCATION
Where within that object?
3. AMOUNT
How much memory?
4. LIFETIME
Is the object currently alive?
5. OWNERSHIP / AUTHORITY
Does this operation have the right to access it?
This is the fundamental Cobalt memory rule.
A read is valid only if:
source object is live
source location is within object
source range is readable
source representation is valid
source value is initialized
A write is valid only if:
destination object is live
destination location is within object
destination range is writable
destination representation is valid
caller has appropriate ownership/borrow authority
A copy is valid only if:
source object is live
destination object is live
source range is readable
destination range is writable
source amount is valid
destination amount is sufficient
types/representations are compatible
overlap rules are satisfied
A pointer/reference derivation is valid only if:
base identifies a live object
offset is expressed in the correct unit
offset is representable
result remains within the permitted object range
A pointer calculation cannot create authority to access another object.
An access is invalid if:
object lifetime ended
regardless of whether:
old address still contains bytes
or:
allocator has reused the address.
Destruction is valid only if:
caller owns the object
or:
caller possesses an explicitly defined destruction capability.
A borrowed reference never implies ownership.
For a range:
[start, start + length)
the compiler/runtime must establish:
start <= capacity
length <= capacity - start
rather than relying on an unchecked sum.
A checked integer operation does not automatically make a memory operation safe.
The resulting quantity must still correspond to the actual object's capacity.
Likewise, a valid allocation does not prove that later code is using the allocation correctly.
This distinction is central to the source material's treatment of allocators.
For a container:
length <= capacity
and:
capacity * sizeof(T) <= maximum representable allocation size
must hold.
If an operation adds `additional` elements:
Without reallocation:
additional <= capacity - length
With reallocation:
new_capacity >= length + additional
and the new capacity must fit the target representation.
A safe container should not expose mutable independent:
pointer
length
capacity
fields to ordinary code.
These values must be maintained together by the container abstraction.
This is a deliberate structural response to the source material's recommendation to centralize resizing and preserve related invariants.
Operations that can invalidate storage must be rejected while incompatible borrows exist.
Example:
Vec<int> v;
Slice<int> s = v.slice();
v.push(10); // potentially reallocating
use(s); // rejected
A container may explicitly provide stable references:
StableVec<T>
if its implementation guarantees that inserting elements does not invalidate existing object locations.
Every collection API must document which operations invalidate:
references
slices
iterators
pointers
The compiler must enforce invalidation rules where they can be represented statically.
Safe mutable access follows:
many readers
OR
one writer
but not both simultaneously for overlapping memory.
This applies to:
references
slices
iterators
views
Cobalt may borrow disjoint fields independently:
&mut point.x
&mut point.y
because the compiler can prove that the memory regions do not overlap.
Distinct array elements may be mutably borrowed simultaneously when the compiler can establish distinct indexes.
For dynamically computed indexes, an API may require a checked splitting operation:
(left, right) = slice.split_at(index)?;
The language must make unsafe operations visually obvious:
unsafe
{
*raw = value;
}
Unsafe code cannot silently contaminate an entire module.
The boundary is lexical and reviewable.
An unsafe function documents preconditions.
Example:
unsafe fn from_raw_parts(*mut T pointer, usize length) -> MutSlice<T>;
Its contract may require:
pointer identifies a live allocation
pointer is correctly aligned
length does not exceed allocation capacity
elements are initialized
The compiler does not prove these conditions; the caller assumes responsibility.
Unsafe functionality should normally be encapsulated:
public fn parse_packet(ByteSlice input) -> Result<Packet, ParseError>
{
...
}
The unsafe portion is minimized and surrounded by checked invariants.
Every unsafe operation still has specified behavior where its preconditions are satisfied.
Violating an unsafe precondition constitutes a programmer error outside the guarantees of safe Cobalt.
Diagnostics should identify the violated invariant.
For example:
error[E0421]: index out of bounds
values[index]
^^^^^
index has type usize
index may be 10
values contains 10 elements
valid indexes are 0..10
Example:
error[E0310]: use of moved value
Box<Data> b = move a;
^ value moved here
use(a);
^ value used after move
Example:
error[E0412]: cannot mutably borrow while shared borrow exists
Diagnostics should identify both borrow sites.
Compile-time:
error[E0201]: integer overflow in constant expression
Runtime checked arithmetic produces a defined arithmetic failure rather than memory corruption.
error[E0204]: narrowing conversion requires explicit checked conversion
The compiler should explain the possible range loss.
Attempting:
*p
outside unsafe code produces:
error[E0501]: raw pointer dereference requires unsafe context
Safe APIs should make ordinary invalid frees unrepresentable.
If low-level allocator APIs are used incorrectly:
error: allocation ownership does not match deallocation
where statically detectable.
A conforming standard library should provide at least:
Box<T>
Rc<T>
Arc<T>
Weak<T>
Slice<T>
MutSlice<T>
Vec<T>
String
StringView
ByteSlice
MutByteSlice
MaybeUninit<T>
Result<T,E>
Option<T>
The standard library should provide safe equivalents of common C operations:
copy
move_range
fill
compare
search
split
subslice
read
write
These APIs operate on typed ranges rather than independent raw pointers and lengths wherever possible.
Cobalt may expose wrappers:
c_memcpy
c_memmove
c_memset
but they remain unsafe when called with raw pointers.
The preferred API is the Cobalt range-aware equivalent.
A generic function with the conceptual signature:
memcpy(void* dst, void* src, usize size)
cannot be a safe primitive.
Its safety requires independent proofs of:
source validity
destination validity
source size
destination size
size arithmetic
lifetime
aliasing
The language therefore uses richer types to carry those facts.
Memory safety does not require every access to perform a runtime check.
The implementation may use:
static proofs
range analysis
loop analysis
inlining
ownership analysis
bounds-check elimination
to remove checks.
The semantic guarantee remains unchanged.
When a safety property is statically provable, no runtime cost is required for that property.
Example:
for (usize i = 0; i < array.length; i++)
{
sum += array[i];
}
may compile to the same machine-level loop a careful C programmer would write.
When a property cannot be proven statically, the implementation may insert a runtime check.
Failure is defined.
It cannot become memory corruption.
The optimizer may assume all safe-language invariants hold.
It may not transform a safe program into one whose behavior violates those invariants.
Implementations should provide optional instrumentation for:
bounds
ownership
borrow/lifetime
integer overflow
uninitialized memory
FFI boundaries
Sanitizers are diagnostic tools, not substitutes for the language's safety model.
The standard toolchain should support fuzzing of:
parsers
deserializers
allocators
unsafe wrappers
FFI boundaries
The language's defined failure semantics make fuzzing failures distinguishable from arbitrary memory corruption.
The abstract memory model is:
Allocation
{
identity
type/representation
base
size
alignment
lifetime
permissions
}
An access consists of:
object
location
amount
access mode
An access is valid only if all corresponding constraints hold.
Memory permissions are conceptually:
Read
Write
ReadWrite
Move
Destroy
A borrow grants only the minimum required permission.
A shared reference grants:
Read
for its lifetime.
A mutable reference grants:
ReadWrite
for its lifetime.
It excludes incompatible aliases.
Ownership grants:
Move
Destroy
subject to the object's type semantics.
Only the owner, or an explicitly designated destruction mechanism, may end the object's lifetime.
A valid access must satisfy both:
spatial:
location + amount is inside object
temporal:
object is alive
Neither property substitutes for the other.
This is the combined model explicitly developed in Chapter 9.
Cobalt considers these independent security properties:
arithmetic correctness
allocation correctness
object identity
location correctness
range correctness
initialization correctness
lifetime correctness
ownership correctness
A program must preserve all of them.
Compiler diagnostics, static analyzers, and security tooling should prefer identifying the earliest violated invariant.
For example:
overflow
↓
wrong allocation
↓
wrong pointer
↓
wrong range
↓
out-of-bounds write
The preferred diagnostic is the earliest provable violation, not merely the final crash.
This reflects the central analytical approach of the source material.
For complex APIs, implementations may expose contracts such as:
count:
source = input
type = usize
invariant = count <= max_count
size:
derived = count * sizeof(T)
invariant = representable
allocation:
size = size
offset:
derived = index * sizeof(T)
invariant = offset <= capacity
access:
amount = sizeof(T)
invariant = amount <= capacity - offset
This is especially useful for static analysis and formal verification.
Cobalt libraries should prefer:
types that carry invariants
over:
integers that require callers to remember invariants.
Prefer:
ByteSlice
over:
void* + usize
Prefer:
Vec<T>
over:
T* + length + capacity
Prefer:
Box<T>
over:
T* + manually managed lifetime
An API should make invalid states difficult or impossible to represent.
For example:
copy(dst, src);
is preferable to:
memcpy(dst.ptr, src.ptr, length);
because the former keeps:
object
location
amount
lifetime
ownership
closer together.
Cobalt intentionally retains:
if (...)
for (...)
while (...)
struct
enum
fn
return
x[i]
x.field
x->field
but changes their relationship to memory.
In particular:
[] → checked range operation
& → borrow
move → explicit ownership transfer
Box<T> → owning heap object
Slice<T> → bounded borrowed range
*mut T → explicitly unsafe raw pointer
struct Packet
{
Vec<u8> data;
}
fn append(Packet&mut packet, ByteSlice input) -> Result<(), Error>
{
packet.data.reserve(input.len())?;
for (u8 byte in input)
{
packet.data.push(byte);
}
return Ok(());
}
There is no separate:
pointer
length
capacity
allocation size
that callers must keep synchronized.
fn parse(ByteSlice input) -> Result<Packet, ParseError>
{
u32 count = input.read_u32()?;
if (count > MAX_ENTRIES)
{
return Err(ParseError::TooManyEntries);
}
Vec<Entry> entries = Vec<Entry>::with_capacity(checked_cast<usize>(count))?;
for (usize i = 0; i < count; i++)
{
Entry entry = parse_entry(input)?;
entries.push(move entry);
}
return Ok(Packet{ entries });
}
The parser never treats an external count as automatically authoritative.
fn duplicate(ByteSlice input) -> Result<Vec<u8>, Error>
{
Vec<u8> result = Vec<u8>::with_capacity(input.len())?;
result.extend(input)?;
return Ok(result);
}
There is no independent destination pointer plus arbitrary copy length.
fn first(ByteSlice input) -> Option<&u8>
{
if (input.len() == 0)
{
return None;
}
return Some(&input[0]);
}
The returned reference cannot outlive `input`.
fn consume(Box<Resource> resource)
{
resource.use();
}
fn process()
{
Box<Resource> r = acquire();
consume(move r);
// r cannot be used here
}
The compiler enforces the transfer.
extern "C"
{
fn c_process(*const u8, usize);
}
fn process(ByteSlice input)
{
unsafe
{
c_process(input.ptr(), input.len());
}
}
The safe wrapper is responsible for proving that the C function's contract is satisfied.
fn call_c(ByteSlice input) -> Result<(), Error>
{
if (input.len() > MAX_INPUT)
{
return Err(Error::TooLarge);
}
unsafe
{
c_process(input.ptr(), input.len());
}
return Ok(());
}
The unsafe region is intentionally small.
In safe Cobalt, the following conceptual C patterns are rejected or replaced:
count * sizeof(T)
↓
wraparound
↓
small allocation
↓
large loop
↓
overflow
pointer + attacker_offset
↓
wrong location
↓
write
pointer + offset
↓
copy(pointer, length)
↓
offset + length > capacity
pointer
↓
free
↓
stale pointer
↓
use
pointer
↓
free
↓
free again
pointer
↓
wrong allocator
↓
release
These are not merely discouraged patterns; safe Cobalt's types and semantics are designed to prevent them.
Unsafe code exists for legitimate systems-level requirements:
operating-system interfaces
device drivers
custom allocators
hardware registers
FFI
specialized memory pools
SIMD
lock-free structures
runtime implementation
embedded systems
Unsafe code is not intended to be necessary for ordinary:
arrays
strings
network parsing
file parsing
serialization
dynamic containers
business logic
An unsafe function should document:
Safety:
required caller invariants
Example:
// Safety:
// `ptr` must refer to a live allocation containing at least
// `len` initialized T objects and must be correctly aligned.
unsafe fn from_raw_parts(...) -> Slice<T>;
A conforming implementation must:
1. reject programs violating static safety rules;
2. preserve ownership and lifetime semantics;
3. implement checked integer semantics;
4. enforce safe bounds;
5. prevent invalid safe memory access;
6. preserve Cobalt's defined failure behavior;
7. implement specified ABI rules;
8. provide required standard types and operations.
The specification does not mandate:
reference counting vs tracing GC
specific CPU
specific allocator
specific register allocation
specific optimizer
specific garbage collector for future GC types
provided the observable semantics and safety guarantees remain intact.
Cobalt's core ownership model is deterministic and does not require garbage collection.
A future garbage-collected type system may be added, but GC-managed references must remain distinguishable from ordinary ownership and borrowing.
GC cannot be used to weaken:
bounds
type safety
data-race safety
representation validity
Cobalt-native ABI is implementation-defined within a compiler release family unless explicitly marked stable.
C ABI compatibility is available through:
@repr(C)
extern "C"
and related annotations.
Language versions follow:
MAJOR.MINOR
Breaking semantic changes require a major version.
Additive syntax or library features may use minor versions where compatibility is preserved.
A compiler may not introduce a new language feature that silently weakens existing safe-code guarantees.
Any feature capable of bypassing those guarantees must be:
unsafe
or otherwise explicitly constrained.
For every successfully compiled safe program, the implementation guarantees:
No out-of-bounds memory access
No use-after-free
No double free
No invalid free
No dangling reference
No null reference dereference
No safe raw-pointer dereference
No data race
No unchecked integer overflow
No invalid typed read
No invalid typed write
subject to explicitly documented implementation bugs, hardware faults, or violations at unsafe/foreign boundaries.
For a valid safe Cobalt memory operation:
Object O
Location L
Amount N
Lifetime T
Authority A
the compiler/runtime establishes:
O is live
L belongs to O
L + N belongs to O
the operation is permitted by A
the representation is valid
the access does not violate aliasing rules
Therefore the operation cannot access unrelated or dead memory through ordinary safe semantics.
The language design maps to the source chapters as follows:
| Failure | Cobalt mechanism |
| ------------------------ | ---------------------------------------- |
| Integer overflow | checked arithmetic |
| Truncation | explicit checked conversion |
| Wrong allocation size | typed containers + checked layout |
| Wrong pointer arithmetic | bounded slices/restricted raw pointers |
| Wrong copy amount | range-based copy |
| Wrong read amount | validated source ranges |
| Wrong write amount | validated destination ranges |
| Wrong location | object-associated references/slices |
| Use-after-free | ownership + borrow checker |
| Double free | unique ownership |
| Invalid free | typed allocation ownership |
| Wrong allocator | allocator-associated allocation identity |
| Uninitialized read | initialization tracking |
| Data race | ownership + concurrency rules |
The source material's final model identifies these errors as interacting rather than independent categories; Cobalt therefore treats them as one memory model rather than eleven unrelated checks.
EXTERNAL DATA
│
▼
typed value
│
▼
checked arithmetic
│
▼
size / count / offset
│
▼
allocation
│
▼
object
│
┌─────────┴─────────┐
▼ ▼
location lifetime
│ │
└─────────┬─────────┘
▼
range
│
▼
access rights
│
▼
READ/WRITE
At every transition, Cobalt attempts to preserve the invariant.
Cobalt does not define memory as:
address + number of bytes
It defines memory as:
object
+
location
+
amount
+
lifetime
+
ownership
This is the fundamental design decision of the language.
The central rule of Cobalt is:
> **A value that describes memory must remain connected to the memory it describes.**
An integer is not automatically a size.
A size is not automatically an allocation.
An address is not automatically an object.
A pointer is not automatically valid.
A valid location is not automatically a valid range.
A valid range is not automatically live.
A reference is not ownership.
Ownership cannot be silently duplicated.
A successful allocation does not prove that later accesses are correct.
A successful arithmetic operation does not prove that its result describes the intended memory.
The language therefore attempts to preserve the relationship from:
value
↓
meaning
↓
object
↓
range
↓
lifetime
↓
authority
↓
access
through the type system, ownership system, borrow checker, runtime checks, and explicit unsafe boundaries.
That is the foundation of Cobalt 1.0.