/
Search
EN

Cheat sheet: what doesn't exist and common mistakes

kola is a small language, and most beginner mistakes come from habits carried over from C, C++, Rust or Python. This page lists what is deliberately missing, what replaces it, and the places where people stumble most often. For an unfamiliar term, see the glossary.

What doesn't exist, and what replaces it

Habit

In kola

null, null pointers

Option[T]. Absence is visible in the type.

Exceptions, panic!, unwinding

Result[T, E] with ? for expected errors. A violated assumption is a trap, a deterministic halt. A domain d { … } recover(t: Trap) { … } block narrows what a trap kills from the process to that block. A trap is still a bug, and kola audit counts every domain.

malloc/free, new/delete, GC

Regions. A region is a block of code that owns an arena, a chunk of memory handed out piece by piece and freed all at once: you allocate with alloc in r, and everything is freed at the closing brace. Long-lived graph-shaped data goes in a Pool[T].

Destructors, RAII, Drop (cleanup code that runs automatically when a value goes out of scope)

Linear resources. Each one is consumed exactly once on every path, and the compiler checks it.

Borrow checker, lifetimes 'a (Rust's machinery for proving references stay valid)

Region liveness is checked lexically. Within a region, shared mutation is allowed. The &mut rule is call-site exclusivity plus one view rule: while a view into a place is still used, that place is not changed through any other name.

Traits, interfaces, dyn, inheritance

Generics in square brackets, compiled into a separate copy per type they are used with, and fn references. impl blocks are namespaces only.

Closures, lambdas, .map().filter()

Fn references, which capture nothing, and ordinary for loops. There are no iterators: for walks a range a..b or a slice and nothing else.

Macros, #[derive], attributes

None. Every piece of meaning is in the text the reviewer reads.

unsafe, FFI, extern "C", raw pointers, inline asm

None, and no other way to leave the checked language. Hand-tuned machine code enters only as a named per-architecture leg (hw x86_64(...) = "symbol") whose kola body stays the definition and is tested against it. To reuse a C program, run it as a process.

Global variables, static, singletons

No global state. Every access to the world goes through a capability passed as a parameter. const is compile-time data. The one exception is the log effect, which is write-only and needs no capability.

Implicit conversions, as

Three explicit ones: widen[u64](x) (lossless), narrow[u8](x) returning Option[u8], and wrap[i16](x) (reinterpret the low bits; never traps). Writing as is a compile error.

size_of, alignof

kola never computes a layout; it claims one. layout_assert[T](SIZE, ALIGN); is a module-level item, and the C backend turns it into a _Static_assert.

Vec::remove(i), reserve, retain, dedup

