Skip to content

CeeSafe for Rust programmers ​

CeeSafe's safety model is Rust's: single ownership, moves, shared and exclusive borrows, deterministic destruction, no garbage collector. What differs is how much you write down, where memory comes from, and how much the language does for you. This page assumes you know Rust well.

At a glance ​

RustCeeSafeNotes
let x = 1; / let mut x = 1;let x = 1; / var x = 1;
shadowing (let x = x + 1;)not allowedtwo locals in scope never share a name
&T, &mut Tborrow T, borrow mut T
f(&x), f(&mut x)f(x), f(mut x)a shared borrow is implicit at the call; a mutable one is marked
fn f<'a>(a: &'a T, b: &T) -> &'a Tfn f(a: borrow T, b: borrow T) -> borrow(a) Tthe result names its sources; no lifetime parameters
struct S<'a> { s: &'a str }struct S<region r> { s: StringView<r>; }only types that store references declare regions
Box<T>, Vec<T>, String, &strBox<T>, Vector<T>, String, StringView
Option<T>: Some(x), NoneT?: Some(x), nulla T converts to T? where one is expected
Result<T, E>, ?Result<T, E>, ?the same
.unwrap()! (postfix)r!, o!
panic! (unwinds by default)panics always abortno unwinding, no catch_unwind
impl Drop for Sdrop { … } inside the struct
trait, impl Trait for T, T: Traitinterface, impl Interface for T, T: Interfacedefault methods, Self, associated types and constants, supertraits
Box<dyn Trait>, &dyn TraitBox<dyn I>, borrow dyn I
fn f<const N: usize>(), f::<3>(), f::<{ X + 1 }>()fn f<const N: usize>(), f<3>(), f<Y>() with const Y: usize = X + 1;bool, integer and fieldless-enum values; a const fn is an ordinary fn
#[derive(Clone, Copy)]automatica struct is Copy when all its fields are and it has no drop
x as u8 (truncates)truncate<u8>(x)as only converts where nothing can be lost
wrapping_add, checked_add, saturating_addwrappingAdd, checkedAdd, saturatingAdd
|x| x + 1(x) => x + 1a closure literal borrows its captures and cannot escape
Box::new(move |x| x + n)Box.create(move (x) => x + n)!a Box<dyn fn(u64) -> u64>, which owns its captures
match x { A => …, }match (x) { E.A => { … } }variants are qualified; statement arms are blocks
Some(n) if n > 3 =>, A | B, 0..=9, x @ PSome(n) if (n > 3) =>, A | B, 0..=9, x @ Pthe guard is parenthesised
"get" =>, b"cjs" =>, [first, rest @ ..] =>"get" =>, "cjs" =>, [first, rest @ ..] =>a string pattern is typed by the value: a StringView or a Slice<u8>
if let Some(v) = e, while let …, matches!(e, P)if (e is Some(v)), while (e is …), e is Pone operator; it chains with &&
std::thread::spawn(move || …)Thread.spawn(move () => …)!with Arc, Mutex and atomics
Send, SyncSend, Share
println!("{}", x)Console.writeLine(x)no macros, no format strings yet
#![no_std]--profile freestanding
unsafe { } with a // SAFETY: conventionunsafe { } with a required /// Safety: doc linethe compiler enforces it
Cargo, Cargo.toml, Cargo.lockceesafec, ceesafe.toml, ceesafe.lockpath, git and registry dependencies
rustfmt, rustdocceesafec fmt, ceesafec doc

The same program ​

rust
fn longest_word(text: &str) -> Option<&str> {
    let mut best: Option<&str> = None;
    for word in text.split(' ') {
        if best.map_or(true, |b| word.len() > b.len()) {
            best = Some(word);
        }
    }
    best
}

fn main() {
    match longest_word("the quick brownish fox") {
        Some(w) => println!("{}", w),
        None => println!("empty"),
    }
}
ceesafe
fn longestWord<region r>(text: StringView<r>) -> StringView<r>? {
    var best: StringView<r>? = null;
    for (word in text.split(" ")) {
        match (best) {
            Some(b) if (b.length >= word.length) => {}
            _ => { best = word; }
        }
    }
    return best;
}