Not there. The surface is push and insert(i, x) (which allocate), pop, truncate(n), clear() and swap(i, j) (which don't), the bulk methods extend_from_slice, resize and copy_from_slice, .len and indexing. To remove from the middle, swap with the last element and truncate.

Overloading, default arguments, <T>

No overloading and no defaults. Generics and regions use square brackets: [T], [r].

async/await, Mutex

No async. Threads and channels exist (sys.spawn, Thread[R], sys.chan, Tx[T], Rx[T]) and run as real OS threads; see Threads and channels. There is no Mutex because no reference ever crosses a thread: ownership moves instead, so there is nothing shared to guard.

What does exist, and is often assumed missing

  • Octal and binary literals: 0o644, 0b1010_1010, alongside decimal and hex. _ between digits is a separator.

  • Bitwise NOT: ~x, which is exactly x ^ all-ones of its width, signed included, and never traps.

  • f64: IEEE 754 binary64, bit-identical on every backend, with explicit conversions only (f64::of_i64, f64::to_i64 returning Option). NaN in any operation traps, f64 is banned in a ct fn, and there are no transcendental functions.

  • Byte strings: b"GET " is a [u8; 4], and &b"GET " is the &heap [u8] you pass to a byte API. Raw literals r"...", r#"..."# and br#"..."# hold text exactly as written, which is convenient for JSON and shell snippets.

  • Owned vectors: let mut v = Vec::new(); is an owned Vec (a growable array) whose storage is on heap, the region that lives as long as the program. Growing it is alloc(heap). For memory that comes back at a brace, allocate the Vec in a region instead.

  • Trap domains: domain and recover(t: Trap), on every executor.

  • Atomics, for memory shared with another core or a device. An atomic operation reads or updates a memory cell in one indivisible step, so another CPU core never sees it half done. Atomic::add64_relaxed(cells, i, 1u64) and its siblings work on a shared slice plus an index, on the hw effect. They are not how two kola threads talk; those move ownership.

  • The environment (the process's NAME=value variables): sys.env.get(b"HOME") returns Option[&heap [u8]]. It returns bytes, because an environment need not be UTF-8, and it is empty under kola test.

  • Unguessable randomness: Entropy::draw[[u8; 32]](&mut rng) and Rng::fill_os read OS entropy on every call. Rng::weak_u64 is a fast seeded stream for jitter and shuffling. Its state can be recovered from one output, so never use it for keys, tokens or nonces.

  • External programs: sys.proc.spawn(...) runs a binary the operator granted by name (--proc-bin ffmpeg=/abs/path). The returned Child is linear.

  • Attenuated capabilities: dial.to_host(host, port), dir.ro(), w.ro(), w.atomic(), and split() on Clock, Rng and Dial. A capability moves once, so you narrow it rather than copy it.

  • Sorting and copying: seq::sort_scratch is the O(n log n) stable sort; plain seq::sort is an insertion sort. bytes::copy(dst, src) copies slice to slice.

  • Integer limits: std::num::limits has U8_MAX … U128_MAX and I8_MIN … I64_MAX. There is no USIZE_MAX, because usize is 32 bits on a 32-bit target and the constant would mean different things in two builds of the same source.

  • Option helpers: o.take() moves the value out of a writable Option place and leaves None; o.with_mut(f) runs a named function on the payload in place.

  • The data-trust markers: untrusted on a parameter (data you must not trust), Secret[T] (data you must not reveal) and ct fn (code whose running time must not depend on secret values, because an attacker who can measure timing could otherwise recover them).

  • SIMD lanes: types that hold several numbers and operate on all of them in one instruction (SIMD: single instruction, multiple data). There are the constant-time lanes u32x4/u32x8, and the non-ct media lanes i16x8/u8x16/i32x4 with 256-bit twins.

  • Reports: kola report has checks, reachable, leaks, regions, effects, secrets, tainted-reqs, wipe-windows, stack-frames, deps, perf and more; kola help report lists them all.

Common mistakes

Most of these come with a compiler message that names the rule and the repair. The example below collects several of the repairs in one test:

use std::log;

fn count_spaces(line: &[u8]) -> u64 !{} {
    let mut n: u64 = 0;
    for b in line {
        if b == 32u8 { n = n + 1; }   // 32 is ' '
    }
    n
}

test the_usual_repairs {
    let mut v = Vec::new();
    v.push(7u64);
    v.push(9u64);
    let small: Option[u8] = narrow[u8](v[1]);
    assert small == Some(9u8);
    let wide: u64 = widen[u64](3u8);
    assert wide == 3;
    assert count_spaces(&b"GET / HTTP/1.1") == 2;
    region r {
        let s = f"len={v.len}";
        assert s.len == 5;
    }
    log::debug(f"total {v[0] + v[1]}");   // passed straight to a call: no allocation
}
  1. Interpolating in a plain string. A plain "..." is text and its braces stay braces. Interpolation needs the f prefix: log::info(f"x={v}"). Writing log::info("x={v}") with v in scope is refused, and the help line offers the f.

  2. Binding an interpolated string with no region open. f"..." allocates when you bind it. Its region comes from the destination's type, or else from the innermost open region. With neither, the compiler refuses it rather than putting it on heap silently. Passing it straight to a call-scoped &str parameter, as log::info(f"n={n}") does, allocates nothing. If you mean program lifetime, say so: let s: &heap str = f"...";.

  3. The wrong kind of reference in a parameter. Default to &T, which accepts stack and region values alike and allocates nothing. Use &r T only when the function stores or returns the reference.

  4. Expecting a region to be an expression. let x = region r { ... }; is refused. A region that is a function's last statement hands its tail back as the return value; anywhere else, assign an outer let mut inside the block.

  5. Forgetting mut on a growing Vec. let v = Vec::new(); v.push(x); fails with error[card §4]: `push` mutates an immutable receiver, and the help line tells you to write let mut v. Growing an owned Vec also needs alloc(heap) in the effect row.

  6. Reading a value after passing it by value. An owned Vec, a Map, and any value that carries one move at every by-value position: a call argument, a let, a struct field, Some(v), a channel message, a return. Reading the source afterwards is an error. Borrow instead (&v, &mut v) or stop reading it. Out of a reference (h: &H), a field cannot be moved at all; borrow &h.v.

  7. Reaching for a character literal. 'v' does not exist. Write the number with a comment, 118u8 // 'v', or compare against a byte string.

  8. Treating str as a byte buffer. A str is an immutable UTF-8 view. It has .len and a no-copy .bytes view, and no &mut form. Parse wire data as &[u8]. The only way to make a str from bytes is utf8::to_str, which validates and returns Option.

  9. An unsuffixed literal in a shift, or a counter that goes negative. An unsuffixed literal takes its width from context, and a width nothing pins is u64 (or i64 if a negative value ever enters it). So 1 << n shifts in 64-bit space; write 1u32 << n if you mean 32. And let mut c = 0; c = c - 1; traps at zero, because - on an unsigned width is checked. Annotate i64 where a value can go negative.

  10. Mutation inside assert. A proven assert is removed, so its expression must not change anything. assert get(&mut n); is refused with error[card §8]: assert expression mutates state. Bind first: let ok = get(&mut n); assert ok;.

  11. Shadowing a live resource. Ordinary let shadowing works as in Rust, but a live resource cannot be hidden this way. Consume it first.

  12. .. in a struct pattern. Every field must be named. Trailing commas are legal in any list.

  13. Looking for print. The compiler answers error[card §7]: `print` no longer exists — and neither does the `Io` capability. kola splits what print conflated. A diagnostic goes to the log, which needs no capability: use std::log; then log::info("..."), effect log. The program's output, the bytes another program reads, goes to a stream and needs one: sys.stdout.write(&bytes), effect io. A stray diagnostic on stdout would corrupt the output, which is why they are separate.

  14. A contract on attacker-supplied bytes. Mark a parameter untrusted when it carries data an attacker may control: bytes from the network, a file, standard input or another process. A req on it is then refused: error[card §8]: `req` depends on untrusted input `p`. A contract is a claim about the caller, and these bytes have no caller, so a violated one would be a remote denial of service. Check the value at run time and return Result or Option.

  15. fn main() -> i32. main returns u8, the eight bits a parent process can observe, or nothing at all. Its only optional parameter is sys: Sys by value. A computed exit code narrows explicitly: narrow[u8](n).

  16. {0} inside an f-literal. f"{0}" would interpolate the number 0, so it is refused. For a literal catalog placeholder, use a plain string "{0}" or escape the braces inside the f-literal: f"{{0}}".

  17. Inventing syntax. The most expensive mistake. If a construct is not on SPEC-CARD.md or STD-CARD.md, it is not in the language, however natural it looks.

Discarding a Result

One rule catches test bodies more often than any other: a Result cannot be thrown away.

pub fn drop_it(c: Conn) !{io} {
    c.close();
}

The compiler answers:

error[card §3]: a `Result` cannot be discarded by an expression statement
  = help: handle it — `match`, `?`, or `.unwrap()` — or discard it deliberately with `let _ = ...;`

The four repairs are not equivalent. match says what each outcome does, ? propagates the error, .unwrap() traps on failure (a claim that it cannot fail), and let _ = ...; discards on purpose, visibly. Here is the match:

use std::log;

pub fn hang_up(c: Conn) !{io, log} {
    match c.close() {
        Ok(_) => {},
        Err((_, os)) => log::warn(f"close failed: {os}"),
    }
}

The rule covers an expression statement and the tail of a block whose value nobody receives: a while, for, loop, region, domain, recover or test body. Option is exempt, because a None is an answer while an Err is an ignored failure. let _ = does not excuse an unconsumed resource; that is a separate rule and it still applies. In a test, prefer the match with Err(_) => { assert false; }: a test that ignores a refusal passes for the wrong reason.

If the compiler complains

Every diagnostic cites a section of the card, as in error[card §5]: .... Open that section before trying variants at random; the format is explained in the quick start. Most diagnostics also carry a help: line with the repair. If you think you have found a bug or a hole in the language, reduce it to a minimal file and check docs/confusion-log.md in the repository, which is where such findings are recorded. Treat an explanation you came up with from one case as a guess until the minimal file confirms it.