fn main() {
    match (longestWord("the quick brownish fox")) {
        Some(w) => { Console.writeLine(w); }
        null => { Console.writeLine("empty"); }
    }
}
text
brownish

The CeeSafe version loops over the split iterator where Rust would call map_or (there are no adapters), tests with a guard, returns explicitly, and its signature says the result views the input through the region r. With a single reference parameter, as here, the region could also be left out: fn longestWord(text: StringView) -> StringView? means the same.

Ownership and borrowing: what is the same ​

Moves, Copy types, "one writer or many readers", borrows ending at their last use, two-phase borrows for v.push(v.length), references that cannot outlive their referent, and destruction at scope end in reverse order: all as in Rust. Error messages name the borrow, where it was made and where it is used later.

What is different ​

Less annotation. A function that returns a reference names the parameters it may borrow from, -> borrow(a, b) T, instead of declaring lifetime parameters. Types that store references declare region parameters, and so do functions that pass such types through; plain functions over references never do. Regions are erased: Token<a> and Token<b> are the same type.

A simpler checker. CeeSafe's borrow checker is intraprocedural and keeps one set of loans per value, where Rust reasons about lifetimes. Borrows end at their last use, as with NLL, but some programs Rust accepts are rejected; the usual fixes are to end a borrow earlier, copy a value out, or use an index or a slot-map handle. Reference locals are let, never var: assigning to a reference writes through it.

Allocators per allocation. Every allocating type takes its allocator as a type parameter (Box<T, A>, Vector<T, A>), and every allocation returns a Result: there is no infallible push. Arenas, pools, fixed buffers and scratch frames are built in, and allocations from a region borrow it, so they cannot outlive it.

Checks in every build. Integer overflow panics in release builds too; there is no --release that turns it off. Bounds and overflow checks the compiler proves unnecessary are removed, and --explain-bounds-checks shows which.

Interfaces are traits without the corners. Interfaces have default methods, Self, associated types and constants, and supertraits, and impl<T> I for Wrapper<T> implements one for a generic type; there are no blanket impl<T: A> B for T and no impl Trait in argument or result position. A collection of different types is an enum or holds Box<dyn I>.

Closures borrow, unless they are boxed. A closure literal borrows what it captures and has a reference type (borrow fn(i32) -> bool, borrow mut fn(i32)), so it cannot be returned or stored. A move closure owns its captures and is boxed where it is made: Box.create(move (x: u64) => x + n)! is a Box<dyn fn(u64) -> u64>, and one that changes its captures a Box<dyn mut fn()>. There is no Fn/FnMut/FnOnce trait or impl Fn: a parameter takes borrow fn(…) or borrow mut fn(…).

Threads and tasks. Thread.spawn(move () => …) and Thread.both run threads; Arc, Mutex and lib/core's atomics share state, and Send and Share are Rust's Send and Sync. async fn and await run tasks on lib/std's executor, on one thread or several (type-system.md §38, §40).

No macros, no iterator chains. for walks ranges, arrays, slices, vectors and iterators: lib/core's Iterator<T> (fn next(borrow mut this) -> T?), through a bound I: Iterator<T> or a dyn Iterator<T>, and any struct with such a next. The item is an interface parameter, not an associated type: Iterator<u8>, not Iterator<Item = u8>. There are no adapters: a chain of map, filter and collect is the loop's body, and slices and vectors have sortBy, forEach, any, all and retain.

Through C. CeeSafe compiles to C11, which any C toolchain or kernel build can take, including MSVC and compilers for targets LLVM does not support. The generated C is readable and maps back to CeeSafe lines.

Kernel checking. Context effects (@context(atomic), @effects(sleep)) reject sleeping under a spin lock across calls, at compile time. Rust has no equivalent in the language.

Things you will miss ​

Rc (CeeSafe has Arc, and lib/core's Cell and RefCell, whose read() and write() are Rust's borrow() and borrow_mut(): type-system.md §41), macros, iterator adapters, blanket impls, impl Trait, shadowing, the standard library's breadth, and crates.io. Several are planned or recorded as open decisions.

Pre-1.0: the language and its tools are still changing.