CobaltC

Preface

Within the narrative and theoretical scope of the book, this appendix serves primarily to demonstrate how a memory-safe, C-successor language ought to define its semantic boundaries, rather than to provide a drop-in replacement for a rigorous ISO-level standard.

In this context, CobaltC can best be described as a systems language designed to address the safety problems of C while adopting a more robust structural model.

If nothing else, it illustrates the exact pain points and formal guarantees required to make a safe systems programming language actually function.

As argued throughout The Wrong Memory essays, attempting to tack a "safe subset" onto an inherently unsafe language usually fails because the core language semantics still allow invariant violations. By embedding ownership and borrowing into the core type system, CobaltC makes safety an intrinsic property of the language rather than an optional discipline imposed upon it.

CobaltC unashamedly draws on the ownership and memory-safety techniques demonstrated by Rust. Although Rust isn't the origin of most of these ideas, its significant contribution was demonstrating how they could be combined into a practical, high-performance systems language. CobaltC adopts that safety architecture without attempting to reproduce Rust's syntax or overall language philosophy. Instead, it applies the broader body of ideas to the problem identified in Appendix II: making semantic invariants such as ownership, lifetime, aliasing, mutability, and bounds properties that the compiler can represent and enforce, rather than obligations the programmer must maintain by convention.

The CobaltC Proposition

The distinction is perhaps easiest to see in a deliberately small example. Allocate some storage, keep a second name for it, destroy the storage, and use the second name.

C
#include <stdlib.h>

int main(void)
{
    int* data = malloc(3 * sizeof *data);
    data[0] = 7;

    int* alias = data;    /* a second name for the same storage */
    free(data);           /* the storage is gone... */

    return alias[0];      /* ...but this still compiles, and reads
                             the wrong memory */
}

C accepts this program without complaint — GCC and Clang compile it silently even with every common warning enabled. At best, a modern compiler may warn about a case this obvious; move the free behind one function call and the warning disappears, while the bug does not. The program compiles, runs, and reads the wrong memory. The fact that alias still refers to freed storage exists only in the programmer's head.

C

"You have the power. Don't screw it up."

CobaltC
import std;

fn main()
{
    Vec<i32> data = Vec::new();
    Vec::push(&mut data, 7);

    ref<i32, shared> alias = &data[0];   // a second name for the same storage
    drop(data);                          // ✗ rejected before the program runs:
                                         //   diag.destroy-while-aliased
    printf("%v\n", *alias);
}

The same program, the same shape — allocate, alias, destroy, use — and it does not run. The fact that alias still points into data lives in the language, so destroying data underneath it is refused before the program starts, with a named diagnostic: heuristic warnings on one side, a semantic guarantee on the other.

CobaltC

"You have the power. Let’s prove you’re exercising it correctly."

The guarantee is not bought by removing capability. Below, two exclusive (mutable) references into the same array are held at once — something safety-minded languages commonly refuse — legal here because the compiler has proven they touch disjoint elements. CobaltC forbids the crime, not the capability.

CobaltC
import std;
fn main()
{
    array<i32, 2> values = [0, 0];

    // Two mutable (exclusive) references into the same array, allowed
    // because their targets are statically proven to be disjoint
    // (pointing both at values[0] is rejected before the program runs:
    // diag.aliasing-conflict).
    ref<i32, exclusive> a = &mut values[0];
    ref<i32, exclusive> b = &mut values[1];

    *a = 7;
    *b = 8;
}

CobaltC retains the same fine-grained control, while allowing the compiler to establish the relevant safety properties: a and b refer to distinct elements of values, their mutable accesses are disjoint, and the underlying storage remains owned by values.

This guide describes the CobaltC specification, edition 2026.101401.

Introduction

Why CobaltC exists, what it promises, and which features were chosen to keep those promises.

hello.cbruns
import std;

fn main()
{
    printf("hello, world\n");
}
output
hello, world

What CobaltC is

CobaltC is a small systems programming language with a C-family surface syntax. It is designed around one idea: the facts that correct low-level code depends on should live in the language, not in the programmer's memory. When you write C, you have to remember that a pointer is still valid, that an integer cannot overflow here, that a buffer has been initialised, that nobody else is writing to that struct right now, and that this handle is closed exactly once. CobaltC's goal is that every such fact is either represented and checked by the language, or explicitly marked as trusted by you.

CobaltC is not a safe language obtained by removing low-level capability. You still get deterministic control over resources, raw storage, foreign functions, threads, and predictable data layout. What changes is that each of those capabilities comes with a precise account of what is checked, when it is checked, and what you are asserting when the language cannot check it.

The two implementations: coby and cobc

CobaltC ships with two implementations, both written in Rust, and they share one front end — the same parser, name resolution and static checks — so they accept and reject exactly the same programs, with the same diagnostics.

coby is the reference interpreter. It runs a program straight from its source on the abstract machine the specification defines, performing every check the specification requires on every operation. It is the specification's meaning made executable: when this guide says a program prints something or stops with a diagnostic, that is what coby does. There is no build step, so it is the quickest way to run a program or try an idea.

cobc is the native compiler. After the shared front end has checked the program, it translates it to plain C and compiles that with the system's C compiler (GCC or Clang) against cbrt, its run-time library. A compiled program keeps every run-time check the specification requires except those the compiler proves can never fail — the rest becomes ordinary C — so it runs many times faster than the interpreter while behaving identically. cobc --keep-c leaves the generated C beside the executable if you want to read what your program became; Reading the C that cobc generates, below, says what is in it and what you can do with it.

The two are kept honest against each other: the test suites require cobc's executables to match coby's output and diagnostics byte for byte on every program, under both GCC and Clang — including every complete example in this guide. Use coby to run a program at once; use cobc to build an executable, or to see how fast the program can be.

On Linux the two work side by side, and everything in this guide runs under either. On Windows, coby runs programs today, and the formatter cobfmt (below) runs there too; cobc does not yet follow them there.

Toolchain
CobaltC's interpreter (coby) and compiler (cobc) are available here; the repository's README says how to build them.

The objectives

The language design was derived from a short list of objectives, in priority order. Where two objectives conflict, the earlier one wins.

  1. Preserve semantic invariants. Arithmetic stays in range, references point at live objects, a resource has exactly one owner, an object is initialised before it is read, two threads never race on one location, and external data is treated as a claim until validated.
  2. Safety of valid safe programs. A well-formed program that writes no unsafe block of its own cannot violate any of those invariants on any conforming implementation. The standard library's own bodies do use unsafe (a Vec writes its elements through a raw pointer); those blocks are the implementation's to keep true, each listed in the specification with the fact that justifies it, so they cost the program nothing (§20, Who asserts what). If the language cannot establish a fact, the operation is one of: statically rejected, dynamically checked, explicitly fallible (returns a Result or Option), explicitly trusted (inside unsafe), or simply not provided.
  3. Consistency and composability. A handful of mechanisms that combine cleanly, instead of many overlapping features. Every value-placing construct (declarations, arguments, fields, returns) follows one rule; every access (through a name, a field, an index, a reference) follows one rule.
  4. Formal precision and deterministic diagnostics. The specification is written as rules, so two implementations must reject exactly the same programs and report the same named diagnostic. Every error you see has a stable identifier such as diag.aliasing-conflict.
  5. A small language. Every feature starts as absent and must earn its place. The result is a language you can hold in your head: no traits, no inheritance, no exceptions, no implicit conversions, no operator overloading, no macros.
  6. Low-level capability. Raw pointers, manual allocation, FFI, threads, and defined representation are all available, gated by unsafe where the language cannot check them.

Which features were chosen, and why

Each mechanism in CobaltC exists because a specific invariant needed a home. The table below is the map from objective to feature; the rest of this guide explains each feature in turn.

Invariant to protectMechanism CobaltC providesWhere to read
Arithmetic stays in rangeInteger + - * and / % are checked: overflow is a fault, never silent wrap. Wrapping, saturating and checked_ families are explicit opt-ins. There are no implicit numeric conversions; widen, narrow, reinterpret and friends are named calls.§06
A resource is owned once and released onceResource types (anything that owns something, marked resource or containing a resource) have a single holder. Assigning or passing one moves it. The old name becomes stale. Destruction is automatic at scope end, in reverse order, running the type's drop destructor.§07
No conflicting simultaneous accessTwo reference modes: ref<T, shared> (read-only, any number) and ref<T, exclusive> (read and write, alone). Conflicts are checked at every use, so a stale or conflicting reference is always caught. There is no mut on variables: mutability is a property of the access path, not the binding.§08, §09
A reference never outlives what it points atReferences cannot be returned or stored beyond their referent's scope; the obvious cases are rejected at compile time, the rest at run time. Functions with one reference parameter may return a reference without annotations.§10
No read of uninitialised storageDefinite assignment: a variable must be written on every path before it is read. Aggregates are initialised whole.§11
Types mean what they sayNominal typing, no subtyping, no coercions. Generics are monomorphic: each use with concrete types becomes its own copy of the function, so there is no run-time type information; a type parameter's operations come from one of five built-in bounds. Type inference is local: auto infers from the initialiser, and generic arguments come from arguments or the expected type.§12
Evaluation order is definedStrict left-to-right evaluation everywhere. Blocks, if and match are expressions.§13, §14
Function interfaces carry the whole contractA by-value resource parameter takes ownership; a shared reference can only read; an exclusive reference can mutate but not destroy. Closures declare their captures and the language checks the list.§15
Aggregates are whole and exhaustiveStructs, fixed arrays and enums with payloads. Array indexing is bounds-checked. match must be exhaustive.§16
Names are unambiguousModules with export/import, private by default. Name resolution is a fixed, documented order.§17
Failure is never silentTwo channels only: a checked fault is fatal and unwinds destructors; recoverable errors are Result values with the ? operator. There is no catch.§18
Threads do not racespawn/join with owning handles; a mutex whose interior is reachable only through a lock guard. The same aliasing rules apply across threads. A fault in a spawned thread ends that thread alone and reaches its supervisor as a value; any wait can be cancelled; deadlocks are reported.§19
External data is a claim, not a factunsafe blocks record what you assert. rawptr<T>, reclaim, allocate and unsafe extern fn are the only doors to the outside, and FFI signatures are restricted to plain types.§20
A text literal is a value; owned text is a resourceA string literal "…" is a str: a copyable value like an integer, valid UTF-8 by construction, never owned or freed. printf writes one. Owned, growable text is String, a resource: moved, destroyed at scope end, built from a str (a literal written where a String is declared becomes one there) or validated from bytes.§21
The library proves the language is sufficientVec<T>, String and Rc<T> are written in CobaltC itself, using nothing the language does not give you.§21

What CobaltC deliberately leaves out

These are considered decisions, not gaps. Each one either duplicated an existing mechanism or would have hidden a semantic fact the language wants to keep visible.

  • Implicit conversions and subtyping. i32 and i64 never mix silently.
  • User-defined traits or bounds. A type parameter's operations come from five built-in bounds — eq, ordered, number, integer, clone (§12) — and nothing else: there is no way to declare a new bound. Your own struct or enum satisfies clone alone, by its fields or its own T::clone; to compare or combine values of your own types in generic code, pass the operation in as a function value.
  • Exceptions. A fault terminates the program after running destructors; expected failure is a Result.
  • Garbage collection. Ownership and scope-end destruction are deterministic; Rc<T> is a library type.
  • Character literals, and indexing or slicing a str. A str is a whole value; its bytes are reached with str_byte, and a single byte is written b'a': a u8, or the wider unsigned type its context asks for (§06). A String is sliced into views, &s[i..j], and taken apart by String::split, find and chars (§21). Text that arrives from outside is still bytes until String::from_utf8 validates it.
  • Method-call syntax. A call names the function's owner and writes the borrow it takes: Vec::push(&mut v, x), never v.push(x). The mode of every access path is visible where the path is formed, and . means one thing, field access. This is final (CHG-0132): no receiver sugar, no uniform call syntax, no auto-borrowing dot, no chaining.
  • A mut qualifier on bindings, switch and goto, import … as renaming, partial initialisation, lifetime annotations, atomics.

How to read this guide

The next section is a complete program you can read top to bottom to absorb the syntax. After that there is one section per chapter of the normative specification, numbered the same way (§06 to §22) so you can cross-reference. If you are new, a good order is: the tour, then §22 (syntax reference), then §06 onward. Each section ends with a short list of rules of thumb.

Every code block that is a complete program has been run through both implementations, the reference interpreter coby and the native compiler cobc, which agree on every one. A green runs badge means it executes to completion; a red badge names the diagnostic the language reports for that program. Diagnostics are labelled static (the program is rejected before it runs) or dynamic (a checked fault at run time); where a check is static only when the operands are literal, both labels may appear in the text. Comments inside examples use ✓ for accepted lines and ✗ for rejected ones.

The language and its standard library are still moving: an edition may change what an earlier one accepted, and the specification's register says so in each such edition's row. When a part is offered as stable it is first a candidate for thirty days, then stable, and after that a program written against it keeps compiling and keeps doing what it did, apart from a repair that closes a soundness hole or a change announced a release ahead (spec/STABILITY.md).

Running a program

Save a program as a .cb file. coby program.cb interprets it; cobc --run program.cb compiles it to native code and runs it, and cobc -o program program.cb keeps the executable. Both take the program's entry file (it may name other files as modules, §17), both exit with status 0 when the program runs to completion, and both print a rejected program's diagnostic the same way and exit with status 1. coby --check program.cb (or cobc --check) runs only the checks made before a program starts, and prints the same diagnostic for a program they reject; nothing runs, so a program that reads input, writes files or serves the network can be checked without doing so. Given a directory, it checks every .cb file in it.

There is no project file and no build step besides these commands. A program that spans several files names them itself: module parser "./parser.cb"; loads a file as a module, its path relative to the file that says so (§17), and extern "./fast.c"; or extern "z"; links C code of the program's own or an installed library (§20). cobc main.cb then builds the whole program from that one file, and coby main.cb runs it, with nothing else to keep in step (D-0231).

Testing a program

Tests live beside the code they test, in unittest blocks: a block at the top level of a file or a module, with an optional name. A test sees the module's private items, and is checked every time the program is, so a test that no longer compiles stops the build at once. Nothing runs it until you ask for a test run.

port.cbruns
import std;

fn parse_port(StringView s) : Option<u16>
{
    match (StringView::parse<u32>(s))
    {
        Ok(n)  : if (n > 65535) { None } else { Some(narrow<u16>(n)) },
        Err(_) : None,
    }
}

unittest "parse_port: bounds"
{
    assert(parse_port(StringView::of("0")) == Some(0));
    assert(parse_port(StringView::of("65535")) == Some(65535));
    assert(parse_port(StringView::of("65536")) == None);
}

unittest
{
    assert(parse_port(StringView::of("x")) == None);
}

fn main()
{
    printf("%v\n", parse_port(StringView::of("8080")) == Some(8080));
}
output
true

coby port.cb runs the program as always. coby --unittest port.cb runs its tests instead of main, each in a thread of its own: a failed assert, or any other fault, is reported, and the next test still runs. cobc --unittest --run port.cb does the same with the compiled program, and reports it identically; cobc --unittest -o port_tests port.cb keeps the test program.

coby --unittest port.cb
test port.cb:12 parse_port: bounds ... ok
test port.cb:19 ... ok
2 tests: 2 passed, 0 failed

The exit status is 1 when a test failed, so a test run can gate a build. --unittest-filter TEXT runs only the tests whose name contains TEXT. A file that holds only a library and its tests needs no main. A /// comment on a test right after a function makes it that function's example in the generated documentation (D-0230).

All code in this guide uses Allman brace style (every opening brace on its own line) and four-space indentation. That is a documentation convention: CobaltC ignores whitespace between tokens, so any brace placement is equally valid.

cobfmt, the formatter, writes that style for you: cobfmt program.cb rewrites the file in place, cobfmt --backup src/ does every .cb file under src/ and keeps each changed file's previous contents as NAME.cb.format.bak, and cobfmt --check src/ only lists the files it would change (for a test or a commit hook). It changes layout and nothing else: indentation, braces, spacing, blank lines, the : of a match's arms and of a bitstruct's fields aligned in one column, and trailing comments kept in theirs. Before writing, it checks that the result is the same program token for token, so formatting can never change what a program does. It keeps your line breaks — a short if (n == 0) { return; } stays on one line, though a function's body always opens on its own line — and a line longer than 120 characters that holds a list of arguments, parameters or array elements is broken one item per line. --width N sets another width, and --no-wrap leaves every line as long as it is. cobfmt --help lists the rest. Like coby, cobfmt runs on Windows as well as Linux, and it keeps a file's Windows line endings (\r\n) as they are.

Reading the C that cobc generates

cobc does not produce machine code itself. Once the shared front end has checked your program, cobc translates it into one C file and hands that file to GCC or Clang, which builds the executable against libcbrt.a, the run-time library. Normally the C file is deleted afterwards; cobc --keep-c -o prog prog.cb keeps it as prog.c, beside prog (without -o, a.out gets a.c). Under --run without -o it lives in a temporary directory and is deleted with it. If the C compiler ever rejects the file, cobc keeps it whatever you asked and prints its path.

The file is the complete translation of your program, not an excerpt: nothing but libcbrt.a is needed to build it. Even a seven-line program comes to a few hundred lines, because the file carries everything it uses, in this order:

  • The run-time interface, cbrt.h, copied in at the top: the C declarations of every cb_… function the program calls.
  • /* ---- types ---- */: each struct, enum and generic instantiation as a C struct, each followed by a _Static_assert that its size and alignment are the ones the specification fixes. Vec<i32> becomes struct s_std__Vec_Li32_R.
  • /* ---- string literals ---- */: your text literals as byte arrays.
  • /* ---- functions ---- */: one C function for each function the program actually uses, std's included, and one copy per instantiation of a generic one (f_std__Vec__push_Li32_R is Vec::push for i32). Your main is f_main; every other function is static, so the C compiler may inline it where it is called.
  • Call thunks and type descriptors: what fn values and the run-time checks need to know about each type.
  • int main, last: it hands the program's arguments to the runtime and starts f_main.

Inside a function, the C follows your statements one by one, and every run-time check the specification requires is an explicit line, so you can see exactly which checks your code still pays for. This is printf("%v\n", v[0]); with v a Vec<i32> (without the #line directives described below):

from prog.c
cb_at(0u, 7u);                                   // now at line 7 of file 0, for diagnostics
struct s_std__Vec_Li32_R *v9 = &(l_v_1);
if (t8 >= v9->len) cb_fault("diag.index-out-of-bounds", CB_NOLOC, 0);
int32_t *e10 = v9->ptr + (int64_t)t8;            // the element's address
int32_t t11 = (*e10);

Elsewhere you will meet cb_borrow, cb_read and cb_write (the access and aliasing checks of §08–§09), __builtin_add_overflow and its relatives (checked arithmetic, §06), and cb_frame_push, cb_stmt_push and their _pop partners (the bookkeeping that runs destructors at scope end). A check the compiler has proved can never fail is simply absent.

Each line of C made from one of your statements is preceded by a directive such as #line 7 "prog.cb", which tells the C compiler which line of your file the next line of C came from. The C compiler's own messages, a debugger and a sanitizer then name prog.cb:7 rather than a line of prog.c; code made for std keeps the C file's own numbering. cobc --no-line-directives leaves the directives out, for reading the C on its own terms.

What you can do with it

  • Read it to see how a construct is compiled, which checks a loop costs, or why one way of writing it is faster than another. Performance (chapter P) collects what such reading and measuring found.
  • Build it yourself, with your own flags. cobc's own build of prog.c is the command below, and cobc -v prints the exact one it runs, with the real path of libcbrt.a (and any extern C file's compilation), ready to paste and change; -O2 is the default level and libcbrt.a sits beside the cobc executable. Add -g -O0 for a build to step through in a debugger, -fsanitize=address,undefined to have the sanitizers watch it, -march=native or -flto, or leave out -w to see the C compiler's warnings, which cobc silences.
  • Debug it. cobc -O0 -g prog.cb compiles with debug information and keeps it in the executable; gdb prog then stops at and steps through the lines of prog.cb, and a sanitizer build (--cc naming a wrapper that adds -fsanitize=address) reports a memory error in unsafe code at its .cb line. perf annotate shows the C beside them.
  • Report a compiler bug. If GCC or Clang rejects the generated C, that kept file and the C compiler's message are the whole report.
building prog.c by hand (Linux; clang takes the same flags)
gcc -std=gnu11 -O2 -ffp-contract=off -fno-optimize-sibling-calls -w prog.c \
    -L /path/to/cobc/dir -lcbrt -lpthread -ldl -lm \
    -Wl,--gc-sections -Wl,--strip-debug -o prog

Keep -std=gnu11 and the two -f flags when you change the rest; each is part of the program's meaning. -ffp-contract=off stops the C compiler fusing a * b + c into one fused multiply-add with a single rounding, since the specification rounds each operation on its own (§06). -fno-optimize-sibling-calls keeps every call a real call, so a recursion too deep for the stack stops with the same diagnostic it gets from coby, instead of being turned into a loop that runs to completion. A program that links its own C code (extern "./mine.c";, §20) also needs that code compiled and added before -lcbrt; cobc -v shows both commands. The last two options only make the executable smaller: they leave out the parts of the runtime the program does not use, and the runtime's debug information (a hello world is about 0.8 MB with them, 5.9 MB without); drop them if you link a -g build to debug. cobc -s also leaves out the symbol table (--strip-all in place of --strip-debug, about 0.6 MB), which only profilers and debuggers read.

Watch out
The generated C is for reading and experimenting, not for keeping or editing by hand. Its names are internal and change between versions of cobc, and it is only correct with the libcbrt.a built alongside the same cobc. cobc refuses a mismatched runtime, but a build by hand skips that check.

Why compile to C

Compiling through C is a long tradition rather than a shortcut: C++ began as Cfront, which translated it into C, and Nim, Vala, Cython and several Scheme compilers work this way today. Few compilers write machine code entirely on their own; most hand the last step to a shared back end such as LLVM or GCC's. C as the target brings:

  • Portability for free. Wherever there is a C compiler, the language can run.
  • Decades of optimisation for free. GCC's and Clang's -O2 do register allocation, inlining, vectorisation and instruction scheduling better than a young compiler could.
  • Readable output. You can see what your program became, which is what --keep-c is for; LLVM IR or assembly would be far harder to read.
  • Easy C interoperation. Calling C functions and linking C libraries is natural, which is what unsafe extern fn (§20) needs.

What it costs

  • C's own rules come along. The generated C must avoid C's undefined behaviour, and it has to keep up as C compilers grow stricter: code an older compiler accepted with a warning, a newer one may reject outright.
  • Less control. Some things C cannot say directly (exact stack layout, particular calling conventions, precise debug information), so they are worked around with flags such as -ffp-contract=off above.
  • Slower builds. Every build includes a full C compilation.
  • Debuggers show C lines, not CobaltC lines. cobc emits no #line directives, so gdb steps through the C; the cb_at(file, line) calls lead back to your source.

For CobaltC the trade is a good one. Its central promise is that coby and cobc behave identically, and letting GCC and Clang do the machine-code work leaves cobc free to get the semantics and the checks exactly right.

A tour in one program

A complete, runnable program that introduces the syntax and the main capabilities, and the mistakes the compiler stops, before the reference sections begin.

Here is a complete program. Read it once for the shape of the syntax; every construct it uses has its own section later. It runs under both the interpreter and the compiler and prints the output shown below the code.

tour.cbruns
// ------------------------------------------------------------------
// A first CobaltC program: a temperature log.
//
// One page that touches most of what CobaltC is for: structs, an enum
// with payloads and `match`, a resource with a destructor, Vec, String
// and HashMap, references the compiler checks, slices, Option, Result
// and `?`, checked arithmetic, generics, a closure, and two threads
// sharing a counter through a mutex.
// ------------------------------------------------------------------

import std;

// A module. Its `export` items are reached as `limits::COLD`; the rest
// stay inside. A `const` is a value fixed when the program is compiled.
module limits
{
    export const i32 COLD = 150;                          // tenths of a degree
    export const i32 HOT = 300;
}

// A plain struct: copied on assignment, comparable with ==.
struct Reading
{
    i32 tenths;                                           // temperature in tenths of a degree
    u8 sensor;
}

// An enum. Variants may carry a payload.
enum Verdict
{
    Cold(i32),
    Fine,
    Hot(i32)
}

// `ref<Reading, shared>` is a read-only reference. `if` is an expression,
// so the function's value is whichever branch runs.
fn judge(ref<Reading, shared> r) : Verdict
{
    if (r.tenths < limits::COLD)
    {
        Cold(limits::COLD - r.tenths)
    }
    else if (r.tenths > limits::HOT)
    {
        Hot(r.tenths - limits::HOT)
    }
    else
    {
        Fine
    }
}

// A resource: something that owns other things. It is moved, never
// copied, and destroyed exactly once, automatically, at scope end.
resource struct Log
{
    Vec<Reading> entries;
    HashMap<String, u32> per_sensor;                      // readings counted by sensor name
}

// The destructor. It is never called directly; the language runs it.
fn Log::drop(ref<Log, exclusive> self)
{
    printf("L%d\n", Vec::len(&self.entries));             // printf's format is checked when compiled
}

fn record(ref<Log, exclusive> log, i32 tenths, u8 sensor)
{
    Vec::push(&mut log.entries, Reading { .tenths = tenths, .sensor = sensor });
    *HashMap::entry(&mut log.per_sensor, sprintf("sensor-%d", sensor), 0) += 1;
}

// A slice is a borrowed run of elements. Arithmetic never wraps
// silently: `checked_add` gives None on overflow, and `?` on an Option
// returns that None from this function at once.
fn average(slice<Reading, shared> rs) : Option<i32>
{
    if (slice_len(rs) == 0)
    {
        return None;
    }
    i32 total = 0;
    foreach (r in rs)
    {
        total = checked_add(total, r.tenths)?;
    }
    Some(total / narrow<i32>(slice_len(rs)))              // `narrow` checks that the value fits
}

// A fallible function returns a Result. Err carries the offending byte.
fn digit(u8 b) : Result<i32, u8>
{
    if (b > b'9' || b < b'0')
    {
        return Err(b);
    }
    Ok(widen<i32>(b - b'0'))                              // widening is always safe, still spelled out
}

// `?` unwraps an Ok, or returns the Err from this function at once.
fn parse_two(u8 hi, u8 lo) : Result<i32, u8>
{
    i32 h = digit(hi)?;
    i32 l = digit(lo)?;
    Ok(h * 10 + l)
}

// A generic function: T is any integer type, fixed at each call.
fn larger<T: integer>(T a, T b) : T
{
    if (a > b) { a } else { b }
}

// Runs on its own thread: counts the hot readings in its half of the
// log into a counter the two threads share. Only the lock reaches it.
fn count_hot(ref<mutex<u32>, shared> hot, slice<Reading, shared> part) : usize
{
    foreach (r in part)
    {
        if (r.tenths > limits::HOT)
        {
            *lock(hot) += 1;
        }
    }
    slice_len(part)
}

fn main()
{
    Log log = Log { .entries = Vec::new(), .per_sensor = HashMap::new() };
    record(&mut log, 120, 1);
    record(&mut log, 215, 1);
    record(&mut log, 340, 2);
    record(&mut log, 310, 2);

    // Loop over the entries through a shared reference to each one.
    // `match` takes the Verdict apart; it must handle every variant.
    foreach (r in &log.entries)                           // r : ref<Reading, shared>
    {
        match (judge(r))
        {
            Cold(by) : printf("C%d ", by),                // "C" then the shortfall
            Fine     : printf("F "),
            Hot(by)  : printf("H%d ", by),                // "H" then the excess
        }
    }
    printf("\n");

    // A lookup gives an Option; a pattern condition takes it apart.
    if (Some(n) = HashMap::get(&log.per_sensor, &String::from_str("sensor-2")))
    {
        printf("sensor-2: %d\n", *n);
    }

    // A slice of the Vec, and an average that cannot overflow unseen.
    if (Some(avg) = average(&log.entries[0 .. 2]))
    {
        printf("avg %d\n", avg);
    }

    // Sorting, and a closure: `to_f` captures nothing, `offset` is read.
    Vec<i32> temps = Vec::new();
    foreach (r in &log.entries)
    {
        Vec::push(&mut temps, r.tenths);
    }
    Vec::sort(&mut temps);
    i32 offset = 32;
    auto to_f = [offset](i32 tenths)
    {
        tenths * 9 / 50 + offset
    };
    printf("coldest %dF, warmest %dF\n", to_f(temps[0]), to_f(temps[3]));

    // Result handling: `b"42"` is a byte-array literal (52 and 50).
    auto digits = b"42";
    if (Ok(v) = parse_two(digits[0], digits[1]))
    {
        printf("%d %d\n", v, larger(v, 17));             // T := i32
    }

    // Two threads share the counter through a mutex; each gets its own
    // half of the log as a slice. `join` waits and gives the result.
    auto hot = Mutex::new(0: u32);
    auto h1 = spawn(count_hot, &hot, &log.entries[0 .. 2]);
    auto h2 = spawn(count_hot, &hot, &log.entries[2 .. 4]);
    usize seen = join(h1) + join(h2);
    printf("%d of %d readings hot\n", *lock(&hot), seen);
}   // `log` is destroyed here: its destructor prints "L4", then its Vec and HashMap are freed.
output
C30 F H40 H10 
sensor-2: 2
avg 167
coldest 53F, warmest 93F
42 42
2 of 4 readings hot
L4

What the compiler stops

The program above is correct, so it shows what CobaltC lets you write. Just as much of the language is what it will not let through. Each of these mistakes, a one-line change to the same kind of code, is rejected before the program runs, by both the interpreter and the compiler, with a diagnostic that names the rule; what cannot be decided in advance is checked while the program runs, and stops it at the line where it happens (§08–§10).

A reference kept across a pushrejected: diag.aliasing-conflict
import std;

struct Reading
{
    i32 tenths;
    u8 sensor;
}

fn main()
{
    Vec<Reading> entries = Vec::new();
    Vec::push(&mut entries, Reading { .tenths = 120, .sensor = 1 });
    ref<Reading, shared> first = Vec::index_shared(&entries, 0);
    Vec::push(&mut entries, Reading { .tenths = 215, .sensor = 1 });   // ✗ the push may move the elements
    printf("%d\n", first.tenths);                                     //   while `first` still points into them
}
A value used after it movedrejected: diag.stale-binding
import std;

resource struct Log
{
    Vec<i32> entries;
}

fn archive(Log l)                     // takes the log: it is destroyed when this call ends
{
}

fn main()
{
    Log log = Log { .entries = Vec::new() };
    archive(log);                     // `log` moves into the call
    Vec::push(&mut log.entries, 7);   // ✗ `log` no longer owns anything
}
An error ignoredrejected: diag.result-discarded
import std;

fn digit(u8 b) : Result<i32, u8>
{
    if (b > b'9' || b < b'0')
    {
        return Err(b);
    }
    Ok(widen<i32>(b - b'0'))
}

fn main()
{
    digit(b'x');                      // ✗ an error dropped on the floor would go unseen
    // Handle it (`?`, `match`, `if (Err(e) = …)`), or say you mean to drop
    // it: `_ = digit(b'x');` discards the value on purpose.
}
Arithmetic that would wraprejected: diag.arith-overflow
import std;

fn main()
{
    i32 tenths = 2000000000;
    i32 twice = tenths * 2;           // ✗ the product does not fit in an i32: no silent wrap
    printf("%d\n", twice);
}
A case left outrejected: diag.non-exhaustive-match
enum Verdict
{
    Cold(i32),
    Fine,
    Hot(i32)
}

fn label(Verdict v) : i32
{
    match (v)                         // ✗ `Hot` is not handled: every variant must be
    {
        Cold(by) : -by,
        Fine     : 0,
    }
}

fn main()
{
    label(Fine);
}

Reading a diagnostic

Both tools report a mistake the same way, character for character. This is what either prints for the first example above, a reference kept across a push:

coby tour.cb (or cobc tour.cb)
error[diag.aliasing-conflict]: `first` still borrows `entries`, so it cannot be written or borrowed `mut` here; end that borrow first
  --> tour.cb:14:15
   |
14 |     Vec::push(&mut entries, Reading { .tenths = 215, .sensor = 1 });
   |               ^
   = repair: end the conflicting reference (let its holder go out of scope) before this access, or use `shared` on both sides
  • The first line says what went wrong. error means the program was rejected before it ran; a check that fails while the program runs is a fault instead (fault[diag.index-out-of-bounds]: index out of bounds), and the program stops there. In the brackets is the diagnostic's identifier: it is the same in every implementation and every version, and the specification's diagnostic registry has an entry for each.
  • --> gives the file, the line and the column, in the form editors and terminals turn into a link.
  • The source line, with carets under the token the rule applies to.
  • = repair: is the registry's advice for this diagnostic.

coby --explain tour.cb (or cobc --explain) adds the registry's whole entry: the rule of the specification that rejected the program, the invariant it protects, what it required and what it observed, and the examples that show it. A compiled program shows its faults that way when it runs with COBALTC_EXPLAIN=1 in its environment.

What you just saw

  • Declarations are type-first, like C: i32 x = 5;, Vec<Reading> entries;. auto infers the type from the initialiser. Function return types follow a colon: fn judge(…) : Verdict.
  • Blocks, if and match are expressions. The last expression in a block, with no semicolon, is the block's value. judge returns whatever branch of the if runs.
  • Conditions need parentheses (if (c), while (c), match (e)); match arms are Pattern : expression,, and a match must cover every variant (a missing one is rejected, above). A pattern can also be a condition: if (Some(n) = HashMap::get(…)) runs its block only when the lookup found something, with n bound.
  • &x is a shared (read-only) reference and &mut x an exclusive one. The compiler checks every use: a reference kept into the Vec across a push, a value used after it moved, are rejected before the program runs (above). &log.entries[0 .. 2] is a slice, a borrowed run of elements.
  • Log is a resource: it owns a Vec and a HashMap. Its destructor fn Log::drop runs at the end of main, then its fields are freed. Passing or assigning a resource moves it.
  • String, HashMap and sprintf. sprintf("sensor-%d", s) builds a String; *HashMap::entry(&mut m, key, 0) += 1 counts. The standard library lives in the module std, imported by import std;.
  • Errors are values. A Result must be used (dropping one is rejected), ? returns an Err or a None early, and checked_add turns an overflow into None. Plain arithmetic that overflows is never silent: it is rejected when the compiler can see it, and stops the program when it happens.
  • No implicit conversions. widen<i32>(b - b'0') and narrow<i32>(n) say what happens to the representation; narrow checks that the value fits.
  • Library calls are qualified: Vec::push(&mut v, x), Vec::sort(&mut v), HashMap::get(&m, &k). There is no method-call syntax.
  • Generics, closures and threads. larger<T: integer> works for any integer type; to_f is a closure whose capture list names what it uses; spawn starts a thread and join waits for its result. The two threads share their counter only through a mutex: lock is the only way in.

Further showcases can be found in the repository's showcase directory; its README describes them all.

Core language and std

Which parts of CobaltC are the language itself, always available, and which come from the standard library and need import std;.

CobaltC comes in two layers. The core language is always there: its types, its constructs, and a set of intrinsics, functions defined by a rule of the specification rather than by a body. The standard library, std, is a module, written mostly in CobaltC itself. Nothing of std is in scope until a module writes import std;, and each module imports it for itself (§17). This page says which layer each part belongs to.

Core only: no importruns
// No `import std;`: only the core language.
fn main() : u8
{
    array<i32, 4> a = [3, 4, 5, 6];
    i32 s = 0;
    foreach (x in a)
    {
        s += x;
    }
    u32 m = max_value<u32>();
    s += narrow<i32>(m % 10);            // 4294967295 % 10 is 5
    narrow<u8>(s)                        // the exit status: 23
}
printf needs the importrejected: diag.unbound-name
fn main()
{
    printf("hello\n");                   // ✗ diag.unbound-name (static): printf is std's
}

Core: always available, no import

Types built into the language

  • Integers i8–i128, u8–u128, isize, usize; floats f32, f64
  • bool, str (string literals), void / ()
  • array<T, N>, slice<T, shared> and slice<T, exclusive>
  • ref<T, shared> and ref<T, exclusive>, rawptr<T>
  • fn(…) : R values and closures
  • handle<T> (a thread), mutex<T>, guard<T>

Language constructs

struct, bitstruct, enum (with numeric codes), resource, destructors (Type::drop), generics with the built-in bounds eq, ordered, number, integer and clone, match with nested, literal and text patterns, if and while (either with a pattern as the condition), for, foreach (over collections and ranges), break, continue, ?, const, modules and import, unsafe, unsafe extern fn and extern "code";.

Intrinsics

GroupNames
Resourcesdrop
Conversionswiden, narrow, checked_narrow, narrow_wrapping, reinterpret, to_float, to_int
Bitscount_ones, leading_zeros, trailing_zeros, rotate_left, rotate_right
Arithmeticwrapping_add/sub/mul, saturating_add/sub/mul, checked_add/sub/mul/div/rem, checked_neg
Numeric limitsmin_value, max_value
Layoutsizeof, alignof
str and slicesstr_len, str_byte, str_ptr, slice_len
Faults and compile-time checksfault, static_assert
Threadsspawn, join, cancel, join_status, Mutex::new, lock
Raw memory (most need unsafe)rawptr_of, reinterpret_ptr, dangling, allocate, deallocate, reclaim, release, copy_raw, read_volatile, write_volatile

allocate returns a Result, which a program can use without the import even though it cannot name the Result type without it.

Intrinsics private to std

key_hash, key_eq, key_less, key_less_at, swap_places, append_native, text_write, parse_check, parse_value, str_slice (beneath StringView::of), and the names that begin with $: the machinery std's own code is built on. A program cannot call them, nor std's private externs stderr_write, which eprintf writes through, and stdout_flush, beneath flush_stdout.

std: needs import std;

Mostly written in CobaltC itself; a few parts are provided natively by each implementation. std is divided into submodules by subject, and import std; brings in all of them, so a program never needs to know which holds what; the column says, for a program that wants one subject alone (import std::text;). §21 covers it in full.

AreaItemsSubmodule
Core typesOption<T>, Result<T, E>, AllocError; unwrap, unwrap_or, expect, ok_or, is_some/is_none/is_ok/is_err; map_errstd::core
CollectionsVec<T> (push, pop, index, insert, remove, swap_remove, retain, dedup, clone, from_slice, filled, from_fn, append, contains, position, reverse, truncate, swap, clear, …), HashMap<K, V>, HashSet<K>, Queue<T>, PriorityQueue<T>, slice_partsstd::collections
SortingVec::sort, sort_by, binary_search, binary_search_bystd::collections
Ownership helpersBox<T>, Rc<T>, Weak<T>std::memory
Values behind referencesswap, replace, overwritestd::core
Numeric helpersmin, max, abs, pow, clamp, gcd, is_nan, is_finite (over the built-in bounds)std::math
Floating-point functionssqrt, floor, ceil, round, trunc, ln, exp, log2, log10, sin, cos, tan, asin, acos, atan, atan2, powfstd::math
Random numbersRng, a seeded pseudo-random generator (Rng::from_os for a fresh seed, Rng::shuffle); os_random_bytes, os_random_u64 for secrets; uuid_v4std::random
NetworkingTcpListener (bind, accept), TcpStream (connect, read_line, read, write, printf, shutdown_write, try_clone, timeouts, keepalive), UdpSocket (send_to, recv_from, connect, send, recv), IpAddr, SocketAddr, resolve, NetErrorstd::net
Other programsCommand (arg, current_dir, env, output, status, spawn), Child (read_output_line, read_error_line, write_input, wait, terminate, kill), process_id, watch_interrupts, interrupt_requested, exitstd::process
BytesVec::push_le, push_be, read_le, read_bestd::collections
Checksumcrc32 (CRC-32/IEEE; catches accidents, not tampering)std::crypto
TextString, StringView (split, lines, split_whitespace, chars, code_point, find, trim, starts/ends_with, strip_prefix/suffix, parse), String::parse<T>, String::append, push_code_point, sprintf, String::appendf, the ASCII tests and case changes, Utf8Error, ParseErrorstd::text
Outputprintf, eprintf, flush_stdout; the extern stdout_writestd::io
Checksassertstd::core
Inputread_line, read_all, read_all_bytes; the extern stdin_readstd::io
FilesFile (open, create, create_new, append, open_rw, read, read_line, write, seek, position, flush, len, close, …), read_file, write_file, read_bytes, write_bytes, FileErrorstd::fs
Directories and pathsmake_dir, remove_file, remove_dir, rename, list_dir, path_kind, make_dir_all, remove_dir_all, copy_file, file_info, the path_ functions (join, parent, file_name, stem, extension, is_absolute, normalize, canonical)std::fs
Arguments and environmentarg_count, arg and the extern arg_bytes; Args (flag, option, options, finish, help); env_var, current_dir, set_current_dir, temp_dir, home_dir, current_exe, hostnamestd::env
Clocksmonotonic_ns, unix_seconds, sleep_ms; dates and times are DateTimestd::time
ConcurrencyChannel<T> (new, send, recv, try_recv, recv_timeout_ms, close), cpu_count, try_join, is_finished, ThreadFailure (text, diagnostic, is_cancelled)std::sync
Cryptographysha256, sha384, sha512, sha1 (legacy), blake2b, hmac_sha256, hmac, digest_eq, hkdf_sha256, chacha20_poly1305_seal/open, aes_gcm_seal/open, x25519, ecdh, ed25519_sign/verify, ecdsa_sign/verify, rsa_sign_pss/verify_pss, hash_password/verify_password; to_hex, from_hex, base64_encode, base64_decode in std::textstd::crypto; to_hex, from_hex, base64_encode, base64_decode: std::encoding
HTTPhttp_get, http_post, HttpClient, Url, form_decode/form_encode; HttpServer (bind, bind_tls), HttpConnection::request/respondstd::http
JSONJson (parse, text, pretty, get, at, as_*, set, push), JsonMemberstd::encoding::json
Compressiondeflate/inflate, zlib_deflate/zlib_inflate, gzip/gunzip, adler32, CompressErrorstd::compress
DatabasesPgConnection::connect, query, execute, run_script; PgValue, PgResult, PgRow::get, get_i64, get_f64, get_bool, get_bytesstd::database::postgres
Big integersBigUint (add, sub, mul, div_rem, mod_pow, shifts, text, parse, bytes)std::math
Certificates and TLSCertificate, TrustStore, verify_chain; TlsStream (connect, client, server, read, read_line, read_exact, write, peer_certificate, close)std::x509, std::tls
Dates and timesDateTime (from_unix, now, new, to_unix, to_iso, from_iso, to_http_date, from_http_date, weekday, less, to_local), Weekday, unix_ms, iso_from_unix_ms, unix_ms_from_iso, local_offset_seconds — UTC, and the machine's local timestd::time

What may surprise you

  • printf and assert are std, not core: a program without the import cannot print.
  • Vec, String and Option are std, but array, str, slice, handle and mutex are core.
  • swap, the one to use, is std; swap_places, which it is built on, is private to std.
  • min and max are std; min_value and max_value are core.
  • sqrt, round and the other floating-point functions are std (std::math), not core: they are ordinary library functions, so they need the import, and a function of your own named round is an ordinary name.

§06Arithmetic and numeric types

Governing specification file: spec/06-arithmetic.md

Checked integer arithmetic, explicit conversions, how literals get their types, and the sizes and limits of types.

Numbers are where silent corruption usually starts, so CobaltC's arithmetic is checked by default: an operation whose mathematical result does not fit its type does not wrap around, it faults. Every other policy (wrapping, saturating, or returning None) is a named function you call on purpose. Types never convert implicitly; every change of representation is a named intrinsic with a documented behaviour.

Numeric types

TypeMeaningNotes
i8 i16 i32 i64 i128signed integers of the given widthtwo's complement
u8 u16 u32 u64 u128unsigned integers of the given width
isize usizesigned / unsigned integers of address widththe types of indices, lengths, sizes and pointer offsets; the width (16, 32, 64 or 128 bits) is fixed and documented by each implementation
f32 f64IEEE 754 binary32 / binary64±Inf and NaN are ordinary values; float arithmetic never faults
booltrue or falseproduced by comparisons and && || !

There is no implicit promotion: i32 + i64 is a type error, and a u8 cannot be passed where an i32 is expected. Unary minus applies only to signed and float types.

Literals

Integer literals are digit strings: decimal, or hexadecimal, octal or binary after 0x, 0o or 0b (0xFF, 0o755, 0b1010), with no sign. A _ between two digits groups them and is ignored: 1_000_000, 0xFFFF_0000, 3.141_592 (anywhere else, a _ is a syntax error). A literal's type comes from, in order: an explicit suffix 123: u8; the expected type at that position (a declared variable type, a parameter type, a field type, or the other operand of the same operator); otherwise the default, i32 for integers and f64 for floats. An expression made only of literals and operators takes the expected type as a whole, so u64 x = 1024 * 1024 * 1024 * 16; is computed in u64, while auto y = 65536 * 65536; has no expected type, is an i32, and overflows. A literal that does not fit its type is a static error. A minus written directly before an integer literal is checked with it, as one negative value, so -2147483648 is the smallest i32. A type's limits also have names, min_value<i32>() and max_value<i32>() (below). Float literals are digits . digits, with an optional exponent (1.5e-3, 2.5E3, or 1e5 with no fraction), optionally suffixed : f32; one too large for its type (1e400) is a static error, not infinity. A byte character b'a' is one ASCII character or an escape (b'\n', b'\'', b'\x7f'), for writing bytes readably in code that parses. It is a number like any other literal and takes an unsigned type from its context (u32 c = …; c == b'|' compares two u32s), and is a u8 when nothing fixes its type; a signed context is a type error. A string literal "…" is always a str and a byte literal b"…" always an array<u8, N> (§21, §22); neither takes an expected type.

Literal typingruns
import std;

fn main()
{
    auto a = 5;                       // i32: no suffix, no expected type
    u8 b = 200;                       // u8 from the declared type; 200 fits
    auto c = 7: u64;                  // u64 from the suffix
    auto d = b + 1;                   // the 1 takes u8 from b; d is u8
    auto e = 2.5;                     // f64
    f32 f = 0.5: f32;
    f64 tiny = 1.5e-3;                // an exponent: 0.0015
    u8 comma = b',';                  // a byte character: 44
    u32 mask = 0xFF00;                // hexadecimal; also 0o17 (octal) and 0b1010 (binary)
    u64 ns = 1_000_000_000;           // _ groups digits: one second in nanoseconds
    u64 sixteen_gib = 16 * 1024 * 1024 * 1024;   // literals only: computed in u64
    i64 g = -1;                       // the expected type reaches through unary minus
    i32 lo = -2147483648;             // min(i32): 2147483648 alone would not fit
    assert(d == 201);
    assert(g < 0);
    assert(lo + 2147483647 == -1);
    assert(tiny == 0.0015 && comma == 44 && mask == 65280);
    assert(sixteen_gib == 17_179_869_184 && ns == 1000000000);
    static_assert(sizeof<u64>() == 8);        // sizeof and alignof are intrinsics returning usize
    static_assert(alignof<u16>() == 2);
}
A literal that does not fitrejected: diag.literal-out-of-range
fn main()
{
    256: u8;                          // ✗ diag.literal-out-of-range (static): 256 does not fit u8
}

Checked arithmetic

+, - and * on integers fault with diag.arith-overflow when the result leaves the type's range. / and % truncate toward zero (the remainder takes the sign of the dividend); dividing by zero is diag.div-by-zero, and the one overflowing division, the minimum value of a signed type divided by -1, is diag.div-overflow. Negating the minimum signed value is an overflow too. When both operands are literals the fault is detected statically and the program is rejected; otherwise it is a run-time fault that terminates the program (see §18). A local initialized with a literal counts as that literal until something writes it, borrows it or captures it in a closure, so u8 x = 200; u8 y = x + 100; is rejected too. After such a write, or after an if that writes it on only one branch, the check happens at run time.

Overflow at run timerejected: diag.arith-overflow
fn add(i32 a, i32 b) : i32
{
    a + b                             // checked: faults if the sum leaves i32
}

fn main()
{
    add(2147483647, 1);               // ✗ diag.arith-overflow (dynamic): 2^31 does not fit i32
}
Overflow the checker can seerejected: diag.arith-overflow
fn main()
{
    2147483647 + 1;                   // ✗ diag.arith-overflow (static): both operands are literals
}
The one division that overflowsrejected: diag.div-overflow
fn div(i32 a, i32 b) : i32
{
    a / b
}

fn main()
{
    div(-2147483647 - 1, -1);         // ✗ diag.div-overflow (dynamic): -MIN does not fit
}

Choosing a different overflow policy

When wrapping or clamping is what you actually want, say so. The wrapping_ and saturating_ families never fault; the checked_ family returns an Option so the caller can decide. These are ordinary generic calls with the operand type inferred from the arguments.

CallResultOn overflow
wrapping_add(a, b), wrapping_sub, wrapping_mulsame typewraps modulo 2N
saturating_add(a, b), saturating_sub, saturating_mulsame typeclamps to the type's minimum or maximum
checked_add(a, b), checked_sub, checked_mul, checked_div, checked_remOption<T>None (also for division by zero)
checked_neg(a)Option<T>None for the most negative value, and for any non-zero unsigned value
Explicit overflow policiesruns
import std;

fn main()
{
    u8 top = 255;
    assert(wrapping_add(top, 1: u8) == 0);          // 255 + 1 wraps to 0
    assert(saturating_add(top, 9: u8) == 255);      // clamps at the maximum
    match (checked_add(top, 1: u8))
    {
        Some(_) :                 // must not happen
        {
            assert(false, "unreachable");
        },
        None    : {},                              // overflow reported as None
    }
    match (checked_add(1: u8, 1: u8))
    {
        Some(v) :
        {
            assert(v == 2);
        },
        None    :
        {
            assert(false, "unreachable");
        },
    }
}

Shifts, bitwise operators and comparison

  • << and >> take a u32 shift amount that must be less than the type's bit width; anything else is diag.shift-amount-out-of-range. Left shift discards high bits by definition (it is not an overflow); right shift is arithmetic on signed types.
  • &, |, ^ and ~ work on any integer type; ! is boolean not.
  • == != < <= > >= compare two operands of the same type and yield bool. Floats follow IEEE rules: every comparison involving NaN is false except !=.
Bits and comparisonsruns
import std;

fn main()
{
    u32 one = 1;
    assert(one << 4 == 16);
    static_assert(-8 >> 1 == -4);                          // arithmetic shift keeps the sign
    static_assert((12 & 10) == 8);
    static_assert((12 | 3) == 15);
    static_assert((6 ^ 3) == 5);
    static_assert(~0 == -1);
    f64 zero = 0.0;
    auto nan = zero / zero;                        // floats do not fault
    assert(!(nan == nan));
    assert(nan != nan);
}

Conversions are explicit

There is exactly one way to change a number's type: call the intrinsic that names the conversion. Each takes the target type as a type argument.

Five intrinsics work on an integer's bits directly: count_ones(x), leading_zeros(x) and trailing_zeros(x) (each a u32; zero gives the type's width), and rotate_left(x, k) and rotate_right(x, k) (by k modulo the width, keeping x's type). They act on the value's cell image, so a negative i8 has eight ones, and none of them faults (D-0123).

IntrinsicFrom → toBehaviour
widen<U>(x)integer to a wider integer whose range contains every value; f32 to f64always exact, never faults (u8 → i32, i32 → i64, f32 → f64)
narrow<U>(x)any integer to any integerchecked: diag.narrowing-overflow if x does not fit
checked_narrow<T>(x)any integer to any integerOption<T>: Some(x) when x fits T, else None — for a value from outside, where a fault would be wrong (D-0122)
narrow_wrapping<U>(x)any integer to any integerkeeps the low bits, never faults
reinterpret<U>(x)between the signed and unsigned types of one width (and usize ⇄ isize); between a float and an integer of its width (f32 ⇄ u32/i32, f64 ⇄ u64/i64)the same bit pattern read as the other type
to_float<U>(x)any integer or float to f32/f64nearest representable value (ties to even); beyond the target's range an infinity, as float arithmetic gives; NaN stays NaN
to_int<U>(x)float to integertruncates toward zero; checked: infinities, NaN and out-of-range values fault
Named conversionsruns
import std;

fn main()
{
    u8 small = 200;
    i32 wide = widen<i32>(small);                  // always safe
    assert(wide == 200);
    assert(narrow<u8>(wide) == 200);                // fits, so it succeeds
    static_assert(narrow_wrapping<u8>(300) == 44);  // 300 mod 256
    static_assert(reinterpret<i32>(4294967295: u32) == -1);
    f64 f = to_float<f64>(wide);
    assert(to_int<i32>(f * 1.5) == 300);
    usize n = 12;
    assert(narrow<i32>(n) == 12);                   // usize → i32 is a narrowing, so it is checked
}

Between the two float types: widen<f64>(x) takes an f32 to an f64 exactly, and to_float<f32>(x) takes an f64 to the nearest f32, rounding as float arithmetic does. There is no widen<f32> of an f64, since most f64 values have no exact f32. reinterpret gives a float's IEEE-754 bits as an integer of the same width, and turns such an integer back into a float, with no unsafe.

Between f32 and f64, and a float's bitsruns
import std;

fn main()
{
    f32 sample = 0.1;
    f64 d = widen<f64>(sample);                    // exact: 0.100000001490116...
    assert(to_float<f32>(d) == sample);             // and back, rounded, to the same f32
    static_assert(to_float<f32>(1.0 / 3.0) == 0.33333334: f32);
    u64 bits = reinterpret<u64>(-2.0);
    assert(bits >> 63 == 1);                        // the sign bit
    assert(reinterpret<f64>(bits) == -2.0);
    static_assert(reinterpret<u32>(1.0: f32) == 0x3f80_0000: u32);
    static_assert(max_value<f32>() == 3.4028235e38: f32);  // the largest finite f32
}
A narrowing that does not fitrejected: diag.narrowing-overflow
fn to_byte(i32 x) : u8
{
    narrow<u8>(x)                     // checked at run time
}

fn main()
{
    to_byte(300);                     // ✗ diag.narrowing-overflow (dynamic)
}

widen is only accepted where every value of the source fits the target, so it can never lose one: widen<u64> of an i32 is a static error, because a negative i32 has no u64 value. narrow works between any two integer types, and never faults where the value fits. Use it for usize and isize: whether usize → u64 is a widening depends on the address width, so narrow<u64>(n) is the spelling that is accepted by every implementation.

A widening that could lose a valuerejected: diag.type-mismatch
fn to_u64(i32 x) : u64
{
    widen<u64>(x)                     // ✗ diag.type-mismatch (static): -1 has no u64 value; use narrow
}

fn main()
{
    to_u64(1);
}

Sizes and limits of types

Four intrinsics describe a type rather than a value. Each takes the type as its type argument, written explicitly, and no other arguments.

IntrinsicResultMeaningFor
sizeof<T>()usizethe bytes a T occupies, padding includedany type
alignof<T>()usizethe alignment a T's address must have, in bytesany type
min_value<T>()Tthe smallest value of T (for a float, the most negative finite one)integer and float types
max_value<T>()Tthe largest value of T (for a float, the largest finite one)integer and float types

Every fixed-width type has the same size and limits on every implementation: sizeof<i32>() is 4 and max_value<i32>() is 2147483647. isize and usize have the address width, which each implementation documents, so for them the intrinsics are the portable way to write a size or limit, where a literal is not. A struct's size and alignment follow from its fields (§16): each field is aligned, and the struct's alignment is its largest field's.

Sizes and limitsruns
import std;

struct Pair
{
    u8 tag;
    u32 value;
}

fn main()
{
    static_assert(sizeof<i32>() == 4);
    static_assert(sizeof<u64>() == 8);
    static_assert(alignof<u64>() == 8);                      // a number is aligned to its own size
    static_assert(sizeof<bool>() == 1);
    static_assert(sizeof<array<u16, 10>>() == 20);           // an array has no padding
    static_assert(alignof<Pair>() == 4);                     // its largest field's alignment
    static_assert(sizeof<Pair>() == 8);                      // tag, 3 bytes of padding, value
    static_assert(sizeof<usize>() == sizeof<isize>());       // the address width, whatever it is

    static_assert(min_value<i8>() == -128);
    static_assert(max_value<u8>() == 255);
    static_assert(max_value<i32>() == 2147483647);
    static_assert(min_value<u64>() == 0);
    static_assert(max_value<usize>() == ~(0: usize));        // every bit set, at any address width
    static_assert(wrapping_add(max_value<i32>(), 1) == min_value<i32>());
}

Limits are most often the starting value of a search, a bound to clamp against, or a value that means "none". Here are all three:

Limits as sentinels and boundsruns
import std;

// The spread of a list: start the minimum at the largest i64 and the
// maximum at the smallest, so the first element replaces both.
fn spread(ref<Vec<i64>, shared> v) : i64
{
    i64 lo = max_value<i64>();
    i64 hi = min_value<i64>();
    foreach (p in v)
    {
        i64 x = *p;
        if (x < lo)
        {
            lo = x;
        }
        if (x > hi)
        {
            hi = x;
        }
    }
    hi - lo
}

// Clamp into i32's range, then narrow: the narrow can no longer fault.
fn clamp_to_i32(i64 x) : i32
{
    if (x > widen<i64>(max_value<i32>()))
    {
        max_value<i32>()
    }
    else if (x < widen<i64>(min_value<i32>()))
    {
        min_value<i32>()
    }
    else
    {
        narrow<i32>(x)
    }
}

// The index of the first b, or usize's maximum for "not found".
fn find(ref<Vec<u8>, shared> v, u8 b) : usize
{
    foreach (i, x in v)
    {
        if (*x == b)
        {
            return i;
        }
    }
    max_value<usize>()
}

fn main()
{
    Vec<i64> v = Vec::new();
    Vec::push(&mut v, 7);
    Vec::push(&mut v, -3);
    Vec::push(&mut v, 12);
    assert(spread(&v) == 15);

    assert(clamp_to_i32(5000000000) == 2147483647);
    assert(clamp_to_i32(-5000000000) == -2147483648);
    assert(clamp_to_i32(42) == 42);

    Vec<u8> bytes = Vec::new();
    Vec::push(&mut bytes, 10);
    Vec::push(&mut bytes, 20);
    assert(find(&bytes, 20) == 1);
    assert(find(&bytes, 99) == max_value<usize>());
}

A generic function may use the limits of its own type parameter. It cannot do arithmetic on a T without a bound (§12), but it can produce a T's limit and pass it on. Each use is checked when the function is instantiated, so a T that is not an integer type is rejected before the program runs.

A limit in a generic functionruns
import std;

// The value inside, or T's maximum if there is none.
fn or_max<T>(Option<T> r) : T
{
    match (r)
    {
        Some(x) : x,
        None    : max_value<T>(),
    }
}

fn main()
{
    u64 big = 5000000000;
    assert(or_max(checked_mul(big, big)) == max_value<u64>());   // the product overflows u64
    assert(or_max(checked_add(2: i32, 3)) == 5);
}
Limits are for number typesrejected: diag.type-mismatch
fn main()
{
    max_value<bool>();                // ✗ diag.type-mismatch (static): only number types have these
}
Going past a limitrejected: diag.arith-overflow
fn main()
{
    max_value<i32>() + 1;             // ✗ diag.arith-overflow (dynamic): a limit is a value, not a literal
}
Rules of thumb
  • Default arithmetic is checked. If you want wrapping, write wrapping_add; if you want to handle overflow, write checked_add and match on the Option.
  • Never expect a conversion to happen for you. Reach for widen first; use narrow (checked) or narrow_wrapping (deliberate truncation) when going the other way.
  • Use usize for lengths and indices, isize for pointer offsets, and suffix a literal (5: u8) when the context does not already fix its type.
  • Floats never fault. Remember that NaN is unequal to itself.
  • Write a type's limits as min_value<T>() and max_value<T>(). For isize and usize they are the only portable spelling.

§07Resources, ownership and destruction

Governing specification file: spec/07-resource-authority.md

Resource types have exactly one owner, move instead of copying, and are destroyed automatically and exactly once.

Some values are just data: an i32, a bool, a struct of numbers. Copying them is harmless. Other values own something that must be released exactly once: a heap buffer, a file handle, a lock, a thread. CobaltC calls the second kind resources, and the rules in this section exist so that a resource always has exactly one owner, is never copied, and is destroyed exactly once, at a predictable moment, whether or not you remember to do it.

Plain types and resource types

A type is a resource if it is declared with the resource keyword, or if any of its fields, elements or payloads is a resource. Vec<T>, String, Rc<T>, a thread handle, a mutex and a lock guard are all resources. Numbers, bool, references, raw pointers and function values are plain. A struct of plain fields is plain; a struct containing a Vec is a resource by derivation, with no keyword needed.

Three kinds of struct
struct Point                          // plain: two numbers
{
    i32 x;
    i32 y;
}

struct Path                           // a resource by derivation: it contains a Vec
{
    Vec<Point> points;
}

resource struct Handle                // a resource by declaration: it manages something
{                                     // beyond its fields (an OS handle, say)
    i32 fd;
}

One owner: moves, not copies

A resource lives in exactly one place. When you initialise a variable from it, pass it by value to a function, store it in a field, or return it, the resource moves: the new place becomes the owner and the old name becomes stale. Using a stale name is an error (diag.stale-binding), caught at compile time when the move is visible in the same function and at run time otherwise. A name moved on only some paths (in one branch of an if, or in a loop body that runs again) counts as moved: give it a new value on every path before using it again. Nothing is ever copied silently, so there can never be two owners.

Use after moverejected: diag.stale-binding
resource struct Token
{
    i32 id;
}

fn consume(Token t)                   // a by-value resource parameter takes ownership
{
}                                     // t is destroyed here, at the callee's scope exit

fn main()
{
    Token t = Token { .id = 1 };
    consume(t);                       // ✓ t moves into consume
    consume(t);                       // ✗ diag.stale-binding: t no longer owns anything
}
Moving into a new nameruns
import std;

fn main()
{
    Vec<i32> v = Vec::new();
    auto w = v;                       // moves the Vec into w; v is now stale
    Vec::push(&mut w, 1);             // ✓ use the new owner
}

Because a resource is never copied, it cannot appear where a copy would be made: you cannot compare two Vecs with ==, and you cannot read one out of a field of a live struct. Take a reference instead (§09), or move the whole container.

No value semantics for resourcesrejected: diag.read-of-resource
import std;

fn main()
{
    Vec<i32> a = Vec::new();
    Vec<i32> b = Vec::new();
    auto same = a == b;               // ✗ diag.read-of-resource: comparison would copy both
}

Automatic destruction

Every resource is destroyed automatically: a named resource when the block that declared it ends, and a temporary (an unnamed call result or literal) at the end of the statement that created it. Locals are destroyed in reverse order of creation. A struct's resource fields are destroyed after the struct's own destructor, innermost first, in reverse declaration order. A return, break, continue or run-time fault unwinds the same way. There is no way to leak a resource, so there is no leak diagnostic.

You can also destroy a resource early with drop(x). Afterwards x is stale until you give it a new value: once x's value has gone, moved away or dropped, x = e gives it a new one, which is destroyed at the end of x's own block like its first value. A value whose end only frees memory (a String, a Vec, a HashMap, an Option or struct made of such: a *quiet* type, D-0194) is simply replaced: s = String::from_str("new"); destroys the old String there. Assigning over any other resource that is still there is an error (diag.overwrite-of-live-resource), because its end does something you should see: a destructor of yours runs, a file closes, a thread is joined. Write overwrite(&mut x, e), which destroys the old value and stores the new one, or drop it first. The same holds for a field (g.file = f;) and for a write through a reference (*r = v;, r.rows = v;): where the compiler can see the old value is still there, it rejects the assignment before the program runs; elsewhere, the run does. What counts is whether the value there owns anything: a None of an Option<File> owns nothing, so o = Some(f); over it is fine, while a Some must be dropped first. A type declared resource, or with a destructor, always owns. Declaring the name again in the same block does not: that is diag.duplicate-local (§11).

Where destruction happensruns
import std;

fn make() : Vec<i32>
{
    Vec<i32> v = Vec::new();
    Vec::push(&mut v, 1);
    v                                 // moved out to the caller, not destroyed here
}

fn main()
{
    {
        auto result = make();         // result owns the Vec now
        Vec::push(&mut result, 2);
    }                                 // destroyed here, at the caller's block exit
    make();                           // an unstored temporary: destroyed at this `;`
    Vec<i32> v = Vec::new();
    drop(v);                          // ✓ destroyed early; v is now stale
    v = Vec::new();                   // ✓ v's value has gone, so v takes a new one
    Vec::push(&mut v, 3);
}

Destructors

A type gets a destructor by declaring an associated function named drop that takes an exclusive reference to the value and returns nothing. The language calls it exactly once, when the resource is destroyed; you may not call it yourself. Inside it you can mutate the value but not move out of it. Field resources are destroyed after the body runs. The type must be a resource: declare it resource struct. A plain struct is copied and never destroyed, so a destructor on one would never run, and is refused (diag.destructor-on-plain-type).

Destruction orderruns
import std;

resource struct Tagged
{
    str tag;
}

fn Tagged::drop(ref<Tagged, exclusive> self)
{
    printf("%s", self.tag);          // runs once, when this Tagged is destroyed
}

resource struct Holder
{
    Tagged a;
    Tagged b;
}

fn main()
{
    Tagged x = Tagged { .tag = "1" };
    Tagged y = Tagged { .tag = "2" };
    Holder h = Holder { .a = Tagged { .tag = "3" }, .b = Tagged { .tag = "4" } };
    printf("\n");
}   // h first (its fields in reverse order: 4 then 3), then y, then x
output
4321
Note
Destructors run in reverse declaration order, last declared first, and a struct's fields in reverse field order, so what was built last is torn down first. The output above is a newline followed by 4321.

Things the rules forbid

Overwriting a live resourcerejected: diag.overwrite-of-live-resource
import std;

resource struct Lock
{
    u32 id;
}

fn Lock::drop(ref<Lock, exclusive> self)
{
    printf("released %u\n", self.id);
}

fn main()
{
    Vec<i32> v = Vec::new();
    v = Vec::new();                   // fine: a Vec of numbers is quiet, the old one is destroyed
    Lock l = Lock { .id = 1 };
    l = Lock { .id = 2 };             // ✗ diag.overwrite-of-live-resource: lock 1's release would be hidden
}

The fix is overwrite(&mut l, Lock { .id = 2 });, which destroys the old Lock there (and prints its release where you wrote it), or drop(l); first. Two more rejections you will meet: drop(v) twice is diag.stale-binding (the first drop ended v); and moving a resource out of a field of a live struct (auto inner = holder.field;) is diag.move-out-of-field, because the struct would be left half-owned. Move the whole struct, or take a reference to the field.

Rules of thumb
  • If a type owns something, it is a resource: mark it resource and give it a drop. Types that merely contain resources become resources automatically.
  • Passing a resource by value hands it over. To let a callee look without taking, pass &x; to let it modify, pass &mut x.
  • Do not fight scope-end destruction. Put a resource in the smallest block that needs it and let the block end release it.
  • drop(x) is for early release. After it, x is gone until you give it a new value (x = e) or declare the name again.

§08Aliasing: shared and exclusive access

Governing specification file: spec/08-alias-validity.md

Any number of readers, or one writer, never both. The rule is checked at every access, so a conflict cannot go unnoticed.

Two pieces of code touching one object at the same time is the root of most memory bugs: an iterator invalidated by a push, a struct half-updated by two threads, a buffer freed while something still points into it. CobaltC's answer is a discipline on access paths: every way of reaching an object (its name, a reference, a field projection) has a mode, and the modes that may coexist are fixed.

Two modes

ModeWrittenMayCoexists with
shared&x, type ref<T, shared>readany number of other shared paths
exclusive&mut x, type ref<T, exclusive>read and write; and, for the owner, move or destroynothing else that overlaps

A variable's own name is always an exclusive path: there is no mut keyword on bindings, because whether you may write is decided by what references currently exist, not by how the variable was declared. Restricting access is what a borrow does.

The one rule

At every read and every write, the language checks: is there another live path to the same storage whose mode conflicts with this access? Shared and shared never conflict. Anything involving an exclusive path conflicts, unless the other path is an ancestor of this one (the thing it was borrowed from). Forming a conflicting borrow is reported at once as diag.aliasing-conflict; so is using the owner while a conflicting borrow lives.

Shared readers block writesrejected: diag.aliasing-conflict
fn main()
{
    i32 x = 1;
    auto r1 = &x;                     // ✓ shared
    auto r2 = &x;                     // ✓ any number of shared readers; the owner may still read x
    x = 2;                            // ✗ diag.aliasing-conflict: writing while r1, r2 are live
    i32 sum = *r1 + *r2;
}
An exclusive borrow excludes everythingrejected: diag.aliasing-conflict
fn main()
{
    i32 x = 1;
    auto r1 = &mut x;                 // ✓ exclusive
    auto r2 = &x;                     // ✗ diag.aliasing-conflict: r1 is exclusive and still live
    *r1 = 2;
}

Borrows in one argument list are all formed before the call holds any of them, so passing two conflicting references to the same storage is accepted at the call. The conflict is reported at the first access inside the function that meets both, the point where it actually matters.

Two references to one variable in a callrejected: diag.aliasing-conflict
fn set_then_read(ref<i32, exclusive> a, ref<i32, shared> b) : i32
{
    *a = 1;                           // ✗ diag.aliasing-conflict (dynamic): b reaches the same i32
    *b
}

fn main()
{
    i32 x = 0;
    set_then_read(&mut x, &x);        // accepted here; each borrow formed while the other was not yet held
}

Most conflicts are reported before the program runs, marked (static): whenever the function body itself shows that a conflicting reference is certainly live. That covers a borrow held in a variable, a reborrow through a reference or a guard (&*r, &r.f), the binder of a match (&e) arm, and the arguments a thread was spawned with, until join. What one body cannot see, like the call above, is reported when the conflicting access runs, marked (dynamic).

A conflict through a reference, found before runningrejected: diag.aliasing-conflict
struct Counter
{
    i32 hits;
    i32 misses;
}

fn bump(ref<Counter, exclusive> c)
{
    auto n = &mut c.hits;             // a reborrow of c.hits, through c
    c.misses = 0;                     // ✓ a different field
    c.hits = 0;                       // ✗ diag.aliasing-conflict (static): n still borrows c.hits
    *n += 1;
}

fn main()
{
    Counter k = Counter { .hits = 0, .misses = 0 };
    bump(&mut k);
}

When does a borrow end?

A reference lives as long as something holds it. A reference passed straight to a call dies when the statement ends, which is why consecutive calls that each borrow the same variable are fine. A reference, slice or StringView kept in a local variable lives until the last statement of its block that uses it, and ends there. So does any other local that holds references but owns nothing to destroy: the Option<ref<V, shared>> that HashMap::get returns, or a struct of references. Using it inside a loop, a branch or a nested block counts as a use by that whole statement, so a reference used in a loop lives through the loop. A reference variable lives to the end of its block, as before, when a closure captures it, when another reference was borrowed through it, or when a variable that can hold references (a Vec<StringView>, say) shares a statement with it and is used later, since that variable may keep a reference derived from it.

A reference ends at its last useruns
import std;

fn main()
{
    String cur = "abc";
    ref<Vec<u8>, shared> b = String::as_bytes(&cur);
    assert(Vec::len(b) == 3);         // the last use of b: it ends after this statement
    String::clear(&mut cur);          // ✓ nothing borrows cur any more
    assert(String::len(&cur) == 0);
    HashMap<String, u32> m = HashMap::new();
    HashMap::insert(&mut m, String::from_str("a"), 1);
    Option<ref<u32, shared>> hit = HashMap::get(&m, &String::from_str("a"));
    assert(Option::is_some(&hit));    // the last use of hit
    HashMap::insert(&mut m, String::from_str("b"), 2);   // ✓ hit no longer borrows m
}
A use in a loop keeps the reference for the whole looprejected: diag.aliasing-conflict
import std;

fn main()
{
    Vec<i32> v = Vec::new();
    Vec::push(&mut v, 1);
    ref<i32, shared> first = &v[0];
    foreach (k in 0..2: i32)
    {
        printf("%v\n", *first);      // used in the loop: first lives through the whole loop
        Vec::push(&mut v, k);         // ✗ diag.aliasing-conflict: first still borrows v
    }
}

A block ends every borrow it holds, so wrapping a borrow in a block still works, and reads well when a borrow should visibly end.

Borrows end with their holderruns
import std;

fn main()
{
    Vec<i32> nums = Vec::new();
    Vec::push(&mut nums, 10);         // the borrow dies at this `;`
    Vec::push(&mut nums, 20);         // ✓ a fresh borrow; nothing conflicts
    i32 x = 1;
    {
        auto r = &mut x;              // r lives until the end of this block
        *r = 5;
    }
    x += 1;                           // ✓ r is gone
    assert(x == 6);
}

Reborrowing and ancestors

A path derived from another (a reborrow &*r, or a field projection r.a) has that path as its ancestor. Ancestors never conflict with their descendants, but the ancestor cannot be used in a conflicting way while a descendant lives. Once the descendants are gone, the ancestor is usable again.

Reborrowruns
import std;

fn main()
{
    i32 x = 1;
    {
        auto r1 = &mut x;
        {
            auto r2 = &*r1;           // ✓ a shared reborrow, child of r1
            assert(*r2 == 1);          // writing through r1 here would be diag.aliasing-conflict
        }
        *r1 = 3;                      // ✓ r2 is gone, the parent is usable again
    }
    assert(x == 3);                    // ✓ r1 is gone, the owner is usable again
}

Disjoint fields

Conflicts are decided by overlap of storage, not by which variable the paths come from. Two exclusive borrows of different fields of the same struct do not overlap and may coexist.

Disjoint fields (with a mistake)rejected: diag.aliasing-conflict
import std;

struct Pair
{
    i32 a;
    i32 b;
}

fn main()
{
    Pair p = Pair { .a = 1, .b = 2 };
    auto ra = &mut p.a;
    auto rb = &mut p.b;               // ✓ disjoint from p.a
    p.b = 30;                         // ✗ diag.aliasing-conflict: rb is a live exclusive path to p.b
    *ra = 10;
    *rb = 20;
}

The write to p.b above is rejected because rb, a live exclusive path to exactly that storage, still exists until the block ends. The same applies to reading p.b through the owner: an exclusive child excludes every non-ancestor access, reads included, until it is gone. Confine the borrows to a block and the owner is free again:

Disjoint fieldsruns
import std;

struct Pair
{
    i32 a;
    i32 b;
}

fn main()
{
    Pair p = Pair { .a = 1, .b = 2 };
    {
        auto ra = &mut p.a;
        auto rb = &mut p.b;
        *ra = 10;
        *rb = 20;                     // reading p.a or p.b here would conflict with the live exclusive borrows
    }
    assert(p.a == 10);                 // ✓ both borrows ended with the block
    assert(p.b == 20);
}

Shared means read-only, all the way down

An exclusive path can only be derived from an exclusive path. Given a ref<T, shared> you cannot write through it and cannot reborrow it exclusively: the mode is part of the reference's type, so a function's signature tells the caller exactly what it may do. The other direction is always allowed: an exclusive reference can be passed to a function that asks for a shared one, and the function gets a shared reborrow of it (as if you had written &*r). With v : ref<Vec<T>, exclusive>, Vec::len(v) just works; likewise a slice<T, exclusive> for a slice<T, shared>.

Writing through a shared referencerejected: diag.write-through-shared
struct P
{
    i32 a;
}

fn bump(ref<P, shared> p)
{
    p.a = 1;                          // ✗ diag.write-through-shared
}

fn main()
{
    P v = P { .a = 0 };
    bump(&v);
}

All the way down includes the references a value holds. Read through a ref<S, shared>, an exclusive reference stored in S comes out as a shared one: you can read through it, keep it, pass it or return it as a ref<T, shared>, but not write through it. A copy of a value that holds an exclusive reference cannot be taken out through a shared reference either, and clone cannot copy one (diag.borrow-exceeds-source). A fn field is checked when it is called: a closure that writes a variable it captured cannot be called through a shared reference to the struct that holds it (diag.borrow-exceeds-source, at run time, since a fn type does not show what the closure captured). To change something through a value others share, put it in a mutex (§19).

An exclusive reference held behind a shared onerejected: diag.write-through-shared
import std;

struct Counter
{
    ref<i32, exclusive> total;
}

fn peek(ref<Counter, shared> c) : i32
{
    *c.total                          // ✓ reading is fine
}

fn bump(ref<Counter, shared> c)
{
    *c.total = *c.total + 1;          // ✗ diag.write-through-shared
}

fn main()
{
    i32 n = 0;
    Counter c = Counter { .total = &mut n };
    printf("%v\n", peek(&c));
    bump(&c);
}

Moving or destroying needs solitude

Moving a resource, or destroying it (with drop or at scope end), requires that no other path to it exists at all. Otherwise a reference would be left pointing at nothing.

Destroying while borrowedrejected: diag.destroy-while-aliased
import std;

fn main()
{
    Vec<i32> v = Vec::new();
    auto r = &v;
    drop(v);                          // ✗ diag.destroy-while-aliased: r is still live
    usize n = Vec::len(r);            // (because r is used here)
}
Moving while borrowedrejected: diag.move-while-aliased
import std;

fn take(Vec<i32> v)
{
}

fn main()
{
    Vec<i32> v = Vec::new();
    auto r = &v;
    take(v);                          // ✗ diag.move-while-aliased: r still reaches v
    usize n = Vec::len(r);            // (because r is used here)
}
Rules of thumb
  • Borrow shared (&x) by default; borrow exclusively (&mut x) only for the duration of the change.
  • Keep references short-lived. Passing &mut v directly as an argument creates a borrow that ends at the semicolon.
  • If the checker complains about a conflict, end the earlier borrow: put it in its own block, or reorder the code so the write happens after the last use of the reference.
  • Field-level borrows are precise: borrowing p.a exclusively leaves p.b free.

§09References

Governing specification file: spec/09-identity-origin-extent.md

References are copyable values that name a place; dereferencing gives you that place back with every check intact.

A reference is the value form of an access path: a way to reach an object without owning it. References are plain values (copyable, no destroy authority) and carry their mode in their type. Raw pointers are a different thing entirely and live in §20.

Forming and using references

SyntaxMeaning
&ea ref<T, shared> to the place e
&mut ea ref<T, exclusive> to the place e
*rthe place r refers to: read it in an expression, assign to it, borrow it again, or project a field or element
r.f, r[i]field access and indexing look through a reference automatically
r1 == r2identity: true only if both refer to the same storage of the same object

The operand of & must be a place: a variable, a field, an element, or *r. You cannot borrow a temporary such as a call result or a literal (&make()); bind it to a variable first. This is rejected rather than given a lifetime because the temporary would end at the semicolon and take the reference with it.

References in useruns
import std;

struct Pair
{
    i32 a;
    i32 b;
}

fn main()
{
    i32 x = 5;
    {
        auto r = &mut x;
        *r += 1;                      // read and write through the same reference
    }
    assert(x == 6);

    Pair p = Pair { .a = 1, .b = 2 };
    auto rp = &p;
    assert(rp.a == 1);                 // auto-deref: rp.a means (*rp).a
    auto ra = &p.a;
    auto rb = &p.b;
    assert(!(ra == rb));               // different fields, so different identities
    auto ra2 = &p.a;
    assert(ra == ra2);                 // same field of the same object

    ref<i32, shared> copy = ra;       // references are plain values: copying is fine
    assert(*copy == 1);
}
Borrowing a temporaryrejected: diag.borrow-of-non-place
fn make() : i32
{
    5
}

fn main()
{
    auto r = &make();                 // ✗ diag.borrow-of-non-place: a call result is not a place
}

The one place a temporary may be borrowed is as an argument: f(&make()) or printf("%v", &sprintf(…)). The value lives until the end of the statement, and the call is over before then. This works when the call's own result is not a reference, since such a result could carry the borrow out. An operator's result, the value ? gives, and a number or bool literal may be borrowed there too: HashMap::get(&m, &(k + 1)), Vec::len(&names()?), HashMap::get(&m, &3). A literal takes the type the parameter refers to, so &3 passed where a ref<u64, shared> is expected is a reference to a u64.

A temporary as an argumentruns
import std;

fn total(ref<Vec<i32>, shared> v) : i32
{
    i32 t = 0;
    foreach (x in v)
    {
        t += *x;
    }
    t
}

fn make(i32 n) : Vec<i32>
{
    Vec<i32> v = Vec::new();
    for (i32 i = 1; i <= n; i += 1)
    {
        Vec::push(&mut v, i);
    }
    v
}

fn main()
{
    assert(total(&make(4)) == 10);     // the Vec lives until the end of this statement
    printf("%v\n", &sprintf("%v-%v", 1, 2));

    HashMap<u64, str> names = HashMap::new();
    HashMap::insert(&mut names, 3, "three");
    u64 k = 2;
    assert(HashMap::contains(&names, &(k + 1)));     // an operator's result
    printf("%s\n", *Option::unwrap(HashMap::get(&names, &3)));   // a literal, as a u64
}

Part of a temporary may be borrowed there too: a field (String::len(&person().name)), an element, or a slice of an array literal or a call's result (sum(&[1, 2, 3][0..2])). A String that a call returns may be viewed the same way, so a computed path needs no binding: read_file(&sprintf("%s/%s", dir, name)[0..$]). It lives as long as the temporary does, to the end of the statement. A field can also be moved out of a temporary, String n = person().name;: the rest of the temporary ends at once, as if the struct had been destructured. That needs a struct without a destructor of its own.

Parts of a temporaryruns
import std;

struct Person
{
    String name;
    String city;
}

fn person() : Person
{
    Person { .name = "Ada", .city = "London" }
}

fn sum(slice<i32, shared> xs) : i32
{
    i32 t = 0;
    foreach (x in xs)
    {
        t += *x;
    }
    t
}

fn main()
{
    assert(String::len(&person().name) == 3);        // a field of a temporary, borrowed
    assert(sum(&[1, 2, 3][0..2]) == 3);              // a slice of an array literal
    assert(StringView::len(&person().city[0..3]) == 3);  // a view of a temporary's String
    String n = person().name;                        // moved out; `city` ends here
    assert(String::eq_str(&n, "Ada"));
}

References inside structs

A reference may be stored in a field. It then lives as long as the struct that holds it, which in turn may not outlive the referent (§10).

A reference fieldruns
import std;

struct View
{
    ref<i32, shared> r;
}

fn main()
{
    i32 x = 1;
    auto v = View { .r = &x };        // the borrow is held by v's object
    assert(*v.r == 1);                 // ✓ valid as long as v lives
}

References as parameters

Passing &x or &mut x to a function creates a borrow that the parameter holds for the duration of the call and releases when the call returns. Inside the function the parameter is an ordinary reference. A parameter of type ref<T, exclusive> can be passed on to another call directly, without a new &mut, because it already is an exclusive reference.

Reference parametersruns
import std;

fn bump(ref<i32, exclusive> counter) : i32
{
    *counter += 1;
    *counter
}

fn bump_twice(ref<i32, exclusive> c) : i32
{
    bump(c);                          // pass the reference on as a value
    bump(c)
}

fn main()
{
    i32 n = 0;
    assert(bump(&mut n) == 1);         // the borrow lives for the call only
    assert(bump_twice(&mut n) == 3);
    assert(n == 3);
}
Rules of thumb
  • Use &x to lend, &mut x to lend for writing. The callee's signature tells you which it needs.
  • *r is a place, so *r = v, &*r, and (*r).f all work; for fields, r.f is the same as (*r).f.
  • Bind a call result before keeping a borrow of it; borrowing one just for a call, f(&make()), is fine.
  • == on references asks whether they point at the same thing, not whether the values are equal; compare *a == *b for values.

§10Lifetimes and temporal validity

Governing specification file: spec/10-temporal-validity.md

A reference is checked statically where the escape is visible and dynamically everywhere else; either way it never outlives its target.

A reference must never outlive the object it refers to. CobaltC enforces this with two layers: a lexical check that rejects the obvious escapes before the program runs, and a use-time check that catches everything else as diag.stale-binding at the first use of a dead reference. No lifetime annotations exist; the language decides validity from where things are declared.

A reference cannot leave its referent's scope

If a reference to a local is returned from the function, or stored into a variable declared in an enclosing block, it is rejected statically as diag.reference-escapes-scope.

Returning a reference to a localrejected: diag.reference-escapes-scope
fn dangling() : ref<i32, shared>
{
    i32 x = 1;
    &x                                // ✗ diag.reference-escapes-scope: x dies with this call
}

fn main()
{
    auto r = dangling();
    *r;
}

The corrected form returns the value itself: fn ok() : i32 { i32 x = 1; x }.

When the escape is not lexically visible (the reference is stored through another reference, into an aggregate the checker cannot see the owner of, or through a call), the static rule does not fire and the run-time check takes over. The program is still rejected, just later.

Caught at use timerejected: diag.stale-binding
struct Holder
{
    ref<i32, shared> r;
}

fn stash(ref<Holder, exclusive> h)
{
    i32 local = 5;
    h.r = &local;                     // a store through a reference: not lexically visible as an escape
}                                     // local ends here; whatever h points at now holds a dead reference

fn main()
{
    i32 outer = 0;
    auto held = Holder { .r = &outer };
    stash(&mut held);
    *held.r;                          // ✗ diag.stale-binding (dynamic): first use of the dead reference
}

Returning references from functions

A function may return a reference when it has exactly one reference-typed parameter: the result is understood to point into whatever that argument pointed at, and callers are checked accordingly. This is the shape of every library accessor (Vec::index_shared, Rc::get). A function with zero or several reference parameters may not return a reference (diag.lifetime-elision-ambiguous); compose single-reference helpers instead, or return an owned value.

Single-parameter elisionruns
import std;

struct Pair
{
    i32 a;
    i32 b;
}

fn first(ref<Pair, shared> p) : ref<i32, shared>
{
    &p.a                              // ✓ one reference parameter: the result borrows from it
}

fn main()
{
    Pair pr = Pair { .a = 1, .b = 2 };
    auto y = first(&pr);              // y is a shared borrow of pr.a, held by y
    assert(*y == 1);
}
Two reference parameters, one reference result
fn pick(ref<i32, shared> p1, ref<i32, shared> p2) : ref<i32, shared>
{
    p1                                // ✗ diag.lifetime-elision-ambiguous: which argument does the
}                                     //   result depend on? The language will not guess.

Because y above is known to borrow from pr, a later pr.a = 3; while y is still in use is an aliasing conflict (§08), exactly as if you had written auto y = &pr.a;.

Growth invalidates element references

A Vec may move its elements to a new buffer when it grows. A reference to an element obtained before a push that reallocates is stale afterwards. Where the checker can see both the borrow and the push in one function, it rejects the program; where it cannot (the push happens through a reference parameter, say), the stale reference faults at its next use. Either way it does not read freed memory.

Holding an element reference across a pushrejected: diag.aliasing-conflict
import std;

fn main()
{
    Vec<i32> v = Vec::new();
    Vec::push(&mut v, 10);
    auto first = Vec::index_shared(&v, 0);   // a shared borrow into v, held by first
    Vec::push(&mut v, 20);                   // ✗ diag.aliasing-conflict: an exclusive borrow of v
    *first;                                  //   while first (derived from v) is still live
}
Rules of thumb
  • Return values, not references, unless the function has exactly one reference parameter and the result points into it.
  • Do not store a reference in a variable that outlives the block where the referent was declared.
  • Re-fetch element references after any call that may grow or shrink a Vec.
  • If you see diag.stale-binding, ask what ended the referent: a scope exit, a move, a drop, or a reallocation.

§11Initialisation

Governing specification file: spec/11-initialization.md

Every variable is written before it is read, on every path, and every aggregate is built whole; constants name values known before the program runs.

Reading storage that has never been written is undefined behaviour in C. In CobaltC it is impossible in a well-formed program: a variable declared without an initialiser must be assigned on every path before it is read, and the check is static. Aggregates are always initialised whole.

Declarations

FormMeaning
T x = e;declare x of type T, initialised from e (which is checked against T)
auto x = e;declare x with the type of e
T x;declare x uninitialised; the type is required because there is nothing to infer from
Name { f1, f2 } = e;destructure a struct: bind each field to its name and consume the struct

Initialising from a plain value copies it; from a resource, moves it; from a temporary such as a struct literal or a call result, adopts it.

One name, one declaration per block

A block declares a name once. A second i32 total further down the same block is diag.duplicate-local, which says where the first one is: to change the variable, assign to it (total = 10;); for a value of another type, give it another name. A function's parameters count as declared in its body, and so do the names a foreach, a for header, a match arm or an if-pattern binds for the block they govern, so fn f(i32 n) { i32 n = n * 2; } is rejected too: write n = n * 2;.

A block inside it may reuse the name: the inner variable shadows the outer one until the inner block ends, and the outer one is untouched. Separate blocks, successive loops and separate match arms each have their own names, and a local may take the name of a function or a constant (a program's own names never clash with names added to std later). Languages differ here: Rust lets any declaration shadow, Java and C# forbid shadowing a local even from an inner block; CobaltC follows C and C++. Every local can already be assigned, so a second declaration in the same block would only hide a mistake.

Declared once in its blockrejected: diag.duplicate-local
import std;

fn main()
{
    i32 total = 0;
    total += 4;                       // ✓ change it by assigning
    {
        i32 total = 100;              // ✓ an inner block's own total
        assert(total == 100);
    }
    assert(total == 4);               // the outer one is untouched
    i32 total = 10;                   // ✗ diag.duplicate-local: declared above, in this block
}

Constants

const τ NAME = e; names a value, at the top level of any module or inside a function (below). NAME can be used wherever its value could, with the visibility, export, import and qualified paths of any item. e must be a constant expression: literals, other constants, operators, struct, array and variant literals of constant expressions and their fields and elements (SCREEN.w, PRIMES[2]), a function's name, and the pure intrinsics (min_value, sizeof, widen, …). A constant cannot be a resource, cannot depend on itself, and cannot be assigned to or borrowed; an overflow in computing one is reported at the constant, before the program runs. Integer, float, bool and str constants cost nothing at run time: each use becomes the literal.

Constantsruns
import std;

const u64 KIB = 1024;
const u64 MIB = KIB * KIB;
const u32 READ = 0x1;
const u32 WRITE = 0x2;
const u32 READ_WRITE = READ | WRITE;
const i32 LOWEST = -2147483648;

struct Size
{
    u32 w;
    u32 h;
}

const Size SCREEN = Size { .w = 640, .h = 480 };

fn main()
{
    static_assert(MIB == 1048576);
    static_assert(READ_WRITE == 3);
    static_assert(LOWEST < 0);
    static_assert(SCREEN.w * SCREEN.h == 307200);   // a field of a constant is a constant
}

A constant is typed exactly as a declaration τ x = e; would be: in const u64 MIB = 1024 * 1024; the literals are u64s, and an initializer that overflows its type, such as const i32 BIG = 65536 * 65536;, is rejected at the constant. const auto NAME = e; takes its type from e, as auto x = e; does: const auto MIB = KIB * KIB; is a u64 when KIB is, and a bare literal is an i32 (or an f64). When e alone does not settle a type — None, say — write the type.

A constant can also be declared inside a function, as a statement: const u8 LIMIT = 200;. It is the same kind of constant, but its name is scoped like a local: it can be used from its declaration to the end of its block, and an inner block can declare another constant or a variable of the same name, which hides it there (in its own block, a second declaration of the name is diag.duplicate-local, above). A local variable, even one set from a literal, is not a constant — any later statement could change it — so static_assert cannot use it; name the value with const when you want the check made before the program runs. A local constant's initializer can use other constants, but not a local variable or the function's type parameters.

Local constantsruns
import std;

fn checksum(slice<u8, shared> data) : u32
{
    const u32 PRIME = 16777619;       // FNV-1a: the 32-bit prime and offset basis
    const u32 SEED = 2166136261;
    static_assert(PRIME % 2 == 1);
    u32 h = SEED;
    foreach (b in data)
    {
        h = wrapping_mul(h ^ widen<u32>(*b), PRIME);
    }
    h
}

fn main()
{
    u8 b = 200;
    auto d = b + 1;                   // a variable: checked when the program runs
    assert(d == 201);
    const u8 B = 200;
    const auto D = B + 1;             // constants: checked before it runs (auto: D is a u8)
    static_assert(D == 201, "D is %v", D);
    array<u8, 3> abc = [97, 98, 99];
    assert(checksum(&abc[0..$]) == 440920331);
}

const means known when the program is compiled

A const can never change: assigning to it, or borrowing it with &mut, is rejected when the program is compiled. A constant is not a variable at all, but stands for its value, so assigning to it is like assigning to a literal. Copying its value out is fine: i32 y = LIMIT; gives a variable y, which can change, while LIMIT stays as it is.

A constant cannot changerejected: diag.type-mismatch
import std;

const i32 LIMIT = 10;

fn main()
{
    i32 y = LIMIT;                    // a copy of the value: y is an ordinary variable
    y += 1;
    LIMIT = 11;                       // ✗ diag.type-mismatch: a constant cannot be assigned
}

What makes it a constant is when its value is known: before the program runs, like const in Rust, Go and C# or constexpr in C++. That differs from C, C++, Java or JavaScript, where const (or final) often marks a variable that is set once, at run time, and never reassigned. const usize N = arg_count(); is diag.const-not-constant, because the value is only known once the program runs.

CobaltC has no read-only local variables either: every local can be assigned. It does not need them for safety, because every change to a variable shows where it happens. Only two things can change a local: an assignment to its name in its own function, or an exclusive reference to it, &mut x, which must be written out where it is made. A function given &x or a copy of x cannot change it; while a shared reference to it is in use, nothing can (diag.aliasing-conflict, §8); and threads share changing state only through a mutex. So to see whether x can change, look in its function for x = and &mut x. To lend a value without letting it change, lend it with &.

Where a variable can changeruns
import std;

fn peek(ref<i32, shared> r) : i32
{
    *r                                // may read; writing *r is diag.write-through-shared
}

fn bump(ref<i32, exclusive> r)
{
    *r += 1;
}

fn main()
{
    i32 limit = 10;                   // a variable: it can be assigned, as every local can
    assert(peek(&limit) == 10);       // &limit: the callee can only read it
    bump(&mut limit);                 // the one way another function changes it, visible here
    assert(limit == 11);
    ref<i32, shared> view = &limit;
    assert(*view == 11);              // while view is in use, nothing may write limit
    limit = 12;                       // ✓ view's last use is above, so it no longer holds limit
    assert(limit == 12);
}

Definite assignment

Assigned on every pathruns
import std;

fn main()
{
    i32 x;                            // uninitialised
    bool c = true;
    if (c)
    {
        x = 1;
    }
    else
    {
        x = 2;
    }
    assert(x == 1);                    // ✓ assigned on both paths
}
A path with no writerejected: diag.use-of-uninitialized
fn main()
{
    i32 x;
    bool c = false;
    if (c)
    {
        x = 1;
    }
    i32 y = x + 1;                    // ✗ diag.use-of-uninitialized: no write on the else path
}

The analysis is purely about the function's own control flow: it does not know that c happens to be true. If it cannot prove a write on some path, the program is rejected (this is one of the few checks where "unknown" means rejected rather than "check at run time").

Aggregates are initialised whole

You cannot declare a struct uninitialised and then fill its fields one by one. A struct literal supplies every field at once; after that, individual fields can be assigned freely.

Whole initialisationruns
import std;

struct Pair
{
    i32 a;
    i32 b;
}

fn main()
{
    Pair p = Pair { .a = 1, .b = 2 };  // every field, exactly once, in any order
    p.a = 10;                          // ✓ a field write on an initialised object
    assert(p.a == 10);
    assert(p.b == 2);
}

Destructuring a struct

Destructuringruns
import std;

struct Entry
{
    String item;
    i64 change;
}

fn main()
{
    Entry e = Entry { .item = "pens", .change = -3 };
    Entry { item, change } = e;       // binds `item` and `change`; e is consumed
    assert(String::len(&item) == 4 && change == -3);
}

This is the only pattern form outside match and the pattern conditions of if and while (§14). It names fields of the struct, each once, in any order, and consumes the struct: a resource field moves out to its new binding, a plain one is copied. It is how a struct gives up a resource it holds (the library's String::into_bytes uses it to hand back the Vec<u8>), since a resource field cannot otherwise be moved out of a struct that lives on. field : name binds a field to another name, so two values of one type can be taken apart side by side, and a final .. stands for the fields not named: they are not bound, and those holding resources are destroyed when the block ends. Naming a field twice, or leaving one out without .., is a type error, and a struct with its own destructor cannot be destructured, since the destructor would then run on a struct whose fields had left it (diag.move-out-of-field).

Renaming, and leaving fields outruns
import std;

struct Entry
{
    String item;
    i64 change;
    String note;
}

fn main()
{
    Entry a = Entry { .item = "pens", .change = -3, .note = String::new() };
    Entry b = Entry { .item = "ink", .change = 5, .note = String::new() };
    Entry { item : first, change : da, .. } = a;     // renamed; `note` is left and ends with the block
    Entry { item : second, change : db, .. } = b;
    assert(String::len(&first) + String::len(&second) == 7 && da + db == 2);
}
Rules of thumb
  • Prefer T x = e; or auto x = e;. Declare uninitialised only when the value genuinely comes from a branch, and then assign in every branch.
  • Build structs with a literal that names every field. Assign fields afterwards if needed.
  • auto needs an initialiser it can learn a type from; auto v = Vec::new(); is rejected because T is unknown (§12).

§12The type system

Governing specification file: spec/12-type-system.md

Nominal types, no coercions, monomorphic generics (one copy per use with concrete types) with five built-in bounds, and local inference that only flows downward.

CobaltC's type system is deliberately plain: every type is exactly what it is declared to be, nothing converts to anything else, and generics are templates instantiated with concrete types. What it gives you in return is that the type of every expression is decidable from the text, and that a function signature is a complete statement of what the function may do with its arguments.

The built-in types

TypeValuesResource?
integers, f32, f64, boolnumbers and truth values (§06)no
strtext: a valid UTF-8 byte sequence, written "…"; a whole value like an integer, not a reference and not owned (§21)no
void (type) / () (value)the unit type; the type of assignments, of if without else, of loops and of functions with no return type. Written void in type position and () as a value.no
ref<T, shared>, ref<T, exclusive>references (§09)no
rawptr<T>raw addresses (§20)no
array<T, N>N values of T in a row, N a constant expression (§16)if T is
slice<T, shared>, slice<T, exclusive>a borrowed run of elements of an array or Vec (§16)no
fn(T1, T2) : Rfunction valuesno
handle<T>, mutex<T>, guard<T>threads and locks (§19)yes
struct, enumuser-declared aggregates (§16)if marked resource or any part is
bitstructnamed runs of bits packed into one unsigned integer, u8 to u128 (§16)no
neverthe type of return, break and continue: an expression that never yields a value, accepted wherever any type is expectedn/a

Nominal, exact, no coercions

Two types are the same only if they have the same name and the same type arguments. ref<i32, shared> and ref<i32, exclusive> are different types. There is no subtyping and no implicit conversion anywhere: not between integer widths, not from T to ref<T, m>, not from Option<T> to T. This applies to arguments too. A literal is not an exception but a different thing: it has no type of its own until its position gives it one, so u64 x = 5;, StringView v = "x"; and String s = "x"; type the literal (§21) rather than convert a value, and the same positions still reject an i32, a str or any other typed value.

No implicit widening, even for argumentsrejected: diag.type-mismatch
fn takes_i64(i64 x) : i64
{
    x
}

fn main()
{
    i32 y = 5;
    takes_i64(y);                     // ✗ diag.type-mismatch: i32 is not i64; write takes_i64(widen<i64>(y))
}

A bare integer literal is the one flexible thing: takes_i64(5) is fine, because the literal takes its type from the parameter (§06).

Equality

== and != are defined for numbers, bool, (), str (bytewise), references (identity), raw pointers (address), and for structs, arrays and enums whose parts are all comparable (element by element; enums must have the same variant and equal payloads). They are not defined for function values or for anything that is a resource, with one exception: two Strings compare by their text.

< <= > >= order numbers, bool (false before true), and text: two strs, two Strings or two StringViews, by their bytes, a prefix first (for UTF-8 that is code point order). A String operand is not copied or moved: it is borrowed for the comparison, as &s would be, so it stays usable afterwards.

Comparing text and truth valuesruns
import std;

fn main()
{
    String a = "app";
    String b = "apple";
    assert(a != b && a < b);          // text order: a prefix first
    assert("b" > "abc");
    assert(false < true);
    String c = a;                     // a was only borrowed by the comparisons; it can still move
    assert(c == String::from_str("app"));
}

Generics

Functions, structs and enums can take type parameters. Generics are monomorphic: each use with a different set of concrete types is a separate copy of the declaration, compiled (or, in the interpreter, run) as if you had written it out with those types: identity(5) and identity(2.5) below call two functions, one for i32 and one for f64. So there is no run-time type information, no boxing and no indirect call at call sites; the cost is one copy per combination of types used. Inside a generic body a value of a type parameter T can be stored, passed, returned, referenced and moved. To do more with it — compare it, add it — give T a bound (below).

Generic declarationsruns
import std;

struct Box<T>
{
    T value;
}

enum Maybe<T>
{
    Just(T),
    Nothing
}

fn identity<T>(T x) : T
{
    x                                 // all a bare T can do: be moved around
}

fn boxed<T>(T x) : Box<T>
{
    Box { .value = x }                // the field type is T, so this is fine
}

fn main()
{
    assert(identity(5) == 5);          // T := i32, from the argument
    assert(identity<i64>(5) == 5);     // T given explicitly: name<Type>(...)
    Box<i32> b = Box { .value = 7 };  // Box's T from the declared type
    assert(b.value == 7);
    Box<Vec<u8>> c = boxed(Vec::new());   // both T's from the declared type of c
    assert(Vec::len(&c.value) == 0);
}

Bounds

A function's type parameter may carry one of five bounds, written after a colon: fn max<T: ordered>(T a, T b) : T. Each of the first four names a fixed set of built-in types and the operators every one of them has, and each includes the ones before it; the fifth, clone, says a value of T can be copied (below):

BoundSatisfied byAllows on a T
eqintegers, floats, bool, str, String== !=
orderedintegers, floats, bool, str, Stringalso < <= > >=
numberintegers, floatsalso + - * / %, unary -, += and the like; integer literals
integerintegersalso & | ^ ~, << >>
cloneeverything above; String, Rc<T>, Box<T: clone>, Vec<T: clone>, the hash tables; a type with its own T::clone; a struct or enum whose fields are all clone (D-0116)clone(&x): an independent copy
Note
Built-in bounds describe compiler-defined capabilities. They are not user-extensible traits: there is no way to declare a new bound. Your own struct or enum satisfies clone alone — by its fields, or by a fn T::clone(ref<T, shared> self) : T you declare, recognised by its name as T::drop is — and never the four operator bounds: to compare or combine values of your own types in generic code, pass the operation in as a function value, as Vec::sort_by takes less.
Bounded type parametersruns
import std;

fn largest<T: ordered>(slice<T, shared> xs) : T
{
    T best = xs[0];
    foreach (x in xs)
    {
        if (*x > best) { best = *x; }
    }
    best
}

fn total<T: number>(slice<T, shared> xs) : T
{
    T sum = 0;                        // a literal takes T's type, a float one too
    foreach (x in xs)
    {
        sum += *x;
    }
    sum
}

fn main()
{
    array<i32, 3> a = [4, -2, 9];
    array<f64, 2> f = [1.5, 2.25];
    assert(largest(&a[0..$]) == 9);
    assert(total(&a[0..$]) == 11);
    assert(total(&f[0..$]) == 3.75);
}

The body is checked once against the bound: a + b in a function whose T is only ordered is diag.unbounded-type-parameter. Each call is checked for a type that satisfies it: largest of bools, or of a String, is diag.type-mismatch. Each use is also checked at its concrete type, so -x at T = u32 is still rejected there. Structs and enums take no bounds; put them on the functions that use the operations.

clone(&x) copies a value of any clone type: a plain one by reading it, String and the containers by their own clone, and a struct or enum of clone fields by a copy the compiler derives, field by field. A resource type — one with a destructor — declares its own T::clone or has none, because only it knows how to copy what it manages. Generic code says what it needs with the bound:

The clone boundruns
import std;

struct Person
{
    String name;                     // Person is clone: every field is
    u32 age;
}

enum Shape
{
    Dot,
    Labelled(String),                // Shape is clone too
}

resource struct Ticket
{
    u32 id;
}

fn Ticket::drop(ref<Ticket, exclusive> self)
{
}

fn Ticket::clone(ref<Ticket, shared> self) : Ticket   // a resource says how it is copied
{
    Ticket { .id = self.id + 1000 }
}

fn pair_up<T: clone>(ref<T, shared> x) : Vec<T>     // needs T: clone to call clone(x)
{
    Vec<T> v = Vec::new();
    Vec::push(&mut v, clone(x));
    Vec::push(&mut v, clone(x));
    v
}

fn main()
{
    Person a = Person { .name = "Ada", .age = 36 };
    Person b = clone(&a);                              // derived: name copied, age copied
    String::push_ascii(&mut b.name, b'!');
    assert(String::eq_str(&a.name, "Ada") && String::eq_str(&b.name, "Ada!"));
    Shape s = Labelled(String::from_str("x"));
    Shape s2 = clone(&s);
    Vec<Person> people = pair_up(&a);
    Vec<String> names = Vec::from_fn(2, [](usize i) : String { sprintf("n%d", i) });
    Vec<String> copy = Vec::clone(&names);              // Vec::clone needs T: clone; String is
    Ticket t = Ticket { .id = 1 };
    Vec<Ticket> tickets = Vec::filled(2, clone(&t));    // Ticket::clone, three times
    assert(Vec::len(&people) == 2 && Vec::len(&copy) == 2 && tickets[1].id == 2001);
}

Inference is local and downward. A type parameter is found from, in order: an explicit type argument (identity<i64>(5)), the type of an argument in the same call, or the expected type where the call appears (a declared variable type, a parameter type, a field type). An unsuffixed literal argument comes last: it takes the type the other arguments or the expected type give its parameter, so with i64 x, pick(true, 0, x) makes T an i64 and the 0 with it; only when nothing else fixes T does a literal's own default (i32, f64) decide. Nothing is ever inferred from how a value is used later, so a call that fixes nothing is rejected:

Nothing to infer fromrejected: diag.cannot-infer-type-parameter
import std;

fn main()
{
    auto v = Vec::new();              // ✗ diag.cannot-infer-type-parameter: nothing says what T is
    Vec::push(&mut v, 1);             //   (a later use does not count)
}

The same holds when the call is an argument to another generic call. Mutex::new<T> is one: with auto nothing reaches it, so nothing reaches the Vec::new() inside it either. Give the outer binding its type and both are fixed.

Inference through Mutex::newruns
import std;

fn main()
{
    mutex<Vec<u8>> m = Mutex::new(Vec::new());   // T of Mutex::new, then T of Vec::new, from the declaration
    mutex<i64> big = Mutex::new(5000000000);     // and the literal is an i64 for the same reason
    assert(Vec::len(&*lock(&m)) == 0);
}
Nothing to infer fromrejected: diag.cannot-infer-type-parameter
import std;

fn main()
{
    auto m = Mutex::new(Vec::new());  // ✗ diag.cannot-infer-type-parameter: nothing fixes Vec's T
}
Where inferred types come fromruns
import std;

struct Box<T>
{
    T value;
}

fn identity<T>(T x) : T
{
    x
}

fn pick<T>(bool first, T a, T b) : T
{
    if (first) { a } else { b }
}

fn main()
{
    Vec<i32> a = Vec::new();                  // T from the declaration
    Box<Vec<u8>> c = Box { .value = Vec::new() };   // both T's from the declared type
    Vec::push(&mut c.value, 200);             // the literal becomes u8 from the field type
    assert(Vec::len(&c.value) == 1);
    assert(Vec::len(&a) == 0);
    i64 w = identity<i64>(3);                 // explicit: name<Type>(...)
    assert(w == 3);
    i64 big = 5000000000;
    Vec<i64> both = Vec::new();
    Vec::push(&mut both, pick(true, 0, big)); // T from big; the 0 is an i64
    assert(both[0] == 0);
}

auto and the expected type

auto x = e; gives x the type of e. Since inference is downward only, auto works whenever e has a type of its own: a literal (i32/f64 by default), an arithmetic expression, a call whose type parameters are already fixed, a struct literal with a concrete type. Where e needs help (Vec::new()), write the type on the left instead: auto m = Mutex::new(HashMap::new()); cannot know the map's types, but mutex<HashMap<String, i64>> m = Mutex::new(HashMap::new()); can. The branches of an if or match share one type, and a branch that is only a literal takes it from the others: in auto n = if (big) { count } else { 0 }; with count : i64, the 0 is an i64.

Branch types and declared typesruns
import std;

fn main()
{
    i64 count = 5000000000;
    auto n = if (count > 10) { count } else { 0 };     // 0 : i64, from the other branch
    Option<i64> o = None;
    auto m = match (o) { None : -1, Some(v) : v };     // -1 : i64, from the Some arm
    assert(n + m == 4999999999);
    mutex<HashMap<String, i64>> totals = Mutex::new(HashMap::new());   // the declaration fixes K and V
    assert(HashMap::len(&*lock(&totals)) == 0);
}
Rules of thumb
  • Write the type you mean; the language will not adjust it for you. Convert integers with widen/narrow, and take references explicitly.
  • Use auto freely for values whose type is obvious from the right-hand side, and a written type for empty containers and generic constructors.
  • Keep generic code to plumbing (containers, wrappers, moving values around). Anything that computes on T needs a concrete type.
  • A void function returns (); you may write : void explicitly or leave the return type off.

§13Expressions

Governing specification file: spec/13-expression-semantics.md

Places versus values, strict left-to-right evaluation, and the operators.

CobaltC has a small expression language with a fixed evaluation order and a clear line between places (things you can assign to or borrow) and values. Almost everything is an expression, including blocks, if and match, which is what lets you write i32 x = if (c) { 1 } else { 2 };.

Places and values

A place is a variable name, a field access e.f, an index e[i], or a dereference *r. A place can be read (yielding its value), written (place = e), borrowed (&place, &mut place), or projected further. Everything else, such as a literal, an arithmetic result, a call result or a struct literal, is a value or a temporary. Reading a place that holds a resource is not allowed (that would copy the resource, §07); such a place may only be moved, borrowed, or destructured.

Placesruns
import std;

struct Pair
{
    i32 a;
    array<i32, 3> items;
}

fn main()
{
    Pair p = Pair { .a = 1, .items = [10, 20, 30] };
    p.a = 99;                         // a field is a place
    p.items[1] = 200;                 // so is an element
    {
        auto r = &mut p.items[2];
        *r = 300;                     // and so is *r
    }
    assert(p.a == 99);
    assert(p.items[1] == 200);
    assert(p.items[2] == 300);
}

Evaluation order

Every expression is evaluated strictly left to right: the left operand before the right, the callee before its arguments, arguments in written order, struct-literal fields in written order. Assignment evaluates the target place first, then the value. && and || short-circuit: the right operand runs only when needed.

Left to right, and short-circuitruns
import std;

fn bump(ref<i32, exclusive> counter) : i32
{
    *counter += 1;
    *counter
}

fn main()
{
    i32 n = 0;
    i32 left = bump(&mut n);
    i32 right = bump(&mut n);
    assert(left == 1);                 // the first call ran first
    assert(right == 2);
    bool touched = false;
    if (true || bump(&mut n) == 0)    // the right side is never evaluated
    {
        touched = true;
    }
    assert(touched);
    assert(n == 2);
}

Expressions that produce values

if, match and blocks as valuesruns
import std;

fn main()
{
    bool c = true;
    i32 x = if (c)                            // if is an expression; both arms must have one type
    {
        1
    }
    else
    {
        2
    };
    i32 y =                                   // a block's value is its trailing expression
    {
        i32 t = x * 3;
        t + 1
    };
    Option<i32> o = Some(x);
    i32 z = match (o)                         // so is match
    {
        Some(v) : v * 10,
        None    : 0,
    };
    assert(x == 1);
    assert(y == 4);
    assert(z == 10);
    i32 w = 0;
    w = 5;                                    // assignment is an expression of type void
    assert(w == 5);
}
Tip
A block, if, while or match used as a statement needs no semicolon after its closing brace. When one of them is the right-hand side of a declaration or assignment, the statement as a whole still ends with ;, as in i32 x = if (c) { 1 } else { 2 };.

Operator precedence

From tightest to loosest binding. Comparison is non-associative: a < b < c is a syntax error, and a < b == c needs parentheses.

LevelOperatorsNotes
postfixe.f e[i] e(args) e?field, index, call, propagate
unary-e !e ~e &e &mut e *enegate, boolean not, bitwise not, borrow, dereference
multiplicative* / %left-associative
additive+ -left-associative; p + n on a rawptr is pointer offset (§20)
shift<< >>the right operand is a u32
bitwise and&
bitwise xor^
bitwise or|
comparison== != < <= > >=non-associative
logical and&&short-circuit
logical or||short-circuit
assignment= += -= *= /= %= &= |= ^= <<= >>=right-associative; the left side must be a place

x op= e is x = x op e with x evaluated once: the same types, the same overflow check (x += 1 can overflow), the same write rules. Evaluating the place once matters only when it contains a call, as in *Vec::index_exclusive(&mut v, i) += 1, which calls index_exclusive once. There is no ++ or --; write += 1.

Watch out
Shifts bind tighter than the bitwise operators, as in C and Rust: hi << 8 | lo is (hi << 8) | lo. Unlike C, the bitwise operators bind tighter than comparisons, so x & 1 == 0 parses as (x & 1) == 0. When in doubt, add parentheses.
Rules of thumb
  • Use if, match and blocks as values instead of declaring a variable and assigning in each branch.
  • Side effects in one operand are visible to the next: order is defined, so you may rely on it, but readers will thank you for not making them.
  • Parenthesise mixed comparisons and bitwise expressions.

§14Control flow and scopes

Governing specification file: spec/14-control-flow.md

Blocks, if, while, for, foreach, break and continue; scope exit as the moment of destruction; and the fixed line between compile-time and run-time checks.

Control flow is C's if, while and for, without switch and goto, plus expression-valued if and match and a foreach over collections. What is new is how carefully the language ties scope to destruction, and the precise line between what the compiler proves and what the run time checks.

Blocks and scopes

A block { … } opens a scope. Variables declared in it die when it ends, in reverse order; resources are destroyed at that point (§07); references held by those variables end too. A block is an expression: its value is its trailing expression, or () if there is none. Every statement is a scope of its own for temporaries: a value created during a statement and not stored anywhere is destroyed at the semicolon.

if and else

if (cond) { … } else { … } with mandatory parentheses and mandatory braces. else if chains as expected. An if without else has type void; an if used as a value needs both arms, of the same type.

The condition may also be a pattern test, if (Some(x) = e) { … } else { … }: it means match (e) { Some(x) : { … }, _ : { … } }, so x exists in the first block only, e is consumed exactly as that match would consume it, and the whole if can be a value. Any match pattern works — nested (Ok(Some(l))), a literal (if (0 = n)), a qualified variant — as long as it can fail: a bare name on the left of = is the assignment it always is, which is not a bool and is rejected, so if (x = e) never silently tests anything. A match stays the form when both cases do work, and the only form that checks every case is handled.

A pattern as a conditionruns
import std;

fn main()
{
    HashMap<String, i32> ages = HashMap::new();
    HashMap::insert(&mut ages, String::from_str("Ada"), 36);
    if (Some(a) = HashMap::get_str(&ages, "Ada"))
    {
        assert(*a == 36);             // a: ref<i32, shared>, in scope here only
    }
    Option<i32> o = Some(4);
    i32 doubled = if (Some(x) = o) { x * 2 } else { 0 };
    assert(doubled == 8);
    Result<Option<i32>, str> r = Ok(Some(7));
    if (Ok(Some(n)) = r) { assert(n == 7); } else { assert(false); }
    if (Err(_) = r) { assert(false); }
}

while, for, break, continue

while (cond) { … } loops while cond holds; for (init; cond; step) { … } is C's counted loop: init runs once (a declaration there belongs to the loop), cond is tested before each iteration, and step runs after each one. Any of the three may be left out; a missing cond is true, so for (;;) loops until a break. The body is a fresh scope each iteration, so anything declared inside it is destroyed before the next test. break exits the innermost loop; continue goes to its next iteration, running a for's step first. Both are typed never and must appear inside a loop. A body is always a block, so C's trap for (…); (an empty loop, then a block that runs once) is a syntax error here, and the message says to remove the ;.

A while condition may be a pattern test too: while (Some(x) = e) { … } is while (true) { match (e) { Some(x) : { … }, _ : break } } — e is evaluated again each time round, the body runs with x bound while the pattern matches, and the first value that does not match ends the loop. That last point matters for a Result: while (Ok(Some(line)) = read_line()) stops at the end of input and on a read error alike, which is the loop's written decision; a loop that must tell them apart keeps the match.

A pattern loopruns
import std;

fn main()
{
    Vec<String> stack = Vec::from_fn(3, [](usize i) : String { sprintf("job %d", i) });
    usize done = 0;
    while (Some(job) = Vec::pop(&mut stack))     // each job moved out, destroyed at the body's end
    {
        if (String::ends_with(&job, "1")) { continue; }
        done += 1;
    }
    assert(done == 2 && Vec::len(&stack) == 0);
    u32 n = 10;
    while (Some(m) = checked_sub(n, 3))          // stops when the subtraction would go below zero
    {
        n = m;
    }
    assert(n == 1);
}
Loopsruns
import std;

fn main()
{
    i32 sum = 0;
    i32 i = 0;
    while (i < 10)
    {
        i += 1;
        if (i == 3)
        {
            continue;                 // skip 3
        }
        if (i == 7)
        {
            break;                    // stop before adding 7
        }
        sum += i;
    }
    assert(sum == 1 + 2 + 4 + 5 + 6);
    Vec<i32> v = Vec::new();
    Vec::push(&mut v, 4);
    Vec::push(&mut v, 9);
    i32 total = 0;
    for (usize k = 0; k < Vec::len(&v); k += 1)   // an index loop; foreach (below) is shorter
    {
        total += v[k];
    }
    assert(total == 13);
    u32 odd = 0;
    for (u32 j = 0; j < 10; j += 1)
    {
        if (j % 2 == 0)
        {
            continue;                 // still runs j += 1
        }
        odd += j;
    }
    assert(odd == 25);
}
break outside a looprejected: diag.break-outside-loop
fn main()
{
    break;                            // ✗ diag.break-outside-loop
}

Loops that only end by returning. A while (true) that no break leaves can end only by return, a fault or exit, so nothing after it ever runs: its type is never, like return's (D-0175). A function may therefore end with such a loop and needs no value after it. A break inside a nested loop leaves only that loop and does not count; a break that leaves the while (true) itself makes it an ordinary loop again, after which the function must still give its value. Only the literal true has this meaning: the compiler does not look into a condition that merely happens to stay true. There is no separate loop keyword; while (true) is the way to say "forever".

A function that ends in its loopruns
import std;

// The first line that starts with `#`, read until one does; no value is
// needed after the loop, because nothing after it can run.
fn first_comment(ref<Vec<String>, shared> lines) : usize
{
    usize i = 0;
    while (true)
    {
        if (String::starts_with(&lines[i], "#"))
        {
            return i;
        }
        i += 1;
    }
}

fn main()
{
    Vec<String> lines = Vec::new();
    Vec::push(&mut lines, String::from_str("x = 1"));
    Vec::push(&mut lines, String::from_str("# note"));
    printf("line %v\n", first_comment(&lines));
}
output
line 1

foreach

foreach (x in c) { … } visits every element of a Vec, an array, a slice, a HashSet or a HashMap, in order: by index for a Vec or an array, in insertion order for a set or a map. How the loop holds the collection is written at the loop, the same three ways a function takes a value:

WrittenEach element isAfterwards
foreach (x in c)the element itself, moved out of cc is gone: the loop consumed it
foreach (x in &c)a shared reference, ref<T, shared>: read itc is unchanged and usable
foreach (x in &mut c)an exclusive reference: *x = … changes the elementc is usable, with its changes

Use &c most of the time. A second name gives the position: foreach (i, x in &v) has i : usize. A HashMap is always looped over with two names, the key and the value: foreach (k, v in &m), or three to have the position too, foreach (i, k, v in &m). printf prints what a reference refers to, so printf("%d", x) works with x a ref<i32, shared>; elsewhere write *x for the number.

The three formsruns
import std;

fn shout(String s)                    // takes ownership
{
    printf("%s!\n", &s);
}

fn main()
{
    Vec<String> names = Vec::new();
    Vec::push(&mut names, String::from_str("ada"));
    Vec::push(&mut names, String::from_str("lin"));
    foreach (i, name in &names)       // read: names is still usable afterwards
    {
        printf("%d: %s\n", i, name);
    }

    Vec<i32> numbers = Vec::new();
    Vec::push(&mut numbers, 3);
    Vec::push(&mut numbers, 5);
    foreach (n in &mut numbers)       // change in place
    {
        *n *= 2;
    }
    foreach (n in &numbers)
    {
        printf("%d ", n);             // printf reads through the reference
    }
    printf("\n");

    HashMap<str, u32> stock = HashMap::new();
    HashMap::insert(&mut stock, "pens", 12);
    HashMap::insert(&mut stock, "ink", 3);
    foreach (item, count in &stock)
    {
        printf("%-5s %2d\n", item, count);
    }

    foreach (name in names)           // consume: each String moves into shout
    {
        shout(name);
    }
    // names is gone here; using it would be diag.stale-binding
}
output
0: ada
1: lin
6 10 
pens  12
ink    3
ada!
lin!

The loop's borrow of the collection lasts the whole loop, so a body that tries to change the collection itself (push to it, clear it) is rejected before the program runs, the same aliasing rule as anywhere else. There is no iterator invalidation to debug. A consuming loop left early, by break, return or a fault, destroys the elements it had not reached, exactly once.

The collection cannot change under the looprejected: diag.aliasing-conflict
import std;

fn main()
{
    Vec<i32> v = Vec::new();
    Vec::push(&mut v, 1);
    foreach (x in &v)
    {
        Vec::push(&mut v, *x);        // ✗ diag.aliasing-conflict (static): v is borrowed by the loop
    }
}

An array of plain values, such as array<i32, 5>, is copied by the by-value form: foreach (i, e in arr) walks a copy, so arr is neither borrowed nor used up, a write to arr[j] in the body is allowed (and not seen by e), and arr is still there afterwards. To change the elements themselves, loop over &mut arr and write through the reference: *e = …. While that loop runs, arr is borrowed by it, so reach the elements through e, not through arr[j].

Changing an array in placeruns
import std;

fn main()
{
    array<i32, 5> arr = [0, 0, 0, 0, 0];

    foreach (i, e in &mut arr)
    {
        *e = narrow<i32>(i) * 10;       // write through the element's reference
    }
    arr[4] += 1;                        // direct indexing works too, outside the loop

    foreach (i, e in &arr)
    {
        printf("[%v] = %v\n", i, *e);
    }
}
output
[0] = 0
[1] = 10
[2] = 20
[3] = 30
[4] = 41

Counting with a range

foreach (x in lo..hi) counts from lo up to, not including, hi, the same way &a[lo..hi] takes elements lo up to hi. So 0..n runs exactly n times, and 1..10 stops at 9. A second name gives the position, counted from 0: foreach (i, x in lo..hi). Both bounds are evaluated once, lo first. If hi is not above lo, the body never runs.

x has the bounds' integer type. A plain number takes the other bound's type, so with u8 top, 0..top counts in u8. Two plain numbers are usize, so the counter can index an array or a Vec directly; for a negative range, give one bound a type: -3..0: i32. Bounds of two different types are rejected rather than converted. A range can end at its type's largest value without overflowing, because the counter never passes hi.

Rangesruns
import std;

fn main()
{
    foreach (v in 1..10)
    {
        printf("%v ", v);
    }
    printf("\n");

    auto names = ["ada", "lin", "kay"];
    foreach (i in 0..3)                        // i : usize, so it can index
    {
        printf("%v: %v\n", i, names[i]);
    }

    u8 top = 255;
    foreach (i, b in 253..top)                // b : u8; i is the position
    {
        printf("step %v is %v\n", i, b);
    }
}
output
1 2 3 4 5 6 7 8 9 
0: ada
1: lin
2: kay
step 0 is 253
step 1 is 254

A range to count over is written only in a foreach (a slice's lo..hi, §16, is the same notation for elements). It is not a value you can store, and it has no inclusive form or step. To count down or in steps, use a for loop.

Early exit and unwinding

return, break, continue, and a run-time fault all leave scopes the same way: every statement scope and block between here and the target is exited in order, destroying what each one owns. A return e first moves e out so it survives the unwinding. You never have to write cleanup code on an early exit.

What is checked when

Many rules in this guide say a program is "rejected" in one example and "faults at run time" in another. The boundary is fixed by the language, not left to the compiler's cleverness, so that every implementation accepts and rejects the same programs. A compiler may prove more than this checker does and leave out the run-time checks it has proved unnecessary (that is what cobc does; see the performance chapter), but it may not accept a program the language rejects or reject one it accepts: coby --check is the reference, and what it accepts is the language. Within one function body, the checker tracks a few facts about each variable: whether it has been moved from, whether it has been initialised, which references were visibly borrowed from it, and whether a reference to it has been handed somewhere the checker cannot follow (a call, a field, a thread). At each checked operation the outcome is one of three:

OutcomeWhenEffect
proventhe tracked facts guarantee the conditionno run-time check
refutedthe tracked facts guarantee it failsthe program is rejected (a static diagnostic)
unknownthe facts do not decide (the value came from a call, a loop merged two states, a reference passed through a parameter)the run-time check runs, and faults if it fails (a dynamic diagnostic)

The analysis looks only at the current function and only at literals for value ranges. So 2147483647 + 1 is refuted statically, add(2147483647, 1) is checked when add runs, and borrowing x twice exclusively in one function is refuted while doing so through two reference parameters is checked at the use. Three exceptions are stricter: definite assignment (§11), a use of a variable that may have been moved (§7), and reference escape (§10) reject on "unknown" rather than deferring.

Rules of thumb
  • Let scopes do your cleanup. Open a block to bound a borrow or a lock; close it to release.
  • A while body is re-entered fresh each iteration; declare per-iteration variables inside it.
  • Prefer trailing expressions to return at the end of a function; use return for early exits.
  • If a diagnostic says static, the checker saw the problem in the text; if dynamic, it saw it when the values arrived. Both are the same rule.

§15Functions and closures

Governing specification file: spec/15-function-semantics.md

Signatures that state the whole contract, associated functions, function values, and closures with checked capture lists.

Functions in CobaltC carry their whole contract in the signature. A parameter's type says whether the function takes ownership, may only look, or may modify. Closures are ordinary structs that capture what they use, and the capture list is checked, not trusted.

Declaring and calling

Functionsruns
import std;

// Parameters are type-first; the return type follows a colon.
fn area(i32 w, i32 h) : i32
{
    w * h                             // the trailing expression is the result
}

// No return type means void; `return;` leaves early.
fn clamp_in_place(ref<i32, exclusive> x, i32 hi)
{
    if (*x <= hi)
    {
        return;
    }
    *x = hi;
}

// An associated function: a plain function whose name is qualified by a type.
struct Rect
{
    i32 w;
    i32 h;
}

fn Rect::square(i32 side) : Rect
{
    Rect { .w = side, .h = side }
}

fn Rect::area(ref<Rect, shared> r) : i32
{
    area(r.w, r.h)
}

fn fact(i32 n) : i32
{
    if (n <= 1)                       // recursion needs nothing special
    {
        1
    }
    else
    {
        n * fact(n - 1)
    }
}

fn main()
{
    i32 v = 50;
    clamp_in_place(&mut v, 10);
    assert(v == 10);
    auto sq = Rect::square(4);
    assert(Rect::area(&sq) == 16);     // called by qualified name; there is no method syntax
    assert(fact(5) == 120);
}

Recursion needs nothing special, and its depth is limited only by the stack. Every implementation allows at least 10 000 nested calls, and both tools allow far more: they run a program and each of its threads on a large stack. A call beyond the limit is not a crash but a fault at the call, diag.stack-exhausted, so a recursion that runs away ends with a diagnostic like any other failure.

A parameter list and an argument list may end with a comma, as array and struct literals may: handy when a long call is split one argument per line. A comma is never the whole list: f(,) is a syntax error.

Trailing commasruns
import std;

fn span(
    i32 from,
    i32 to,
) : i32
{
    to - from
}

fn main()
{
    assert(span(
        3,
        10,
    ) == 7);
}

What a parameter type promises

Parameter typeThe caller writesThe callee mayAfter the call
plain T (numbers, plain structs)f(x)use its own copythe caller's x is unchanged
resource T (Vec<i32>, handle<T>, …)f(v)own it: mutate, move on, or let it be destroyed at its scope endthe caller's v is stale
ref<T, shared>f(&x)read through itthe borrow is released; x is unchanged
ref<T, exclusive>f(&mut x)read and write through it, but not destroy or move xthe borrow is released; x may have changed

Function values

A function type is written fn(T1, T2) : R (or fn(T1) for a void result). A parameter of that type accepts a function with that signature, or a closure with the same parameters and result. Function values cannot be compared.

A closure's type is fn(parameters) : result, the result being what its body gives. Where a fn type is expected, as when a closure is declared fn(u32) : u32 f = … or passed to such a parameter, the body is checked against that result, and its literals take that type. A closure declared with auto takes its body's own type: [](u32 x) { 1 } gives an i32, as auto y = 1 does. A closure can be stored like any value: in a variable, a field, a Vec<fn(…) : …> or a Box, and called from there.

A function value owns the closure it holds and destroys it when it ends. Copying the value (g = f, passing it by value) copies the closure with its captures, so each copy keeps its own state. The one exception is a closure that owns a resource, such as a move closure that captured a String: copying it would duplicate the resource, so it moves instead, and the value it came from is left empty. Calling an empty function value is diag.stale-binding; the function type does not say what its closure owns, so this is found when the program runs. Calling through a reference, a field or an element ((*f)(x), (t.run)(), (fs[i])()) calls the value where it is and moves nothing.

Closures

Functions do not nest: a fn declared inside another function is a syntax error, unless it is a function of a type declared in the same block (fn T::name, §16). A function needed only inside another is written there as a closure, which can also use the variables around it.

A closure is written [captures](params) { body }, in the style of a C++ lambda. The bracket must list exactly the outer variables the body uses, no more and no fewer; it is documentation that the compiler checks (diag.capture-list-mismatch). How each variable is captured is decided by the body, not the list: a variable the body only reads is captured by shared reference; one the body writes, borrows exclusively, or moves is captured by exclusive reference. Prefix move to capture by value instead, moving resources into the closure. The result type is taken from the body, or from the fn(…) : R the closure is passed as; write it after the parameters, as for a function, when neither says enough: [](i64 a) : i64 { 0 }.

A closure's result type, writtenruns
import std;

fn main()
{
    auto zero = [](i64 a) : i64 { 0 };        // without `: i64` the 0 would be an i32
    auto small = [](u8 a) : bool { a < 10 };
    assert(zero(5000000000) == 0 && small(3));
}
Capturingruns
import std;

fn apply(fn(i32) : i32 f, i32 x) : i32
{
    f(x)
}

fn main()
{
    i32 k = 10;
    auto add_k = [k](i32 x)           // k is read only: captured as ref<i32, shared>
    {
        x + k
    };
    assert(add_k(5) == 15);
    assert(apply(add_k, 4) == 14);     // a closure where a fn(i32) : i32 is expected

    Vec<i32> v = Vec::new();
    Vec::push(&mut v, 7);
    auto reader = move [v]()          // v moves into the closure; the closure owns it now
    {
        v[0]
    };
    assert(reader() == 7);
    assert(reader() == 7);             // calling twice is fine: nothing is consumed
}                                     // reader is destroyed here, and its Vec with it
The capture list must matchrejected: diag.capture-list-mismatch
fn main()
{
    i32 x = 1;
    i32 unused = 2;
    auto f = [x, unused]()            // ✗ diag.capture-list-mismatch: unused is not used by the body
    {
        x
    };
    f();
}

A closure that captures by reference holds those borrows for as long as the closure object lives, so the captured variables are subject to the usual aliasing rules until it goes out of scope: a closure that writes total blocks every other use of total while it exists. Put such closures in a block, or use move. Calling a closure borrows it shared for the call: its writes go through the captures, which are checked as any reference is, so a closure that writes a capture cannot be called through a shared reference to it.

An exclusive capture, bounded by a block
fn main()
{
    i32 total = 0;
    {
        auto add = [total](i32 x)     // the body writes total: captured as ref<i32, exclusive>
        {
            total += x;
        };
        add(5);
        add(7);
    }                                 // add ends here and releases its borrow of total
    i32 seen = total;                 // ✓ 12: no borrow remains
}

Generic functions

fn name<T>(...) declares type parameters; see §12 for what a body may do with T and how T is inferred at each call.

main

A program is a set of declarations with exactly one main at the top level, taking no parameters: fn main(), or fn main() : u8. Execution is the evaluation of main(); the program's observable behaviour is its termination outcome (success or a named fault) plus the sequence of extern calls it makes.

A main that returns a u8 sets the program's exit status; fn main() exits with 0. Returning from main runs every destructor still pending and waits for every thread, as leaving any block does, so the status is reported only after all of that. A fault still ends the program with its diagnostic instead. The arguments the program was started with come from arg_count() and arg(i) (§21); argument 0 is the first one after the program, not the program's name.

An exit status and an argument: coby greet.cb Adaruns
import std;

fn main() : u8
{
    if (arg_count() != 1)
    {
        printf("usage: greet NAME\n");
        return 2;                     // exit status 2
    }
    match (arg(0))
    {
        Ok(name) :
        {
            printf("hello, %v\n", &name);
            0
        },
        Err(_)   :
        {
            printf("the name is not UTF-8\n");
            1
        },
    }
}
output
hello, Ada

Run with no argument, the same program prints its usage line and exits with status 2. A main with parameters, or returning anything other than u8, is rejected with diag.no-main.

Rules of thumb
  • Choose the parameter kind that says the least: a shared reference if the function only reads, an exclusive reference if it modifies, by value only if it needs to keep the thing.
  • Return values rather than out-parameters; a struct or an enum is cheap to return.
  • List every captured variable in the closure brackets and let the compiler tell you if you got it wrong.
  • Use move closures for anything that outlives the current block or runs on another thread.

§16Structs, arrays, enums and match

Governing specification file: spec/16-aggregates.md

The four aggregate kinds, how they nest, and exhaustive matching over enums with payloads.

CobaltC has four aggregate kinds: structs (named fields), bitstructs (named runs of bits packed into one unsigned integer), arrays (a fixed number of elements) and enums (one of several variants, each with an optional payload). They compose freely, and all but bitstructs generically. match is how you take an enum apart, and it must handle every variant.

Structs

Struct declaration, literal, accessruns
import std;

struct Point
{
    i32 x;
    i32 y;
}

struct Segment
{
    Point from;                       // structs nest by value
    Point to;
}

fn main()
{
    auto a = Point { .x = 1, .y = 2 };           // every field, once, in any order
    Point b = Point { .y = 2, .x = 1 };
    assert(a == b);                                // plain structs compare field by field
    auto s = Segment { .from = a, .to = Point { .x = 4, .y = 6 } };
    s.to.y = 7;                                   // nested fields are places
    assert(s.to.y == 7);
    Point c = a;                                  // plain structs copy
    c.x = 100;
    assert(a.x == 1);
}

Field initialisers use C99's designated form .name = value. Fields are private to the module that declares the struct unless marked export (§17). A struct with no fields is allowed. Mark a struct resource if it manages something beyond its fields (§07).

bitstruct: named bits of one integer

A bitstruct is a struct whose fields are runs of bits of one unsigned integer, its backing type: bitstruct Instr : u32 { op : 6; rd : 5; rs : 5; imm : 16; }. The fields are laid out from the low bit up in declaration order and must fill the backing type exactly (name the unused bits, pad : 3;). Each field has the smallest unsigned type that holds its width — u8 up to 8 bits, u16 up to 16, and so on — and is read and written like any field, i.op, i.imm = 500. A write that does not fit its field faults with diag.narrowing-overflow, or is rejected outright when the value is a literal; nothing is ever truncated. Instr::bits(i) is the whole integer and Instr::from_bits(w) the fields of one, both total. A bitstruct is a plain value: copied by a read, clone, equal by its bits, a HashMap key, and its bytes in raw memory are the integer's. A field has no address of its own, so it cannot be borrowed — borrow the whole value — and a bitstruct is not taken apart by destructuring. Byte order is not its concern: Vec::push_le and push_be write it out (D-0119), and an unsafe extern fn takes Instr::bits(i).

A bitstructruns
import std;

bitstruct Double : u64
{
    mantissa : 52;                    // bits 0 .. 52
    exponent : 11;                    // bits 52 .. 63
    sign     : 1;                     // bit 63
}

bitstruct Instr : u32
{
    op  : 6;
    rd  : 5;
    rs  : 5;
    imm : 16;
}

fn main()
{
    Double d = Double::from_bits(reinterpret<u64>(-2.5));
    assert(d.sign == 1 && d.exponent == 1024);           // exponent: u16, sign: u8
    d.exponent = 1023;                                    // fits in 11 bits: checked
    assert(reinterpret<f64>(Double::bits(d)) == -1.25);

    Instr i = Instr { .op = 3, .rd = 1, .rs = 2, .imm = 500 };
    u32 word = Instr::bits(i);
    assert(word == 3 | 1 << 6 | 2 << 11 | 500 << 16);
    Instr j = Instr::from_bits(word);
    assert(j == i && sizeof<Instr>() == 4);
    j.imm = 65535;                                        // the largest 16-bit value
    assert(j.imm == 65535 && j.op == 3);
}
A value too wide for its fieldrejected: diag.narrowing-overflow
import std;

bitstruct Flags : u8 { carry : 1; zero : 1; pad : 6; }

fn main()
{
    Flags f = Flags { .carry = 2, .zero = 0, .pad = 0 };  // ✗ 2 does not fit 1 bit: diag.narrowing-overflow
}

Arrays

array<T, N> holds exactly N elements; N is a constant expression — a literal, a const name (m::N too), or + - * / % & | ^ << >> ~ ( ) over them, folded before the program runs (D-0126); outside parentheses > ends it, so Vec<array<u8, N * 2>> reads as expected. The literal form is [a, b, c]. Indexing takes a usize and is bounds-checked: a literal index out of range is rejected statically, a computed one faults at run time with diag.index-out-of-bounds. Arrays of plain elements are plain (copied on assignment); an array of resources is a resource.

Fixed-size arraysruns
import std;

fn sum(ref<array<i32, 4>, shared> a) : i32
{
    i32 t = 0;
    foreach (i in 0..4)
    {
        t += a[i];                    // indexing looks through the reference
    }
    t
}

fn main()
{
    array<i32, 4> a = [10, 20, 30, 40];
    a[2] = 33;
    assert(sum(&a) == 103);
    auto grid = [[1, 2], [3, 4]];     // array<array<i32, 2>, 2>
    assert(grid[1][0] == 3);
    array<u32, 256> counts = [0; 256];    // one value in every element
    counts[65] += 1;
    auto flags = [false; 8];          // array<bool, 8>
    array<str, 3> names = [
        "ann",
        "bo",
        "cy",                         // a trailing comma is allowed
    ];
    assert(counts[65] == 1 && !flags[7] && names[2] == "cy");
}

[v; N] makes an array of N copies of v, which is evaluated once. N is written as in the type array<T, N>, a constant expression: a number, a constant (const usize W = 24; then array<u8, W> and [0; W], in this module or, qualified, a visible one of another, array<u8, screen::W>), or arithmetic on them ([0; W * 2]). Because the value is copied, its type must be plain: a String or other resource, a reference, or a function value cannot be repeated (diag.read-of-resource or diag.type-mismatch); build such arrays element by element, or use a Vec.

Out of boundsrejected: diag.index-out-of-bounds
fn get(array<i32, 3> a, usize i) : i32
{
    a[i]                              // checked against 3 when the index arrives
}

fn main()
{
    array<i32, 3> a = [1, 2, 3];
    get(a, 3);                        // ✗ diag.index-out-of-bounds (dynamic)
}

Arrays of arrays

CobaltC has no separate multidimensional array type. An array's element can itself be an array, so a grid, a cube, or anything with more dimensions is the one constructor used again: array<array<i64, 3>, 2> is an array of 2 elements, each an array<i64, 3>. The same holds for the literal forms, [[1, 2, 3], [4, 5, 6]] and [[0; 3]; 2], and for auto, which infers the nesting from the literal.

Two directions are worth keeping straight:

  • The type reads from the inside out. In array<array<array<i64, 3>, 2>, 4> the innermost array<i64, 3> is a row of 3 numbers, the next array<…, 2> holds 2 such rows (a block), and the outermost array<…, 4> holds 4 blocks. The length written last is the outermost one.
  • Indexing and loops go from the outside in. cube[i][j][k] picks block i (below 4), then row j of it (below 2), then element k of that row (below 3). Nested loops follow the same order: the outer loop walks the blocks, the innermost the elements of one row, so the elements are visited in the order they lie in memory.

Each index is checked against its own length. Because every level is an ordinary array, cube[1] is a whole block and cube[1][0] a whole row, values you can copy, pass and loop over. Writing those inner types out gets long; where the type is clear from the right-hand side, auto reads better. To print every value, nest one foreach per level: foreach (i, block in &cube) borrows each block, and the loop inside it over block borrows each row of that block, so nothing is copied.

A three-dimensional arrayruns
import std;

fn main()
{
    // Read the type from the inside out: rows of 3 i64s, 2 rows to a
    // block, 4 blocks.
    array<array<array<i64, 3>, 2>, 4> cube = [[[0; 3]; 2]; 4];
    // Index from the outside in: cube[block][row][column].
    foreach (i in 0..4)
    {
        foreach (j in 0..2)
        {
            foreach (k in 0..3)
            {
                cube[i][j][k] = narrow<i64>(100 * i + 10 * j + k);
            }
        }
    }
    printf("cube[3][1][2] = %v\n", cube[3][1][2]);
    // Print every value: each loop borrows one level of the one outside it.
    foreach (i, block in &cube)
    {
        foreach (j, row in block)
        {
            printf("cube[%v][%v]:", i, j);
            foreach (x in row)
            {
                printf(" %v", x);
            }
            printf("\n");
        }
    }
    // `auto` spares spelling the inner types; each is a copy (the elements are plain).
    auto block = cube[1];             // array<array<i64, 3>, 2>
    auto row = block[0];              // array<i64, 3>
    auto grid = [[1, 2, 3], [4, 5, 6]];    // array<array<i32, 3>, 2>
    printf("row %v %v %v, grid[1][2] = %v\n", row[0], row[1], row[2], grid[1][2]);
}
output
cube[3][1][2] = 312
cube[0][0]: 0 1 2
cube[0][1]: 10 11 12
cube[1][0]: 100 101 102
cube[1][1]: 110 111 112
cube[2][0]: 200 201 202
cube[2][1]: 210 211 212
cube[3][0]: 300 301 302
cube[3][1]: 310 311 312
row 100 101 102, grid[1][2] = 6

For a growable sequence use Vec<T> (§21). A Vec is indexed the same way, v[i]: it reads through Vec::index_shared, and writes (an assignment, &mut v[i]) through Vec::index_exclusive, so every rule about borrowing the Vec applies.

Slices

A slice is a borrowed run of consecutive elements of an array, a Vec, or another slice: &a[i..j] is elements i up to (not including) j, and &mut a[i..j] the same for writing. Inside the brackets, $ stands for the length of what is being indexed, so &a[0..$] is all of it and &a[$ - 3 .. $] the last three, and v[$ - 1] is a Vec's last element. $ is a usize like any index, so v[$ - 1] of an empty v is an overflow fault, never a wrapped-around index.

The type is slice<T, shared> or slice<T, exclusive>. One parameter of that type takes any array, any Vec, or part of either, so a function need not be written once per container or per array length. slice_len(s) is its length; s[i] is bounds-checked; foreach works over it. A slice is a borrow like a reference: while it lives, the elements it views cannot be changed except through it, its source cannot be pushed to or popped from, and a slice of a local cannot be returned.

One function for arrays, Vecs and parts of themruns
import std;

// Sorts any run of i32 in place: an array, a Vec, or part of one.
fn sort(slice<i32, exclusive> s)
{
    for (usize i = 1; i < slice_len(s); i += 1)
    {
        usize j = i;
        while (j > 0 && s[j - 1] > s[j])
        {
            i32 t = s[j - 1];
            s[j - 1] = s[j];
            s[j] = t;
            j -= 1;
        }
    }
}

fn sum(slice<i32, shared> s) : i32
{
    i32 total = 0;
    foreach (x in s)
    {
        total += *x;
    }
    total
}

fn main()
{
    auto a = [5, 3, 9, 1, 7, 2];
    sort(&mut a[0..3]);                     // only the first three: [3, 5, 9, 1, 7, 2]
    assert(a[0] == 3 && a[3] == 1);
    assert(sum(&a[$ - 3 .. $]) == 10);       // the last three

    Vec<i32> v = Vec::new();
    foreach (x in [40, 10, 30, 20])
    {
        Vec::push(&mut v, x);
    }
    sort(&mut v[0..$]);                     // the same function, on a Vec
    assert(v[0] == 10 && v[$ - 1] == 40);
    v[0] += 1;                              // indexing a Vec, written
    assert(sum(&v[0..$]) == 101);
}
A slice keeps its source borrowedrejected: diag.aliasing-conflict
import std;

fn main()
{
    Vec<i32> v = Vec::new();
    Vec::push(&mut v, 1);
    auto s = &v[0..1];
    Vec::push(&mut v, 2);             // ✗ diag.aliasing-conflict: s still borrows v
    i32 first = s[0];                 // (because s is used here)
}

A slice borrows only its own elements. Two slices of one array or Vec conflict only if their ranges share an element, so a buffer can be split into parts that are each written through their own exclusive slice, at the same time: the halves of a divide-and-conquer algorithm, or one part per thread. What reaches the whole source (&mut v, Vec::push) still conflicts with every live slice of it.

Splitting a Vec between two threadsruns
import std;

fn sum_into(slice<u64, exclusive> s, u64 start) : u64
{
    u64 total = 0;
    foreach (i in 0..slice_len(s))
    {
        s[i] = start + widen<u64>(i);
        total += s[i];
    }
    total
}

fn main()
{
    Vec<u64> v = Vec::new();
    for (usize i = 0; i < 100; i += 1)
    {
        Vec::push(&mut v, 0);
    }
    usize mid = slice_len(&v[0 .. $]) / 2;
    {
        slice<u64, exclusive> left = &mut v[0 .. mid];     // elements 0 .. 50
        slice<u64, exclusive> right = &mut v[mid .. $];    // elements 50 .. 100: disjoint
        auto a = spawn(sum_into, left, 0: u64);           // each half filled by its own thread
        auto b = spawn(sum_into, right, 1000: u64);
        assert(join(a) + join(b) == 1225 + 51225);
    }
    assert(v[49] == 49 && v[50] == 1000);
}

Types declared inside a function

A struct, enum or bitstruct used by one function can be declared inside it, as a statement, and so can its functions, fn T::name(…) (a destructor too). It is an ordinary type whose name is scoped like a local: usable from its declaration to the end of its block. Two functions can each have their own Entry. A local type and its functions see module items and the local types and constants around them, but not the function's variables or type parameters; a plain local function is written as a closure (§15).

A local helper typeruns
import std;

fn shortest(ref<Vec<String>, shared> words) : usize
{
    struct Entry                      // visible only in this function
    {
        usize len;
        usize at;
    }

    fn Entry::better(ref<Entry, shared> a, ref<Entry, shared> b) : bool
    {
        a.len < b.len || (a.len == b.len && a.at < b.at)
    }

    Entry best = Entry { .len = max_value<usize>(), .at = 0 };
    foreach (i, w in words)
    {
        Entry e = Entry { .len = String::len(w), .at = i };
        if (Entry::better(&e, &best))
        {
            best = e;
        }
    }
    best.at
}

fn main()
{
    Vec<String> w = Vec::new();
    Vec::push(&mut w, String::from_str("banana"));
    Vec::push(&mut w, String::from_str("fig"));
    Vec::push(&mut w, String::from_str("pea"));
    assert(shortest(&w) == 1);        // "fig": as short as "pea", and first
}

A bitstruct declared in a function is the same as one at module level: it gets its Name::bits and Name::from_bits, and its fields read and write in the same way. A packed header that one decoder reads can live inside that decoder.

A local bitstructruns
import std;

fn payload_len(u16 raw) : u16
{
    bitstruct Header : u16            // bits 0..4, 4..14, 14..16
    {
        kind  : 4;
        len   : 10;
        flags : 2;
    }

    Header h = Header::from_bits(raw);
    h.len
}

fn main()
{
    assert(payload_len(0x0123) == 18);        // 0x0123 >> 4 is 0x12
    assert(payload_len(0xFFFF) == 1023);
}

Enums and match

An enum lists its variants; each may carry one payload type. Variant names are used bare (Cold(3), Fine) or qualified (Verdict::Fine) when two visible enums share a name. A variant with no payload behaves as if it carried ().

match (e) { … } compares the discriminant and runs the arm for that variant. Each arm is Variant(binder) : expression, for a payload, Variant : expression, for none, or _ : expression, as a catch-all; every arm ends with a comma. The arms must cover every variant or include _. Arms are expressions, so a match produces a value; use a block as the arm body for statements. Arms are tried in order and the first that fits is taken.

Enumsruns
import std;

enum Shape
{
    Circle(i32),                      // radius
    Square(i32),                      // side
    Dot
}

fn area(Shape s) : i32
{
    match (s)
    {
        Circle(r) : 3 * r * r,        // the payload is bound to r for this arm
        Square(w) : w * w,
        Dot       : 0,
    }
}

fn is_round(Shape s) : bool
{
    match (s)
    {
        Circle(_) : true,             // _ as a binder ignores the payload
        _         : false,            // a wildcard arm covers the rest
    }
}

fn main()
{
    assert(area(Circle(2)) == 12);
    assert(area(Shape::Dot) == 0);
    assert(is_round(Square(1)) == false);
    array<Shape, 2> shapes = [Circle(1), Square(3)];
    assert(area(shapes[1]) == 9);
}
A missing armrejected: diag.non-exhaustive-match
enum Sign
{
    Pos,
    Neg,
    Zero
}

fn describe(Sign s) : i32
{
    match (s)
    {
        Pos : 1,
        Neg : -1,                     // ✗ diag.non-exhaustive-match: Zero is not covered
    }
}

fn main()
{
    describe(Zero);
}

Nested patterns

Inside the parentheses, a pattern can say what the payload itself must be, as deep as needed: Ok(Some(line)) matches an Ok whose payload is a Some, and binds the Some's payload. A name there is a binder, unless it is a variant's name (Some(None)), and _ ignores that level. Since a variant has one payload, an arm binds one name, the innermost payload it reaches, unless that payload is a struct taken apart by a struct pattern (below). Every value must reach some arm (the error names one that does not, such as Ok(None)), and every arm must be reachable: an arm whose values all go to earlier arms is rejected, since it could never run.

Nested patternsruns
import std;

fn classify(Result<Option<i32>, ParseError> r) : i32
{
    match (r)
    {
        Ok(Some(n))      : n,         // two levels tested, the inner payload bound
        Ok(None)         : 0,
        Err(Invalid(at)) : -100 - narrow<i32>(at),
        Err(_)           : -1,
    }
}

fn main()
{
    assert(classify(Ok(Some(7))) == 7);
    assert(classify(Ok(None)) == 0);
    assert(classify(Err(Invalid(3))) == -103);
    assert(classify(Err(Empty)) == -1);
}
An arm that can never runrejected: diag.unreachable-arm
import std;

fn main()
{
    Option<Option<i32>> o = Some(None);
    i32 v = match (o)
    {
        Some(Some(x)) : x,
        Some(None)    : 1,
        Some(_)       : 2,            // ✗ diag.unreachable-arm: every Some went to the arms above
        None          : 0,
    };
}

Struct patterns

Where a binder could stand, a struct pattern can take the payload apart at once: Some(Question { name, end }) binds name and end, as Question { name, end } = q; would after binding q. The rules are destructuring's (§11): every field is named once, or .. stands for the rest; end : e binds a field to another name; and the fields are taken by value, so a String field moved out consumes the value matched, as any binder of a resource payload does. A struct pattern always fits, so it counts as a binder for exhaustiveness. It works in match arms and in if and while patterns. Through a reference (match (&o)) it is rejected: bind the payload and read its fields through it.

Struct patternsruns
import std;

struct Question
{
    String name;
    u32 end;
}

fn ask(u32 k) : Option<Question>
{
    if (k == 0)
    {
        return None;
    }
    Some(Question { .name = "who", .end = k })
}

fn main()
{
    if (Some(Question { name, end }) = ask(4))
    {
        assert(name == "who" && end == 4);
    }
    u32 n = match (ask(7))
    {
        Some(Question { end : e, .. }) : e,       // renamed; `name` left and ended
        None                           : 0,
    };
    assert(n == 7);
}

Literal patterns

A pattern can also be a literal: an integer (3, -1, 0x10), a byte (b'x'), true or false. It can stand alone, when the value being matched is an integer or a bool, or at the end of a nested pattern: Ok(0), Some(b'\n'). This is CobaltC's multi-way branch in place of switch, and it is checked: a literal must be of the type it is compared with, a repeated case is an arm that can never run, and an integer match needs a _ arm, since literals never list every value (true and false do cover a bool). Text is matched the same way: a match on a str, a String or a StringView takes text literals ("get" : …), compared byte for byte where the text is, so a String you match on is still yours afterwards; Some("quit") works inside a payload too, and if ("quit" = cmd) as a condition. Floats are not patterns. With that, patterns are _, a literal, a variant, and a variant with a pattern for its payload; there is nothing else to learn.

Literal patternsruns
import std;

fn step(u8 op, i64 acc) : i64
{
    match (op)
    {
        0x01 : acc + 1,               // a byte from an instruction stream
        0x02 : acc * 2,
        0x10 : 0,
        _    : acc,                   // any other opcode: no effect
    }
}

fn is_end(Result<usize, FileError> r) : bool
{
    match (r)
    {
        Ok(0)  : true,                // a read of 0 bytes: the end of the file
        Ok(_)  : false,
        Err(_) : true,
    }
}

fn main()
{
    array<u8, 5> program = [0x01, 0x02, 0x02, 0x7f, 0x01];
    i64 acc = 0;
    foreach (op in program)
    {
        acc = step(op, acc);
    }
    assert(acc == 5);
    assert(is_end(Ok(0)) && !is_end(Ok(16)));
}
Text patternsruns
import std;

fn opcode(str word) : i32
{
    match (word)
    {
        "load"  : 1,
        "store" : 2,
        "jump"  : 3,
        _       : -1,                 // any other word: not an instruction
    }
}

fn is_quit(ref<String, shared> line) : bool
{
    match (*line)                     // a String is read where it is, not moved
    {
        "quit" : true,
        "exit" : true,
        _      : false,
    }
}

fn main()
{
    assert(opcode("jump") == 3 && opcode("halt") == -1);
    String cmd = "exit";
    assert(is_quit(&cmd) && String::len(&cmd) == 4);
    Option<String> first = Some(String::from_str("load"));
    i32 k = match (first) { Some("load") : 1, Some(_) : 2, None : 0, };
    assert(k == 1);
}

Ranges and several values

An arm names one pattern: there are no range patterns (10..15 : …) and no lists of alternatives (1, 4, 6 : … or 1 | 4 | 6 : …); each is a syntax error. Two forms cover these cases. To test a range, match on the test itself: the condition is a bool, and true and false cover it, so no _ arm is needed. Write the bounds as a range would mean them, the upper one left out (v < 15 for 10..15).

A range testruns
import std;

fn check(i32 v)
{
    match (v >= 10 && v < 15)         // 10 to 14, as 10..15 would mean
    {
        true  : printf("%v\n", v),
        false : printf("%v is not in the correct range\n", v),
    }
}

fn main()
{
    check(12);
    check(15);
    check(9);
}
output
12
15 is not in the correct range
9 is not in the correct range

For several values that share an action, give each value its own arm, and put an action longer than a line in a function the arms call: the match still checks every literal's type and reports a repeated one. When the values are the whole question, match on the test instead, as for a range. For a longer list, keep the values in an array and ask contains, or in a HashSet when the list is built at run time. Text works the same way: one arm per word.

Several values in one decisionruns
import std;

fn weekend(ref<array<str, 7>, shared> names, u32 day)
{
    printf("%v is a weekend day\n", names[widen<usize>(day)]);
}

fn describe(u32 day)
{
    array<str, 7> names = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
    match (day)
    {
        0 : weekend(&names, day),     // one arm per value; the shared action in a function
        6 : weekend(&names, day),
        _ : printf("%v is a weekday\n", names[widen<usize>(day)]),
    }
}

fn is_square(u32 v) : bool
{
    match (v == 1 || v == 4 || v == 9)   // several values as one test: true and false cover it
    {
        true  : true,
        false : false,
    }
}

fn main()
{
    describe(0);
    describe(3);
    assert(is_square(4) && !is_square(5));
    array<u32, 6> small = [2, 3, 5, 7, 11, 13];                             // a longer list
    Vec<u32> primes = Vec::from_slice(&small[0 .. $]);
    assert(Vec::contains(&primes, &7) && !Vec::contains(&primes, &9));
    str cmd = "exit";
    bool quit = match (cmd)
    {
        "quit" : true,
        "exit" : true,
        _      : false,
    };
    assert(quit);
}
output
Sun is a weekend day
Wed is a weekday
A repeated caserejected: diag.unreachable-arm
import std;

fn main()
{
    u8 op = 2;
    i32 cycles = match (op)
    {
        1 : 3,
        2 : 5,
        1 : 4,                        // ✗ diag.unreachable-arm: 1 is taken by the first arm
        _ : 1,
    };
}

Binders and named constants

A name on its own is a pattern too. If it names a constant of integer or bool type, it is that constant's value, a literal by name (PUSH : …); if it names a variant, it is that variant; otherwise it is a new binding for the whole value, which matches anything, like _, and is usually the last arm (other : …). A binding of a resource moves it, as a payload binding does. Mistaking a constant for a binding cannot go unnoticed: a match on an integer then lacks _.

Constants and a binder in a matchruns
import std;

const u8 PUSH = 0x01;
const u8 POP = 0x02;

fn describe(u8 op) : i32
{
    match (op)
    {
        PUSH  : 1,
        POP   : 2,
        other : 100 + widen<i32>(other),      // every other opcode, bound to `other`
    }
}

fn main()
{
    assert(describe(1) == 1);
    assert(describe(2) == 2);
    assert(describe(9) == 109);
}

Matching through a reference

match (e) binds each payload by value: a plain one is copied, and a resource one (a String, a Vec, a Box) is moved out, which consumes the enum. To look at an enum without taking it apart, match a reference to it: match (&e) binds each payload as a shared reference, match (&mut e) as an exclusive one, through which the payload can be changed. A scrutinee that already is a reference, such as a function's ref<Expr, shared> parameter, binds in its own mode. The enum is not consumed, and the usual borrow rules apply to what is bound: nothing can replace the enum while a payload reference is still in use. This is how a tree built from Boxes (§21) is walked.

Looking inside without taking apartruns
import std;

enum Expr
{
    Num(i64),
    Neg(Box<Expr>),
}

fn eval(ref<Expr, shared> e) : i64
{
    match (e)                         // e is a reference: n and x are references too
    {
        Num(n) : *n,
        Neg(x) : -eval(Box::get(x)),
    }
}

fn main()
{
    Expr e = Neg(Box::new(Neg(Box::new(Num(7)))));
    assert(eval(&e) == 7);

    Option<String> name = Some(String::from_str("ada"));
    match (&mut name)                 // change the payload in place
    {
        Some(s) : String::push_ascii(s, b'!'),
        None    : {},
    }
    usize len = match (&name)         // look without consuming
    {
        Some(s) : String::len(s),
        None    : 0,
    };
    assert(len == 4);
    String kept = Option::unwrap_or(name, String::new());   // name still owns its String
    assert(String::len(&kept) == 4);
}

A nested pattern looks through a reference payload in the same way. HashMap::get gives an Option<ref<V, shared>>; when V is an enum, Some(Num(v)) matches its variant through the reference and binds v as a reference of that mode, so a lookup is taken apart in one match. A literal compares through a reference too: Some(0) against an Option<ref<u32, shared>>. The binder borrows what the reference borrows, so the map cannot change while v is in use.

A pattern through a lookup's referenceruns
import std;

enum Cell
{
    Num(i64),
    Text(String),
    Empty,
}

fn width(Option<ref<Cell, shared>> c) : i64
{
    match (c)
    {
        Some(Num(v))  : *v,                                  // v : ref<i64, shared>
        Some(Text(t)) : narrow<i64>(widen<u64>(String::len(t))),
        Some(Empty)   : 0,
        None          : -1,
    }
}

fn main()
{
    HashMap<u32, Cell> sheet = HashMap::new();
    HashMap::insert(&mut sheet, 1, Num(12));
    HashMap::insert(&mut sheet, 2, Text(String::from_str("total")));
    assert(width(HashMap::get(&sheet, &1)) == 12 && width(HashMap::get(&sheet, &2)) == 5);
    assert(width(HashMap::get(&sheet, &9)) == -1);
    match (HashMap::get_mut(&mut sheet, &1))
    {
        Some(Num(v)) : { *v += 1; },                         // v : ref<i64, exclusive>
        _            : {},
    }
    assert(width(HashMap::get(&sheet, &1)) == 13);
}

An arm that takes the payload consumes the enum

If the enum carries a resource payload, an arm that binds that payload by value moves it into the binder and consumes the enum: if that arm ran, the scrutinee is stale afterwards. This is what makes match (Vec::pop(&mut v)) { Some(x) : …, None : … } hand you the popped element. An arm that takes nothing out — _, a payload-less variant, or Some(_) — leaves the scrutinee alone, so match (o) { None : …, _ : use(o) } is fine. To look inside and keep the enum whatever the arm, match on a reference to it (above).

A resource payload moves outruns
import std;

enum Slot
{
    Full(Vec<i32>),
    Empty
}

fn main()
{
    Vec<i32> payload = Vec::new();
    Vec::push(&mut payload, 4);
    auto s = Slot::Full(payload);     // payload moves into the enum
    usize n = match (s)               // the Full arm consumes s
    {
        Full(v) : Vec::len(&v),       // v owns the Vec for the arm, and destroys it at the arm's end
        Empty   : 0,
    };
    assert(n == 1);

    Option<String> o = Some(String::from_str("kept"));
    match (o)
    {
        None : {},
        _    :                        // takes nothing out: o is still here
        {
            String t = Option::unwrap_or(o, String::new());
            assert(String::len(&t) == 4);
        },
    }
}

Numeric codes

An enum without payloads can carry a number per variant, for a file format, a protocol or a C interface: enum Op : u8 { Add = 1, Sub, Mul = 10 }. The type after the colon is the code type, any integer type. A variant without = n takes the previous code plus one (the first, 0). Two variants may not share a code. The enum then has two associated functions: Op::code(e) gives a variant's code, and Op::from_code(n) gives Some(variant) for a code in use and None for any other.

Enum codesruns
import std;

enum Op : u8
{
    Add = 1,
    Sub,                              // 2
    Mul = 10,
}

fn main()
{
    assert(Op::code(Sub) == 2 && Op::code(Mul) == 10);
    match (Op::from_code(10))
    {
        Some(Mul) : {},
        _         : { assert(false); },
    }
    assert(Op::from_code(3) == None);
}

Generic aggregates

struct Box<T> { T value; } and enum Maybe<T> { Just(T), Nothing } declare families of types. std's Option<T> and Result<T, E> are ordinary enums of exactly this kind (§21). Type arguments may be left off a literal when the declared type of the variable, parameter or field fixes them (§12).

Layout

Struct fields are laid out in declaration order with implementation-documented padding; arrays are contiguous with no padding; an enum is a discriminant of documented width followed by the largest payload, aligned. sizeof<T>() and alignof<T>() report the result (examples in §06, "Sizes and limits of types"). You need this mostly when talking to foreign code or managing raw memory (§20).

Rules of thumb
  • Reach for an enum whenever a value is one of several cases; match forces you to handle each one.
  • Use _ sparingly: a wildcard arm silently absorbs variants you add later.
  • Give match arms a block body when they need statements, and remember the comma after each arm.
  • Build aggregates whole with a literal, then mutate fields as places.

§17Modules and name resolution

Governing specification file: spec/17-modules.md

module, export and import; re-export; private by default; a module's body may be another file; a fixed lookup order for every name.

Modules group declarations and control which names are visible where. They introduce no new semantics: ownership, borrowing and every other rule apply unchanged across module boundaries.

Declaring modules

module name { … } encloses any declarations, including nested modules. Everything is private by default: usable from its own module and the modules nested inside it. export on a declaration makes it usable from outside. An exported item inside a private module is only reachable where that module is.

Each declaration adds one qualified name (bank::fee, bank::Account::open, Shape::Circle), and a name may be added only once, so declaring fee twice in bank is diag.duplicate-item. The same simple name in two different modules is two different names. An associated function fn T::name belongs in the module that declares T; anywhere else it is diag.foreign-associated-fn, so no program can add to, or replace, the functions of std's Vec.

Modules and visibilityruns
module bank
{
    export fn fee(i32 amount) : i32
    {
        percent(amount, 2)            // a private helper is visible inside its own module
    }

    fn percent(i32 v, i32 pct) : i32
    {
        v * pct / 100
    }

    export module audit                // nested modules; both must be exported to reach `twice`
    {
        export fn twice(i32 v) : i32
        {
            fee(v) * 2                 // the enclosing module's items are in scope
        }
    }
}

fn main()
{
    std::assert(bank::fee(1000) == 20);      // qualified with ::
    std::assert(bank::audit::twice(1000) == 40);
}
Private by defaultrejected: diag.name-not-visible
module inner
{
    fn helper() : i32                 // not exported
    {
        42
    }
}

fn main()
{
    inner::helper();                  // ✗ diag.name-not-visible
}

import

import a::b::name; lets name be used unqualified in the importing module. It creates no alias and no copy; it only extends name lookup. There is no import … as renaming: if two imports would give one name two meanings, qualify.

importruns
module maths
{
    export fn square(i32 v) : i32
    {
        v * v
    }
}

import maths::square;

fn main()
{
    std::assert(square(6) == 36);           // resolves through the import
    std::assert(maths::square(7) == 49);    // the qualified form still works
}

import m; naming a module does two things: m itself resolves, as with any import, and every item m exports, including the variants of its exported enums, can be used unqualified, except its submodules, which keep their paths (std::text) or are imported themselves. This is how programs use the standard library: import std;. A name is looked up in a fixed order: the module's own declarations first, then single-name imports, then module imports, then the enclosing modules. So a module's own printf wins over std::printf, which stays reachable by its full name. If two imported modules both export a name, using that name unqualified is diag.ambiguous-name; importing both is fine, so a module that gains an export never breaks a program that does not use it. An import acts only in the module that writes it: a nested module, or a module in a file of its own, writes its own import std;.

Importing a moduleruns
import std;

module shapes
{
    import std;

    export enum Shape
    {
        Square(i32),
        Dot,
    }

    export fn describe(Shape s)
    {
        match (s)
        {
            Square(_) : printf("square\n"),
            Dot       : printf("dot\n"),
        }
    }
}

import shapes;

fn printf(str s)                      // the root's own printf is found before std::printf
{
    std::printf("> ");
    std::printf("%v", s);
}

fn main()
{
    describe(Square(2));              // shapes::describe and shapes::Shape::Square, unqualified
    printf("done\n");
}
output
square
> done

Re-export: export import

An import can be exported like any other declaration. export import p; in a module M does what import p; does, and also passes on what p brings in as M's own items: every item the module p exports, or the one item p names. A program that imports M then finds those names as if M had declared them, and M::name works too. They are the same items, not copies: a type passed on this way is one type under two paths. This is how a library divided into modules is still used with one import, and how std itself is built (§21): import std; brings in std::text, std::io and the rest because std re-exports each of them.

One import for a library in partsruns
import std;

module geometry
{
    export module shapes
    {
        export struct Square
        {
            export i32 side;
        }

        export fn area(Square s) : i32
        {
            s.side * s.side
        }
    }

    export module units
    {
        export fn cm(i32 n) : i32
        {
            n * 10
        }
    }

    export import geometry::shapes;   // shapes' items become geometry's
    export import geometry::units::cm;     // one item only
}

import geometry;

fn main()
{
    Square s = Square { .side = cm(2) };      // geometry::shapes::Square, through geometry
    printf("%v\n", area(s));
    geometry::shapes::Square t = geometry::Square { .side = 1 };   // one type, two paths
    printf("%v\n", area(t));
}
output
400
1

Only what is exported passes through: a private item never becomes reachable by being re-exported. A name the module already has for a different item is diag.duplicate-item; the same item arriving twice, or two modules re-exporting each other, is not an error.

Constants as items

A const declared at a module's top level is an item like a function: it is private unless marked export, it can be imported, and other modules name it by its path (config::LIMIT). Constants themselves, and the static_assert checks built on them, are described with declarations (§11) and with assertions (§18). The specification keeps their rules in its modules chapter (spec/17 §1a and §1b), because a constant is an item.

A module in another file

A module's body can be a separate file. module geo "./geometry.cb"; means exactly module geo { … } with the file's declarations as the body: the same visibility, the same import, the same lookup. The declaring file names the module; the other file's top level is anonymous, just as an inline body is. The path is relative to the directory of the file that writes it, so a file can itself declare file-backed modules with paths relative to its own location, and a library moves as a unit. Use forward slashes and stay inside the tree (no leading /, no ..); anything beyond that is up to the implementation.

geometry.cb
// geometry.cb -- the body of a module. Nothing here names the module;
// the file that declares it does. Only `export` items are visible there.
export struct Rect
{
    export f64 w;
    export f64 h;
}

export fn area(Rect r) : f64
{
    r.w * r.h
}

fn scale_factor() : f64               // private to this module
{
    1.0
}
main.cbruns
// main.cb -- run this one: `coby main.cb`
module geo "./geometry.cb";           // module geo { … } with geometry.cb's items as the body

import geo::Rect;

fn main()
{
    Rect r = Rect { .w = 3.0, .h = 4.0 };
    std::assert(geo::area(r) == 12.0);      // qualified, as with any module
    // geo::scale_factor();           // ✗ diag.name-not-visible: private, exactly as in one file
}

Three things are rejected before anything runs: a path that names no file (diag.module-file-not-found), a file whose body would load itself again through some chain of declarations (diag.module-cycle), and one file named by two declarations (diag.module-file-duplicate — it would be two distinct modules and two distinct Rect types). A fn main in a loaded file is just the item geo::main; the program's entry is the root file's main, and a program with none is diag.no-main. Both implementations report a diagnostic inside a loaded file against that file and its own line.

A library in several files

Files and re-export (above) together give a library that is split across files but used with one import. Its root file declares the parts, keeps its helpers in a private module, and passes on what users should see: here every exported item of shapes, and cm alone from units. The helpers are usable everywhere inside the library and nowhere outside it, and the files can be reorganized later without changing any program that imports the root.

geometry.cb
// geometry.cb -- the library's root: what `import geometry;` brings in
export module shapes "shapes.cb";
export module units "units.cb";
module detail "detail.cb";            // private: the library's own helpers

export import geometry::shapes;       // every exported item of shapes
export import geometry::units::cm;    // one item; units::mm keeps its path
shapes.cb
// shapes.cb
export struct Square
{
    export i32 side;
}

export fn Square::new(i32 side) : Square
{
    Square { .side = side }
}

export fn area(Square s) : i32
{
    detail::square(s.side)            // geometry's private module, visible inside geometry
}
units.cb
// units.cb
export fn cm(i32 n) : i32
{
    n * 10
}

export fn mm(i32 n) : i32
{
    n
}
detail.cb
// detail.cb -- exported from a private module: reachable only inside geometry
export fn square(i32 v) : i32
{
    v * v
}
main.cbruns
// main.cb -- run this one: `coby main.cb`
import std;

module geometry "geometry.cb";
import geometry;

fn main()
{
    Square s = Square::new(cm(2));    // shapes and units, through one import
    printf("%v\n", area(s));
    printf("%v\n", geometry::units::mm(5));     // not passed on: written in full
    // geometry::detail::square(3);   // ✗ diag.name-not-visible: detail is private
}
output
400
5

How a name is found

An unqualified name is first looked up as a local variable or parameter. Failing that, in this order: an item of the current module; an import in the current module whose last segment matches; an item of an enclosing module, outward to the root; finally a variant: first of the enums of this module, or else of the nearest enclosing module with one of that name, and only if there is none, of an imported enum. So your own enum Shape { …, Empty } keeps its bare Empty even though std's ParseError has an Empty too. More than one candidate at a step is diag.ambiguous-name; none at all is diag.unbound-name. A qualified name a::b::c starts by resolving a the same way and then walks into it, checking visibility at the end.

Two special cases: an associated function declared as fn Type::name(…) is addressed as Type::name (or mod::Type::name from outside), and an enum's variants are addressable as Enum::Variant as well as bare.

An unbound namerejected: diag.unbound-name
fn main()
{
    does_not_exist(1);                // ✗ diag.unbound-name
}

Structs and fields across modules

A struct or enum declared in a module is an item like any other: export it to use it outside. Each field has its own visibility too; write export i32 x; for fields that callers may read and write directly, and leave the rest private to give the module full control over its invariants.

Field visibility
module shapes
{
    export struct Rect
    {
        export i32 w;                 // visible to users of the module
        export i32 h;
        i32 cached_area;              // private: only this module's functions may touch it
    }
}
A private fieldrejected: diag.name-not-visible
module shapes
{
    export struct Rect
    {
        export i32 w;
        i32 cached_area;
    }
}

fn main()
{
    auto r = shapes::Rect { .w = 1, .cached_area = 0 };   // ✗ diag.name-not-visible: cached_area is private
}

Naming a private field from outside its module — reading it, writing it, giving it in a struct literal, or destructuring it — is diag.name-not-visible, the same diagnostic as for a private function. Give the module a constructor function if callers need to build the struct.

There are no read-only (const) fields: in CobaltC const means a value known before the program runs (§11 constants), not "fixed once built". A field that other code may read but not change is a private field with an exported function that reads it. Only the module can then change it, which is also what keeps the module's invariant in one place.

A field others can read but not changeruns
import std;

module bank
{
    export struct Account
    {
        export u32 id;                // callers may read and write it
        i64 balance;                  // private: changed only by this module
    }

    export fn Account::open(u32 id, i64 deposit) : Account
    {
        std::assert(deposit >= 0, "an account opens with a deposit, not a debt");
        Account { .id = id, .balance = deposit }
    }

    export fn Account::balance(ref<Account, shared> a) : i64
    {
        a.balance
    }

    export fn Account::withdraw(ref<Account, exclusive> a, i64 amount) : bool
    {
        if (amount > a.balance)
        {
            return false;             // the invariant: the balance never goes below zero
        }
        a.balance -= amount;
        true
    }
}

fn main()
{
    auto acct = bank::Account::open(7, 100);
    assert(bank::Account::balance(&acct) == 100);   // read through the module
    assert(!bank::Account::withdraw(&mut acct, 500));
    assert(bank::Account::withdraw(&mut acct, 30));
    assert(bank::Account::balance(&acct) == 70);
    // acct.balance = 1000000;        // ✗ diag.name-not-visible: only bank can change it
}
Rules of thumb
  • Start every module with everything private; export only what callers need.
  • Prefer qualified calls (maths::square(x)) in code that reads like a library; import the handful of names you use constantly.
  • Module boundaries are not safety boundaries: unsafe is lexical (§20). To confine trusted code, keep the functions that wrap it un-exported.
  • Split a program across files with module m "./m.cb";, one file per module; keep paths relative and let the declaring file do the naming.

§18Errors and failure

Governing specification file: spec/18-error-failure-semantics.md

Fatal checked faults that unwind, assertions checked at run time or before the program runs, and recoverable Result values with the ? operator. Nothing in between.

There are exactly two ways for something to go wrong in CobaltC, and they never mix. A checked fault is the language catching a violated invariant (overflow, an index out of range, a stale reference, a conflicting borrow): it is fatal. An error value is your program reporting an expected failure (bad input, a full buffer) through Result or Option: it is data, and the caller decides what to do. There are no exceptions and no catch.

Checked faults

When a run-time check fails, the current thread unwinds every open scope, running every destructor exactly as a normal scope exit would, and the program terminates reporting the fault by name. Nothing can intercept this within a thread. (A fault in a thread you spawned ends that thread alone, and its failure becomes a value at the thread's boundary: §19, When a thread fails.) The point is that a fault cannot corrupt state or leak an external resource on the way out, and that a program that reaches its end has never once violated a checked rule.

A fault unwinds, then terminatesrejected: diag.div-by-zero
import std;

resource struct Guarded
{
    i32 tag;
}

fn Guarded::drop(ref<Guarded, exclusive> self)
{
    // runs during unwinding, before the program terminates
}

fn divide(i32 a, i32 b) : i32
{
    a / b
}

fn main()
{
    Guarded g = Guarded { .tag = 1 };
    Vec<i32> v = Vec::new();
    divide(1, 0);                     // ✗ diag.div-by-zero (dynamic): v and g are destroyed, then the program ends
}

Assertions: assert

assert(c) states something the program relies on at that point: when c is false, the program stops with the checked fault diag.assert-failed, naming the line, and unwinds like any other fault. assert(c, "format", args…) adds a message, formatted as printf formats, so the failure can say which values were wrong. The condition is always evaluated, exactly once: there is no build in which assertions are left out. The message, and its arguments, are only evaluated when the condition is false, so they cost nothing while it holds. Every example in this guide checks its own results with assert; a condition that is constant is checked with static_assert (below) before the program runs.

An assertion that fails, with its messagerejected: diag.assert-failed
import std;

fn average(ref<Vec<u32>, shared> v) : u32
{
    assert(Vec::len(v) > 0, "average of an empty list");
    u32 sum = 0;
    foreach (x in v)
    {
        sum += *x;
    }
    sum / narrow<u32>(Vec::len(v))
}

fn main()
{
    Vec<u32> v = Vec::new();
    Vec::push(&mut v, 4);
    Vec::push(&mut v, 8);
    assert(average(&v) == 6);
    usize cap = 1;
    assert(Vec::len(&v) <= cap, "%v items exceed the capacity %v", Vec::len(&v), cap);
    // ✗ diag.assert-failed (dynamic), reporting "2 items exceed the capacity 1"
}

Faults are named diag.… and listed in the appendix. Both the interpreter and a compiled program print the name, the rule that fired, what was required and what was observed, and a repair hint.

Static assertions

static_assert(c) and static_assert(c, "why") check a constant condition when the program is compiled, not when it runs: a false one stops it with diag.static-assert-failed and shows the message. As with assert, the message may carry values, static_assert(D == 201, "D is %v", D), provided they are constants too. The condition is a constant expression (the same kind a const has, §11), so it can use constants, sizeof, alignof, the limits and the conversions. It is checked in every function, called or not, and in a generic function once for each type it is used with, so static_assert(sizeof<T>() <= 8, "…") limits what T may be. At run time it does nothing. For a condition only known as the program runs, use assert (above): a static_assert whose condition names a local variable, calls a function or is not a bool is rejected before the program runs, with diag.static-assert-not-constant, which says so.

Checked before the program runsruns
import std;

struct Header
{
    u32 magic;
    u16 version;
    u16 flags;
    u64 length;
}

const usize BUF = 4096;

// Stores a value that must fit in one machine word.
fn word<T>(T x) : T
{
    static_assert(sizeof<T>() <= 8, "word<T> needs T to fit in 8 bytes");
    x
}

fn main()
{
    static_assert(sizeof<Header>() == 16, "Header must match the C struct");
    static_assert(BUF & (BUF - 1) == 0, "BUF must be a power of two");
    assert(word(5: i64) == 5 && word(2.5) == 2.5);
}
A variable is not a constantrejected: diag.static-assert-not-constant
import std;

fn main()
{
    i32 x = 1;
    static_assert(x == 1);            // ✗ diag.static-assert-not-constant: x is a variable;
                                      //   write assert(x == 1), or const i32 X = 1;
}
A failed assertionrejected: diag.static-assert-failed
fn word<T>(T x) : T
{
    static_assert(sizeof<T>() <= 8, "word<T> needs T to fit in 8 bytes");
    x
}

fn main()
{
    word(1: i128);                    // ✗ diag.static-assert-failed (static): an i128 is 16 bytes
}

Result, Option and ?

Result<T, E> is Ok(T) or Err(E); Option<T> is Some(T) or None. Both are plain enums of std, taken apart with match. The postfix ? operator unwraps a Result: on Ok(v) it yields v, on Err(e) it returns Err(e) from the enclosing function immediately. That function must therefore return Result<_, E> with exactly the same E; convert first with Result::map_err(r, f) if the error types differ. On an Option, ? works the same way inside a function that itself returns an Option: Some(v) gives v, None returns None at once, so usize end = checked_add(start, n)?; reads "the sum, or give up" (D-0130). Nothing converts between the two: in a function returning a Result, Option::ok_or(o, e) turns the Option into a Result — Ok(v) from Some(v), Err(e) from None — so u8 c = Option::ok_or(peek(p), fail(p, "expected a value"))?; reads "the byte, or return that error". Where a default will do, Option::unwrap_or(o, d) and Result::unwrap_or(r, d) give the value, or d when there is none; the default (and ok_or's error) is evaluated either way, and one that goes unused is destroyed. Result::ok(r) turns a Result into an Option, dropping the error. Where there must be a value, Option::unwrap(o) and Result::unwrap(r) give it or fault with diag.unwrap-failed, and Option::expect(o, "…") and Result::expect(r, "…") do the same with your message.

unwrap, expect, replacing a String, and clone_byrejected: diag.unwrap-failed
import std;

fn main()
{
    Vec<String> names = Vec::new();
    Vec::push(&mut names, String::from_str("ada"));
    Vec<String> copy = Vec::clone(&names);                    // each String copied by its clone (§12)
    String first = Option::expect(Vec::pop(&mut copy), "the copy has one name");
    first = String::from_str("grace");                        // a String is quiet: the old one is destroyed
    u32 n = Result::unwrap(String::parse<u32>(&String::from_str("42")));
    assert(String::eq_str(&first, "grace") && n == 42 && Vec::len(&names) == 1);
    Option<u32> none = None;
    u32 fails = Option::expect(none, "no value where one was promised");
    // ✗ diag.unwrap-failed (dynamic), reporting "no value where one was promised"
}
Result and ?runs
import std;

fn digit(u8 b) : Result<i32, u8>
{
    if (b > b'9' || b < b'0')         // not a digit
    {
        return Err(b);
    }
    Ok(widen<i32>(b - b'0'))
}

fn two_digits(u8 hi, u8 lo) : Result<i32, u8>
{
    i32 h = digit(hi)?;               // Err returns from two_digits right here
    i32 l = digit(lo)?;
    Ok(h * 10 + l)
}

fn main()
{
    match (two_digits(b'4', b'2'))
    {
        Ok(v)  :
        {
            assert(v == 42);
        },
        Err(_) :
        {
            assert(false, "unreachable");
        },
    }
    match (two_digits(b'4', b'x'))
    {
        Ok(_)  :
        {
            assert(false, "unreachable");
        },
        Err(e) :
        {
            assert(e == b'x');
        },
    }
}
? needs a matching return typerejected: diag.propagate-outside-fallible-context
import std;

fn f() : i32
{
    Result<i32, i32> r = Ok(1);
    r?                                // ✗ diag.propagate-outside-fallible-context: f does not return Result<_, i32>
}

fn main()
{
    f();
}

Because ? is just a match in disguise, it consumes a resource-bearing Result the same way a match would, and the early return runs destructors like any other.

A Result is never dropped silently: an expression statement whose value is a Result, such as a bare call write_all(&f, &data);, is diag.result-discarded, since the error it may carry would vanish. Handle it (?, match, unwrap), or say that you mean to ignore it with _ = e;, which evaluates e and destroys the value at once. _ = e; works for any value.

Ignoring a Result on purposeruns
import std;

fn check(i32 n) : Result<i32, str>
{
    if (n < 0) { Err("negative") } else { Ok(n) }
}

fn main()
{
    _ = check(-1);                    // ignored on purpose
    assert(Result::unwrap(check(2)) == 2);
}
A discarded Resultrejected: diag.result-discarded
import std;

fn check(i32 n) : Result<i32, str>
{
    if (n < 0) { Err("negative") } else { Ok(n) }
}

fn main()
{
    check(-1);                        // ✗ diag.result-discarded: the error would vanish unseen
}

Choosing a channel

SituationUse
Input that may legitimately be malformed, a container that may be empty, an allocation that may failResult / Option; let the caller decide
A condition that can only be false if the program is wrong (an index you computed, arithmetic on trusted values)let the language's checked fault fire; do not wrap it
Recovering from overflow rather than faultingchecked_add and friends return Option (§06)
Recovering from allocation failurecall allocate yourself; Vec and Rc treat failure as a fault
Rules of thumb
  • Reserve faults for bugs. If a failure is part of normal operation, return a Result.
  • Use ? to keep the happy path readable; keep error types consistent within a call chain and map_err at the boundaries.
  • Trust the unwinding: a fault runs your destructors, so external handles are released even on the way down.

§19Concurrency

Governing specification file: spec/19-concurrency.md

Threads owned by handles, a fault contained to its thread and handed over as a value, cancellation at any wait, deadlocks reported, mutexes reachable only through guards, channels, and the same aliasing rules everywhere.

CobaltC's concurrency is small: threads you start and must join, and a mutex whose contents can only be reached by locking it. The aliasing rules of §08 already forbid two threads from touching one object in conflicting ways, so most of the work is done before this section starts. What the primitives add is a way to share mutable state on purpose, safely.

Threads: spawn and join

spawn(f, args…) runs f(args…) on a new thread and returns a handle<R>, where R is f's return type. f must be a named function or a move closure (a closure borrowing locals of the spawning thread is rejected: diag.spawn-borrow-closure). Arguments are passed exactly as for a call: plain values are copied, resources move to the new thread, references are lent. join(h) waits for the thread and returns its result, consuming the handle. The result may itself be a reference (or carry one, in a struct or an Option): the handle then holds the borrow until the join hands it over, so the source cannot be written meanwhile. A handle is a resource: if it is still alive when its scope ends, the thread is joined then and its result discarded, so a thread never outlives its handle. What a thread borrows is still checked at every use, as in any call: if the referent ends first (a handle kept in a variable declared outside the borrowed local's block, or an element released by a reallocating Vec::push), the thread's next use of it is diag.stale-binding.

spawn and joinruns
import std;

fn work(i32 n) : i32
{
    n * 2
}

fn main()
{
    auto h = spawn(work, 21);         // the handle owns the thread
    assert(join(h) == 42);             // wait and take the result

    Vec<i32> data = Vec::new();
    Vec::push(&mut data, 5);
    auto h2 = spawn(move [data]()     // a move closure carries the Vec to the other thread
    {
        Vec::len(&data)
    });
    assert(join(h2) == 1);

    {
        auto h3 = spawn(work, 1);
    }                                 // unjoined: joined automatically here, result discarded
}
A borrowing closure cannot be spawnedrejected: diag.spawn-borrow-closure
fn main()
{
    i32 x = 5;
    auto h = spawn([x](i32 n)         // ✗ diag.spawn-borrow-closure: the closure borrows x
    {
        n + x
    }, 1);
    join(h);
}

A reference passed to spawn is held by the new thread until it finishes, so the spawning thread may not use the referent in a conflicting way before join(h). Whether the thread happened to finish first would otherwise decide the outcome; the checker rejects the program instead.

A thread's borrows last until joinrejected: diag.aliasing-conflict
fn read(ref<i32, shared> r) : i32
{
    *r
}

fn main()
{
    i32 x = 1;
    auto h = spawn(read, &x);         // the thread borrows x until join(h)
    i32 y = x;                        // ✓ reading alongside a shared borrow
    x = 2;                            // ✗ diag.aliasing-conflict (static): the thread may still be reading x
    join(h);
}

Sharing state: mutex and guard

Mutex::new(v) wraps a value in a mutex<T>. The value inside is reachable only through lock(&m), which returns a guard<T>: an exclusive path into the interior that exists for as long as the guard does. *g is the place inside. The guard is a resource; dropping it, or letting its scope end, unlocks the mutex. Only one guard can exist at a time; another thread's lock waits. Locking a mutex your own thread already holds is a fault, diag.mutex-reentrant-lock, not a deadlock.

A mutex is shared between threads by passing &m, a ref<mutex<T>, shared>. Any number of threads may hold that shared reference; it is the one case where an exclusive path (the lock guard) is allowed to coexist with shared references to the same object, because the lock itself serialises access.

A counter shared by two threadsruns
import std;

fn worker(ref<mutex<i32>, shared> m, i32 n) : i32
{
    foreach (i in 0..n)
    {
        auto g = lock(m);             // blocks until the mutex is free
        *g += 1;
    }                                 // each iteration's guard is destroyed here, unlocking
    n
}

fn main()
{
    auto m = Mutex::new(0);
    auto h1 = spawn(worker, &m, 3);   // both threads hold a shared reference to m
    auto h2 = spawn(worker, &m, 4);
    i32 a = join(h1);
    i32 b = join(h2);
    assert(a + b == 7);
    i32 total = *lock(&m);            // a temporary guard: it lives until the semicolon
    assert(total == 7);                // whatever the interleaving, every increment was serialised
}
Locking twice on one threadrejected: diag.mutex-reentrant-lock
fn main()
{
    auto m = Mutex::new(0);
    auto g1 = lock(&m);
    auto g2 = lock(&m);               // ✗ diag.mutex-reentrant-lock: this thread already holds it
}

Through a guard, a field or element of the locked value is reached directly, as through a reference: g.count, g.items[0], Vec::push(&mut g.items, x) mean (*g).count and so on. *g is how you name the whole value.

A mutex may also be a field: a struct holding one or more mutexes is shared by reference like a bare one, each thread locking the field it needs (lock(&board.scores)). Each mutex field is its own lock.

A struct of mutexes shared by two threadsruns
import std;

struct Stats
{
    u32 hits;
    Vec<u32> seen;
}

struct Board
{
    mutex<Stats> stats;
    mutex<u32> rounds;
}

fn play(ref<Board, shared> b, u32 me) : u32
{
    {
        auto g = lock(&b.stats);          // one mutex field...
        g.hits += 1;                      // g.hits is (*g).hits
        Vec::push(&mut g.seen, me);
    }
    auto r = lock(&b.rounds);             // ...and another, locked independently
    *r += 1;
    me
}

fn main()
{
    Board b = Board { .stats = Mutex::new(Stats { .hits = 0, .seen = Vec::new() }), .rounds = Mutex::new(0: u32) };
    {
        auto x = spawn(play, &b, 1);
        auto y = spawn(play, &b, 2);
        assert(join(x) + join(y) == 3);
    }
    auto g = lock(&b.stats);
    assert(g.hits == 2 && Vec::len(&g.seen) == 2 && *lock(&b.rounds) == 2);
}

Passing values: Channel

A Channel<T> from std (std::sync) is a queue between threads, shared by reference as a mutex is. Channel::new(cap) holds at most cap values (at least 1). Channel::send(&ch, v) moves v in, waiting while the channel is full; Channel::recv(&ch) moves the oldest value out as Some(v), waiting while it is empty. A value in a channel has one owner, the channel, so a String or a Vec travels between threads without being shared or copied. A waiting thread uses no processor time.

Channel::close(&ch) says nothing more is coming: the values already sent are still received, and then recv gives None, which is what ends a receiver's loop. A send after close does not lose its value: it gives it back as Err(v). Destroying a channel destroys the values left in it, oldest first. The usual shape is a pool of workers: the main thread sends the jobs, closes the channel, and joins the workers, each of which stops at None.

A pool of workersruns
import std;

fn worker(ref<Channel<u64>, shared> jobs, ref<Channel<u64>, shared> results)
{
    while (true)
    {
        match (Channel::recv(jobs))
        {
            Some(n) : Channel::send(results, n * n),
            None    : break,                     // closed and empty: this worker is done
        }
    }
}

fn main()
{
    Channel<u64> jobs = Channel::new(4);        // at most 4 jobs waiting at once
    Channel<u64> results = Channel::new(16);
    {
        auto a = spawn(worker, &jobs, &results);
        auto b = spawn(worker, &jobs, &results);
        foreach (i in 1..11: u64)
        {
            _ = Channel::send(&jobs, i);            // waits while the channel is full
        }
        Channel::close(&jobs);                  // the workers stop once it is empty
        join(a);
        join(b);
    }
    Channel::close(&results);
    u64 total = 0;
    while (Some(sq) = Channel::recv(&results))   // None once it is closed and empty
    {
        total += sq;
    }
    assert(total == 385);                        // 1 + 4 + … + 100, whichever worker did which

    Channel<String> names = Channel::new(1);
    Channel::close(&names);
    match (Channel::send(&names, String::from_str("late")))
    {
        Ok(_)  : assert(false, "unreachable"),
        Err(s) : assert(String::len(&s) == 4),   // closed: the value comes back
    }
}

A wait that can never end because no other thread exists — the main thread receiving from an empty channel, or sending to a full one, with no spawned thread running — is a fault, diag.channel-deadlock. Other deadlocks are not detected.

Waiting alonerejected: diag.channel-deadlock
import std;

fn main()
{
    Channel<i32> ch = Channel::new(1);
    _ = Channel::send(&ch, 1);
    _ = Channel::send(&ch, 2);            // ✗ diag.channel-deadlock: full, and no one to receive
}

Without waiting for ever (D-0185). Channel::try_recv(&ch) answers at once: Ok(Some(v)) a value, Ok(None) nothing there, Err(()) closed and empty — three answers, because a loop that also watches a socket or interrupt_requested() must tell "nothing yet" from "the senders are done". Channel::recv_timeout_ms(&ch, ms) waits at most ms milliseconds for the same three answers. cpu_count() is how many threads the machine runs at once, the size to give a pool.

How threads run

Every CobaltC thread is a thread of the operating system. In a program built by cobc they run at once, on as many cores as the machine has. The checks of §08 and §10 still run as the program goes, in every thread, and two threads that use the same object meet in them: that is how a conflict between threads is caught at the access that would have raced.

To keep threads from slowing each other down, the run-time checks keep each thread's records apart. What a thread makes, and what is moved into it, are its own; most checks on them involve no other thread. Where threads do meet:

  • Data reached through a reference to another thread's (spawn(f, &v)) stays the owner's, and each access through it is checked against the owner's records too. It works, and costs a little more on every access than data the thread owns.
  • Waiting — on join, on a lock someone else holds, on a full or empty Channel — stops only the waiting thread, which spins briefly before it sleeps: most waits for a lock are short. Locking, sending and receiving involve no other thread's checks.
  • A block that only reads and writes through its guard ({ auto g = lock(&m); *g += 1; }, plain data inside, no early return) costs a lock and an unlock and nothing more: cobc makes no guard object for it.
  • Running code of the program on a value's end — a destructor you wrote, a fn value, a Box — briefly pauses the other threads' checks. Everything else the runtime does at a block's end stays within the threads involved.

So the program that runs fastest in parallel gives each thread its work: move a chunk of the data into the thread (an argument it takes by value, which then belongs to it) and hand the result back through join or a Channel. The performance chapter's Threads section has the figures.

Each thread its own dataruns
import std;

fn total(Vec<u64> chunk) : u64
{
    u64 t = 0;
    foreach (x in &chunk)
    {
        t += *x;
    }
    t
}

fn main()
{
    Vec<handle<u64>> hs = Vec::new();
    foreach (k in 0..4: u64)
    {
        Vec<u64> chunk = Vec::new();                 // made here, then moved into its thread
        foreach (i in 0..1000: u64)
        {
            Vec::push(&mut chunk, k * 1000 + i);
        }
        Vec::push(&mut hs, spawn(total, chunk));   // `chunk` now belongs to that thread
    }
    u64 sum = 0;
    while (Vec::len(&hs) > 0)
    {
        sum += join(Option::unwrap(Vec::pop(&mut hs)));
    }
    assert(sum == 7998000);                         // 0 + 1 + … + 3999
}

The guarantee

What a program without unsafe can rely on, however its threads are timed (rule.conc.guarantee):

  • No data races. Two threads never touch the same storage at once with one of them writing. Either something orders the two accesses (a join, a mutex's lock, a value sent and received), or the second access is diag.aliasing-conflict.
  • One step at a time. Every run is some interleaving of the threads' steps, each step whole: a thread never sees half of another's write, and there is no reordering to reason about.
  • Nothing undefined. An access that is not allowed is a diagnostic, in every interleaving.

A reference passed to spawn belongs to the new thread from the moment of the spawn, until the thread ends, even before it has run a single step. Timing can still choose between two permitted outcomes: a thread that has already finished holds nothing, so an access that would have conflicted with it while it ran is fine afterwards. Use join (or a channel) to make the order part of the program. Deadlock is not ruled out, but it is reported rather than left to hang (below).

How the guarantee is checked: beyond the tests of the two implementations, a separate program, cobmodel (impl/model), runs a fragment of the language straight from the rules, with every interleaving of its threads, and lists every outcome they allow. Tens of thousands of generated programs about references, threads, mutexes, channels and vectors of references have been compared with what coby and cobc --explore do. This is evidence, not a proof: it covers that fragment, up to the program sizes it tried.

Caught, whatever the timingrejected: diag.aliasing-conflict
import std;

fn bump(ref<i64, exclusive> x, ref<Channel<u8>, shared> go)
{
    _ = Channel::recv(go);                    // waits, holding x
    *x += 1;
}

fn main()
{
    Vec<i64> v = Vec::filled(4, 0: i64);
    Channel<u8> go = Channel::new(1);
    {
        auto h = spawn(bump, &mut v[0], &go);
        i64 other = v[1];                      // fine: another element
        i64 same = v[0];                       // ✗ diag.aliasing-conflict: the thread holds v[0]
        _ = Channel::send(&go, 1);
        join(h);
    }
}

An exclusive reference belongs to one thread. Copying a ref<T, exclusive> copies one path, and two copies in two threads would be two writers no check could tell apart. So spawn and Channel::send lend each exclusive reference they hand over: the thread gets a path of its own, borrowed from yours, and your copy conflicts with it until that thread is done with it. Lending the same reference to two threads at once is diag.aliasing-conflict, and so is writing through your copy while it is lent. A thread may use an exclusive reference only if it was handed it this way: one it finds inside a struct it was lent, inside a Vec moved to it, or inside a mutex belongs to the thread that made it, and using it faults while that thread runs. To give a thread exclusive access to something, pass the reference to spawn or send it through a channel.

One exclusive reference, one threadrejected: diag.aliasing-conflict
fn bump(ref<i32, exclusive> a)
{
    *a += 1;
}

fn main()
{
    i32 x = 0;
    ref<i32, exclusive> r = &mut x;
    auto h1 = spawn(bump, r);         // r is lent to this thread until join(h1)
    auto h2 = spawn(bump, r);         // ✗ diag.aliasing-conflict: r is lent already
    join(h1);
    join(h2);
}

When a thread fails

A checked fault in a thread you spawned ends that thread: it unwinds, its destructors run, the threads it started are stopped and waited for (cancelled, then joined; what they report, their own fault or the cancellation, is discarded), and its result becomes the failure — its own, never one of theirs. The rest of the program goes on. This is what lets a server give each connection a thread and survive a bug in one of them. Nothing continues in a state the failed check protected:

  • A mutex the thread held is poisoned. Its interior may be half-changed, so the next lock of it faults with diag.mutex-poisoned.
  • A thread lent an exclusive reference is not contained. Given one at spawn, or by locking a mutex whose contents can hold one (receiving from a Channel of &mut references, say), it may be changing data that belongs to a thread still running, so its fault ends the program at once, as a fault always did.
  • The main thread's fault ends the program, as before.

Nobody has to ask for this. join(h) of a failed thread raises its failure again in the joiner, and so does the handle's end if nobody joined it: a program that never looks at failures still fails as a whole, one scope at a time. To take a failure as a value, use try_join(h), which returns Result<R, ThreadFailure>. ThreadFailure::text is the report the program would have printed, diagnostic its diag.… name. is_finished(&h) says, without waiting, whether a thread has ended, so one loop can accept new work and collect the threads that are done.

A supervisor takes failures as valuesruns
import std;

fn handler(usize i) : u64
{
    Vec<u64> v = Vec::filled(3, 7: u64);
    v[i]                                               // a bug when i >= 3
}

fn main()
{
    usize served = 0;
    usize failed = 0;
    foreach (i in 0..5)
    {
        match (try_join(spawn(handler, i)))
        {
            Ok(_)  : served += 1,
            Err(f) :
            {
                String d = ThreadFailure::diagnostic(&f);
                assert(String::as_view(&d) == "diag.index-out-of-bounds");
                failed += 1;
            },
        }
    }
    assert(served == 3 && failed == 2);                // the program went on after both
}
join raises a failure nobody tookrejected: diag.index-out-of-bounds
import std;

fn handler(usize i) : u64
{
    Vec<u64> v = Vec::new();
    v[i]
}

fn main()
{
    auto h = spawn(handler, 0);
    join(h);                          // ✗ diag.index-out-of-bounds (dynamic): raised again here
}
A failed thread's mutex is poisonedrejected: diag.mutex-poisoned
import std;

fn update(ref<mutex<i64>, shared> m, usize i)
{
    auto g = lock(m);
    *g += 1;
    Vec<i64> v = Vec::new();
    *g += v[i];                       // faults while holding the lock
}

fn main()
{
    mutex<i64> m = Mutex::new(0: i64);
    _ = try_join(spawn(update, &m, 0));
    auto g = lock(&m);                // ✗ diag.mutex-poisoned (dynamic): *g may be half-updated
}

Stopping a thread: cancel

cancel(&h) asks a thread to stop. Its current wait, or its next one, ends with the fault diag.thread-cancelled, contained like any other: the thread unwinds and its result is that failure. The waits are join, lock, a channel's send and recv, sleep_ms, and every socket operation, so a handler blocked reading a client that never writes can be stopped. A thread that never waits finishes its work, and one inside a call outside the program (input, a file, a child process) is stopped at its next wait. A thread cancelled while it holds a guard unwinds like a faulting one: the mutex is poisoned, since what it was changing may be half-changed. ThreadFailure::is_cancelled tells a cancellation from a bug, and a handle whose thread you cancelled raises nothing when it ends. Together with a channel this is "first answer wins" without a select: start one thread per source, all sending to one channel, take the first value, and cancel the rest.

cancel ends a waitruns
import std;

fn listen(ref<Channel<u8>, shared> c) : u8
{
    Option::unwrap_or(Channel::recv(c), 0)           // nobody ever sends
}

fn main()
{
    Channel<u8> c = Channel::new(1);
    auto h = spawn(listen, &c);
    cancel(&h);                                      // its wait ends at once
    match (try_join(h))
    {
        Ok(_)  : assert(false),
        Err(f) : assert(ThreadFailure::is_cancelled(&f)),
    }
}

Deadlock

A wait that can never end is diag.deadlock, reported where it happens. A thread whose join or lock would close a cycle (it waits on a thread that, through holders and joins, waits on it) faults at once. And when every thread is waiting on another thread, a mutex or a channel, with none on the clock, a socket or input, the runtime faults one thread of a cycle, or, if there is no cycle, the threads at the ends of the chains of waits (those waiting on a channel); the threads waiting on them then get the failure through join, or find the mutex poisoned. The result does not depend on timing. A channel wait while some thread is in a socket call or asleep is never called a deadlock: that thread may send when its call returns. Give such a wait a limit (recv_timeout_ms) or cancel it from a supervisor. Like any fault in a spawned thread it is contained, so two deadlocked handlers in a server are reported and the server goes on. (The main thread waiting alone on a channel is still diag.channel-deadlock.)

Every thread waitingrejected: diag.deadlock
import std;

fn waits(ref<Channel<u8>, shared> c)
{
    _ = Channel::recv(c);            // nobody will send
}

fn main()
{
    Channel<u8> c = Channel::new(1);
    auto h = spawn(waits, &c);
    join(h);                          // ✗ diag.deadlock (dynamic): every thread waits, none can go on
}

Exploring schedules: --explore

A result that depends on timing is easy to miss: the timing you test is rarely the one that goes wrong. coby --explore N prog.cb runs the program N times, each under a different order of its threads chosen by a seed, and groups the runs by outcome (output and exit status). One outcome is what you want; more than one means the program's result depends on timing, and coby exits with 1. Each outcome comes with a seed, and coby --schedule-seed S prog.cb replays that run exactly, as often as you need to find out why. Under a schedule the threads take turns one at a time, so exploration needs no cores and finds orders a parallel run would rarely produce.

cobc --explore N prog.cb does the same for the compiled program: it compiles once and runs the executable N times. There a thread keeps its turn between the points where threads interact -- a spawn, a wait, a mutex released, a call outside the program -- and now and then between two of the program's checks, which is where a program whose checks all pass can end differently. cobc --schedule-seed S prog.cb replays one run, and so does setting COBALTC_SCHEDULE_SEED=S in a compiled program's environment. In either tool, a thread that is asleep or in a socket or input call leaves the schedule until the call returns, so a program that talks to the network explores only the orders around those calls.

counter.cb: a lost update, in some orders
import std;

fn bump(ref<mutex<i64>, shared> m)
{
    i64 seen = 0;
    {
        auto g = lock(m);
        seen = *g;
    }
    auto g = lock(m);
    *g = seen + 1;                    // a lost update when the other thread ran in between
}

fn main()
{
    mutex<i64> m = Mutex::new(0: i64);
    {
        auto a = spawn(bump, &m);
        auto b = spawn(bump, &m);
    }
    auto g = lock(&m);
    printf("total %v
", *g);
}
Two outcomes: the result depends on timing
$ coby --explore 50 counter.cb
explored 50 schedules of counter.cb: 2 distinct outcomes

19 runs, exit 0 -- replay: coby --schedule-seed 1 counter.cb
  | total 1

31 runs, exit 0 -- replay: coby --schedule-seed 2 counter.cb
  | total 2

Data parallelism: slice_parts

slice_parts(&mut v[0..$], k) splits a vector (or any exclusive slice) into k consecutive runs as nearly equal as they can be, each an exclusive slice of its own. Give each run to a thread and the threads write one vector at once, with no lock: the runs are disjoint, and the checker proves it the way it proves any two ranges apart. The vector is usable again once every thread is joined. A thread takes a named function here, not a closure: a closure is passed by move (§15), so it goes to one thread, and one closure cannot run in several threads at once.

Each thread one part of a vectorruns
import std;

fn square_all(slice<u64, exclusive> s)
{
    for (usize i = 0; i < slice_len(s); i += 1)
    {
        s[i] = s[i] * s[i];
    }
}

fn main()
{
    Vec<u64> v = Vec::new();
    foreach (i in 0..1000: u64)
    {
        Vec::push(&mut v, i);
    }
    {
        Vec<slice<u64, exclusive>> parts = slice_parts(&mut v[0..$], cpu_count());
        Vec<handle<void>> hs = Vec::new();
        while (Vec::len(&parts) > 0)
        {
            Vec::push(&mut hs, spawn(square_all, Vec::swap_remove(&mut parts, 0)));
        }
        foreach (h in hs)
        {
            join(h);
        }
    }
    assert(v[999] == 998001);
}

What is not provided

No atomics, no memory orderings, no condition variables, no thread-local storage. Steps of different threads interleave at the granularity of single operations and every operation sees a consistent state; a join, a lock, a send or a recv that cannot proceed simply waits. Rc<T> is not thread-safe; to share across threads, share a mutex or pass values through a Channel.

Parallel or interleaved
The compiler cobc runs threads in parallel on separate cores (above); the interpreter coby runs them one at a time. A program that follows the rules gets the same result from both. Where the language leaves an outcome to timing — writing a variable while another thread may still be reading it through a reference is diag.aliasing-conflict if the thread is still running, and fine if it has finished — each is free to show either permitted outcome, and they may differ.
Rules of thumb
  • Every thread has an owner: the handle. Join it explicitly to get the result; let scope end join it if you do not care.
  • Supervise threads that may fail: try_join takes the failure as a value; cancel the ones you no longer need.
  • Lend a thread exclusive access to your data only when its failure should end the program; otherwise move the data in.
  • Share a mutex by shared reference; keep each guard in the smallest block that needs it.
  • To hand values from thread to thread, send them through a Channel; close it when the last value is sent.
  • Move data into a thread with a move closure or by-value arguments; lend it with references only when the mutex rule allows.

§20Unsafe, raw pointers and FFI

Governing specification file: spec/20-trust-boundaries.md

unsafe blocks record what you assert; rawptr, reclaim, allocate and extern fn are the only doors to the outside.

Everything so far is checked. This section is about the doors to the outside world: raw addresses, manual allocation and foreign functions. CobaltC does not pretend those can be checked; instead it makes the boundary explicit. Each trusted operation is only legal inside an unsafe { } block, and the block is your signed statement that the operation's preconditions hold.

unsafe blocks

unsafe { … } is an ordinary block (it has a value, it opens a scope) with one extra permission: the trusted operations listed below may appear inside it. Outside, they are rejected with diag.trusted-outside-unsafe. Nothing else changes: the aliasing, ownership and initialisation rules still apply to every ordinary access inside an unsafe block.

OperationWhat you assert
*p (read) / *p = v (write) for p : rawptr<T>p addresses sizeof<T>() initialised bytes of a T, no live reference reaches them, and no other thread is writing them
p + n (pointer offset, n : isize)the result stays within, or one past the end of, one allocation or object
reclaim<T>(p)the bytes at p hold a valid T that no ordinary object owns; you now treat them as an object
release(p, n), copy_raw(dst, src, n), deallocate(p, n, align)the ranges are valid and nothing live overlaps them; deallocate matches one earlier allocate
an unsafe extern fn declarationthe linked code has a function of this name with exactly this signature
a call to an unsafe extern fnthe foreign code honours its signature and touches only what the pointer arguments reach

Who asserts what

The standard library's own bodies use unsafe: Vec::push writes an element through a raw pointer, File::write calls the operating system, Rc::clone reclaims its box. Those blocks are not yours. They belong to the implementation, which must keep every one of them true to conform, and the specification lists each of them with the fact that makes it hold (spec/21 §0, "What std asserts": for Vec::push, that len < cap after grow and that no element object was ever established in the slot). So a program that writes no unsafe of its own has the language's guarantee unconditionally, and the guarantee is lost only by a false claim in a block the program itself wrote.

Raw pointers

rawptr<T> is an address. It has no mode, no lifetime, and no checks; it is plain and copyable. Getting one is safe: rawptr_of(&x) (or rawptr_of(&mut x)) takes the address of a place, and reinterpret_ptr<U>(p) changes the pointee type without changing the address. Using one to reach memory is trusted.

Raw reads, writes and offsetsruns
import std;

fn read_through(rawptr<i32> p) : i32
{
    unsafe
    {
        *p                            // trusted: the caller passed a pointer to a live i32
    }
}

fn main()
{
    i32 x = 7;
    assert(read_through(rawptr_of(&x)) == 7);
    auto bytes = [1: u8, 2: u8, 3: u8];
    rawptr<u8> b = reinterpret_ptr<u8>(rawptr_of(&bytes));
    unsafe
    {
        assert(*(b + 2) == 3);         // pointer offset, in units of the pointee size
    }
}
No unsafe blockrejected: diag.trusted-outside-unsafe
fn bad(rawptr<i32> p) : i32
{
    *p                                // ✗ diag.trusted-outside-unsafe
}

fn main()
{
    i32 x = 1;
    bad(rawptr_of(&x));
}

From raw bytes back to objects: reclaim

Raw memory is not an object until you say it is. reclaim<T>(p) is a place expression: it establishes (or re-attaches to) an object of type T over the bytes at p, and from then on every ordinary rule applies to it. You can borrow it (&reclaim<T>(p)), assign to it, or drop it. Reclaiming the same address twice gives the same object, so two references obtained that way are checked against each other like any two references. This is how Vec hands out references to its elements.

reclaimruns
import std;

fn main()
{
    Vec<i32> v = Vec::new();
    Vec::push(&mut v, 7);
    auto p = rawptr_of(&v[0]);                      // the element's address; obtaining it is safe
    unsafe
    {
        auto r = &reclaim<i32>(p);    // a checked reference to the element, via its address
        assert(*r == 7);
    }
}

Manual allocation

allocate(n, align) returns Result<rawptr<u8>, AllocError>: fresh uninitialised bytes, or Err. It is safe to call (failure is a value). deallocate(p, n, align) gives them back and is trusted. A resource written into raw memory with *p = v moves there and is tracked; reading *p of a resource type moves it back out. Plain data that holds references is tracked too: the cells it is written to hold those references, exactly as a variable would, until they are released. The library's Vec::grow in §21 is the reference example of the whole pattern.

When a promise is false

An unsafe block's promises are not checked; that is what makes them promises. If one is false — *p reads bytes that are not a valid T, reclaim claims cells another object owns, a foreign function writes past what it was given — the program has left the language's rules at that step. Everything it did before that step happened exactly as the rules say. From that step on, no rule says what happens: the program may carry on, print nonsense, fault, or crash, and every one of those is allowed. None of the guarantees in this guide — no use after free, no data race, no out-of-bounds access — covers what comes after. A program with no unsafe block makes no promises, so they cover all of it.

struct Holder
{
    Vec<i32> v;
}

fn main()
{
    Holder h = Holder { .v = Vec::new() };
    rawptr<Vec<i32>> p = reinterpret_ptr<Vec<i32>>(rawptr_of(&h));
    unsafe
    {
        auto taken = *p;              // false promise: h still owns these bytes
    }
    h.v = Vec::new();                 // from the line above on, no rule applies
}

The tools do not have to notice a broken promise, and in general they cannot. When one does, it is encouraged to stop with a diagnostic rather than crash, since a report names the problem and a crash does not; but that is a courtesy, not a rule. Keep unsafe blocks small, so that what each one promises can be read and checked by eye.

Foreign functions

unsafe extern fn name(params) : ret; declares a function provided from outside. Parameters and result may only be integers, floats, bool, void and rawptr<T>; anything else, including a ref or a struct, is rejected (diag.extern-non-ffi-type). The declaration itself says unsafe: no tool can check that the C function really has that name and exactly those types, and if it does not, every call is wrong, so the promise is marked where it is made, where a search for unsafe finds it. Without unsafe the declaration is a syntax error. Calling it needs unsafe too, for what the function does. Its result is a claim: a genuine value of its declared type, but one whose relationship to anything else (that a returned length is right, that a pointer is valid) is not established until your code checks it.

std declares two externs: stdout_write(rawptr<u8> buf, usize len) : isize, which printf (§21) writes through, with the unsafe block inside std, and its counterpart stdin_read, which read_line reads through. Calling conventions and how code is linked are outside the language and documented by each implementation; a program names the C code it needs with extern "…"; (below).

Both implementations call real C functions on x86-64 Linux: an unsafe extern fn names a function in the C library or the maths library by its declared name. The compiler passes any FfiType, pointers included. The interpreter's memory is simulated, so it can pass only integers, bool and floats, and reports a call that needs a pointer as unsupported rather than guessing.

Calling C functionsruns
import std;

unsafe extern fn sqrt(f64 x) : f64;          // from the maths library
unsafe extern fn labs(i64 x) : i64;          // from the C library
unsafe extern fn toupper(i32 c) : i32;

fn main()
{
    f64 root = unsafe
    {
        sqrt(2.0)
    };
    assert(root * root > 1.9999 && root * root < 2.0001);   // a claim, checked before it is trusted
    i64 distance = unsafe
    {
        labs(3 - 10)
    };
    assert(distance == 7);
    assert(unsafe { toupper(99) } == 67);                   // 'c' -> 'C'
}
Calling the stdout_write externruns
import std;

fn main()
{
    auto msg = b"Hi\n";                          // a byte-array literal: array<u8, 3>
    rawptr<u8> p = reinterpret_ptr<u8>(rawptr_of(&msg));
    isize written = unsafe
    {
        stdout_write(p, 3)            // the extern call; its result is a claim about how much was written
    };
}
output
Hi
Only plain types cross an extern boundaryrejected: diag.extern-non-ffi-type
import std;

unsafe extern fn takes_vec(Vec<i32> v);      // ✗ diag.extern-non-ffi-type: a Vec cannot cross the boundary

fn main()
{
}

Your own C code

extern "…"; names C code for the compiler to link, so that the program's unsafe extern fn declarations can be found in it. It can go anywhere an item can, declares no name, and can be repeated; each piece of code is linked once. The program still builds with nothing but cobc main.cb.

WrittenLinksNotes
extern "./shapes.c";a C source filecompiled on its own, with the C compiler's warnings shown
extern "./lib/fast.o";, extern "./lib/libfast.a";an object file or static librarylinked as it is
extern "./lib/libplugin.so";a shared libraryfound wherever the program is run from
extern "z";an installed libraryas cc -lz

A string beginning ./, ../ or / is a file, and a relative path is relative to the file it is written in, exactly as in module m "./m.cb";. So a module that wraps some C code can name it next to itself, and every program that uses the module gets it linked. Anything else is a library name. A missing file or library is reported at the line that names it.

Watch out
Only the compiler links C code. The interpreter accepts extern "…"; and ignores it; the first call into that code stops with unsupported: no C function … which only cobc links (exit status 3), and everything before it runs as usual.

The usual shape is a module that keeps the unsafe calls to itself and exports safe functions. Two things make the wrappers below safe. First, rect_area takes u8 sides, so the product always fits in the C function's int32_t. Second, first_squares lets the C function fill storage that comes from allocate, because a C function may write only outside the program's live objects (§20's rule for extern calls); the count it returns is a claim, checked before it is used, and the values are copied into a Vec.

main.cbruns
import std;

module geometry "./geometry.cb";

fn main()
{
    printf("%v", geometry::rect_area(6, 7));
    printf("\n");
    Vec<i64> sq = geometry::first_squares(5);
    foreach (x in &sq)
    {
        printf("%v ", x);
    }
    printf("\n");
}
output
42
0 1 4 9 16 
geometry.cb
import std;

extern "./shapes.c";                  // relative to this file

unsafe extern fn area(i32 w, i32 h) : i32;
unsafe extern fn squares(rawptr<i64> out, i64 n) : i64;

// u8 sides: the product always fits the C function's int32_t.
export fn rect_area(u8 w, u8 h) : i32
{
    unsafe { area(widen<i32>(w), widen<i32>(h)) }
}

// C fills storage from allocate; the count it returns is checked,
// then the values are copied out.
export fn first_squares(u8 n) : Vec<i64>
{
    Vec<i64> out = Vec::new();
    usize bytes = widen<usize>(n) * sizeof<i64>();
    rawptr<u8> raw = match (allocate(bytes, alignof<i64>()))
    {
        Ok(p)  : p,
        Err(_) :
        {
            return out;
        },
    };
    rawptr<i64> buf = reinterpret_ptr<i64>(raw);
    i64 written = unsafe { squares(buf, widen<i64>(n)) };
    if (written >= 0 && written <= widen<i64>(n))
    {
        foreach (i in 0..narrow<usize>(written))
        {
            Vec::push(&mut out, unsafe { *(buf + reinterpret<isize>(i)) });
        }
    }
    unsafe
    {
        deallocate(raw, bytes, alignof<i64>());
    }
    out
}
shapes.c
#include <stdint.h>

int32_t area(int32_t w, int32_t h)
{
    return w * h;
}

// Fills out[0..n) with the first n squares; returns how many it wrote.
int64_t squares(int64_t *out, int64_t n)
{
    for (int64_t i = 0; i < n; i++)
    {
        out[i] = i * i;
    }
    return n;
}

From claim to fact

Data from outside (bytes read by an extern, a pointer handed back by a library, a length in a header) starts as a claim. The pattern for turning it into a fact is a validator that returns Result: the library's String::from_utf8(bytes) (§21) is the canonical example. Once the validator has accepted the bytes, the resulting String is guaranteed valid UTF-8 for the rest of its life, because there is no operation that could break it.

Rules of thumb
  • Keep unsafe blocks tiny and wrap them in a safe function whose signature makes the precondition impossible to violate from outside.
  • Never let a raw pointer stand in for a reference in an API. Return ref<T, m> (from reclaim) so callers get checked access.
  • Validate external data once, at the boundary, into a type that cannot represent the invalid state.

§21The standard library, std

Governing specification file: spec/21-standard-library-semantics.md

import std; Option, Result, the intrinsics, str and printf, and Vec, String, HashMap, HashSet, Queue, PriorityQueue, Rc and Box written in CobaltC.

The standard library is the module std. It holds the Option and Result enums and the library's error types; the containers and text types, written in CobaltC itself: Vec<T>, String, StringView, HashMap<K, V>, HashSet<K>, Queue<T>, PriorityQueue<T>, Box<T>, Rc<T>, Weak<T> and Channel<T> (§19); output, input and files: printf, eprintf, sprintf, read_line, read_file, write_file, File and the directory functions; the program's arguments, arg_count and arg; and smaller helpers such as swap, replace, min, max, crc32 and Rng. A program uses it with import std;, or by qualified paths such as std::Vec. Its source is part of the specification, which is the language's proof that the mechanisms in this guide are enough to build real containers. A function joins std when it names one very common intent, so that its name says what a loop or a match would otherwise have to be read to discover; one that is only shorter, not clearer, stays out. Being a module, it keeps its private parts private: no program can touch Vec's length or buffer, String's bytes, a HashMap's table or Rc's count.

std is divided into submodules by subject, and its root passes each one's items on as its own (export import, §17), so import std; brings in everything and std::Vec names the same type as std::collections::Vec. A program that wants one subject alone may import just that one. Four submodules divide again, the same way: std::crypto into digest, kdf, aead and pk, std::http into client and server, std::database into one submodule per database server, postgres today, and std::encoding into one submodule per data format or encoding of bytes as text, json, hex and base64. Each is re-exported by its parent, so sha256 is also std::crypto::digest::sha256, PgConnection is std::database::postgres::PgConnection and Json is std::encoding::json::Json. One submodule is declared and not re-exported: std::extensions, the area an implementation may fill with modules of its own, which the specification leaves undefined and conformance never tests. A program reaches one only by naming it (import std::extensions::m;), so its dependence on that implementation shows in its source; the reference implementation's std::extensions is empty.

What the library asserts, and what it does not

A claim about std rests on one of four tiers, each on the one before: the language's own guarantee (a program without unsafe blocks of its own has no undefined behaviour); the trusted library base, the unsafe blocks inside std whose claims the implementation keeps (every one is listed in spec/21 §0); the conformance suite, which shows that the bodies do what its rows say on the inputs the rows give; and, for the security-relevant modules, a security profile: the standard targeted, the vectors held, what is claimed about timing, the limits on input, and what was tested outside the suite. A green suite is evidence for the third tier and nothing more. The full table is in spec/21 §0 ("Security profiles"); the short form:

ModuleStandardConstant timeLimits on input
std::crypto::digestSHA-1, SHA-2, BLAKE2b, CRC-32yes; digest_eq compares every bytenone
std::crypto::kdfHMAC, HKDF, PBKDF2, Argon2idyes, except Argon2id's data-dependent half (by design)Argon2id parameters are checked
std::crypto::aeadChaCha20-Poly1305, AES-GCMyes (bitsliced AES, constant-time GHASH)key and nonce lengths are checked
std::crypto::pkX25519, P-256/P-384, ECDSA, Ed25519, RSAthe private operations, yes; verification and RSA key generation, noencodings are checked
std::x509X.509, PEM, PKCS #8 keysnot claimed (public data)a chain of at most ten certificates
std::tlsTLS 1.3 client and serverthe primitives'; digest_eq for Finishedrecords of at most 16 KiB (+256 encrypted); timeouts
std::httpHTTP/1.1—head 64 KiB; body max_body (16 MiB); max_redirects (10); timeout_ms
std::compressDEFLATE, zlib, gzip—output at most the max you pass
std::encoding::jsonJSON—nesting at most 512 deep; numbers are f64
std::database::postgresPostgreSQL protocol 3.0, SCRAM-SHA-256digest_eq for the server signaturemessages at most 1 GiB; results at most max_result (16 MiB)

Every parser in that table was also fed ten thousand mutated inputs under GCC, Clang and AddressSanitizer, each in its own thread (the hostile-input round, D-0211): the expectation is an Err or a clean Ok for every one, never a fault or a crash. The round found two faults, in the PEM readers and in the HTTP client's status line, both now an Err.

Every module and its items

Below is every module of std with every item it declares, generated from the reference implementation's source. Names in faint red are not exported: they are the module's own working parts, which a program cannot name or call, listed so that nothing in the source is left out. Everything else is exported. An item marked n has no CobaltC body: both tools build it in (printf, sqrt, exit, …). A row headed T:: lists T's associated functions. A parent module also re-exports the submodules it lists, so their items are its own too.

std, std::core, std::collections, std::text, std::io, std::fs, std::env, std::memory, std::sync, std::random, std::math, std::time, std::process, std::net, std::crypto, std::crypto::digest, std::crypto::kdf, std::crypto::aead, std::crypto::pk, std::x509, std::tls, std::http, std::http::client, std::http::server, std::database, std::database::postgres, std::encoding, std::encoding::json, std::encoding::hex, std::encoding::base64, std::compress, std::extensions

std impl/std/std.cb

KindItems
Submodulescore, collections, text, io, fs, env, memory, sync, random, math, time, process, net, crypto, x509, tls, http, database, encoding, compress, extensions
Functionsbyte_answer, byte_op, bytes_ptr, code_error, file_error, fs_answer, path_result, path_text, printn
Externs (unsafe)clock_read, event_op, file_at, file_op, file_read, file_write, fs_op, fs_query, net_op, os_random, path_op, proc_op, sleep_ns, stderr_write, stdin_read_line, stdout_flush, task_op, tz_offset

std::core impl/std/core.cb

KindItems
TypesAllocError, Option, Result
Functionsassertn, overwrite, replace, swap
Option::expect, is_none, is_some, ok_or, unwrap, unwrap_or
Result::expect, is_err, is_ok, map_errn, ok, unwrap, unwrap_or

std::collections impl/std/collections.cb

KindItems
TypesDrain, HashDrain, HashMap, HashSet, PriorityQueue, Queue, SetDrain, Vec
Functionskey_before, read_be, read_le, slice_eq, slice_parts, slice_swap
Vec::append, apply_order, binary_search, binary_search_by, clear, clone, clone_by, contains, dedup, drain, drop, extend_from, filled, from_fn, from_slice, grow, index_exclusive, index_of, index_shared, insert, len, new, pop, position, push, push_be, push_le, remove, remove_at, replace, reserve, retain, reverse, sort, sort_by, sorted_order, sorted_order_keys, swap, swap_remove, take_raw, truncate
Drain::drop, take, total
HashMap::clear, clone, contains, contains_str, drain, entry, find, find_str, get, get_mut, get_mut_str, get_str, index_of, insert, key_at, len, new, remove, remove_slot, remove_str, reserve_one, reslot, swap_remove, unslot, value_at
HashDrain::drop, take_key, take_value, total
HashSet::clear, clone, contains, contains_str, difference, drain, insert, intersection, key_at, len, new, remove, remove_str, swap_remove, union
SetDrain::take, total
Queue::back, clear, drop, front, len, new, pop_back, pop_front, push_back, push_front, shift
PriorityQueue::before, clear, len, new, new_by, peek, pop, push, sift_up

std::text impl/std/text.cb

KindItems
TypesParseError, String, StringView, Utf8Error
Functionsascii_is_alnum, ascii_is_alpha, ascii_is_digit, ascii_is_lower, ascii_is_space, ascii_is_upper, ascii_to_lower, ascii_to_upper, byte_index_from, parse_bytes, space_run_end, sprintfn, utf8_error_at
Utf8Error::text
ParseError::text
String::append, append_str, append_string, append_text, append_view, appendfn, as_bytes, as_view, char_count, chars, clear, clone, code_point, contains, ends_with, eq, eq_str, eq_view, find, from_str, from_utf8, from_view, into_bytes, join, len, less, lines, new, parse, push_ascii, push_code_point, replace, reserve, split, split_whitespace, starts_with, strip_prefix, strip_suffix, to_ascii_lower, to_ascii_upper, trim, trim_end, trim_start, truncate, view
StringView::as_bytes, at, boundary, char_count, chars, code_point, contains, ends_with, eq, eq_str, find, from_utf8, join, len, less, lines, of, parse, ptr, replace, space, split, split_whitespace, starts_with, strip_prefix, strip_suffix, sub, trim, trim_end, trim_start

std::io impl/std/io.cb

KindItems
Functionseprint_str, eprintfn, flush_stdout, print_str, print_string, printfn, read_all, read_all_bytes, read_line
File::printfn
Externs (unsafe)stdin_read, stdout_write

std::fs impl/std/fs.cb

KindItems
TypesFile, FileError, FileInfo, PathKind
Functionscopy_file, file_error_at, file_info, fs_path_op, list_dir, make_dir, make_dir_all, path_canonical, path_extension, path_file_name, path_is_absolute, path_join, path_kind, path_normalize, path_parent, path_stem, read_bytes, read_file, remove_dir, remove_dir_all, remove_file, rename, write_bytes, write_file
FileError::text
File::append, close, create, create_new, drop, flush, len, open, open_rw, position, read, read_line, read_to_end, seek, start, take, write, write_formatted, write_text

std::env impl/std/env.cb

KindItems
TypesArgs
Functionsarg, arg_count, current_dir, current_exe, env_var, home_dir, hostname, set_current_dir, temp_dir
Args::finish, flag, from, help, is_option, new, option, options, spec
Externs (unsafe)arg_bytes

std::memory impl/std/memory.cb

KindItems
TypesBox, Rc, RcBox, Weak
Rc::clone, downgrade, drop, get, new
Weak::clone, drop, upgrade
Box::cells, clone, drop, get, get_mut, into_inner, new

std::sync impl/std/sync.cb

KindItems
TypesChannel, ChannelState, ThreadFailure
Functionscpu_count, failure_text, is_finished, try_join
ChannelState::pop
Channel::close, drop, new, recv, recv_timeout_ms, send, try_recv
ThreadFailure::diagnostic, is_cancelled, text

std::random impl/std/random.cb

KindItems
TypesRng
Functionsos_random_bytes, os_random_u64, os_random_vec, uuid_v4
Rng::below, from_os, new, next_u64, rotl, shuffle, splitmix, unit_f64

std::math impl/std/math.cb

KindItems
TypesBigDivision, BigUint
Functionsabs, acosn, asinn, atann, atan2n, ceiln, clamp, cosn, expn, floorn, gcd, is_finite, is_nan, lnn, log10n, log2n, max, min, pow, powfn, roundn, sinn, sqrtn, tann, truncn
BigUint::add, bit, bit_len, clone, compare, div_rem, eq, from_bytes_be, from_u64, is_zero, less, mod_inverse, mod_pow, mul, parse, rem, shift_left, shift_right, shifted_limbs, sub, text, to_bytes_be, to_bytes_be_padded, trim, zero

std::time impl/std/time.cb

KindItems
TypesDateTime, Weekday
Functionsdays_in_month, floor_div, floor_mod, http_date_words, http_time, iso_byte, iso_digits, iso_from_unix_ms, leap_year, local_offset_seconds, monotonic_ns, month_name, month_number, sleep_ms, unix_ms, unix_ms_from_iso, unix_seconds
DateTime::eq, from_http_date, from_iso, from_unix, less, new, now, read_iso, to_http_date, to_iso, to_local, to_unix, valid, weekday
Weekday::text

std::process impl/std/process.cb

KindItems
TypesChild, ChildInput, ChildOutput, Command, Output
Functionsexitn, interrupt_requested, pipe_op, process_id, watch_interrupts
Command::arg, clear_env, current_dir, encode, env, new, output, output_with_input, put, run, spawn, status
Child::cap, close_input, done, drop, id, kill, kill_tree, op, read_error, read_error_line, read_output, read_output_line, take, take_input, take_output, terminate, try_wait, wait, write_input
ChildInput::drop, write
ChildOutput::drop, read, read_line

std::net impl/std/net.cb

KindItems
TypesDatagram, IpAddr, NetError, SocketAddr, TcpListener, TcpStream, UdpSocket
Functionsnet_addr, net_error, net_num, net_open, resolve
IpAddr::localhost_v4, localhost_v6, parse, text, unspecified_v4, unspecified_v6
SocketAddr::from_record, new, parse, record, text
NetError::text
TcpListener::accept, bind, drop, local_addr, set_accept_timeout_ms
TcpStream::connect, connect_timeout, drop, local_addr, peer_addr, printfn, read, read_exact, read_line, set_keepalive, set_nodelay, set_read_timeout_ms, set_write_timeout_ms, shutdown_write, take, try_clone, write, write_formatted, write_text
UdpSocket::bind, connect, drop, local_addr, recv, recv_from, send, send_to, set_read_timeout_ms, try_clone

std::crypto impl/std/crypto.cb

KindItems
Submodulesdigest, kdf, aead, pk
Functionsblock_len, hash_len
ConstantsBLAKE2B_G

std::crypto::digest impl/std/crypto/digest.cb

KindItems
TypesBlake2b, Hasher, HashKind, Sha1, Sha256, Sha384, Sha512
Functionsblake2b, blake2b_absorb, blake2b_block, crc32, digest_eq, hash, sha1, sha1_absorb, sha1_block, sha256, sha256_absorb, sha256_block, sha384, sha512, sha512_absorb, sha512_block, sha512_bytes
Sha256::finish, new, update
Sha512::final_words, finish, new, update
Sha384::finish, new, update
Sha1::finish, new, update
Hasher::finish, new, update
Blake2b::finish, new, new_keyed, update
ConstantsBLAKE2B_IV, BLAKE2B_SIGMA, SHA256_K, SHA512_K

std::crypto::kdf impl/std/crypto/kdf.cb

KindItems
TypesHmac, HmacSha256
Functionsargon2_g, argon2_get, argon2_h_prime, argon2_load, argon2_p, argon2_put, argon2id, b64_decode_unpadded, b64_unpadded, hash_password, hkdf_expand, hkdf_expand_label, hkdf_expand_label_with, hkdf_expand_with, hkdf_extract, hkdf_extract_with, hkdf_sha256, hkdf_with, hmac, hmac_sha256, pbkdf2, pbkdf2_sha256, verify_password
HmacSha256::finish, new, update
Hmac::finish, new, update

std::crypto::aead impl/std/crypto/aead.cb

KindItems
TypesAesKey, Poly1305
Functionsaead_tag, aes_block, aes_encrypt4, aes_encrypt_block, aes_expand, aes_gcm_open, aes_gcm_seal, aes_mix_columns, aes_pack, aes_rot1, aes_rot2, aes_rot3, aes_sbox_bytes, aes_sbox_ct, aes_shift_rows, aes_transpose8, aes_unpack, aes_xtime, chacha20, chacha20_poly1305_open, chacha20_poly1305_seal, chacha_block, chacha_quarter, crypto_check_lengths, gcm_block, gcm_bytes, gcm_ctr, gcm_j0, gcm_tag, ghash_bmul64, ghash_mul, ghash_rev64, ghash_update, poly1305, poly1305_blocks
Poly1305::block, finish, new, update_last, update_padded
ConstantsAES_ROW0, AES_ROW1_HI, AES_ROW1_LO, AES_ROW2_HI, AES_ROW2_LO, AES_ROW3_HI, AES_ROW3_LO

std::crypto::pk impl/std/crypto/pk.cb

KindItems
TypesBorrowed, DerSpan, EcCurve, Ed, EdExpanded, Field, Jacobian, Mont, RsaPrivateKey, RsaPublicKey
Functionsbiguint_of, curve_b_hex, curve_base, curve_hex, curve_point, der_positive_int, der_push_uint, digest_info_prefix, digest_scalar, ec_affine, ec_mul, ec_private_key, ec_public_key, ec_scalar, ecdh, ecdsa_sign, ecdsa_verify, ed25519_private_key, ed25519_public_key, ed25519_sign, ed25519_verify, ed_add, ed_base, ed_decode, ed_double, ed_encode, ed_expand, ed_identity, ed_mul, ed_order, ed_select, f_add, f_add_raw, f_bit, f_from_be, f_from_mont, f_invert, f_is_zero, f_less, f_nibble, f_select, f_small, f_sub, f_sub_borrow, f_sub_raw, f_to_mont, fe_add, fe_carry, fe_cswap, fe_eq, fe_from_bytes, fe_invert, fe_is_zero, fe_mul, fe_neg, fe_pow_p58, fe_select, fe_sub, fe_to_bytes, field_of, jac_add, jac_double, jac_select, jac_table, jac_twin, jac_twin_bits, le_limbs, limbs7_add, limbs_be, limbs_of, limbs_sub_mod, mgf1, mont_mul, mont_mul_v, mont_new, mont_pow, mont_pow_limbs, p256_ecdh, p256_private_key, p256_public_key, pkcs1v15_encode, probably_prime, random_prime, rsa_generate_key, rsa_open, rsa_private_op, rsa_sign_pkcs1v15, rsa_sign_pss, rsa_verify_pkcs1v15, rsa_verify_pss, sc_bytes, sc_from_hash, sc_mont, small_primes, x25519, x25519_ladder, x25519_private_key, x25519_public_key
RsaPrivateKey::new, public_key
ConstantsED_2D, ED_BX, ED_BY, ED_D, ED_SQRT_M1, FE_MASK

std::x509 impl/std/x509.cb

KindItems
TypesCertError, Certificate, CertKey, Der, EcPrivateKey, EcPublicKey, PrivateKey, SignatureScheme, TrustStore
Functionsascii_lower_eq, certificates_from_pem, check_time, common_name_of, curve_oid_hex, der_big, der_bit_string, der_bytes, der_eq_hex, der_expect, der_oid_hex, der_push_len, der_read, der_text, der_time, der_uint, der_wrap, digits, ec_private_of, hash_oid, key_algorithm, name_matches, pem_block, pem_of, public_key_of, rsa_private_of, same_bytes, sign, signature_algorithm, verify_chain, verify_signature
CertError::text
CertKey::from_der, from_pem, to_der, to_pem
Certificate::matches_host, parse
TrustStore::add, from_pem_file, new, system
PrivateKey::from_der, from_pem, public_key, to_der, to_pem

std::tls impl/std/tls.cb

KindItems
TypesClientHello, Handshake, Inner, Record, RecordKeys, ServerHello, TlsError, TlsStream
Functionscertificate_list, certificate_verify_ok, client_hello, client_hello_parse, derive_secret, finished_data, handshake_record, keys_from, next_message, open_record, push_extension, push_u24, read_record, record_nonce, same_session, scheme_of, seal_record, send_handshake, send_plain_alert, server_hello, server_hello_bytes, server_scheme, suite_hash, suite_key_len, transcript_add, transcript_hash, u16_list, write_all
TlsError::text
TlsStream::client, close, connect, fill, local_addr, peer_addr, peer_certificate, read, read_exact, read_line, server, write, write_text

std::http impl/std/http.cb

KindItems
Submodulesclient, server
TypesBodyLength, Conn, Header, HttpError, Param, Reader, Request, Response, Url
Functionsascii_ieq, ascii_ieq_str, available, body_length, conn_close, conn_read, conn_write, default_port, fill_buffer, find_header, find_param, form_decode, form_encode, form_encode_part, form_part, has_header, hex_digit, host_text, http_line, http_newline_from, is_unreserved, name_eq, names_eq, percent_decode, percent_encode, read_body, read_headers, read_n, read_to_end, reason_phrase
HttpError::text
Url::parse, query_pairs, target, text
Param::new
Header::new
Request::header, new
Response::header, text
ConstantsMAX_HEAD

std::http::client impl/std/http/client.cb

KindItems
TypesHttpClient, IdleConn, StatusLine
Functionsexchange, exchange_on, http_get, http_post, idempotent, method_has_body, not_content_encoding, open_connection, origin_of, parse_status_line, redirect_target, request_bytes
HttpClient::new, send

std::http::server impl/std/http/server.cb

KindItems
TypesHttpConnection, HttpServer
HttpServer::accept, bind, bind_tls, local_addr
HttpConnection::close, peer_addr, request, respond

std::database impl/std/database.cb

KindItems
Submodulespostgres
TypesScram
Functionsattribute, scram_salted_password
Scram::client_first, respond, start, verify

std::database::postgres impl/std/database/postgres.cb

KindItems
TypesMessage, PgColumn, PgConn, PgConnection, PgError, PgParameter, PgReader, PgResult, PgRow, PgServerError, PgSslMode, PgUrl, PgValue
Functionsauthenticate, broken, collect, connect_url, cstr_at, decode_columns, decode_row, decode_server_error, decoded, float_text, frame, open_tcp, pg_available, pg_close, pg_fill, pg_read, pg_read_n, pg_write, push_cstr, read_message, tag_count
PgError::text
PgUrl::parse
PgValue::text
PgRow::get, get_bool, get_bytes, get_f64, get_i64
PgResult::column
PgConnection::close, connect, connect_with, execute, parameter, query, run_script
ConstantsMAX_MESSAGE

std::encoding impl/std/encoding.cb

KindItems
Submodulesjson, hex, base64

std::encoding::json impl/std/encoding/json.cb

KindItems
TypesJson, JsonMember, JsonReader, JsonStr
Functionsjson_digits_end, json_hex4, json_indent, json_number, json_quote, json_quote_bytes, json_space_end, json_string, json_write
Json::as_array, as_bool, as_f64, as_i64, as_object, as_text, at, get, is_null, parse, pretty, push, set, text
JsonReader::digits, number, peek, skip_space, text, value, word
ConstantsMAX_DEPTH

std::encoding::hex impl/std/encoding/hex.cb

KindItems
Functionsfrom_hex, hex_decode, hex_value, to_hex

std::encoding::base64 impl/std/encoding/base64.cb

KindItems
Functionsbase64_decode, base64_decode_bytes, base64_encode, base64_value

std::compress impl/std/compress.cb

KindItems
TypesBitOut, BitReader, BitsStep, BitWriter, CompressError, DeflateScan, Huffman, HuffStep, InflateRun, Token, TokenCounts
Functionsadler32, bits_step, canonical_codes, code_lengths, deflate, deflate_scan, distance_code, fixed_codes, fixed_distances, gunzip, gzip, hash3, huff_step, inflate, inflate_codes, inflate_from, inflate_run, length_code, put_tokens, read_dynamic, token_counts, write_block, zlib_deflate, zlib_inflate
CompressError::text
BitReader::align, take
Huffman::build, decode
BitWriter::flush, put, put_code
ConstantsCHAIN_LIMIT, CLEN_ORDER, DIST_BASE, DIST_EXTRA, HASH_SIZE, LENGTH_BASE, LENGTH_EXTRA, MAX_TOKENS, WINDOW

std::extensions impl/std/extensions.cb

No items: the reference implementation declares nothing here.

One subject at a timeruns
import std::text;
import std::io;

fn main()
{
    String s = "only text and output";
    printf("%s\n", &s);
}
output
only text and output

This guide writes import std; and the short names throughout; each part below says which submodule it lives in.

The intrinsics and the text value type str are not part of std. They belong to the language itself and need no import. This section keeps the two apart, as the specification's tables do: the intrinsics first, then std at a glance, then each part of std in turn.

Types in std

Declared in std
export enum Option<T>
{
    Some(T),
    None
}

export enum Result<T, E>
{
    Ok(T),
    Err(E)
}

export struct AllocError
{
}

export struct Utf8Error
{
    export usize offset;              // where validation failed
}

export enum FileError                 // why reading or writing failed: a file, or standard input
{
    NotFound,
    Denied,
    Io,
    Utf8(Utf8Error)
}

export enum ParseError                // why parse failed
{
    Empty,
    Invalid(usize),                   // the offset where the number stops
    OutOfRange
}

Intrinsics: part of the language

Intrinsics are called like functions; their behaviour is given by rules rather than by a body, and they need no import. Type arguments are written name<T>(…) where the signature needs one. Their names are not reserved: a function or variable of your own with the same name is what the name means, and the intrinsic is used only where there is none.

Values and arithmetic

IntrinsicPurposeSee
drop(x)destroy a resource now§07
wrapping_*, saturating_*, checked_*alternative overflow policies§06
widen<U>, narrow<U>, checked_narrow<U>, narrow_wrapping<U>, reinterpret<U>, to_float<U>, to_int<U>numeric conversions§06
count_ones(x), leading_zeros(x), trailing_zeros(x), rotate_left(x, k), rotate_right(x, k)an integer's bits§06
sizeof<T>(), alignof<T>()layout queries, in bytes§06, §16
min_value<T>(), max_value<T>()a number type's smallest and largest value (a float's finite extremes)§06

Text, slices, checks and faults

IntrinsicPurposeSee
str_len(s), str_byte(s, i), str_ptr(s)a str's byte count, one byte (bounds-checked), and the address of its bytes (safe)§21
slice_len(s)a slice's number of elements (also through a reference)§16
static_assert(c), static_assert(c, "why")reject the program when the constant c is false§18
fault(name), fault(name, msg)stop with the run-time fault diag.name (a dynamic diagnostic, _ for -, such as index_out_of_bounds), with msg if given; for code that implements containers as std does§18

Raw memory and threads

IntrinsicPurposeSee
dangling<T>()a well-aligned non-null address that must never be dereferenced; used for empty containers§21
rawptr_of(&x), reinterpret_ptr<U>(p)obtain and retype raw pointers (safe)§20
reclaim<T>(p), release(p, n), copy_raw(dst, src, n)raw memory as objects (unsafe)§20
allocate(n, align), deallocate(p, n, align)raw allocation (deallocate is unsafe)§20
read_volatile(p), write_volatile(p, v)raw accesses performed exactly as written (unsafe; below)§20
Mutex::new(v), lock(&m)mutual exclusion§19
spawn(f, args…), join(h)threads§19

read_volatile(p) and write_volatile(p, v) are raw accesses that the compiler must perform exactly as written — never merged, dropped or reordered against another volatile access or a call — for storage that changes on its own, such as a device register (D-0121). They take a rawptr<T> to a plain T, inside unsafe; a bitstruct (§16) names the register's fields and Name::from_bits(read_volatile(p)) reads them.

std at a glance

Everything here is an item of std, most of it written in CobaltC, and needs import std; (or a qualified path). The containers and text types have their own parts of this section below; this is the rest, for finding a name quickly.

Option, Result and the errors

FunctionPurposeSee
Option::unwrap(o), Option::expect(o, msg), Result::unwrap(r), Result::expect(r, msg)the value, or the fault diag.unwrap-failed (with msg) when there is none§18
Option::unwrap_or(o, d), Result::unwrap_or(r, d)the value, or the default d when there is none§18
Option::ok_or(o, e)the Option as a Result, Err(e) for None, so ? applies to it§18
Result::ok(r)the Result as an Option: Some of the value, None for an error (which is destroyed)§18
Option::is_some(&o), is_none, Result::is_ok(&r), is_errwhich variant, without taking the value§18
Result::map_err(r, f)convert a Result's error type with f§18
FileError::text(&e), ParseError::text, Utf8Error::texta std error as a sentence for a person, a str§21

Output, input and the program

FunctionPurposeSee
printf("%v", x)write a str, a number, a bool, a String (through &), a StringView, or an enum's variant name to standard output§21
printf(fmt, args…), eprintf(fmt, args…), sprintf(fmt, args…), String::appendf(&mut s, fmt, args…)C-style formatted output, to standard output, to standard error, as a new String or into one, checked when compiled§21
assert(c), assert(c, fmt, args…)fault with diag.assert-failed (and the formatted message) when c is false§18
flush_stdout()write out what printf has buffered, for output watched through a pipe while the program runs§21
read_line()read the next line of standard input as a String§21
read_all(), read_all_bytes()the rest of standard input at once, as a String or as bytes§21
arg_count(), arg(i)the number of program arguments, and argument i as a String§21
read_file(path), write_file(path, text)read a whole file as a String, or replace a file's contents; path and text are StringViews: a literal, or &s[0..$] of a String§21
stdout_write(buf, len), stdin_read(buf, len), arg_bytes(i, buf, len)the externs beneath them: raw bytes to standard output, from standard input, and of argument i (unsafe)§20

Numbers

FunctionPurposeSee
String::parse<T>(&s)read a whole String as a number of type T§21
min(a, b), max(a, b)the smaller or larger of two values of an ordered type (§12)§21
abs(x), pow(x, n)absolute value and power (n a u32) of a number type (§12), checked as the operators they use§21
sqrt(x), floor(x), ceil(x), round(x), trunc(x)an f32 or f64's square root and its roundings to an integral value (round: halfway cases away from zero), exact or correctly rounded, so the same bits everywhere§21
ln(x), exp(x), log2(x), log10(x), sin(x), cos(x), tan(x), atan2(y, x), powf(x, y)an f32 or f64's logarithms, exponential, trigonometry and power, from the platform's maths library (within one unit in the last place; the two operands of atan2 and powf of one type)§21

abs and pow are checked like the operators they use: abs of a signed type's minimum, or a power that does not fit, is diag.arith-overflow. The floating-point functions (std::math) take an f32 or an f64 and give the same type; an integer is converted first, sqrt(to_float<f64>(n)). The logarithms, the exponential, the trigonometric functions and powf come from the platform's maths library, so their last bit may differ between platforms; sqrt and the roundings are exact everywhere.

str

CobaltC has two text types, and the short version is: str is a text constant, String is text you own. Fixed text you write in your program is a str; text you build, receive or keep ownership of is a String (below). The end of this section compares them side by side.

A string literal "…" is a value of type str: a sequence of bytes that is valid UTF-8 by construction, because the literal syntax has no way to write an arbitrary byte. A str is not a reference and not a resource. It behaves exactly like an integer: copying it is free, nothing owns it, nothing frees it, and you can pass it, store it in a struct or a Vec<str>, compare it with ==, and hand it to another thread without any annotation. Its bytes are reached with str_len and str_byte; there is no indexing or slicing syntax. Escapes are \n \r \t \0 \\ \" and \u{…} for a Unicode scalar value; a literal must close on its own line. Adjacent literals are one literal, as in C, so longer text is written a line at a time: "one\n" "two\n" is the single str "one\ntwo\n", and a long printf format can be split the same way.

printf("%v", s) writes a str to standard output. It prints numbers, bool and String too (see "Printing: printf" below). The unsafe block that reaches the stdout_write extern is inside std, so a program that only prints never writes unsafe itself.

str valuesruns
import std;

struct Named
{
    str name;
    i32 n;
}

fn shout(str s)
{
    printf("%v!\n", s);
}

fn main()
{
    str greeting = "hello";
    str again = greeting;             // copied, not moved: greeting stays valid
    assert(str_len(greeting) == 5);
    assert(str_byte(greeting, 0) == 104);
    assert(greeting == again);
    static_assert("caf\u{e9}" != "cafe");     // \u{e9} is é, two bytes
    assert(str_len("caf\u{e9}") == 5);
    auto named = Named { .name = greeting, .n = 1 };
    auto copy = named;                // a struct of plain values is plain
    assert(copy.name == "hello");
    Vec<str> words = Vec::new();
    Vec::push(&mut words, greeting);
    Vec::push(&mut words, "world");
    shout(words[1]);
    printf("%v\n", greeting);
}
output
world!
hello
str_byte is bounds-checkedrejected: diag.index-out-of-bounds
fn main()
{
    str s = "hi";
    u8 b = str_byte(s, 2);            // ✗ diag.index-out-of-bounds (static): a literal index, refuted ahead of time
}

A byte literal b"…" is different: it is an array<u8, N>, exactly the array you would have written by hand, and admits \xNN for any byte value (and only ASCII characters otherwise). Use it where bytes are the honest type, such as data handed to stdout_write or to a validator.

Vec<T>

In std::collections.

A growable array with a raw buffer, a length and a capacity. It is a resource whose destructor destroys each element and frees the buffer. A plain element is copied when read (v[i]); a resource element is only borrowed, or moved out by pop or remove, never copied (so Vec<Vec<i32>> works without any special support).

FunctionSignatureNotes
Vec::new() : Vec<T>T from the expected type or written Vec<i32>::new(); allocates nothing
Vec::len(ref<Vec<T>, shared>) : usize
Vec::push(ref<Vec<T>, exclusive>, T)moves a resource element in; grows 0 → 4 → 8 → …, which relocates elements
Vec::reserve(ref<Vec<T>, exclusive>, usize n)room for n more elements, so the next n pushes do not grow it; the elements are unchanged (D-0129)
Vec::pop(ref<Vec<T>, exclusive>) : Option<T>None when empty; moves a resource element out
Vec::insert(ref<Vec<T>, exclusive>, usize i, T)puts the value at i (i ≤ len), moving the later elements up one
Vec::remove(ref<Vec<T>, exclusive>, usize i) : Ttakes the element at i out, moving the later elements down one
Vec::swap_remove(ref<Vec<T>, exclusive>, usize i) : Ttakes the element at i out and moves the last element into its place: no other element moves, the order is not kept
Vec::retain(ref<Vec<T>, exclusive>, fn(ref<T, shared>) : bool keep)keeps the elements keep accepts, in order, and destroys the rest (D-0181)
Vec::dedup(ref<Vec<T>, exclusive>)removes each element equal to the one before it, so a sorted Vec keeps one of each value (T must be eq, D-0181)
Vec::index_shared(ref<Vec<T>, shared>, usize) : ref<T, shared>bounds-checked: diag.index-out-of-bounds
Vec::index_exclusive(ref<Vec<T>, exclusive>, usize) : ref<T, exclusive>as above, for writing
v[i], &v[i..j]indexing and slicing (§16)v[i] reads through index_shared, writes through index_exclusive; $ is Vec::len(&v)
Vec::clear(ref<Vec<T>, exclusive>)destroys every element, first to last; keeps the buffer for reuse
Vec::truncate(ref<Vec<T>, exclusive>, usize n)keeps the first n elements and destroys the rest, last first; n past the length changes nothing
Vec::clone(ref<Vec<T: clone>, shared>) : Vec<T>a copy, element by element, each by clone (§12): Vec<String>, Vec<Vec<u8>>, …
Vec::clone_by(ref<Vec<T>, shared>, fn(ref<T, shared>) : T) : Vec<T>a copy made by the function, one element at a time, when the copy wanted is not the type's own clone
Vec::from_slice(slice<T: clone, shared>) : Vec<T>a new Vec of the slice's elements, each by clone: Vec::from_slice(&bytes[4..$])
Vec::filled(usize n, T x) : Vec<T>n copies of x by clone: Vec::filled(80, b' '), Vec::filled(3, Vec::filled(4, 0: u8)); T: clone
Vec::from_fn(usize n, fn(usize) : T) : Vec<T>f(0), f(1), …, f(n - 1), any T: a grid is Vec::from_fn(rows, [](usize r) : Vec<u8> { Vec::filled(cols, 0) })
Vec::append(ref<Vec<T>, exclusive>, Vec<T> b)moves b's elements onto the end, in order; b is consumed
Vec::extend_from(ref<Vec<T>, exclusive>, slice<T, shared>)copies a slice's elements onto the end by clone: Vec::extend_from(&mut out, &bytes[0..$]); T: clone
Vec::position(ref<Vec<T>, shared>, fn(ref<T, shared>) : bool) : Option<usize>the first index whose element satisfies the test, or None; a closure may capture the key it looks for
Vec::index_of, Vec::contains(ref<Vec<T>, shared>, ref<T, shared>) : Option<usize> / : boolthe first index equal to the value, or whether there is one; T: eq — a number, bool, str or String (a struct takes position)
Vec::reverse(ref<Vec<T>, exclusive>)the order inverted in place, by swaps; nothing is destroyed
slice_eq(slice<T, shared>, slice<T, shared>) : boolthe same length and equal elements in order; T: eq. == does not compare vectors (they are resources): slice_eq(&a[0..$], &b[0..$])
Vec::push_le(&mut v, x), Vec::push_be(ref<Vec<u8>, exclusive>, T: integer)x's bytes onto a byte Vec, least (le) or most (be) significant first; any width, signed as two's complement (D-0119)
read_le<T>(&v[0..$], at), read_be<T>(slice<u8, shared>, usize) : Tthe T whose bytes start at at; diag.index-out-of-bounds when fewer than sizeof<T>() remain
crc32(&v[0..$])(slice<u8, shared>) : u32CRC-32/IEEE, the checksum of zlib, gzip and PNG; crc32 of "123456789"'s bytes is 0xCBF43926 (§21 of the specification writes the algorithm out)

When you know how many elements are coming, say so first: Vec::reserve(&mut v, n) makes room for n more at once, so filling the Vec allocates and copies once instead of every time it doubles. String::reserve(&mut s, n) does the same for bytes. Neither changes what the program computes: no function reports a capacity, the length and the elements stay as they were, and reserving room that is already there does nothing. Like a push that grows, a reserve that grows moves the elements, so a reference into the old cells is stale afterwards. std's own builders (String::from_str, String::clone, Vec::from_slice) already reserve what they need.

Room in advanceruns
import std;

fn main()
{
    Vec<u32> squares = Vec::new();
    Vec::reserve(&mut squares, 1000);         // one allocation for all of them
    foreach (i in 0..1000: u32)
    {
        Vec::push(&mut squares, i * i);
    }
    assert(Vec::len(&squares) == 1000 && squares[999] == 998001);

    String line = String::new();
    String::reserve(&mut line, 80);
    foreach (i in 0..10)
    {
        String::append(&mut line, "-=-=-=-=");
    }
    assert(String::len(&line) == 80);
}
Using Vecruns
import std;

fn main()
{
    Vec<i32> v = Vec::new();
    foreach (i in 0..9: i32)          // crosses two growth boundaries (4 and 8)
    {
        Vec::push(&mut v, i * i);
    }
    assert(Vec::len(&v) == 9);
    assert(*Vec::index_shared(&v, 8) == 64);
    {
        auto slot = Vec::index_exclusive(&mut v, 0);
        *slot = 100;                  // write through an exclusive element reference
    }                                 // slot must be gone before v is borrowed again
    assert(*Vec::index_shared(&v, 0) == 100);
    match (Vec::pop(&mut v))
    {
        Some(x) :
        {
            assert(x == 64);
        },
        None    :
        {
            assert(false, "unreachable");
        },
    }
    Vec<Vec<i32>> nested = Vec::new();
    Vec::push(&mut nested, v);        // v moves into the outer Vec
    assert(Vec::len(Vec::index_shared(&nested, 0)) == 8);
}                                     // nested is destroyed: each inner Vec, then the outer buffer
Vec boundsrejected: diag.index-out-of-bounds
import std;

fn main()
{
    Vec<i32> v = Vec::new();
    Vec::push(&mut v, 1);
    Vec::index_shared(&v, 5);         // ✗ diag.index-out-of-bounds (dynamic)
}

The helpers of D-0112 name the loops programs kept writing: filling, building by index, joining, searching and reversing. filled and extend_from copy plain elements; from_fn and append are the forms for resources.

Filling, joining, searching, reversingruns
import std;

fn main()
{
    Vec<u8> row = Vec::filled(4, b'.');                                   // "...."
    Vec<Vec<u8>> grid = Vec::from_fn(3, [](usize r) : Vec<u8> { Vec::filled(4, b'.') });
    grid[1][2] = b'#';
    Vec<String> names = Vec::from_fn(3, [](usize i) : String { sprintf("n%d", i) });

    Vec<String> more = Vec::new();
    Vec::push(&mut more, String::from_str("x"));
    Vec::append(&mut names, more);                                        // more is consumed
    Vec::extend_from(&mut row, &grid[1][0..$]);                           // copies bytes
    assert(Vec::len(&names) == 4 && Vec::len(&row) == 8 && row[6] == b'#');

    String key = "n1";
    assert(Vec::index_of(&names, &key) == Some(1));
    assert(Vec::contains(&row, &b'#') && !Vec::contains(&row, &b'?'));
    assert(Vec::position(&names, [key](ref<String, shared> s) : bool { *s == key }) == Some(1));
    Vec::reverse(&mut names);
    assert(String::eq_str(&names[0], "x"));
}

A String in hand can be searched and split directly: String::find, starts_with, ends_with, contains, replace, trim and split are the StringView functions applied to the whole-string view &s[0..$]. And a map keyed by String can be asked with a literal: HashMap::get_str, contains_str and remove_str find the entry a String of the same text would, without building one (D-0113).

Text and maps without extra Stringsruns
import std;

fn main()
{
    String line = "  name = Ada, born = 1815  ";
    assert(String::trim(&line) == "name = Ada, born = 1815");
    assert(String::find(&line, "born") == Some(14) && String::ends_with(&line, "  "));

    HashMap<String, i64> fields = HashMap::new();
    foreach (part in String::split(&line, ","))
    {
        Vec<StringView> kv = StringView::split(part, "=");
        match (StringView::parse<i64>(StringView::trim(kv[1])))
        {
            Ok(n)  : { HashMap::insert(&mut fields, String::from_view(StringView::trim(kv[0])), n); },
            Err(_) : {},
        }
    }
    assert(HashMap::contains_str(&fields, "born") && !HashMap::contains_str(&fields, "name"));
    match (HashMap::get_str(&fields, "born"))
    {
        Some(y) : assert(*y == 1815),
        None    : assert(false),
    }
    assert(HashMap::remove_str(&mut fields, "born") == Some(1815) && HashMap::len(&fields) == 0);
}

Binary data: Vec::push_le and Vec::push_be write an integer's bytes in a fixed order and read_le/read_be read them back, so a file header or a network message never spells a shift by hand; all four are in std::collections. crc32, the CRC-32/IEEE checksum those formats use (D-0119), is in std::crypto beside the hashes, but it only catches accidents: anyone can make data with a chosen CRC, so use a hash or a MAC against tampering (D-0177). A bitstruct (§16) crosses into bytes the same way, through Name::bits(v).

Bytes in a fixed order, and a checksumruns
import std;

fn main()
{
    Vec<u8> header = Vec::new();
    Vec::push_be(&mut header, 0x89504E47: u32);          // a PNG signature's first four bytes, big-endian
    Vec::push_le(&mut header, 640: u16);                  // a little-endian width
    Vec::push_le(&mut header, -1: i8);
    assert(Vec::len(&header) == 7 && header[0] == 0x89 && header[4] == 0x80 && header[5] == 2);
    assert(read_be<u32>(&header[0..$], 0) == 0x89504E47 && read_le<u16>(&header[0..$], 4) == 640);
    assert(read_le<i8>(&header[0..$], 6) == -1);
    String text = "123456789";
    assert(crc32(&String::as_bytes(&text)[0..$]) == 0xCBF43926);
}

A Vec can hold references. While it does, they count as live borrows: the referent cannot be written past them, and a stored &mut is the only way in until the element is popped or the Vec is destroyed.

A Vec of referencesruns
import std;

fn main()
{
    i32 a = 10;
    i32 x = 1;
    Vec<ref<i32, shared>> readers = Vec::new();
    Vec::push(&mut readers, &a);
    assert(*readers[0] == 10);

    Vec<ref<i32, exclusive>> writers = Vec::new();
    Vec::push(&mut writers, &mut x);
    *writers[0] = 7;                                 // write through the stored reference
    Vec::pop(&mut writers);                          // the Vec lets go of it
    assert(x == 7);
    x = 8;                                           // x is free again
}
Writing past a stored borrowrejected: diag.aliasing-conflict
import std;

fn main()
{
    i32 x = 1;
    Vec<ref<i32, exclusive>> writers = Vec::new();
    Vec::push(&mut writers, &mut x);
    x = 5;                            // ✗ diag.aliasing-conflict: the Vec still holds &mut x
}
Tip
Vec::push is written in CobaltC: a resource struct with a rawptr<T>, an unsafe write of the new element at ptr + len, and a private grow that calls allocate, copy_raw and deallocate. Reading it in the specification is the best way to see how the pieces of §07 and §20 fit together.

Random numbers

In std::random.

Rng is a general-purpose pseudo-random generator — for simulations, games, sampling, randomised tests, shuffling — whose sequence the specification fixes, so a program that seeds it with a constant prints the same numbers under both tools and on every machine; seed it with Rng::from_os() for a fresh sequence each run. below(&mut r, n) is unbiased, unit_f64 is in [0, 1). It is not suitable for cryptographic purposes: its output is predictable from a few values, so never use it for keys, tokens, nonces, passwords, or anything an adversary must not guess (D-0120).

FunctionDoes
Rng::new(s), Rng::next_u64(&mut r), Rng::below(&mut r, n), Rng::unit_f64(&mut r)a general-purpose pseudo-random generator (xoshiro256**, fixed by the specification): the same seed gives the same numbers everywhere; below is unbiased in 0 .. n. It is not suitable for cryptographic purposes — keys, tokens, nonces, anything an adversary must not predict (D-0120)
Rng::shuffle(&mut r, &mut v[0..$]), uuid_v4()a slice put in random order (Fisher-Yates; the same seed, the same order everywhere); a version-4 UUID as text, from the operating system's secure source (D-0181)
A reproducible random sequenceruns
import std;

fn main()
{
    Rng r = Rng::new(42);
    assert(Rng::next_u64(&mut r) == 1546998764402558742);      // the specification's check value
    Rng dice = Rng::new(7);
    u64 total = 0;
    foreach (k in 0..100)
    {
        total += Rng::below(&mut dice, 6) + 1;                  // a die: 1 .. 6
    }
    assert(total >= 100 && total <= 600);
    f64 x = Rng::unit_f64(&mut dice);
    assert(x >= 0.0 && x < 1.0);
}

For anything secret, take bytes from the operating system's cryptographically secure source instead (std::random): os_random_bytes(&mut buf[0..$]) fills a slice, os_random_u64() gives one number. These are what keys, session tokens and nonces are made of. Rng::from_os() seeds an Rng from them, which makes its sequence differ between runs but not secret: the sequence is still Rng's. Every system both tools run on has such a source; if one did not, the call would be the fault diag.entropy-unavailable, never a quiet fall back to the clock.

A session tokenruns
import std;

fn main()
{
    Vec<u8> token = Vec::filled(16, 0: u8);
    os_random_bytes(&mut token[0..$]);
    String hex = String::new();
    foreach (byte in &token)
    {
        String::appendf(&mut hex, "%02x", *byte);
    }
    assert(String::len(&hex) == 32);
    Rng shuffle = Rng::from_os();                    // differs each run; not for secrets
    assert(Rng::below(&mut shuffle, 52) < 52);
}

String

In std::text.

A String is a Vec<u8> that is known to be valid UTF-8: owned, heap-allocated text, a resource. String::new() makes an empty one, String::from_str copies a str's bytes (already valid, so nothing is checked), and String::from_utf8 validates bytes that came from anywhere else. Where the declared type already says String, a literal is enough: String s = "text";, a struct literal's String field, an argument whose parameter is a String, and the value a function declared : String returns are each String::from_str of the literal, written short (D-0149), and so is a literal argument whose generic parameter the other arguments make a String: Result::unwrap_or(arg(0), "default"), Vec::push(&mut names, "x") for a Vec<String> (D-0162). That is the one place a literal allocates, and the word String in a declaration nearby says so; a str variable is never made a String without String::from_str, and auto s = "text"; is a str. It changes only through functions that keep it valid UTF-8 — append and appendf add text, push_ascii an ASCII byte, truncate cuts at a character boundary, clear empties it — so the guarantee holds for its whole life. from_utf8 is the trust-transition pattern of §20 in miniature.

FunctionSignature
String::new() : String (empty)
String::from_str(str) : String (copies; cannot fail); a literal where a String is declared is this call already (D-0149)
String::from_utf8(Vec<u8>) : Result<String, Utf8Error>
StringView::from_utf8(slice<u8, shared>) : Result<StringView, Utf8Error> -- the same check, and a view of the bytes instead of a copy
String::len(ref<String, shared>) : usize (bytes)
String::into_bytes(String) : Vec<u8> (gives the bytes back, consuming the string)
String::as_bytes(ref<String, shared>) : ref<Vec<u8>, shared> (read the bytes in place)
String::append(ref<String, exclusive>, T) for any T %v takes: adds what printf("%v", x) would write
String::clone(ref<String, shared>) : String (a copy with the same bytes)
String::reserve(ref<String, exclusive>, usize n): room for n more bytes; the text is unchanged (D-0129)
String::eq, String::eq_str(ref<String, shared>, ref<String, shared>) : bool, (ref<String, shared>, str) : bool: the same text? (a == b does the same for two Strings, §12)
String::less, StringView::less(ref<String, shared>, ref<String, shared>) : bool: byte order, a prefix first, as a < b (§12); Vec::sort(&mut v) sorts text
String::to_ascii_lower, String::to_ascii_upper(ref<String, shared>) : String: a copy with ASCII letters changed; other bytes as they are
String::truncate(ref<String, exclusive>, usize n): keeps the first n bytes; n inside a character faults diag.not-char-boundary
String::push_ascii(ref<String, exclusive>, u8): adds one ASCII byte; one of 128 or more faults diag.not-ascii
String::clear(ref<String, exclusive>) (empties it, keeping the buffer)

For bytes, std has the ASCII tests and conversions a parser needs: ascii_is_digit(b), ascii_is_alpha, ascii_is_alnum, ascii_is_upper, ascii_is_lower, ascii_is_space (a space, tab, newline, carriage return, vertical tab or form feed) take a u8 and give a bool; ascii_to_lower(b) and ascii_to_upper(b) give the u8 with an ASCII letter's case changed. Unicode case is not provided.

ASCII helpersruns
import std;

fn main()
{
    assert(ascii_is_digit(b'7') && !ascii_is_alpha(b'7') && ascii_is_space(b'\t'));
    assert(ascii_to_upper(b'q') == b'Q');
    String shout = String::to_ascii_upper(&String::from_str("caf\u{e9} au lait"));
    assert(String::eq_str(&shout, "CAF\u{e9} AU LAIT"));
}
Validating bytes into a Stringruns
import std;

fn main()
{
    String owned = "caf\u{e9}";   // a str's bytes, copied; no validation needed
    assert(String::len(&owned) == 5);                // 5 bytes: é is two of them
    assert(String::char_count(&owned) == 4);         // but 4 characters
    assert(Vec::len(&String::chars(&owned)) == 4);   // each a view of its bytes

    Vec<u8> good = Vec::new();
    Vec::push(&mut good, 226);        // the three bytes of U+20AC, the euro sign
    Vec::push(&mut good, 130);
    Vec::push(&mut good, 172);
    String s = match (String::from_utf8(good))     // good moves in
    {
        Ok(v)  : v,
        Err(_) :
        {
            return;
        },
    };
    assert(String::len(&s) == 3);
    Vec<u8> back = String::into_bytes(s);          // and out again
    assert(Vec::len(&back) == 3);

    Vec<u8> bad = Vec::new();
    Vec::push(&mut bad, 226);         // a truncated sequence
    Vec::push(&mut bad, 130);
    match (String::from_utf8(bad))
    {
        Ok(_)  :
        {
            assert(false, "unreachable");
        },
        Err(e) :
        {
            assert(e.offset == 0);
        },
    }
}

StringView: part of a String, without copying

In std::text.

&s[i..j] on a String gives a StringView: a read-only view of the bytes i to j, with no copy. Positions are byte positions, $ is the length, and both ends must fall on a character boundary (cutting through a multi-byte character is diag.not-char-boundary). A view prints with %s, compares with == to another view or a str, and can be sliced again, &v[1..$]. It borrows its String exactly as a slice borrows a Vec: while the view lives, the String cannot be changed, and a view cannot outlive it. String::from_view(v) copies it into a String of its own when it must.

FunctionResult
StringView::len(v)its length in bytes
StringView::of(t)a view of a str t; a literal where a view is expected is one already (D-0134)
StringView::as_bytes(v)the view's bytes as a slice<u8, shared>, not a copy: what a hash, a MAC or a socket write takes (D-0163)
StringView::find(v, t)Option<usize>: where the text of the view t first occurs
StringView::starts_with(v, t), ends_withbool
StringView::trim(v), trim_start, trim_endthe view without ASCII white space at its ends
StringView::split(v, ",")a Vec<StringView> of the parts, each a view of the same String
StringView::chars(v)a Vec<StringView> of its characters, in order, one view each (D-0128)
StringView::char_count(v)how many characters it holds (len counts bytes; D-0193)
StringView::contains(v, t)bool: whether the text of t occurs in v (D-0139)
StringView::replace(v, from, to)a new String: v with every occurrence of from replaced by to, left to right, no overlap; an empty from occurs nowhere
StringView::join(&parts[0..$], sep), String::join(&strings[0..$], sep)a new String: the parts with sep between each two; the inverse of split
StringView::parse<T>(v)a number, as String::parse
String::from_view(v), String::append(&mut s, v)the text copied into a String
String::find(&s, t), starts_with, ends_with, contains, replace, trim, trim_start, trim_end, split, chars, char_count, as_viewthe same, straight from a String: each is the StringView:: function applied to &s[0..$], the whole-string view (D-0113)

Text that a function only reads is a StringView throughout std: a file's path, the text to search for, the text to write (D-0134). A String passes its whole-string view, &s[0..$]. A literal passes itself: a str literal where a StringView is expected is a view of it, and since a str never ends, neither does the view. A str held in a variable is not a literal; view it with StringView::of(t). (split's separator is the one str: split returns views of its first argument, and a function returning views may borrow only one thing, §10.)

Views of a String, of a literal, of a strruns
import std;

fn main()
{
    String text = "the cat sat on the mat";
    String word = "sat";
    assert(String::find(&text, &word[0..$]) == Some(8));   // a String to search for
    assert(String::find(&text, "mat") == Some(19));        // a literal: already a view
    str w = "on";
    assert(String::find(&text, StringView::of(w)) == Some(12));
    StringView label = "fixed";                            // a view of a literal, for the whole run
    printf("%s %d\n", label, StringView::len(label));
}
output
fixed 5
Reading fields out of a lineruns
import std;

// The first word of a line: a view into the line itself.
fn first_word(ref<String, shared> s) : StringView
{
    StringView t = StringView::trim(&(*s)[0..$]);
    match (StringView::find(t, " "))
    {
        Some(i) : &t[0..i],
        None    : t,
    }
}

fn main()
{
    String line = "  set width=80, height = 24  ";
    assert(first_word(&line) == "set");

    StringView args = StringView::trim(&line[6..$]);     // "width=80, height = 24"
    i64 area = 1;
    foreach (field in StringView::split(args, ","))
    {
        match (StringView::find(field, "="))
        {
            Some(eq) :
            {
                StringView value = StringView::trim(&field[eq + 1..$]);
                match (StringView::parse<i64>(value))
                {
                    Ok(n)  : { area *= n; },
                    Err(_) : { assert(false, "unreachable"); },
                }
            },
            None     : { assert(false, "unreachable"); },
        }
    }
    assert(area == 1920);
    printf("%s -> %d\n", args, area);
}
output
width=80, height = 24 -> 1920

Three questions come up so often that they have their own names (D-0139). contains is find with a yes-or-no answer. replace makes a new String with every occurrence of one text replaced by another, scanning left to right so occurrences never overlap; the empty text occurs nowhere, so replacing it changes nothing. join is the inverse of split: the parts with a separator between each two, from a slice of views (what split gives) or a slice of Strings (what a program builds). Each takes its texts as StringViews, so a literal, a String's &s[0..$] and another view all serve.

contains, replace and joinruns
import std;

fn main()
{
    String path = "/usr/local/lib/cobaltc";
    assert(String::contains(&path, "/local/") && !String::contains(&path, "bin"));

    String windows = String::replace(&path, "/", "\\");          // a new String; path unchanged
    printf("%s\n", &windows);
    String aaa = "aaa";
    printf("%s\n", &String::replace(&aaa, "aa", "b"));            // left to right, no overlap: "ba"

    Vec<StringView> parts = String::split(&path, "/");          // "", "usr", "local", "lib", "cobaltc"
    printf("%s\n", &StringView::join(&parts[1..$], " > "));
    assert(StringView::join(&parts[0..$], "/") == path);        // join undoes split

    Vec<String> names = Vec::new();
    Vec::push(&mut names, String::from_str("ada"));
    Vec::push(&mut names, String::from_str("grace"));
    printf("%s\n", &String::join(&names[0..$], ", "));
}
output
\usr\local\lib\cobaltc
ba
usr > local > lib > cobaltc
ada, grace
The viewed String cannot changerejected: diag.aliasing-conflict
import std;

fn main()
{
    String name = "ada";
    StringView v = &name[0..$];
    String::push_ascii(&mut name, b'!');   // ✗ diag.aliasing-conflict (static): v still views name
    assert(v == "ada");
}

Going through the characters

foreach does not take a String itself, and there is no char type; String::chars(&s) gives the characters instead: a Vec<StringView>, one view per character, in order. A character is one to four bytes of UTF-8 (é is two, most emoji four), so Vec::len of it counts characters where String::len counts bytes. Each view is text like any other: it prints, compares with == to a str, is matched by text patterns, and appends to a String; and like any view it borrows the String while it is in use. StringView::chars(v) does the same for a view, such as a word from split. Building the Vec is one allocation; there is no form that visits the characters without it, because foreach walks only the collections the language defines and there is no iterator protocol for a library to plug into (D-0128 explains). Where that matters, walk String::as_bytes(&s) instead.

The characters of a Stringruns
import std;

fn main()
{
    String s = "caf\u{e9} \u{1F600}!";
    foreach (c in String::chars(&s))
    {
        printf("[%v]", c);
    }
    printf("\n%v characters in %v bytes\n", Vec::len(&String::chars(&s)), String::len(&s));

    // Reversed by characters, so the two-byte é and the four-byte emoji stay whole.
    Vec<StringView> cs = String::chars(&s);
    Vec::reverse(&mut cs);
    String back = String::new();
    foreach (c in &cs)
    {
        String::append(&mut back, *c);
    }
    printf("%v\n", &back);

    // Positions and text patterns work on each character.
    foreach (i, c in String::chars(&s))
    {
        match (c)
        {
            "\u{e9}" : printf("\u{e9} is character %v\n", i),
            "!"      : printf("! is character %v\n", i),
            _        : {},
        }
    }
}
output
[c][a][f][é][ ][😀][!]
7 characters in 11 bytes
!😀 éfac
é is character 3
! is character 6

Lines, words, prefixes and code points (D-0181). lines splits text at , dropping a before it and giving no empty line after a final newline; split_whitespace gives the words between runs of ASCII whitespace, none empty; strip_prefix(v, t) and strip_suffix give the rest of the text as Some(view) when it begins (or ends) with t, and None otherwise, which is how --name=value is read. code_point(v) is the number of the first character (None for empty text), and String::push_code_point(&mut s, cp) appends one as UTF-8, turning a surrogate or a value above U+10FFFF into U+FFFD so the String stays valid. Each exists on a String too (String::lines(&s), …). String::parse<bool> reads exactly true or false.

Lines, words, prefixes and code pointsruns
import std;

fn main()
{
    String config = "name = demo\r\nlevel = 3\n\n# a comment\nflags = loud fast\n";
    foreach (line in String::lines(&config))
    {
        if (StringView::starts_with(line, "#") || StringView::len(line) == 0)
        {
            continue;
        }
        Vec<StringView> words = StringView::split_whitespace(line);
        printf("%v -> %v word(s)\n", words[0], Vec::len(&words) - 2);
    }
    foreach (a in ["--level=7", "--quiet", "file.txt"])
    {
        match (StringView::strip_prefix(StringView::of(a), "--level="))
        {
            Some(v) : printf("level %v\n", Result::unwrap_or(StringView::parse<u32>(v), 0)),
            None    : printf("other: %s\n", a),
        }
    }
    String s = "é";
    printf("U+%04X\n", Option::unwrap_or(String::code_point(&s), 0));
    String t = String::new();
    String::push_code_point(&mut t, 0x1F600);
    printf("%v has %v bytes\n", &t, String::len(&t));
}
output
name -> 1 word(s)
level -> 1 word(s)
flags -> 2 word(s)
level 7
other: --quiet
other: file.txt
U+00E9
😀 has 4 bytes

Which text type?

Rust programmers will recognise the pair, and C programmers may wonder why there are two text types at all. The reason is that a constant should not have to be managed. If "hi" were a String, every literal would be a heap allocation with an owner: it would be moved, destroyed at scope end, and allocated again on every pass through a loop, all for text that never changes. So a literal is a str, which behaves like the number 5. String is kept for text that really does need an owner.

strStringb"…"
What it isa text constantowned textraw bytes
Written as"hello"String::from_str("hello"), or "hello" where a String is declared (D-0149); String::from_utf8(bytes)b"hello"
TypestrStringarray<u8, N>
Valid UTF-8yes, by the literal grammaryes, by construction or validationnot guaranteed
Resource (moved, destroyed)no: copied like an integeryesno
Allocatesneveryesno (an ordinary array)
Passed toprintf, ==, str_len, str_byte; any StringView parameterprintf (as &s), String::len, String::into_bytes; a StringView parameter as &s[0..$]stdout_write and other raw-byte code
Rules of thumb
  • Text written in your source code: use str. Pass it, store it in structs and Vec<str>, and print it freely.
  • Text built at run time, or text you need to own: use String, created explicitly from a str with String::from_str.
  • Part of a String, to read, compare, split or parse without copying: a StringView, &s[i..j].
  • A function that only reads text, from anywhere, takes a StringView, as std's do (D-0134): a caller passes a literal as it is, f("key"), and a String as f(&s[0..$]); nothing is copied.
  • Bytes from outside the program (a file, a socket, FFI): they stay Vec<u8> until String::from_utf8 accepts them.
  • Bytes that are not text (a protocol header, data for stdout_write): use b"…".

Unicode. All three are UTF-8 bytes, and std works on them as bytes: lengths and indices count bytes, == and the orderings compare bytes, and String::chars hands out the code points when you need them. std has no Unicode tables (D-0226): case mapping is ASCII only (String::to_ascii_lower), there is no normalization, so "é" and "e\u{301}" are different strings, and no splitting into user-perceived characters. A program that needs those brings its own tables.

The two text types never convert into each other silently (§12's no-implicit-conversion rule). Going from a str value to a String is an explicit call that makes a copy, and nothing turns a String back into a str: a str is always a literal's value, so it cannot refer to bytes that something else owns. A literal is different in kind: like the 5 in u64 x = 5;, it has no type until its position gives it one, so String s = "hi"; is the literal typed as a String (String::from_str of it, D-0149) and StringView v = "hi"; as a view, not a value converted; the same holds for a constructor's payload, so Option<StringView> o = Some("hi"); is a view too (D-0225). A str variable in the same place is still a type error. A String compares with == and != to a str or a StringView, either way round, by its bytes (name == "sha256", D-0164); the orderings < … >= stay within one text type.

Printing: printf

In std::io (printf, eprintf, flush_stdout) and std::text (sprintf, String::appendf).

printf is the one way a program writes output. It is C's, with one difference that matters: the format is checked against its arguments when the program is compiled. The format must be a string literal; each % specifier must match its argument's type and there must be exactly one argument per specifier, or the program is rejected (diag.type-mismatch, or diag.format-invalid for a malformed specifier). So a printf can never misprint or crash at run time. sprintf(fmt, args…) returns the same text as a new String (C's sprintf without a buffer to overflow), and String::appendf(&mut s, fmt, args…) adds it to an existing one. eprintf(fmt, args…) writes the same text to standard error, for messages that are not the program's output. There is no println; end a line with \n.

Standard output is buffered, so printing line by line is fast. The text reaches the screen, a pipe or a file in pieces, but never later than the next eprintf, the next read of standard input, the start of a child that shares standard output (Command::status), and the program's end, however it ends. On a terminal each line also appears as soon as it is complete. So the two streams keep the program's order, and a prompt is shown before the program waits for the answer. Call flush_stdout() only when standard output goes to a pipe or a file and someone watches it while the program runs: a progress line, a log.

SpecifierTakesPrints
%d %i %uany integer typedecimal; no %ld/%lld/%zu needed, the type is known
%x %X %o %bany integer typehex, octal, binary (# adds 0x, 0X, 0o, 0b)
%f %e %g (and %F %E %G)f32, f64fixed, exponent, or shortest of the two, 6 digits by default
%sstr, &String, StringView, boolthe text
%vany of the abovethe value as CobaltC writes it (below)
%%nothinga %

Flags, a width and a precision work as in C: %-8s pads on the right, %05d pads with zeros, %+d always shows a sign, %.2f rounds to two decimals, %8.3f does both. Widths count characters, so text in any language lines up; to size a column to its text, count the characters too, Vec::len(&String::chars(&s)), since String::len counts bytes. There is no %c. A * in place of the width or the precision takes it from the next argument, which must be a usize: printf("%*d", w, n), printf("%.*s", n, &s).

Computed widthsruns
import std;

fn main()
{
    usize w = 6;
    printf("[%*d]\n", w, 42);
    printf("[%-*s]\n", w, "ab");
    printf("[%.*f]\n", 2: usize, 3.14159);
}
output
[    42]
[ab    ]
[3.14]
printf, sprintf and appendfruns
import std;

fn main()
{
    String item = "widget";
    u32 qty = 12;
    f64 price = 3.5;
    printf("%-8s|%5d|%8.2f|%#06x\n", &item, qty, price, 255);
    printf("%.1f%% done, %e left\n", 99.5, 0.00042);
    String line = sprintf("%s x %d", &item, qty);
    String::appendf(&mut line, " = %.2f", price * 12.0);
    printf("%v\n", &line);
}
output
widget  |   12|    3.50|0x00ff
99.5% done, 4.200000e-04 left
widget x 12 = 42.00
Checked when compiledrejected: diag.type-mismatch
import std;

fn main()
{
    printf("%d items\n", "twelve");   // ✗ diag.type-mismatch: %d takes an integer
}
Errors go to standard error: coby greet.cb with no argumentruns
import std;

fn main() : u8
{
    if (arg_count() != 1)
    {
        eprintf("usage: greet NAME\n");   // to standard error, not mixed into the output
        return 2;
    }
    match (arg(0))
    {
        Ok(name) :
        {
            printf("hello, %s\n", &name);
        },
        Err(e)   :
        {
            eprintf("greet: the name is not UTF-8 (byte %d)\n", e.offset);
            return 1;
        },
    }
    0
}
output

%v: any value

%v is not C's: it writes a value the way CobaltC writes it, whatever its type, so printf("%v\n", x) needs no thought about x. It takes any of the types below, and nothing else: a struct or a Vec is a static type error, so printing a composite value means printing its parts; an enum prints its variant's name. It takes a width and -, but no precision.

ArgumentWritten asExamples
strits bytesprintf("%v", "hi") → hi
any integer typedecimal, - if negative-42, 18446744073709551615
f32, f64the shortest digits that read back as the same value0.1, 1.0, 0.3333333333333333, 1.0e21, 1.0e-7, NaN, inf
booltrue or false
ref<String, shared>the string's bytesprintf("%v", &s)
StringViewits textprintf("%v", word)
an enum, or a reference to oneits variant's name (not its payload)Some, Mul, NotFound

A float prints as few digits as identify it exactly: 0.1 prints as 0.1, even though the stored value is slightly more than a tenth, and reading the printed text back gives the same value. A whole number keeps a .0, so floats and integers look different. Between 0.000001 and 1021 the number is written out in full; beyond that range it gets an exponent. An f32 chooses its digits as an f32, so 0.1: f32 also prints 0.1. This is the rule JavaScript and Python use.

%v's argument type is checked where the printf is, and a generic function can print its own T. The check happens for each type the function is used with, so show below accepts numbers and text, and would be rejected for a struct.

Printing valuesruns
import std;

fn show<T>(T x)                       // T is checked where show is used
{
    printf("%v\n", x);
}

fn main()
{
    show(-42);
    show(max_value<u64>());
    show(true);
    show(0.1);
    show(1.0);
    show(1.0 / 3.0);
    show(0.1: f32);
    show(1000000000000000000000.0);
    f64 zero = 0.0;
    show(zero / zero);

    String name = "owned text";
    printf("%v", &name);                     // a String through a reference: name stays usable
    printf("\n");
    printf("x = %v, y = %v\n", 3, 4.5);
}
output
-42
18446744073709551615
true
0.1
1.0
0.3333333333333333
0.1
1.0e21
NaN
owned text
x = 3, y = 4.5
A String prints through a referencerejected: diag.type-mismatch
import std;

fn main()
{
    String s = "text";
    printf("%v", s);                         // ✗ diag.type-mismatch (static): a String prints through &s
}
Composite values are not printablerejected: diag.type-mismatch
import std;

struct Point
{
    i32 x;
    i32 y;
}

fn main()
{
    printf("%v", Point { .x = 1, .y = 2 });  // ✗ diag.type-mismatch (static): print the fields instead
}

Raw bytes are not text, so printf does not take a u8 as a character or a Vec<u8>: a u8 prints as a number. To write bytes, use the stdout_write extern the way std does, around an unsafe block. put writes one byte and print_bytes a whole Vec<u8>:

Printing bytesruns
import std;

// Write one byte to standard output.
fn put(u8 b)
{
    auto buf = [b];                   // an array<u8, 1>
    rawptr<u8> p = reinterpret_ptr<u8>(rawptr_of(&buf));
    unsafe
    {
        stdout_write(p, 1);           // trusted: p points at one valid byte
    }
}

fn print_bytes(ref<Vec<u8>, shared> v)
{
    foreach (b in v)
    {
        put(*b);
    }
}

fn main()
{
    put(b'a');                        // the byte of `a`
    printf("%v", b'a');                      // a u8 prints as a number: 97
    put(b'\n');
    Vec<u8> v = Vec::new();
    Vec::push(&mut v, b'o');
    Vec::push(&mut v, b'k');
    Vec::push(&mut v, b'\n');
    print_bytes(&v);
}
output
a97
ok
A str value is not a Stringrejected: diag.type-mismatch
import std;

fn main()
{
    str t = "hi";
    String s = t;                     // ✗ diag.type-mismatch (static): t is a str value;
                                      // String::from_str(t) makes the String
}
The bridge is explicit for a value, implied for a literalruns
import std;

fn main()
{
    String s = "hi";                  // ✓ a literal where a String is declared is String::from_str of it (D-0149)
    str t = "there";
    String u = String::from_str(t);   // ✓ the explicit, allocating bridge for a str value
    assert(String::len(&s) == 2 && String::len(&u) == 5);
}

Reading input

In std::io.

read_line() reads the next line of standard input. It returns a Result<Option<String>, FileError>:

ResultMeaning
Ok(Some(line))the next line, without its \n (a final \r\n is removed whole); a last line with no \n counts too
Ok(None)the input has ended (and every later call says so again)
Err(Utf8(e))the line is not valid UTF-8; e.offset is the bad byte's position in the line, and the next call reads the line after it
Err(Io)the input could not be read

A line ends at \n, and a \r just before it is removed with it, so a line typed on Windows reads the same as one typed on Linux. The line is a String that has already been checked, so a program that reads never writes unsafe, and ? passes a FileError up to the caller: the same error a file gives (D-0134), whose NotFound and Denied never come from standard input, so a match that names Io and Utf8 ends with _. Underneath is the stdin_read extern, the counterpart of stdout_write, which you can call yourself inside unsafe to read raw bytes.

Both coby and cobc read standard input, from the terminal or from a pipe, for example printf 'ada\nlin\n' | coby greet.cb. Standard output is flushed before each read, so a prompt printed with printf appears before the program waits.

Reading lines
import std;

// Greets every line of input; blank lines are skipped.
fn greet_all() : Result<usize, FileError>
{
    usize count = 0;
    while (Some(name) = read_line()?) // None: the end of input; an error goes back to main
    {
        if (String::len(&name) > 0)
        {
            printf("hello, %v\n", &name);
            count += 1;
        }
    }
    Ok(count)
}

fn main()
{
    match (greet_all())
    {
        Ok(n)  :
        {
            printf("%v greeted\n", n);
        },
        Err(e) :
        {
            match (e)
            {
                Io      :
                {
                    printf("could not read input\n");
                },
                Utf8(u) :
                {
                    printf("not UTF-8 at byte %v\n", u.offset);
                },
            }
        },
    }
}

A program that needs all of its input before it can start, to sort it or parse it as one document, takes it at once with read_all() (a String, Err(Utf8(e)) when it is not text) or read_all_bytes() (a Vec<u8>, any bytes). Each gives what is left after any read_line calls, line ends exactly as they came, and an empty result at the end of input.

All of the input at once
import std;

fn before(ref<StringView, shared> a, ref<StringView, shared> b) : bool
{
    StringView::less(*a, *b)
}

// Sorts the lines of its input: `sort` in a few lines.
fn main() : u8
{
    String all = match (read_all())
    {
        Ok(s)  : s,
        Err(e) :
        {
            eprintf("sort: %s\n", FileError::text(&e));
            return 1;
        },
    };
    Vec<StringView> lines = String::split(&all, "\n");
    if (Vec::len(&lines) > 0 && StringView::len(lines[Vec::len(&lines) - 1]) == 0)
    {
        Vec::pop(&mut lines);                       // the empty piece after the last line end
    }
    Vec::sort_by(&mut lines, before);
    foreach (l in &lines)
    {
        printf("%s\n", *l);
    }
    0
}

Program arguments

In std::env.

arg_count() is the number of arguments the program was started with, and arg(i) is argument i as a Result<String, Utf8Error>. Argument 0 is the first one after the program: coby prog.cb a b and cobc --run prog.cb a b both give a and b, and the program's own name is not an argument. Each argument is checked on its own, so one that is not UTF-8 is an Err without affecting the others. arg(i) with i ≥ arg_count() faults diag.index-out-of-bounds, as indexing a Vec does. Both tools pass arguments. Underneath is the arg_bytes extern, which you can call inside unsafe for an argument's raw bytes.

Listing the arguments: coby args.cb one tworuns
import std;

fn main()
{
    foreach (i in 0..arg_count())
    {
        match (arg(i))
        {
            Ok(a)  : printf("%v: %v\n", i, &a),
            Err(e) : printf("%v: not UTF-8 at byte %v\n", i, e.offset),
        }
    }
}
output
0: one
1: two

Command-line options: Args

In std::env.

A tool declares its options as it reads them (D-0184): Args::flag(&mut a, "loud", "l", help) says whether --loud or -l was given and takes every such argument; Args::option(&mut a, "times", "n", "N", help) gives the value of --times=3, --times 3 or -n 3 (the last one, or None), and Args::options every value of a repeated option; each call also adds a line to Args::help(&a). Args::finish(&mut a) then gives what is left — the positional arguments, in order, with everything after -- among them — or an Err naming the first option nobody declared, or an option missing its value, for the program to print. A lone - is an argument. Args::new(usage) reads the program's own arguments; Args::from(items, usage) takes any Vec<String>.

A tool with optionsruns
import std;

fn main() : u8
{
    Args a = Result::unwrap(Args::new("greet [options] NAME..."));
    bool loud = Args::flag(&mut a, "loud", "l", "shout");
    bool help = Args::flag(&mut a, "help", "h", "show this help");
    usize times = Result::unwrap_or(StringView::parse<usize>(String::as_view(&Option::unwrap_or(Args::option(&mut a, "times", "n", "N", "how many times"), String::from_str("1")))), 1);
    if (help)
    {
        printf("%v", &Args::help(&a));
        return 0;
    }
    Vec<String> names = match (Args::finish(&mut a))
    {
        Ok(rest) : rest,
        Err(m)   :
        {
            eprintf("greet: %v\n", &m);
            return 2;
        },
    };
    foreach (i in 0..times)
    {
        foreach (n in &names)
        {
            printf("%v, %v!\n", if (loud) { "HELLO" } else { "hello" }, n);
        }
    }
    0
}
output
HELLO, world!
HELLO, world!

Run with --help the same program prints:

Its help text
Usage: greet [options] NAME...

Options:
  -l, --loud     shout
  -h, --help     show this help
  -n, --times N  how many times

Numbers and text

In std::text.

String::append(&mut s, x) adds to s exactly what printf("%v", x) would write, for the same types: a str, any integer, f32/f64, bool, a StringView, or another String through &. So a line can be built piece by piece and printed, or kept, once. String::parse<T>(&s) goes the other way: it reads the whole String as a number of type T, any integer type, f32 or f64, and returns a Result<T, ParseError>.

ResultMeaning
Ok(v)the whole text is a number, and it fits in T
Err(Empty)the text is empty
Err(Invalid(k))byte k cannot continue a number ("12x4" → 2), or the text ends too early ("-" → 1)
Err(OutOfRange)a number, but not one T can hold ("300" as u8)

Parsing is strict: an optional + or -, then digits, and for a float an optional fraction and exponent (2.5e-3), or inf or NaN. No spaces anywhere; read_line removes a Windows line’s \r\n whole, so its lines parse, but a \r from any other source is Invalid. Floats are rounded correctly, and every float %v writes parses back to the same value. To take a line apart first, split, trim or search it as views (String::split, trim, find, the StringView functions above) and parse a piece with StringView::parse<T>.

Parsing arguments, building a line: coby sum.cb 1.5 2 0.25runs
import std;

// Adds up its arguments: `coby sum.cb 1.5 2 x` reports the `x`.
fn main() : u8
{
    f64 total = 0.0;
    foreach (i in 0..arg_count())
    {
        String a = Result::unwrap_or(arg(i), String::new());
        match (String::parse<f64>(&a))
        {
            Ok(v)  : total += v,
            Err(_) :
            {
                String msg = String::new();
                String::append(&mut msg, "argument ");
                String::append(&mut msg, i);
                String::append(&mut msg, " is not a number: ");
                String::append(&mut msg, &a);
                String::append(&mut msg, "\n");
                printf("%v", &msg);
                return 1;
            },
        }
    }
    String line = String::new();
    String::append(&mut line, "total ");
    String::append(&mut line, total);
    String::append(&mut line, "\n");
    printf("%v", &line);
    0
}
output
total 3.75

Numeric helpers in std::math (D-0181): clamp(x, lo, hi) holds a value within bounds; gcd(a, b) is the greatest common divisor, never negative; is_nan(x) and is_finite(x) ask about a float (an integer is always finite); and asin, acos and atan join the floating-point functions. Vec::sort orders f32 and f64 too, with every NaN last.

Files

In std::fs.

read_file(path) reads a whole file as a String; write_file(path, text) makes the file hold exactly text, creating it or replacing what it held. Both take the path, and the text, as a StringView (D-0134): a literal, read_file("notes.txt"), or a String's whole-string view, read_file(&path[0..$]) for a path that came from arg. They return a Result whose error is a FileError: NotFound, Denied, Utf8(e) for a file that is not valid UTF-8 (with the offset of the first bad byte), or Io for anything else. A relative path is relative to the directory the program runs in. Both tools read and write files. FileError::text(&e) is the error as a sentence for a person ("not found", "permission denied", …), as ParseError::text and Utf8Error::text are for theirs (D-0117); %v of an error prints its variant's name instead.

FunctionSignature
read_file(StringView path) : Result<String, FileError>
write_file(StringView path, StringView text) : Result<void, FileError>

These two handle the whole file at once; File, below, reads and writes one in pieces.

Writing a file and reading it backruns
import std;

fn main() : u8
{
    String text = "first line\nsecond line\n";
    if (Err(_) = write_file("notes.txt", &text[0..$]))
    {
        printf("cannot write notes.txt\n");
        return 1;
    }
    String back = Result::unwrap_or(read_file("notes.txt"), String::new());
    printf("%v", &back);
    match (read_file("no-such-file.txt"))
    {
        Ok(_)         : printf("?\n"),
        Err(NotFound) : printf("no-such-file.txt: not found\n"),   // a pattern inside a pattern
        Err(_)        : printf("no-such-file.txt: cannot read it\n"),
    }
    0
}
output
first line
second line
no-such-file.txt: not found

Open files: File

In std::fs.

A File is an open file, for what read_file cannot do: bytes that are not text, files too large to hold, appending to a log. It is a resource, like Vec: it has one owner, can move to another thread, and its destructor closes it. close closes it too, and reports whether that worked (for a written file, whether the bytes were stored); after close the File is gone, so it cannot be used again. Every operation returns a Result whose error is a FileError.

FunctionDoes
File::open(path)open for reading; the file must exist (path a StringView, as for read_file)
File::create(path)open for writing, empty: created, or its contents discarded
File::append(path)open for writing at the end, created if missing
File::open_rw(path)open for reading and writing, created if missing, contents kept
File::read_line(&mut f)the next line without its \n (nor a \r before it), None at the end (as read_line)
File::read(&mut f, &mut buf, max)append up to max bytes to a Vec<u8>; how many, 0 at the end
File::read_to_end(&mut f, &mut buf)append the rest of the file to a Vec<u8>
File::write(&mut f, &bytes[i..j]), File::write_text(&mut f, text)write all of a slice of bytes, or text (a StringView: a literal, or &s[0..$])
File::printf(&mut f, "format", …)write formatted text, as printf formats it
File::seek(&mut f, pos), File::len(&f)move to byte pos (a u64); the length in bytes
File::create_new(path)Ok(Some(file)) on a new file, Ok(None) when something is already there: a lock or pid file that two programs race for (D-0181)
File::flush(&mut f), File::position(&f)make everything written durable (fsync) — flush, then rename, for a file that must survive a crash; the byte the next read or write starts at (D-0181)
File::close(f)close it and say whether that worked
read_bytes(path), write_bytes(path, &bytes[i..j])read_file and write_file for bytes: a Vec<u8>

Reading is buffered, so read_line is quick, and File::read fills up to what was asked for. Writing is buffered too (D-0201), so writing a file line by line is fast. The program itself always sees what it wrote: the bytes reach the file before the same file's next operation, before any other file operation of the program (opening, read_file, file_info, removing, renaming), before a child process starts, and when the program ends, however it ends. Another program watching the file while yours runs sees them at those points; File::flush writes them out at once.

Writing a log, reading it line by line, and seekingruns
import std;

fn main()
{
    File log = Result::expect(File::create("app.log"), "cannot create the log");
    foreach (i in 1..4)
    {
        _ = File::printf(&mut log, "event %v\n", i);   // `_ =`: the result is not needed
    }
    _ = File::close(log);

    File input = Result::expect(File::open("app.log"), "cannot open the log");
    printf("%v bytes\n", Result::unwrap_or(File::len(&input), 0));
    while (Ok(Some(line)) = File::read_line(&mut input))   // stops at the end, or an error
    {
        printf("read: %s\n", &line);
    }
    _ = File::seek(&mut input, 6);
    Vec<u8> bytes = Vec::new();
    _ = File::read(&mut input, &mut bytes, 1);
    printf("byte 6 is %v\n", bytes[0]);
}
output
24 bytes
read: event 1
read: event 2
read: event 3
byte 6 is 49

Directories, the environment and the clocks

Directories, paths and file metadata in std::fs; the environment in std::env; the clocks in std::time.

Files live in directories, which a program can make, list and clean up; it can also read its environment and measure time. Paths are StringViews, as for files, and failures are FileErrors.

FunctionDoes
make_dir(path)make a directory: Ok(true) made, Ok(false) one was already there
remove_file(path), remove_dir(path)remove a file, or an empty directory
rename(from, to)move a file or directory, replacing a file at to
list_dir(path)the names in a directory, sorted: a Vec<String>
path_kind(path)File(len), Dir or Other; Err(NotFound) when nothing is there
env_var(name)Ok(Some(value)), or Ok(None) when the variable is not set
current_dir()the working directory, which relative paths start from
monotonic_ns(), unix_seconds()a clock for measuring time (never goes back), and the date
sleep_ms(ms)the calling thread does nothing for at least ms milliseconds; other threads run meanwhile (D-0124)
make_dir_all(path), remove_dir_all(path)make a directory and any missing ones above it; remove a directory and everything in it
copy_file(from, to)copy a file, replacing one at to; the number of bytes copied
file_info(path)a FileInfo: kind (as path_kind's), len, modified_unix (seconds since 1970) and readonly
set_current_dir(path)change the working directory, for the whole program
temp_dir(), home_dir()the system's directory for temporary files; the user's home directory, if known
current_exe(), hostname()the running program's own executable, for the files installed beside it; the machine's name, for a log line (D-0181)

Paths have their own functions, because a path's grammar is the operating system's: / on Unix, \ (and /) on Windows, where C:\ starts an absolute path. They read the text alone and never touch the file system, except path_canonical, which asks the system where a path really leads (following links; the path must exist). Use path_join rather than gluing with sprintf: it puts in the right separator, only one, and an absolute tail replaces the base.

FunctionGives
path_join(base, tail)base and tail with one separator between, as a String
path_parent(p), path_file_name(p)Some part, or None: /a/b and c.txt of /a/b/c.txt
path_stem(p), path_extension(p)archive.tar and gz of archive.tar.gz; .bashrc has a stem and no extension
path_is_absolute(p), path_normalize(p)whether p starts from a root; p with . and x/.. removed, by the text alone
path_canonical(p)the absolute path the system resolves p to
A log file's name and placeruns
import std;

fn main() : u8
{
    DateTime day = DateTime::from_unix(1791022620);
    String name = sprintf("app-%d-%02d-%02d.log", day.year, day.month, day.day);
    String dir = path_join(&temp_dir()[0..$], "cobaltc-guide-logs/2026");
    if (Err(_) = make_dir_all(&dir[0..$]))
    {
        return 1;
    }
    String log = path_join(&dir[0..$], &name[0..$]);
    if (Err(_) = write_file(&log[0..$], "started\n"))
    {
        return 1;
    }
    String stem = Option::unwrap_or(path_stem(&log[0..$]), String::new());
    String ext = Option::unwrap_or(path_extension(&log[0..$]), String::new());
    printf("%s: stem %s, extension %s, absolute %v\n", &name, &stem, &ext, path_is_absolute(&log[0..$]));
    match (file_info(&log[0..$]))
    {
        Ok(i)  : printf("%u bytes, written in the last minute: %v\n", i.len, unix_seconds() - i.modified_unix < 60),
        Err(e) : printf("file_info: %s\n", FileError::text(&e)),
    }
    String top = path_join(&temp_dir()[0..$], "cobaltc-guide-logs");
    _ = remove_dir_all(&top[0..$]);
    String up = path_normalize("logs/2026/../2025/./app.log");
    printf("normalized: %s\n", &up);
    0
}
output
app-2026-10-03.log: stem app-2026-10-03, extension log, absolute true
8 bytes, written in the last minute: true
normalized: logs/2025/app.log
Making, listing and removingruns
import std;

fn kind(StringView p) : str
{
    match (path_kind(p))
    {
        Ok(File(_))   : "file",
        Ok(Dir)       : "directory",
        Ok(Other)     : "other",
        Err(NotFound) : "missing",
        Err(_)        : "error",
    }
}

fn main() : u8
{
    if (Err(_) = make_dir("work"))
    {
        return 1;
    }
    _ = write_file("work/notes.txt", "remember\n");
    _ = rename("work/notes.txt", "work/old.txt");
    match (list_dir("work"))
    {
        Ok(names) :
        {
            foreach (n in &names)
            {
                printf("%s\n", n);
            }
        },
        Err(_)    : return 1,
    }
    printf("notes.txt: %s, old.txt: %s\n", kind("work/notes.txt"), kind("work/old.txt"));
    _ = remove_file("work/old.txt");
    _ = remove_dir("work");
    printf("work: %s\n", kind("work"));
    u64 t0 = monotonic_ns();
    printf("time goes forward: %v\n", monotonic_ns() >= t0);
    0
}
output
old.txt
notes.txt: missing, old.txt: file
work: missing
time goes forward: true

Dates and times

In std::time.

A DateTime is a moment in UTC to the second: year (an i64), month, day, hour, minute and second, all fields a program can read. It converts to and from the seconds of unix_seconds() and to and from ISO 8601 text, the form logs, file names and protocols use. The one time zone std knows is the machine's own: DateTime::to_local shifts a moment into it for showing to a person; there are no named zones and no other zone's rules.

FunctionDoes
DateTime::now(), DateTime::from_unix(s)the moment now, or s seconds after 1970-01-01T00:00:00Z (before it when negative); every i64 is a moment
DateTime::new(y, mo, d, h, mi, s)Some moment, or None when the fields name none (a 30 February, an hour 24, a second 60)
DateTime::to_unix(&t)the seconds since 1970; a DateTime written by hand with impossible fields is the fault diag.invalid-datetime
DateTime::to_iso(&t), DateTime::from_iso(text)2026-10-03T10:17:00Z as a String, and back; from_iso also takes a date alone, as its midnight, and gives a ParseError for anything else
DateTime::weekday(&t), Weekday::text(&w)the day of the week, Monday … Sunday, and its name
DateTime::eq(&a, &b), DateTime::less(&a, &b)the same moment; earlier (fits Vec::sort_by)
unix_ms()the wall clock in milliseconds since 1970
DateTime::to_local(&t)the same moment written in the machine's local time, daylight saving included, by the operating system's rule
local_offset_seconds(unix)how many seconds local time is ahead of UTC at that moment (negative when behind)

Measure elapsed time with monotonic_ns(), never with the wall clock. The wall clock is whatever the computer's owner or its time service last set it to, and it can jump backwards while a program runs; unix_seconds(), unix_ms() and DateTime::now() say what time it is, monotonic_ns() how long something took.

Dates as text and backruns
import std;

fn main() : u8
{
    DateTime launch = DateTime::from_unix(1791022620);
    String text = DateTime::to_iso(&launch);
    Weekday w = DateTime::weekday(&launch);
    printf("%s, a %s\n", &text, Weekday::text(&w));
    DateTime due = match (DateTime::from_iso("2026-12-24"))
    {
        Ok(t)  : t,
        Err(e) :
        {
            printf("bad date: %s\n", ParseError::text(&e));
            return 1;
        },
    };
    i64 days = (DateTime::to_unix(&due) - DateTime::to_unix(&launch)) / 86400;
    printf("%d whole days to %d-%02d-%02d\n", days, due.year, due.month, due.day);
    match (DateTime::from_iso("2026-02-29"))
    {
        Ok(_)  : printf("a leap day?\n"),
        Err(e) : printf("2026-02-29: %s\n", ParseError::text(&e)),
    }
    0
}
output
2026-10-03T10:17:00Z, a Saturday
81 whole days to 2026-12-24
2026-02-29: out of range

Local time is for showing, not for keeping. Store and log UTC, convert with DateTime::to_local as the last step before printing, and print a local value by its fields: it is a plain DateTime that knows nothing of its zone, so to_iso of it would end in Z and to_unix of it is not the moment. The offset depends on the machine and on the moment (clocks change twice a year in many zones), so this program's output differs from place to place:

Showing a moment in local time (output depends on the machine)
import std;

fn main() : u8
{
    DateTime now = DateTime::now();
    DateTime here = DateTime::to_local(&now);
    i64 offset = local_offset_seconds(DateTime::to_unix(&now));
    str sign = if (offset < 0) { "-" } else { "+" };
    i64 magnitude = if (offset < 0) { 0 - offset } else { offset };
    printf(
        "%s UTC is %04d-%02d-%02d %02d:%02d:%02d here (UTC%s%02d:%02d)\n",
        &DateTime::to_iso(&now),
        here.year,
        here.month,
        here.day,
        here.hour,
        here.minute,
        here.second,
        sign,
        magnitude / 3600,
        magnitude % 3600 / 60,
    );
    0
}

Milliseconds and HTTP dates (D-0181). A log line or a JSON document wants the moment to the millisecond: iso_from_unix_ms(unix_ms()) writes 2026-10-07T12:34:56.789Z, and unix_ms_from_iso(text) reads it back (from_iso reads the same text and drops the fraction). DateTime::to_http_date(&t) writes the form HTTP headers carry, Sun, 06 Nov 1994 08:49:37 GMT, and DateTime::from_http_date(text) reads it and the two older forms a server may still send.

Running other programs

In std::process.

A Command names a program and its arguments; running it starts a child process. There is no shell in between: each arg reaches the program exactly as written, spaces, quotes, $ and ; included, so text from a user can be passed as an argument without any way to run something else. A program that wants a shell's features runs one by name, /bin/sh with -c, and then quoting is its own business.

FunctionDoes
Command::new(program), Command::arg(&mut c, a)a command; one more argument
Command::current_dir(&mut c, dir), Command::env(&mut c, name, value), Command::clear_env(&mut c)where the child starts; an environment variable for it; start it with no inherited variables
Command::output(&c), Command::output_with_input(&c, input)run to the end; an Output with status, stdout and stderr (bytes)
Command::status(&c)run to the end with this program's own input and output; the status
Command::spawn(&c)start it and return a Child at once, its three streams piped to this program
Child::write_input, close_input, read_output, read_output_line, read_errortalk to a running child, as to a File
Child::wait(&mut ch), try_wait, killwait for the end (its status), ask without waiting, or end it at once

A status is the program's exit code, or minus the signal that ended it (-9 after kill on Unix). Failing to start is a FileError, as every input and output failure is: NotFound when there is no such program. Dropping a Child closes its pipes but leaves it running, like a job sent to the background; wait for it if it should finish first.

Capturing a program's outputruns
import std;

fn main() : u8
{
    Command c = Command::new("/bin/sh");
    Command::arg(&mut c, "-c");
    Command::arg(&mut c, "echo \"$1 world\"; echo oops >&2; exit 3");
    Command::arg(&mut c, "sh");
    Command::arg(&mut c, "hello; echo injected");   // one argument, never run as a command
    match (Command::output(&c))
    {
        Ok(o)  :
        {
            String out = Result::unwrap_or(String::from_utf8(Vec::clone(&o.stdout)), String::new());
            printf("status %d, said: %s", o.status, &out);
            printf("and %u bytes on standard error\n", Vec::len(&o.stderr));
        },
        Err(e) :
        {
            printf("could not run it: %s\n", FileError::text(&e));
            return 1;
        },
    }
    Command gone = Command::new("no-such-program-anywhere");
    if (Err(NotFound) = Command::output(&gone))
    {
        printf("no-such-program-anywhere: not found\n");
    }
    0
}
output
status 3, said: hello; echo injected world
and 5 bytes on standard error
no-such-program-anywhere: not found

Stopping cleanly

Pressing Ctrl-C, or the system asking a service to stop (SIGTERM), ends a program on the spot. A server that should finish the request in hand and close its files first calls watch_interrupts(): from then on an interrupt only sets a flag, and interrupt_requested() reads it. There is no callback; the program's own loop looks at the flag, often enough. A blocking call an interrupt arrives in may return early (a listener's accept and a socket's read give Err(TimedOut)), and TcpListener::set_accept_timeout_ms bounds how long a quiet server waits before looking again.

To end the program from anywhere, exit(status) (D-0150): the calling thread's scopes and frames are unwound and every destructor runs, as they do when a fault ends the program, standard output is flushed, and the process ends with status, with no diagnostic. Other threads take no further steps. exit has type never, so it can stand in for any value: None : exit(2) in a match arm, or exit(1) as a statement after an error message. Returning from main is still the ordinary way to end; exit is for the place that knows the program is over and is not main.

Ending the program with its destructors runruns
import std;

resource struct Lock
{
    i32 id;
}

fn Lock::drop(ref<Lock, exclusive> self)
{
    printf("lock %d released\n", self.id);
}

fn check(i32 value) : i32
{
    Lock inner = Lock { .id = 2 };
    if (value < 0)
    {
        eprintf("negative input\n");
        exit(3);                             // inner and outer are released first
    }
    value * 2
}

fn main() : u8
{
    Lock outer = Lock { .id = 1 };
    i32 r = check(-5);
    printf("never printed %d\n", r);
    0
}
output
lock 2 released
lock 1 released
A loop that stops when askedruns
import std;

fn main()
{
    watch_interrupts();
    u64 handled = 0;
    while (!interrupt_requested() && handled < 3)
    {
        handled += 1;                       // one unit of work: a request, a job, a file
    }
    printf("handled %u, stopping cleanly\n", handled);
}
output
handled 3, stopping cleanly

Also (D-0181): Child::read_error_line(&mut ch) reads the child's standard error a line at a time, as read_output_line does its output; Child::terminate(&mut ch) asks it to stop (SIGTERM on Unix, which a program may catch to finish cleanly) where kill ends it at once; and process_id() is this program's own number, for a pid file. kill and terminate signal the child alone: a sh -c "…" killed leaves the commands it started running. Child::kill_tree(&mut ch) (D-0205) ends the child and every process it started, and theirs (on Linux, found from /proc; on Windows with taskkill /T).

Streaming between processes (D-0195). A Child's pipes all belong to it, and every call borrows it exclusively, so one thread cannot write a child's input while another reads its output. Child::take_input(&mut ch) and Child::take_output(&mut ch) move a pipe out into a ChildInput or ChildOutput of its own (None if it was already taken): give one to a thread (spawn(pump, out_a, in_b)) and keep reading the other child here. ChildInput::write, ChildOutput::read and read_line work as the Child's own calls do; dropping an end closes it, and an end outlives its Child.

Cryptography, and bytes as text

In std::crypto, with hexadecimal and base64 in std::encoding::hex and std::encoding::base64.

Assurance
Implemented to the standards spec/21 §0 names, tested against their published vectors and against OpenSSL and Python's cryptography on loopback and in sweeps, fuzzed continuously (impl/fuzz); not independently audited.

sha256(data) is the SHA-256 digest of some bytes, 32 of them as an array<u8, 32>; hmac_sha256(key, data) is a message authentication code under a key, the way a webhook body, a session cookie or a signed request is protected. Both come in a streaming form for a message that arrives in pieces: Sha256::new(), Sha256::update(&mut h, piece) as often as needed, Sha256::finish(h); HmacSha256 the same. A Sha256 is a plain value, so a copy of a half-done computation continues on its own. Everything is written in CobaltC (the spec lists it), so the two tools compute exactly the same bytes; for a message of a few kilobytes the interpreter's speed does not matter.

Check a MAC with digest_eq, never with ==. digest_eq(a, b) compares every byte whatever the first difference, so how long the check takes says nothing about how many leading bytes a sender guessed right. A digest is shown with to_hex and read back with from_hex; bytes that must travel as text go through base64_encode and base64_decode (RFC 4648, padded). Random keys come from os_random_bytes. A secret is rarely used as it arrives: hkdf_sha256(salt, secret, info, len) derives len bytes of key material from it, bound to what they are for by info, so one secret never keys two purposes (RFC 5869; TLS 1.3's key schedule is this function). When a protocol or a file format names its hash, hash(kind, data), Hasher, hmac(kind, key, data), Hmac and the hkdf_*_with(kind, …) forms take a HashKind (Sha2_256, Sha2_384 or Sha2_512) at run time; the SHA-256 forms are the same functions with Sha2_256.

Encrypting. chacha20_poly1305_seal(key, nonce, aad, plaintext) encrypts and authenticates (RFC 8439): a 32-byte key, a 12-byte nonce that must never repeat under one key, and aad, data sent in the clear but protected from change; the result is the ciphertext and a 16-byte tag. chacha20_poly1305_open gives the plaintext back, or None if a single bit of the message, the tag or the aad was changed, and never any part of a refused message. Agreeing on a key. Each side makes x25519_private_key(), sends x25519_public_key(&private[0..$]), and computes x25519(&private[0..$], &their_public[0..$]): both get the same 32 bytes, which nobody who saw only the public keys can (RFC 7748); None means the other side sent a key that must be refused. Feed the shared secret to hkdf_sha256, never use it as a key directly. The NIST alternatives. aes_gcm_seal and aes_gcm_open are the same pair with AES-GCM (NIST SP 800-38D), for formats and protocols that require it: the key is 16, 24 or 32 bytes (AES-128, AES-192 or AES-256), the nonce 12. p256_private_key, p256_public_key and p256_ecdh agree on a key over the P-256 curve; the public key is 65 bytes, and None means a key that must be refused. Where you choose, X25519 agrees on a key in about half the time P-256 takes; the two AEADs run at about the same speed (AES-128-GCM and ChaCha20-Poly1305 each about 50 ms a MiB here). All of these take the same steps whatever the secret bytes are, so their timing reveals nothing but lengths; a key or nonce of the wrong length is the fault diag.crypto-length.

A key exchange and an encrypted messageruns
import std;

fn main()
{
    array<u8, 32> alice = x25519_private_key();
    array<u8, 32> bob = x25519_private_key();
    array<u8, 32> alice_public = x25519_public_key(&alice[0..$]);
    array<u8, 32> bob_public = x25519_public_key(&bob[0..$]);
    array<u8, 32> shared = Option::unwrap(x25519(&alice[0..$], &bob_public[0..$]));
    Vec<u8> nothing = Vec::new();
    Vec<u8> key = hkdf_sha256(&nothing[0..$], &shared[0..$], &b"example key"[0..$], 32);
    Vec<u8> nonce = Vec::filled(12, 0: u8);
    Vec<u8> sealed = chacha20_poly1305_seal(&key[0..$], &nonce[0..$], &b"header"[0..$], &b"attack at dawn"[0..$]);
    // Bob derives the same key from his side of the exchange.
    array<u8, 32> bobs = Option::unwrap(x25519(&bob[0..$], &alice_public[0..$]));
    Vec<u8> bob_key = hkdf_sha256(&nothing[0..$], &bobs[0..$], &b"example key"[0..$], 32);
    match (chacha20_poly1305_open(&bob_key[0..$], &nonce[0..$], &b"header"[0..$], &sealed[0..$]))
    {
        Some(p) : printf("bob reads: %s\n", &Result::unwrap_or(String::from_utf8(p), String::new())),
        None    : printf("refused\n"),
    }
    sealed[0] = sealed[0] ^ 1;
    printf(
        "changed in transit: %s\n",
        if (Option::is_none(&chacha20_poly1305_open(
            &bob_key[0..$],
            &nonce[0..$],
            &b"header"[0..$],
            &sealed[0..$],
        ))) { "refused" } else { "accepted" },
    );
}
output
bob reads: attack at dawn
changed in transit: refused
FunctionDoes
sha256(data), Sha256::new(), Sha256::update(&mut h, data), Sha256::finish(h)the SHA-256 digest, in one call or in pieces: an array<u8, 32>
hmac_sha256(key, data), HmacSha256::new(key), update, finishHMAC-SHA-256 under key (any length; a long key is hashed first)
digest_eq(a, b)the same bytes, compared in a time that depends only on the lengths
hkdf_sha256(salt, ikm, info, len), hkdf_extract(salt, ikm), hkdf_expand(prk, info, len)HKDF (RFC 5869): len bytes of key material derived from a secret, bound to info; at most 8160 bytes from one extracted key
chacha20_poly1305_seal(key, nonce, aad, plaintext), chacha20_poly1305_open(key, nonce, aad, sealed)authenticated encryption (RFC 8439): ciphertext and tag; the plaintext back, or None for anything changed
chacha20(key, nonce, counter, data), poly1305(key, message)the stream cipher and the one-time MAC the AEAD is built from
aes_gcm_seal(key, nonce, aad, plaintext), aes_gcm_open(key, nonce, aad, sealed), aes_encrypt_block(key, block)AES-GCM (SP 800-38D) with a 16-, 24- or 32-byte key, as the ChaCha20-Poly1305 pair; the bare AES block cipher (FIPS 197)
x25519_private_key(), x25519_public_key(private), x25519(private, their_public)key agreement (RFC 7748): a key pair, and the shared secret, None for a public key that must be refused
p256_private_key(), p256_public_key(private), p256_ecdh(private, their_public)key agreement over P-256: a 65-byte public key, and the 32-byte shared secret, None for a key that must be refused
sha512(data), sha384(data), Sha512, Sha384the 64- and 48-byte SHA-2 digests, one-shot or streaming
hash(kind, data)the digest by a HashKind: Sha2_256, Sha2_384 or Sha2_512
rsa_verify_pkcs1v15(&key, kind, digest, sig), rsa_verify_pss(&key, kind, digest, sig)whether an RSA signature (PKCS #1 v1.5, or PSS with a digest-length salt) is the key's over that digest
ecdsa_verify(curve, public_key, digest, sig)whether a DER ECDSA signature on P256 or P384 is the key's over that digest
hkdf_expand_label(secret, label, context, len)TLS 1.3's key-schedule step (RFC 8446 §7.1)
Hasher::new(kind), hmac(kind, key, data), Hmac::new(kind, key), hkdf_with(kind, salt, ikm, info, len), hkdf_extract_with, hkdf_expand_with, hkdf_expand_label_withthe same over a HashKind chosen at run time (SHA-256, SHA-384 or SHA-512); results are Vec<u8>
ed25519_private_key(), ed25519_public_key(private), ed25519_sign(private, message), ed25519_verify(public, message, sig)Ed25519 (RFC 8032): 32-byte keys, 64-byte deterministic signatures over the whole message
ec_private_key(curve), ec_public_key(curve, private), ecdh(curve, private, their_public), ecdsa_sign(curve, private, digest)P-256 or P-384 keys, key agreement, and a DER ECDSA signature over a digest with an RFC 6979 nonce
rsa_generate_key(bits), RsaPrivateKey::new(n, e, d, p, q), RsaPrivateKey::public_key(&key), rsa_sign_pkcs1v15(&key, kind, digest), rsa_sign_pss(&key, kind, digest)an RSA key (1024 to 8192 bits), and signatures in the two encodings the verify functions check
hash_password(password), verify_password(stored, password)Argon2id password storage as a PHC string, and its check
argon2id(password, salt, passes, memory_kib, lanes, len), pbkdf2(kind, password, salt, iterations, len)the raw key-derivation functions (RFC 9106, RFC 8018)
blake2b(data, len), Blake2b::new(len), Blake2b::new_keyed(key, len), update, finishBLAKE2b (RFC 7693): a digest of 1 to 64 bytes, optionally keyed
to_hex(bytes), from_hex(text)lowercase hexadecimal and back (ParseError for a non-digit or an odd count)
base64_encode(bytes), base64_decode(text)base64 with = padding and back (ParseError for a bad character or length)
A digest, a MAC, and their text formsruns
import std;

fn main()
{
    array<u8, 32> d = sha256(&b"abc"[0..$]);
    printf("%s\n", &to_hex(&d[0..$]));
    array<u8, 32> mac = hmac_sha256(&b"Jefe"[0..$], &b"what do ya want for nothing?"[0..$]);
    printf("%s\n", &to_hex(&mac[0..$]));
    array<u8, 32> again = hmac_sha256(&b"Jefe"[0..$], &b"what do ya want for nothing?"[0..$]);
    printf("verified: %v\n", digest_eq(&mac[0..$], &again[0..$]));
    printf("%s\n", &base64_encode(&b"foobar"[0..$]));
}
output
ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad
5bdcc146bf60754e6a042426089575c75a003f089d2739839dec58b964ec3843
verified: true
Zm9vYmFy

Signatures. Three families, each with a key pair, a signing function and a verifying one. Ed25519 (RFC 8032) is the simplest and the one to choose where you can: ed25519_private_key(), ed25519_public_key(&private[0..$]), ed25519_sign(&private[0..$], message) gives 64 bytes, ed25519_verify(&public[0..$], message, &sig[0..$]) says whether they are that key's signature of that message. Signing is deterministic: the same key and message give the same bytes, and no random number is involved that could repeat. ECDSA on P256 or P384 (what certificates and TLS mostly carry): ec_private_key(curve), ec_public_key(curve, &k[0..$]) (the uncompressed point, 65 or 97 bytes), ecdsa_sign(curve, &k[0..$], &digest[0..$]) (a DER signature over the digest made with hash; the nonce is derived from the key and the digest as RFC 6979 says, so it too is deterministic) and ecdsa_verify(curve, public, digest, sig); the same keys do key agreement with ecdh(curve, &k[0..$], &their_public[0..$]), of which the p256_* functions are the P-256 case. RSA: rsa_generate_key(2048) makes an RsaPrivateKey (minutes under the interpreter, seconds compiled), or RsaPrivateKey::new(n, e, d, p, q) builds one from numbers read elsewhere (None if they do not fit together); rsa_sign_pkcs1v15(&key, kind, &digest[0..$]) and rsa_sign_pss(&key, kind, &digest[0..$]) sign the digest in the two standard encodings, RsaPrivateKey::public_key(&key) gives the RsaPublicKey that rsa_verify_pkcs1v15 and rsa_verify_pss check with. Signing with any of them takes the same steps whatever the key; the RSA operation is blinded and checked against the public key before its result leaves. Verification uses only public values and answers false for anything malformed.

Keys in files. Keys travel in the encodings every tool writes, in std::x509: a public key as a SubjectPublicKeyInfo (CertKey::from_der, from_pem, to_der, to_pem), a private key as PKCS #8 (PrivateKey::from_der, from_pem, to_der, to_pem), where from_pem also reads the older RSA PRIVATE KEY and EC PRIVATE KEY blocks. A PrivateKey is RsaPrivate, EcPrivate or Ed25519Private; PrivateKey::public_key(&key) is its CertKey, and sign(&key, &scheme, message) signs a whole message under a SignatureScheme the way verify_signature checks it, hashing first where the scheme says so. A certificate's key is a CertKey too, so a program can check a signature against a certificate it parsed.

Signing a release noteruns
import std;

fn main()
{
    array<u8, 32> private = ed25519_private_key();
    array<u8, 32> public = ed25519_public_key(&private[0..$]);
    array<u8, 64> sig = ed25519_sign(&private[0..$], &b"release 1.4.0"[0..$]);
    printf("genuine: %v\n", ed25519_verify(&public[0..$], &b"release 1.4.0"[0..$], &sig[0..$]));
    printf("altered: %v\n", ed25519_verify(&public[0..$], &b"release 1.4.1"[0..$], &sig[0..$]));
    // The same key, written the way openssl would.
    PrivateKey key = Ed25519Private(private);
    String pem = PrivateKey::to_pem(&key);
    printf("%s", StringView::sub(String::as_view(&pem), 0, 27));
    printf("\nread back: %v\n", Result::is_ok(&PrivateKey::from_pem(String::as_view(&pem))));
}
output
genuine: true
altered: false
-----BEGIN PRIVATE KEY-----
read back: true

Passwords. Store hash_password(&password[0..$]), a text like $argon2id$v=19$m=19456,t=2,p=1$…$… that records the Argon2id parameters and the random salt beside the result; check a login with verify_password(stored, &attempt[0..$]). Argon2id (RFC 9106) costs memory as well as time (19 MiB and two passes here, the OWASP recommendation), so a stolen table cannot be searched at hardware speed; the parameters can be raised later and old entries still verify, since each carries its own. Never store a plain sha256 of a password, salted or not. For the formats that predate Argon2, pbkdf2(kind, password, salt, iterations, len) is PBKDF2 over HMAC, and argon2id(password, salt, passes, memory_kib, lanes, len) is the raw function. blake2b(data, len) (RFC 7693, up to 64 bytes, Blake2b::new_keyed for a keyed MAC) is the fast hash Argon2 is built on. Under the interpreter hash_password takes many minutes (19 MiB of memory touched twice, word by word); compiled, well under a second.

SHA-1 is there too (Sha1, sha1, and Sha1 as a HashKind for hmac, pbkdf2 and Hasher, D-0181), because older protocols still require it: RFC 6238's one-time passwords default to HMAC-SHA-1, and git names its objects by it. It is legacy — collisions have been made — so it is for checking what others produce, never for a new signature.

Big integers

In std::math.

A BigUint is an unsigned integer of any size: add, sub, mul, div_rem (a BigDivision of quotient and remainder), rem, mod_pow, mod_inverse, clone, shifts, eq and less, and conversions to and from big-endian bytes and decimal text. It owns its digits, so it is a resource: reassign one with overwrite, and take a division apart by destructuring. Its operations take time that depends on the numbers, so it is for public values, never for a secret key.

Thirty factorialruns
import std;

fn main()
{
    BigUint f = BigUint::from_u64(1);
    for (u64 i = 2; i <= 30; i += 1)
    {
        overwrite(&mut f, BigUint::mul(&f, &BigUint::from_u64(i)));
    }
    printf("30! = %s\n", &BigUint::text(&f));
    BigDivision { quotient, remainder } = BigUint::div_rem(&f, &BigUint::from_u64(1000000007));
    printf("mod 1000000007 = %s\n", &BigUint::text(&remainder));
}
output
30! = 265252859812191058636308480000000
mod 1000000007 = 109361473

Certificates

In std::x509.

Assurance
Implemented to the standards spec/21 §0 names, tested against certificates and keys OpenSSL made and against the system's root bundle, fuzzed continuously (impl/fuzz); not independently audited.

Certificate::parse(der) reads an X.509 certificate; certificates_from_pem(text) reads every certificate in PEM text. A TrustStore holds the roots a chain may end at: TrustStore::system() reads the operating system's bundle on Unix (on Windows, pass a bundle to TrustStore::from_pem_file). verify_chain(&leaf, &intermediates[0..$], &roots, host, &now) decides whether a server's certificates prove it is host: the leaf names the host (a DNS name, a one-label wildcard, or an IP address; never the common name), every certificate is in date and signed by the next, every issuer is a CA, and the chain ends at a root. The error says why not: HostMismatch, Expired, UnknownAuthority and so on. Revocation (CRL, OCSP) is not checked.

TLS

In std::tls.

Assurance
Implemented to RFC 8446, tested against its vectors and against OpenSSL's s_client and s_server on loopback, fuzzed continuously (impl/fuzz); not independently audited.

TlsStream::connect(addr, server_name, &roots) opens a TLS 1.3 connection: it checks the server's certificate chain against roots and server_name, and then read, read_exact, write and close carry data encrypted and authenticated. TlsStream::client(tcp, …) does the same over a TcpStream the program connected and configured itself (its timeouts apply). The client speaks only TLS 1.3, with ChaCha20-Poly1305, AES-128-GCM or AES-256-GCM, X25519, P-256 or P-384, and ECDSA, RSA-PSS or Ed25519 signatures: the set every TLS 1.3 server must support and the common additions; a failure is a TlsError saying whether it was the network, the certificate, the handshake or an alert. The example is shown, not run: it needs the network.

Note
Run TLS clients compiled. Checking a server's certificate chain is several public-key signature checks, a few milliseconds compiled but tens of seconds in the interpreter (TrustStore::system() alone parses every root certificate). Many servers close a connection whose handshake stalls for about ten seconds, and the interpreter's client then fails with the network error closed; under cobc the same program completes in well under a second.
Connecting to a DNS-over-TLS resolver (needs the network)
import std;

fn main() : u8
{
    TrustStore roots = match (TrustStore::system())
    {
        Ok(t)  : t,
        Err(_) : return 1,
    };
    IpAddr ip = Result::unwrap(IpAddr::parse("1.1.1.1"));
    TlsStream s = match (TlsStream::connect(SocketAddr::new(ip, 853), "cloudflare-dns.com", &roots))
    {
        Ok(s)  : s,
        Err(e) :
        {
            eprintf("tls: %s\n", TlsError::text(&e));
            return 1;
        },
    };
    // … a DNS-over-TLS query: a 2-byte length, then the DNS message …
    _ = TlsStream::close(&mut s);
    0
}

Serving TLS (D-0186). TlsStream::server(tcp, &key, &chain[0..$]) performs the handshake as the server, with a PrivateKey (RSA, P-256, P-384 or Ed25519, read with PrivateKey::from_pem) and the server's certificate chain (certificates_from_pem, the server's own certificate first); the client's first suite, key share and the key's signature scheme are chosen, and a client that cannot agree is answered with an alert. Afterwards read_line, read, write and close work as on the client side, and peer_certificate (None on a server, the server's certificate on a client) is there for pinning. TcpStream::try_clone has no counterpart on a TlsStream: its record keys are one state. The example is shown, not run: it needs a key and a certificate (openssl req -x509 -newkey ed25519 -nodes -subj /CN=localhost -addext subjectAltName=DNS:localhost makes a self-signed pair for development).

A TLS echo server (needs a key and a certificate)
import std;

fn main() : u8
{
    PrivateKey key = Result::unwrap(PrivateKey::from_pem(String::as_view(&Result::unwrap(read_file("server.key")))));
    Vec<Certificate> chain = certificates_from_pem(String::as_view(&Result::unwrap(read_file("server.crt"))));
    TcpListener l = Result::unwrap(TcpListener::bind(Result::unwrap(SocketAddr::parse("0.0.0.0:8443"))));
    while (true)
    {
        TcpStream tcp = Result::unwrap(TcpListener::accept(&mut l));
        TlsStream s = match (TlsStream::server(tcp, &key, &chain[0..$]))
        {
            Ok(s)  : s,
            Err(e) :
            {
                eprintf("handshake: %s\n", TlsError::text(&e));
                continue;
            },
        };
        while (Some(line) = Result::unwrap_or(TlsStream::read_line(&mut s), None))
        {
            _ = TlsStream::write_text(&mut s, "echo: ");
            _ = TlsStream::write_text(&mut s, String::as_view(&line));
            _ = TlsStream::write_text(&mut s, "\n");
        }
        _ = TlsStream::close(&mut s);
    }
    0
}

HTTP

In std::http.

Assurance
Implemented to RFC 9110 and RFC 9112, tested against curl and Python's servers on loopback, fuzzed continuously (impl/fuzz); not independently audited.

http_get(url) fetches a URL and http_post(url, content_type, body) posts to one; each gives a Response with its status, reason, headers and whole body, or an HttpError saying what went wrong (the network, TLS, a malformed URL or message, a limit). Both follow redirects, decode chunked bodies and speak https through std::tls with the system roots. A program that makes more than one request, or wants a timeout, a header on every request or a smaller body limit, keeps an HttpClient and calls HttpClient::send(&mut client, &request): the roots are loaded once, at the first https request. Url::parse takes a URL apart (scheme, host, port, path, query), percent_encode and percent_decode handle the escaping, find_header (or Response::header) finds a header whatever its case, and Response::text gives the body as a String (an error when it is not UTF-8). HTTP/1.1 only and one connection per request, unless the client's keep_alive is set (D-0195): then the connection stays open while the server allows, and the next request to the same host and port goes on it (a server for such a client must not count on one connection per request). If that connection turns out closed before any answer, a GET, HEAD, PUT, DELETE, OPTIONS or TRACE is sent again on a fresh one; a POST is not -- the server may have acted on it -- and is ConnectionClosed for the program to decide. No compression and no cookies; what a server sends compressed it sends only when asked, so plain requests get plain bodies.

Serving. HttpServer::bind(addr) listens; HttpServer::accept(&mut server) gives each client's HttpConnection, on which HttpConnection::request(&mut c) parses the next request (Ok(None) once the client is done) and HttpConnection::respond(&mut c, status, &headers, body) writes the answer with the right Content-Length, reason phrase and Connection header, keeping the connection open when the client asked for that. A malformed request is an Err the program answers with 400 (or 413 for one over max_body); the connection then closes. Handle each connection in its own thread with spawn, as the example does; the server speaks plain HTTP (a TLS server is a later decision). The example is hermetic: its client and server are the same program.

A health check and an echo, served and fetchedruns
import std;

fn serve(HttpConnection c) : u8
{
    while (true)
    {
        Vec<Header> headers = Vec::new();
        Vec::push(&mut headers, Header::new("Content-Type", "text/plain"));
        match (HttpConnection::request(&mut c))
        {
            Ok(Some(req)) :
            {
                if (req.url == "/health")
                {
                    _ = HttpConnection::respond(&mut c, 200, &headers[0..$], &b"ok\n"[0..$]);
                }
                else if (req.method == "POST" && req.url == "/echo")
                {
                    _ = HttpConnection::respond(&mut c, 200, &headers[0..$], &req.body[0..$]);
                }
                else
                {
                    _ = HttpConnection::respond(&mut c, 404, &headers[0..$], &b"no such page\n"[0..$]);
                }
            },
            Ok(None) : return 0,
            Err(_)   :
            {
                _ = HttpConnection::respond(&mut c, 400, &headers[0..$], &b"bad request\n"[0..$]);
                return 1;
            },
        }
    }
    0
}

fn accept_two(HttpServer s) : u8
{
    for (usize i = 0; i < 2; i += 1)
    {
        match (HttpServer::accept(&mut s))
        {
            Ok(c)  : { _ = join(spawn(serve, c)); },
            Err(_) : return 1,
        }
    }
    0
}

fn main()
{
    HttpServer s = Result::unwrap(HttpServer::bind(SocketAddr::new(IpAddr::localhost_v4(), 0)));
    String base = sprintf("http://127.0.0.1:%u", HttpServer::local_addr(&s).port);
    auto server = spawn(accept_two, s);
    match (http_get(String::as_view(&sprintf("%s/health", &base))))
    {
        Ok(r)  : printf("%u %s: %s", r.status, &r.reason, &Result::unwrap_or(Response::text(&r), String::new())),
        Err(e) : printf("error: %s\n", HttpError::text(&e)),
    }
    match (http_post(String::as_view(&sprintf("%s/echo", &base)), "text/plain", &b"hello there"[0..$]))
    {
        Ok(r)  : printf("%u, %s echoed %v bytes: %s\n", r.status, Option::unwrap_or(Response::header(&r, "content-type"), StringView::of("?")), Vec::len(&r.body), &Result::unwrap_or(Response::text(&r), String::new())),
        Err(e) : printf("error: %s\n", HttpError::text(&e)),
    }
    printf("server thread: %u\n", join(server));
}
output
200 OK: ok
200, text/plain echoed 11 bytes: hello there
server thread: 0

https, forms and compressed bodies. HttpServer::bind_tls(addr, key, chain) makes the same server speak https (D-0186): accept completes each client's TLS handshake first, and gives Err(HttpTlsFailure(_)) for a client that fails it, so accept's error is an HttpError. A request's query string and a form body are read with Url::query_pairs(&url) and form_decode(text), each a Vec<Param> (name, value, compared exactly) searched with find_param; form_encode writes one (D-0181). The client offers Accept-Encoding: gzip, deflate and decodes a body a server sends that way, dropping the header, so r.body is always the plain bytes (D-0183).

Query strings and form bodiesruns
import std;

fn main()
{
    Vec<Param> form = form_decode("name=Ada+Lovelace&note=100%25&tag=x&tag=y");
    foreach (p in &form)
    {
        printf("%v = %v\n", &p.name, &p.value);
    }
    printf("%v\n", Option::unwrap_or(find_param(&form[0..$], "note"), StringView::of("-")));
    printf("%v\n", &form_encode(&form[0..2]));
    Url u = Result::unwrap(Url::parse("https://example.org/search?q=cobalt%20c&page=2"));
    printf("%v\n", Option::unwrap_or(find_param(&Url::query_pairs(&u)[0..$], "q"), StringView::of("-")));
}
output
name = Ada Lovelace
note = 100%
tag = x
tag = y
100%
name=Ada+Lovelace&note=100%25
cobalt c

JSON

In std::encoding::json.

Json::parse(text) reads a JSON document (RFC 8259) into a tree of Json values — JsonNull, JsonBool, JsonNumber (an f64), JsonText, JsonArray and JsonObject (its members in order) — and Json::text(&j) writes one back compact, Json::pretty(&j) indented. Reading a tree: Json::get(&j, "key") and Json::at(&j, i) give a reference to a member or an element, or None, and as_text, as_i64, as_f64, as_bool, as_array, as_object and is_null ask what a value is. Building one: Json::set(&mut obj, "key", value) and Json::push(&mut arr, value). A document that is not JSON is Err(Invalid(byte)), nothing at all Err(Empty), a number beyond f64 or nesting past 512 levels Err(OutOfRange). Every escape is handled, surrogate pairs included; a whole number is written without a fraction; a NaN or an infinity, which JSON cannot carry, is written null (D-0182).

Reading and writing JSONruns
import std;

fn main()
{
    Json j = Result::unwrap(Json::parse("{\"name\": \"Ada\", \"born\": 1815, \"langs\": [\"Analytical\", \"Engine\"]}"));
    StringView name = Option::unwrap_or(Json::as_text(Option::unwrap(Json::get(&j, "name"))), StringView::of("?"));
    i64 born = Option::unwrap_or(Json::as_i64(Option::unwrap(Json::get(&j, "born"))), 0);
    printf("%v, born %v, knew", name, born);
    foreach (l in Option::unwrap(Json::as_array(Option::unwrap(Json::get(&j, "langs")))))
    {
        printf(" %v", Option::unwrap_or(Json::as_text(l), StringView::of("?")));
    }
    printf("\n");
    Json out = JsonObject(Vec::new());
    Json::set(&mut out, "name", JsonText(String::from_str("Ada")));
    Json langs = JsonArray(Vec::new());
    Json::push(&mut langs, JsonText(String::from_str("Analytical")));
    Json::set(&mut out, "langs", langs);
    Json::set(&mut out, "ok", JsonBool(true));
    printf("%v\n%v\n", &Json::text(&out), &Json::pretty(&out));
    match (Json::parse("{\"a\": [1, 2,]}"))
    {
        Err(Invalid(at)) : printf("not JSON at byte %v\n", at),
        _                : {},
    }
}
output
Ada, born 1815, knew Analytical Engine
{"name":"Ada","langs":["Analytical"],"ok":true}
{
  "name": "Ada",
  "langs": [
    "Analytical"
  ],
  "ok": true
}
not JSON at byte 12

Compression

In std::compress.

deflate(data) and inflate(data, max) are DEFLATE (RFC 1951), the format under zip, PNG and git; zlib_deflate/zlib_inflate wrap it with zlib's header and Adler-32 (RFC 1950), and gzip/gunzip with gzip's header and CRC-32 (RFC 1952), so a .gz file or an HTTP body is one call away. Decompression takes a max, the most it may produce, and answers Err(Oversize) beyond it — a small stream can expand without bound, so a limit is always there; Truncated is a stream cut short and Corrupt(byte) one that is not what it claims, a wrong checksum included. Compression uses one fixed strategy, so every implementation writes the same bytes, and reads everything other tools write; adler32 is there beside crc32 (D-0183).

gzip and backruns
import std;

fn main()
{
    String text = String::new();
    foreach (i in 0..20)
    {
        String::append(&mut text, "the same line again and again\n");
    }
    Vec<u8> packed = gzip(StringView::as_bytes(String::as_view(&text)));
    printf("%v bytes became %v\n", String::len(&text), Vec::len(&packed));
    Vec<u8> back = Result::unwrap(gunzip(&packed[0..$], 1 << 20));
    printf("round trip: %v\n", Vec::len(&back) == String::len(&text) && digest_eq(&back[0..$], StringView::as_bytes(String::as_view(&text))));
    match (gunzip(&packed[0..10], 1 << 20))
    {
        Err(e) : printf("cut short: %s\n", CompressError::text(&e)),
        Ok(_)  : {},
    }
}
output
600 bytes became 52
round trip: true
cut short: compressed data ends early

Databases

In std::database::postgres, re-exported by std::database.

Assurance
Implemented to the PostgreSQL protocol 3.0 and SCRAM-SHA-256, tested against a PostgreSQL server on loopback, fuzzed continuously (impl/fuzz); not independently audited.

A PostgreSQL client, speaking the server's own wire protocol over std::net and std::tls, with SCRAM-SHA-256 from std::crypto; nothing is linked, so a program that talks to a database still needs only a C compiler. PgConnection::connect(url) opens a connection from a postgres://user:password@host:port/database URL (the port 5432 and the database the user's name when left out), encrypting with TLS and verifying the server's certificate against the system roots unless the URL says ?sslmode=disable; connect_with(url, &roots, timeout_ms) takes its own TrustStore (a self-signed server's issuer, say) and timeout. There is no unverified TLS.

PgConnection::query(&mut c, sql, &params[0..$]) runs one statement with $1, $2, … bound to the PgValues given (Null, Text, Int, Float, Bool, Bytes): a parameter is sent beside the statement, never spliced into it, so no quoting is ever needed. The PgResult has the columns (name and type OID), the rows (each cell Some(text) as the server prints the value, or None for NULL; read it with PgRow::get(&row, i) and parse it with StringView::parse<T>), the affected count and the command tag. execute is query for a statement without rows, giving the count; run_script runs a file of ;-separated statements without parameters, for schemas and migrations. Transactions are statements: execute(&mut c, "BEGIN", &none[0..$]) and "COMMIT" or "ROLLBACK".

An error the server reports is Err(Server(e)) with its severity, SQLSTATE code, message, detail, hint and position; the connection stays usable after it, as it does after a result over max_result (16 MiB by default, ResultTooLarge). A network or protocol failure closes the connection, and every later call gives Disconnected. Notices the server sends collect in c.notices for the program to read and clear. PgError::text(&e) is a String saying what went wrong, the server's message included. One connection serves one thread; a program that queries from several keeps one per thread.

FunctionDoes
PgConnection::connect(url), connect_with(url, &roots, timeout_ms)open a connection (30 s timeout; the system roots), or with the roots and timeout given
PgConnection::query(&mut c, sql, &params[0..$])one statement with $n bound to the PgValues: its PgResult (columns, rows, affected, tag)
PgConnection::execute(&mut c, sql, &params[0..$])the same, giving only the affected count
PgConnection::run_script(&mut c, sql)several ;-separated statements, no parameters, results dropped, the first error given
PgConnection::parameter(&c, name), c.notices, c.max_resulta setting the server reported at startup (server_version, …); the notices received; the most a result may take
PgRow::get(&row, i), PgResult::column(&res, name)a cell's text (None for NULL or past the end); a column's index by name
PgConnection::close(&mut c)tell the server goodbye and close (a dropped connection is just closed)
PgUrl::parse(text), PgValue::text(&v)the URL taken apart; the text a parameter is sent as
Orders in a PostgreSQL table (needs a server)
import std;

fn main() : u8
{
    PgConnection c = match (PgConnection::connect("postgres://app:secret@db.internal/shop"))
    {
        Ok(c)  : c,
        Err(e) :
        {
            eprintf("connect: %s\n", &PgError::text(&e));
            return 1;
        },
    };
    Vec<PgValue> none = Vec::new();
    if (Err(e) = PgConnection::run_script(&mut c, "CREATE TABLE IF NOT EXISTS orders (id serial PRIMARY KEY, item text, qty int)"))
    {
        eprintf("schema: %s\n", &PgError::text(&e));
        return 1;
    }
    Vec<PgValue> order = Vec::new();
    Vec::push(&mut order, Text(String::from_str("widget")));
    Vec::push(&mut order, Int(3));
    match (PgConnection::execute(&mut c, "INSERT INTO orders (item, qty) VALUES ($1, $2)", &order[0..$]))
    {
        Ok(n)  : printf("inserted %v\n", n),
        Err(e) : printf("insert: %s\n", &PgError::text(&e)),
    }
    Vec<PgValue> at_least = Vec::new();
    Vec::push(&mut at_least, Int(2));
    match (PgConnection::query(&mut c, "SELECT id, item, qty FROM orders WHERE qty >= $1 ORDER BY id", &at_least[0..$]))
    {
        Ok(res) :
        {
            foreach (row in &res.rows)
            {
                StringView item = Option::unwrap_or(PgRow::get(row, 1), StringView::of("?"));
                i64 qty = Result::unwrap_or(StringView::parse<i64>(Option::unwrap_or(PgRow::get(row, 2), StringView::of("0"))), 0);
                printf("%s x %v\n", item, qty);
            }
        },
        Err(Server(e)) : printf("%s: %s\n", &e.code, &e.message),
        Err(e)         : printf("query: %s\n", &PgError::text(&e)),
    }
    PgConnection::close(&mut c);
    0
}

Without a server the pieces can still be seen: the URL's defaults and the text each parameter is sent as.

A URL's defaults and the parameters' textruns
import std;

fn main()
{
    PgUrl u = Result::unwrap(PgUrl::parse("postgresql://app@db.internal"));
    printf("%s@%s:%u/%s tls=%v\n", &u.user, &u.host, u.port, &u.database, match (u.sslmode) { Disable : false, VerifyFull : true });
    Vec<PgValue> params = Vec::new();
    Vec::push(&mut params, Int(-7));
    Vec::push(&mut params, Float(0.5));
    Vec::push(&mut params, Bool(true));
    Vec::push(&mut params, Bytes(Vec::from_slice(&b"\x00\xff"[0..$])));
    Vec::push(&mut params, Null);
    foreach (v in &params)
    {
        printf("%s\n", &Option::unwrap_or(PgValue::text(v), String::from_str("NULL")));
    }
}
output
app@db.internal:5432/app tls=true
-7
0.5
t
\x00ff
NULL

A cell read as a value: PgRow::get_i64(&row, i), get_f64, get_bool and get_bytes (D-0181) give None for NULL, for a column past the end, and for text of another form; get still gives the text.

Networking

In std::net.

TCP connections, UDP datagrams and name resolution, over IPv4 and IPv6. Addresses are values: an IpAddr (V4 or V6) and a port make a SocketAddr, read from text with SocketAddr::parse ("127.0.0.1:8080", "[::1]:8080") or built with SocketAddr::new(IpAddr::localhost_v4(), 8080); resolve(host, port) asks the system's resolver.

Sockets block. accept waits for a connection, read for bytes, connect for the other side. A server handles many connections at once by giving each its own thread: spawn a function with the accepted TcpStream moved into it, and use a Channel (§19) where threads must talk. Every socket can have timeouts; a call that waits longer gives Err(TimedOut), and the socket is still usable. There is no TLS, and nothing is added to what you send: the bytes that arrive are exactly the bytes written, in order, in pieces of any size, so a protocol needs its own framing — lines, read with read_line, are the simplest.

FunctionDoes
TcpListener::bind(addr), TcpListener::local_addr(&l)listen at addr (port 0: any free port); where it listens
TcpListener::accept(&mut l), set_accept_timeout_ms(&mut l, ms)the next connection, a TcpStream; give up after ms with Err(TimedOut)
TcpStream::connect(addr), connect_timeout(addr, ms)connect to a listener
TcpStream::read_line(&mut s), read(&mut s, &mut buf, max), read_exact(&mut s, &mut buf, n)as File's: a line, what has arrived (0: the peer has finished), exactly n bytes
TcpStream::write(&mut s, data), write_text(&mut s, text), printf(&mut s, fmt, …)send all of it
TcpStream::shutdown_write(&mut s)tell the peer nothing more is coming (it reads 0), and keep reading
set_read_timeout_ms, set_write_timeout_ms, set_nodelayper-stream timeouts (0: none); send small writes at once
UdpSocket::bind(addr), send_to(&mut u, data, to), recv_from(&mut u, max)datagrams: one message each, with its sender in the Datagram
UdpSocket::try_clone(&u)a second handle on the same socket: one thread waits in recv_from while another sends on the clone
UdpSocket::connect(&mut u, to), send(&mut u, data), recv(&mut u, max)a socket talking to one peer: recv drops datagrams from anyone else (D-0181)
TcpStream::try_clone(&s)a second handle on the same connection, for one thread to read while another writes; the connection closes when the last handle is dropped (D-0185)
TcpStream::set_keepalive(&mut s, on)have the system probe an idle connection, so a vanished peer is noticed (D-0181)
An echo server and its clients in one programruns
import std;

// One connection: each line back in capitals, until the client finishes.
fn serve(TcpStream s) : u32
{
    u32 lines = 0;
    while (Ok(Some(line)) = TcpStream::read_line(&mut s))
    {
        String up = String::to_ascii_upper(&line);
        if (Err(_) = TcpStream::printf(&mut s, "%s\n", &up))
        {
            break;
        }
        lines += 1;
    }
    lines
}

// Accepts `n` connections, each served by its own thread.
fn listen(TcpListener l, usize n) : u32
{
    Vec<handle<u32>> served = Vec::new();
    for (usize i = 0; i < n; i += 1)
    {
        if (Ok(s) = TcpListener::accept(&mut l))
        {
            Vec::push(&mut served, spawn(serve, s));
        }
    }
    u32 total = 0;
    while (Some(h) = Vec::pop(&mut served))
    {
        total += join(h);
    }
    total
}

fn ask(SocketAddr at, str word) : String
{
    match (TcpStream::connect(at))
    {
        Ok(c)  :
        {
            _ = TcpStream::printf(&mut c, "%s\n", word);
            match (TcpStream::read_line(&mut c))
            {
                Ok(Some(answer)) : answer,
                _                : String::from_str("(no answer)"),
            }
        },
        Err(e) : String::from_str(NetError::text(&e)),
    }
}

fn main() : u8
{
    TcpListener l = match (TcpListener::bind(SocketAddr::new(IpAddr::localhost_v4(), 0)))
    {
        Ok(l)  : l,
        Err(e) :
        {
            printf("bind: %s\n", NetError::text(&e));
            return 1;
        },
    };
    SocketAddr at = TcpListener::local_addr(&l);
    auto server = spawn(listen, l, 3);
    foreach (word in ["alpha", "beta", "gamma"])
    {
        String answer = ask(at, word);
        printf("%s -> %s\n", word, &answer);
    }
    printf("the server answered %u lines\n", join(server));
    0
}
output
alpha -> ALPHA
beta -> BETA
gamma -> GAMMA
the server answered 3 lines

A server that runs until it is stopped combines this with watch_interrupts (above): give the listener an accept timeout, and the accept loop looks at the flag between waits.

A server loop that can be stoppedruns
import std;

fn main() : u8
{
    watch_interrupts();
    TcpListener l = match (TcpListener::bind(SocketAddr::new(IpAddr::localhost_v4(), 0)))
    {
        Ok(l)  : l,
        Err(_) : return 1,
    };
    TcpListener::set_accept_timeout_ms(&mut l, 50);
    u32 waits = 0;
    while (!interrupt_requested() && waits < 3)      // a real server: while (!interrupt_requested())
    {
        match (TcpListener::accept(&mut l))
        {
            Ok(s)         : printf("a client\n"),    // spawn a thread for it here
            Err(TimedOut) : waits += 1,               // nobody came: look at the flag again
            Err(e)        :
            {
                printf("accept: %s\n", NetError::text(&e));
                return 1;
            },
        }
    }
    printf("closing the listener and stopping\n");
    0
}
output
closing the listener and stopping

Sorting and searching

In std::collections.

Vec::sort(&mut v) puts a Vec in order: integers by value, false before true, text by its bytes, and a struct or enum of these field by field in declaration order (an enum by the order its variants are declared in, then by payload). For any other order, or any other element type, Vec::sort_by(&mut v, less) takes a function that says whether its first argument goes before its second. Both are stable: elements that are equal keep the order they had. In a sorted Vec, Vec::binary_search(&v, &key) gives Ok(i), the first position holding the key, or Err(i), where it would go (binary_search_by takes the same less).

Sorting and searchingruns
import std;

struct Player
{
    String name;
    u32 score;
}

fn higher(ref<Player, shared> a, ref<Player, shared> b) : bool
{
    a.score > b.score                 // higher scores first
}

fn main()
{
    Vec<String> names = Vec::new();
    Vec::push(&mut names, String::from_str("mira"));
    Vec::push(&mut names, String::from_str("ada"));
    Vec::push(&mut names, String::from_str("lin"));
    Vec::sort(&mut names);
    assert(&names[0][0..$] == "ada" && &names[2][0..$] == "mira");
    String who = "lin";
    assert(Result::unwrap_or(Vec::binary_search(&names, &who), 99) == 1);
    String bob = "bob";
    assert(match (Vec::binary_search(&names, &bob))
    {
        Ok(_)  : false,
        Err(i) : i == 1,              // between "ada" and "lin"
    });

    Vec<Player> ps = Vec::new();
    Vec::push(&mut ps, Player { .name = "a", .score = 3 });
    Vec::push(&mut ps, Player { .name = "b", .score = 9 });
    Vec::push(&mut ps, Player { .name = "c", .score = 3 });
    Vec::sort_by(&mut ps, higher);
    assert(ps[0].score == 9);
    assert(&ps[1].name[0..$] == "a" && &ps[2].name[0..$] == "c");   // equal scores keep their order
}

HashMap<K, V> and HashSet<K>

In std::collections.

HashMap<K, V> maps keys to values; HashSet<K> holds a set of keys. A key is an integer, a bool, a str, a String, or a struct whose fields (an enum whose payloads) are all keys: struct Pos { i32 x; i32 y; } keys a grid. Any other key type (a float, an array, a struct with a float field) is a static type error, and the message names the field that is not a key. The map owns its keys and values. Lookups take the key by reference, so they copy nothing.

FunctionDoes
HashMap::new(), HashMap::len(&m)an empty map; the number of entries
HashMap::insert(&mut m, k, v)adds k; if it was present, replaces its value and returns the old one as Some
HashMap::entry(&mut m, k, fresh)the value for k as an exclusive reference, adding k with fresh first if absent
HashMap::get(&m, &k), HashMap::get_mut(&mut m, &k)Some reference to the value, or None
HashMap::contains(&m, &k)whether k is present
HashMap::remove(&mut m, &k)removes k and returns its value; every other entry keeps its place; takes time in proportion to the map's size
HashMap::swap_remove(&mut m, &k)the same, fast: the last entry moves into the removed one's place (D-0134)
HashMap::key_at(&m, i), HashMap::value_at(&m, i)entry i, for i < len, in insertion order
HashMap::clear(&mut m)removes every entry, destroying keys and values; the map stays usable
HashMap::clone(&m), HashSet::clone(&s), clone(&m)a copy of a map or set whose contents are clone (§12); Rc::clone shares instead
HashMap::get_str(&m, "k"), get_mut_str, contains_str, remove_strfor a map keyed by String: the same lookups with a str, so no String is built to ask (D-0113); HashSet::contains_str, remove_str likewise
HashSet::new, len, insert, contains, remove, swap_remove, key_at, clearthe same for a set; insert and the removals say whether anything changed
HashSet::union(&a, &b), HashSet::intersection(&a, &b), HashSet::difference(&a, &b)a new set of the keys in either, in both, or in a but not b, copied with clone; a and b are unchanged. The result keeps a's order, and union adds b's new keys after it (D-0138)

Entries are kept in the order they were added, so foreach (k, v in &m) (§14) visits them in that order, the same in the interpreter and the compiler, whatever the keys hash to. key_at and value_at reach entry i directly. remove keeps that order, so it moves every later entry down one; swap_remove is quick because it fills the gap with the last entry instead, for a map whose order does not matter.

Counting words, and a setruns
import std;

fn main()
{
    array<str, 9> words = ["the", "cat", "and", "the", "hat", "and", "the", "bat", "sat"];
    HashMap<String, u32> counts = HashMap::new();
    foreach (w in words)
    {
        *HashMap::entry(&mut counts, String::from_str(w), 0) += 1;   // 0 if new, then + 1
    }
    foreach (word, n in &counts)                        // insertion order
    {
        printf("%-4s %d\n", word, n);
    }

    String cat = "cat";
    match (HashMap::get(&counts, &cat))
    {
        Some(n) : printf("cat: %d\n", *n),
        None    : printf("no cat\n"),
    }

    HashSet<u32> seen = HashSet::new();
    u32 x = 7;
    while (HashSet::insert(&mut seen, x))                 // false once x comes round again
    {
        x = x * x % 10;
    }
    printf("%d distinct, back to %d\n", HashSet::len(&seen), x);
}
output
the  3
cat  1
and  2
hat  1
bat  1
sat  1
cat: 1
3 distinct, back to 1
A struct as a keyruns
import std;

struct Pos
{
    i32 x;
    i32 y;
}

enum Tile
{
    Wall,
    Door(u8),
}

fn main()
{
    HashMap<Pos, Tile> grid = HashMap::new();
    HashMap::insert(&mut grid, Pos { .x = 0, .y = -1 }, Wall);
    HashMap::insert(&mut grid, Pos { .x = 2, .y = 3 }, Door(7));
    Pos at = Pos { .x = 2, .y = 3 };
    match (HashMap::get(&grid, &at))
    {
        Some(Door(k)) : assert(*k == 7),
        _             : assert(false),
    }
    HashSet<Tile> seen = HashSet::new();      // an enum of keys is a key too
    HashSet::insert(&mut seen, Door(7));
    assert(HashSet::contains(&seen, &Door(7)) && !HashSet::contains(&seen, &Wall));
}
A float is not a keyrejected: diag.type-mismatch
import std;

fn main()
{
    HashMap<f64, str> m = HashMap::new();   // ✗ diag.type-mismatch (static): f64 is not a key type
}
Set operationsruns
import std;

fn show(str name, ref<HashSet<str>, shared> s)
{
    printf("%-13s", name);
    foreach (i in 0..HashSet::len(s))
    {
        printf(" %s", *HashSet::key_at(s, i));
    }
    printf("\n");
}

fn main()
{
    HashSet<str> monday = HashSet::new();
    HashSet<str> tuesday = HashSet::new();
    foreach (n in ["Ann", "Bo", "Cy", "Di"])
    {
        HashSet::insert(&mut monday, n);
    }
    foreach (n in ["Cy", "Ed", "Ann"])
    {
        HashSet::insert(&mut tuesday, n);
    }
    show("either day:", &HashSet::union(&monday, &tuesday));
    show("both days:", &HashSet::intersection(&monday, &tuesday));
    show("monday only:", &HashSet::difference(&monday, &tuesday));
}
output
either day:   Ann Bo Cy Di Ed
both days:    Ann Cy
monday only:  Bo Di

Queue<T> and PriorityQueue<T>

In std::collections.

A Queue<T> hands elements back in the order they arrived: push_back adds at the back, pop_front takes from the front, each in constant time. It works at the other ends too (push_front, pop_back), and front and back look without taking. A Vec with a growing head index does the same job, but its storage never shrinks, and Vec::remove(&mut v, 0) moves every other element on each call.

A PriorityQueue<T> hands back the least element first, whatever order they arrived in. PriorityQueue::new() orders by the elements' own order, the one Vec::sort uses (numbers, bool, text, and structs of them field by field), so a struct whose first field is the priority needs no comparison function. PriorityQueue::new_by(less) takes the order as a function, as Vec::sort_by does; for the largest first, pass one that answers "greater". Elements the order calls equal come out in the order they went in, so equally urgent work is first come, first served.

FunctionDoes
Queue::new(), Queue::len(&q)an empty queue; the number of elements
Queue::push_back(&mut q, x), Queue::push_front(&mut q, x)adds x at the back, or before the front
Queue::pop_front(&mut q), Queue::pop_back(&mut q)takes the front (or back) element as Some, or None when empty
Queue::front(&q), Queue::back(&q)Some reference to the front (or back) element, or None; the queue stays as it is
Queue::clear(&mut q)destroys every element, front to back; the queue stays usable
PriorityQueue::new(), PriorityQueue::new_by(less)an empty queue in the elements' own order, or in less's
PriorityQueue::push(&mut q, x), PriorityQueue::len(&q)adds x; the number of elements
PriorityQueue::pop(&mut q)takes the least element as Some (of equals, the earliest pushed), or None when empty
PriorityQueue::peek(&q), PriorityQueue::clear(&mut q)Some reference to the element pop would take; destroys every element

Both own their elements and destroy what is left when they end. A reference from front, back or peek borrows the queue, so the queue cannot change while it is in use. Neither has foreach or q[i]: a queue is read at its ends.

Breadth-first search with a Queueruns
import std;

fn link(ref<Vec<Vec<usize>>, exclusive> adj, usize a, usize b)
{
    Vec::push(&mut (*adj)[a], b);
    Vec::push(&mut (*adj)[b], a);
}

fn main()
{
    Vec<Vec<usize>> adj = Vec::new();
    foreach (_ in 0..7)
    {
        Vec::push(&mut adj, Vec::new());
    }
    link(&mut adj, 0, 1);
    link(&mut adj, 0, 2);
    link(&mut adj, 1, 3);
    link(&mut adj, 2, 3);
    link(&mut adj, 3, 4);
    link(&mut adj, 4, 5);

    // Breadth first: everyone one step from 0, then two steps, ...
    Vec<u32> steps = Vec::filled(7, max_value<u32>());
    Queue<usize> todo = Queue::new();
    steps[0] = 0;
    Queue::push_back(&mut todo, 0);
    while (Some(u) = Queue::pop_front(&mut todo))
    {
        foreach (v in &adj[u])
        {
            if (steps[*v] == max_value<u32>())
            {
                steps[*v] = steps[u] + 1;
                Queue::push_back(&mut todo, *v);
            }
        }
    }
    foreach (i, s in &steps)
    {
        if (*s == max_value<u32>())
        {
            printf("%d: unreachable\n", i);
        }
        else
        {
            printf("%d: %d steps\n", i, *s);
        }
    }
}
output
0: 0 steps
1: 1 steps
2: 1 steps
3: 2 steps
4: 3 steps
5: 4 steps
6: unreachable
Shortest routes with a PriorityQueueruns
import std;

struct Road
{
    usize to;
    u32 km;
}

struct Visit                                   // ordered by dist, then node: a key
{
    u32 dist;
    usize node;
}

fn road(ref<Vec<Vec<Road>>, exclusive> map, usize a, usize b, u32 km)
{
    Vec::push(&mut (*map)[a], Road { .to = b, .km = km });
    Vec::push(&mut (*map)[b], Road { .to = a, .km = km });
}

fn main()
{
    Vec<Vec<Road>> map = Vec::new();
    foreach (_ in 0..5)
    {
        Vec::push(&mut map, Vec::new());
    }
    road(&mut map, 0, 1, 7);
    road(&mut map, 0, 2, 2);
    road(&mut map, 2, 1, 3);
    road(&mut map, 1, 3, 4);
    road(&mut map, 2, 3, 9);
    road(&mut map, 3, 4, 1);

    // Dijkstra: always settle the nearest place not yet settled.
    Vec<u32> best = Vec::filled(5, max_value<u32>());
    PriorityQueue<Visit> frontier = PriorityQueue::new();     // least first
    PriorityQueue::push(&mut frontier, Visit { .dist = 0, .node = 0 });
    while (Some(at) = PriorityQueue::pop(&mut frontier))
    {
        if (at.dist >= best[at.node])
        {
            continue;                                         // reached sooner already
        }
        best[at.node] = at.dist;
        foreach (r in &map[at.node])
        {
            PriorityQueue::push(&mut frontier, Visit { .dist = at.dist + r.km, .node = r.to });
        }
    }
    foreach (i, d in &best)
    {
        printf("to %d: %d km\n", i, *d);
    }
}
output
to 0: 0 km
to 1: 5 km
to 2: 2 km
to 3: 9 km
to 4: 10 km
Largest first, and ties in arrival orderruns
import std;

struct Task
{
    u8 urgency;
    str what;
}

fn more_urgent(ref<Task, shared> a, ref<Task, shared> b) : bool
{
    a.urgency > b.urgency
}

fn main()
{
    PriorityQueue<Task> inbox = PriorityQueue::new_by(more_urgent);   // largest first
    PriorityQueue::push(&mut inbox, Task { .urgency = 1, .what = "tidy desk" });
    PriorityQueue::push(&mut inbox, Task { .urgency = 5, .what = "server down" });
    PriorityQueue::push(&mut inbox, Task { .urgency = 3, .what = "review" });
    PriorityQueue::push(&mut inbox, Task { .urgency = 5, .what = "disk full" });
    while (Some(t) = PriorityQueue::pop(&mut inbox))
    {
        printf("%d %s\n", t.urgency, t.what);                  // equal urgency: first come, first served
    }
}
output
5 server down
5 disk full
3 review
1 tidy desk

Rc<T>

In std::memory.

Shared ownership as a library: a reference-counted box. Rc::clone makes another handle to the same box; the value is destroyed when the last handle is dropped. Access is read-only (a shared reference); put a mutex inside if you need mutation. Rc is not thread-safe. A Weak<T>, from Rc::downgrade, keeps the box but not the value: Weak::upgrade is Some(Rc) while a strong handle exists, else None. It is for a parent pointer, a cache or an observer list, where an Rc would make a cycle (D-0125).

FunctionSignature
Rc::new(T) : Rc<T>
Rc::clone(ref<Rc<T>, shared>) : Rc<T>
Rc::get(ref<Rc<T>, shared>) : ref<T, shared>
Rc::downgrade(ref<Rc<T>, shared>) : Weak<T>
Weak::upgrade(ref<Weak<T>, shared>) : Option<Rc<T>>
Weak::clone(ref<Weak<T>, shared>) : Weak<T>
Reference countingruns
import std;

fn main()
{
    Vec<i32> payload = Vec::new();
    Vec::push(&mut payload, 42);
    auto a = Rc::new(payload);        // payload moves into the box; count = 1
    auto b = Rc::clone(&a);           // count = 2
    assert(Vec::len(Rc::get(&b)) == 1);
    drop(a);                          // count = 1; the box survives
    assert(Rc::get(&b)[0] == 42);
    drop(b);                          // count = 0: the Vec is destroyed, then the box is freed
}

Box<T>

In std::memory.

A value on the heap with exactly one owner: Rc without the count. Box::new(v) moves v in; Box::get(&b) and Box::get_mut(&mut b) borrow it; Box::into_inner(b) moves it back out; Box::clone(&b) copies a box whose value is clone; destroying the Box destroys the value. Its main use is a type that contains itself, which needs something holding a pointer between it and itself: a struct that contains itself directly has no size, and is rejected with diag.recursive-type.

A list that owns its nodesruns
import std;

struct Node
{
    i32 value;
    Option<Box<Node>> next;           // Option<Node> here would be diag.recursive-type
}

fn main()
{
    Option<Box<Node>> list = None;
    list = Some(Box::new(Node { .value = 1, .next = list }));   // each node owns the next
    list = Some(Box::new(Node { .value = 2, .next = list }));

    Box<i32> counter = Box::new(41);
    *Box::get_mut(&mut counter) += 1;
    assert(*Box::get(&counter) == 42);
    i32 n = Box::into_inner(counter);  // the value back; the Box's memory is freed
    assert(n == 42);
}                                     // list: both nodes destroyed, the outer one first

Destroying a chain of Boxes does not use a stack frame per node when each Box is the last thing its node destroys (its first-declared resource field, as next above): the next node is destroyed by a loop, in the same order. So a list of a million nodes goes out of scope like any other value.

A type cannot contain itselfrejected: diag.recursive-type
import std;

struct Node
{
    i32 value;
    Option<Node> next;                // ✗ diag.recursive-type (static): Node would contain itself
}

fn main()
{
}

swap and replace

In std::core.

A value cannot be moved out of something you hold only by reference, since that would leave a hole. So two values behind references are exchanged by swap(&mut a, &mut b), and one is exchanged for a new value by replace(&mut r, v), which returns the old one. Two elements of the same Vec or slice are exchanged by Vec::swap(&mut v, i, j) or slice_swap(s, i, j), which also handle i == j. Nothing is copied or destroyed: each value just changes place. overwrite(&mut r, v) is replace for when the old value is not wanted: it stores v and destroys what was there.

Sorting Strings, and exchanging fieldsruns
import std;

// Sorts any run of Strings by length: the elements are resources, so
// they are exchanged, never copied out.
fn sort_by_len(slice<String, exclusive> s)
{
    for (usize i = 1; i < slice_len(s); i += 1)
    {
        usize j = i;
        while (j > 0 && String::len(&s[j - 1]) > String::len(&s[j]))
        {
            slice_swap(s, j - 1, j);
            j -= 1;
        }
    }
}

struct Pair
{
    String left;
    String right;
}

fn main()
{
    Vec<String> words = Vec::new();
    foreach (w in ["pear", "fig", "banana", "kiwi"])
    {
        Vec::push(&mut words, String::from_str(w));
    }
    sort_by_len(&mut words[0..$]);
    assert(String::len(&words[0]) == 3 && String::len(&words[$ - 1]) == 6);

    Pair p = Pair { .left = "L", .right = "RR" };
    swap(&mut p.left, &mut p.right);
    String old = replace(&mut p.left, String::from_str("new"));   // old is "RR"
    assert(String::len(&old) == 2 && String::len(&p.left) == 3);
}

An empty slot is filled by plain assignment: a None owns nothing, so *slot = Some(...) writes over it (§07). To put a value where one may already be, use replace, which hands the old one back to you.

Filling an empty linkruns
import std;

struct Node
{
    i64 val;
    Option<Box<Node>> next;
}

// Appends at the tail: walk to the empty link, then fill it.
fn push_back(ref<Option<Box<Node>>, exclusive> head, i64 v)
{
    ref<Option<Box<Node>>, exclusive> slot = head;
    while (true)
    {
        match (&mut *slot)
        {
            Some(b) : { slot = &mut Box::get_mut(b).next; },
            None    : { break; },
        }
    }
    *slot = Some(Box::new(Node { .val = v, .next = None }));   // slot holds None: nothing to lose
}

fn main()
{
    Option<Box<Node>> list = None;
    push_back(&mut list, 1);
    push_back(&mut list, 2);
    i64 sum = 0;
    ref<Option<Box<Node>>, shared> cur = &list;
    while (true)
    {
        match (cur)
        {
            Some(b) : { sum += Box::get(b).val; cur = &Box::get(b).next; },
            None    : { break; },
        }
    }
    assert(sum == 3);
}
Rules of thumb
  • Vec<T> for sequences, Option<T> for maybe-absent values, Result<T, E> for fallible calls, Box<T> for one value on the heap (a recursive type's link), Rc<T> when two owners are genuinely needed.
  • Library functions take &v or &mut v; match the mode to what you need.
  • Hold element references briefly; any push or pop may invalidate them (§10).

§22Surface syntax reference

Governing specification file: spec/22-surface-syntax.md

Tokens, declarations, statements and expressions on one page, with the tie-break rules a reader needs.

This section is the syntax in one place: the tokens, the declarations, the statements, the expressions, and the handful of places where the grammar needs a tie-break rule. It is meant for looking things up; the semantics are in the earlier sections.

Lexical structure

  • Comments: // to end of line and /* block */ (block comments do not nest).
  • Identifiers: ASCII letters, digits and _, not starting with a digit.
  • Integer literals: decimal digits, or hexadecimal, octal or binary after 0x, 0o, 0b, optionally : type (255: u8, 0xFF: u8). No sign: - is an operator. Float literals: 1.5, 0.25: f32, 1.0e-7. Digit separators: a _ between two digits is ignored (1_000_000, 0xFFFF_0000). Booleans: true, false. Unit value: ().
  • String literals: "…", type str. Escapes \n \r \t \0 \\ \" and \u{hex}; any other source character stands for itself. A literal must close on its own line. Byte literals: b"…", type array<u8, N>; the same escapes except \xNN in place of \u{…}, ASCII characters only, at least one byte. Byte character literals: b'a', one ASCII character or escape: an unsigned integer literal, u8 unless its context gives another unsigned type. No character literals (a char type).
  • Keywords: fn struct bitstruct enum resource match if else while for foreach return auto const module import export unsafe extern break continue move mut true false as void (as is reserved and unused).
  • Reserved type names (cannot be identifiers): i8 i16 i32 i64 i128 u8 u16 u32 u64 u128 isize usize f32 f64 bool str ref rawptr array slice handle mutex guard. The words shared and exclusive are ordinary identifiers that only have meaning inside ref<…, …>.
  • Whitespace and newlines are insignificant between tokens; brace placement is a matter of style.

Types

Type syntaxMeaning
i32, u8, f64, bool, …built-in scalars
strtext value (§21)
voidthe unit type (its value is written ())
ref<T, shared>, ref<T, exclusive>references
rawptr<T>raw pointer
array<T, N>fixed array, N a constant expression (§16)
slice<T, shared>, slice<T, exclusive>a borrowed run of elements (§16)
fn(T1, T2) : R, fn(T1)function type (result void if omitted)
handle<T>, mutex<T>, guard<T>concurrency types
Name, Name<T1, T2>, mod::Namea declared struct or enum, optionally instantiated

Declarations

Declaration forms
// Functions. `export` makes an item visible outside its module.
fn name(T1 p1, T2 p2) : R                   // return type after a colon
{
    body
}

fn name(T1 p1)                              // no return type: returns void
{
    body
}

export fn name<T, U>(T x) : U               // generic, exported
{
    body
}

fn Type::name(ref<Type, shared> self)       // associated function, called as Type::name(...)
{
    body
}

fn Type::drop(ref<Type, exclusive> self)    // the destructor of Type
{
    body
}

// Foreign functions: parameters and result must be scalars, void or rawptr.
unsafe extern fn name(rawptr<u8> buf, usize len) : isize;

// Structs and enums. `resource` marks a type that manages something.
struct Name<T>
{
    T field;                                // fields end with ;
    export i32 visible;                     // field visibility is per field
}

resource struct Name
{
    i32 field;
}

enum Name<T>
{
    Variant(T),                             // with a payload
    Other                                   // without
}

// Modules and imports.
module name
{
    items
}

export module name
{
    items
}

module name "./path/to/file.cb";            // the same, with that file's items as the body (§17);
                                            // `export module name "…";` as for the inline form

import path::to::item;

Statements and blocks

Statement forms
T x = e;                 // typed local
auto x = e;              // inferred local
T x;                     // uninitialised local (must be assigned before use)
Name { f1, f2 } = e;     // destructuring: every field
e;                       // expression statement

{                        // a block; no ; needed after it
    statements
}

if (c)
{
    statements
}
else if (c2)
{
    statements
}
else
{
    statements
}

while (c)
{
    statements
}

match (e)                // a comma after every arm
{
    A(x) : e1,
    B : e2,
    _ : e3,
}

unsafe
{
    statements
}

A block is { statement* expr? }: zero or more statements, then an optional trailing expression that is the block's value. if, while, match, unsafe and plain blocks may stand as statements without a semicolon, and their value is discarded. When one appears as the initialiser of a declaration or the right side of an assignment, the enclosing statement still ends with ;.

Expressions

FormExample
literals and names42, 3.5, true, (), x, mod::item
string and byte literals"hello\n" (a str), b"\x00\xff" (an array<u8, 2>)
struct literalPoint { .x = 1, .y = 2 }, Box<i32> { .value = 5 }
array literal[1, 2, 3], [0; 256] (256 copies of 0)
enum variantSome(5), None, Shape::Dot
field, index, callp.x, a[i], f(x, y), Vec::len(&v), identity<i64>(5)
borrow, deref&x, &mut x, *r
operatorssee the precedence table in §13
propagatee?
block, if, match, unsafeas values: i32 x = if (c) { 1 } else { 2 };
closure[a, b](i32 x) { a + b + x }, move [v]() { Vec::len(&v) }
controlreturn e, return, break, continue

Reading the grammar in tricky spots

  • Declaration or expression? A statement that begins with a type name, fn, auto, or the name of a struct or enum is a declaration; otherwise it is an expression. Point p = … declares; p.x = 1; assigns.
  • [ starts a closure or an array. It is a closure only if the closing ] is immediately followed by ( … ) {. Otherwise it is an array literal.
  • Generic call or comparison? name<T>(…) with a type inside the angle brackets is always a generic instantiation. Comparisons are non-associative, so a < b > c is never a comparison.
  • Conditions are always parenthesised, which is why if (Point { .x = 1 } == p) { … } needs no special rule.
  • A pattern as a condition. if (Some(x) = e) and while (Some(x) = e) are read as a pattern followed by one = when the condition begins Name(, m::Name or a literal; Some(x) == e is a comparison, and x = e (a bare name) the assignment it is everywhere, which is not a bool.
  • Name { f1, f2 } = e; (bare identifiers in braces) is destructuring; Name { .f = e } is a literal.
  • bitstruct Name : u32 { f : 6; … } declares a struct of bit fields (§16): the widths fill the backing type exactly, low bit first.
  • &mut is two tokens, one operator. x = y = z assigns right to left.

Restrictions and deliberate absences

Things the grammar does not provide, each a considered decision recorded in the specification:

  • no character literals (b'x' is a byte: a u8, or the unsigned type its context asks for), no indexing or slicing of a str; no switch (a match on an integer, §16, is the checked form); no goto
  • no import … as renaming; no method-call syntax (v.push(x) — final, CHG-0132); no operator overloading
  • patterns in match are _, a name (a binder or a constant), a literal, a variant, and a variant with a pattern for its payload, nothing else (no ranges, alternatives, struct patterns or guards); destructuring takes a struct apart whole: every field named, or .. for the rest (destroyed as usual), and field : name renames one
  • no partial initialisation of aggregates
  • no lifetime annotations: a function returning a reference must have exactly one reference parameter
  • no user-declared bounds on generic type parameters: only the five built-in ones (eq, ordered, number, integer, clone), on a function's parameters; a struct or enum satisfies clone alone
  • no mut qualifier on bindings (the keyword is reserved for &mut)
  • no moving a resource out of a field of a live struct; move the whole struct, or take it apart (a field of a temporary may be moved out; the rest ends with the statement)
  • no spawning a closure that borrows; use move

What an implementation must document

The width of isize/usize; byte order; enum discriminant width; struct padding; the value of dangling<T>(); the encoding of function values; the address str_ptr gives for a str's bytes and whether equal values share one; and the form in which faults and diagnostics are reported. Everything else is fixed by the language.

PPerformance

Measured advice for fast compiled programs: what costs time in cobc's output, and how to write code the compiler can see through.

This chapter is about making compiled programs fast. Every piece of advice in it was measured: a small program written two or more ways, each way built by cobc at its default -O2 under GCC and under Clang, and timed. The ratios quoted are from those runs, on the machine described in bench/RESULTS.md. They describe cobc as it is today. The compiler proves more with each release, so a gap measured here may close; where that is likely, the text says so.

None of it applies to coby. The interpreter tracks every object, reference and access path and makes every check the specification leaves to run time, so it is many times slower than compiled code whatever you write, and its relative costs are not cobc's. Use it to run a program at once and to check what it does, never to time it.

A workflow by stage

StageCommandWhat it costs and gives
Writingcoby --check prog.cb or cobc --check src/Every check made before a program starts: syntax, names, types, ownership, exhaustive matches. Nothing runs and no C is compiled, so it takes seconds even for a large program, and a program that reads input, writes files or serves the network is checked without doing so.
Trying itcoby prog.cb, or cobc --run -O0 prog.cb-O0 makes the C compiler's work short, at the price of a slow program: right while you only care whether it behaves correctly.
Measuring and shippingcobc -o prog prog.cb (-O2, the default)What users run. Measure only this: -O0 keeps exactly the work -O2 removes, so a comparison made at -O0 says nothing about the real build.

Beyond -O2: --cc clang and --cc gcc choose the C compiler, and neither wins everywhere (bench/RESULTS.md has the comparison); -march=native lets the C compiler use every instruction your processor has, at the cost of an executable that may not run on an older one. To see where the time goes, perf stat ./prog counts instructions and perf record ./prog then perf report names the functions; cobc --keep-c keeps the generated C, in which every run-time check is a visible line ("Reading the C that cobc generates" in the Introduction).

Where the time goes

A compiled CobaltC program makes every check the specification requires, except those the compiler proves can never fail. Two kinds of check cost very differently:

  • Cheap checks, a compare and a branch the processor predicts: an index against a length, an addition for overflow. In a tight loop they cost a few per cent to a few tens of per cent.
  • Tracking, which the runtime library does for a value the compiler cannot reason about: it records each reference and access path as it is made, checks each access against the others (§08–§09), and retires them at the end of their statement or scope. This costs hundreds of nanoseconds per access, a hundred times a plain access or more.

So the advice that matters most is not about saving checks: it is about writing code whose values the compiler can see through, so that tracking is left out. The sections below are in order of how much they saved in the measurements.

To see where your own program still pays for tracking, run cobc --report-tracking prog.cb. It compiles nothing: it lists the lines whose generated C still calls the runtime's tracking, with the kinds of call on each, lines inside a loop first. A line inside a loop is the one to look at; a line that runs once (a binding, a move into a call) costs next to nothing. The sections below say how to rewrite what it finds.

The other direction is cobc --all-checks prog.cb: it compiles the program with every proof about a function's reference parameters switched off, so every access through a parameter is checked where it is made. It is slower, and it exists for comparison: a program must behave the same both ways, and the project's test battery runs its whole file suite under it (D-0209).

Keep hot vectors confined

cobc proves a Vec confined when nothing but its own name can ever reach it. A confined Vec's accesses compile to plain C with bounds and overflow checks and no tracking. A local Vec, or one a function takes by value, stays confined when it is:

  • made where it is declared (Vec::new(), or a function's result), or received as a parameter Vec<T> v;
  • used through Vec::push, Vec::len, Vec::pop, Vec::reserve, Vec::swap, Vec::reverse, Vec::insert, Vec::remove, Vec::truncate, Vec::clear, Vec::sort, Vec::contains, Vec::index_of, Vec::binary_search, indexing (v[i], v[i] = x, v[i].f += x) and foreach, also foreach (p in &mut v) writing p.f;
  • passed as &v or &mut v to functions that themselves use it only in those ways (the compiler checks each such function), and in no other argument of the same call;
  • at most moved out at the end: as the function's result (also inside a struct or Ok(…) built as the result), or into a call as its last use, such as overwrite(&mut h.v, v).

A shared reference to an element, held across the loop (ref<i64, shared> first = &v[0];), leaves the loop's reads as fast as before: a read cannot clash with a shared reference. Writes to the vector and pushes onto it are then checked, since one of them could. What loses confinement altogether is an exclusive reference to an element (&mut v[i] kept in a binding), &mut v handed to a function that may keep a path to it, a closure that names v, or moving v to another binding; then every access in the loop is tracked, about ten times the cost in a summing loop.

A vector that is a field of a local struct (h.v) is confined the same way while the function uses it only by indexing (h.v[i], h.v[i] = x, h.v[i].f += x) and Vec::len(&h.v), and never uses h whole (moving it, borrowing it, capturing it); the struct's other fields are used freely. Where the function also pushes to h.v, borrows an element of it or hands it on, indexing it there is tracked; then hand it to a helper (sum_helper(&h.v)), loop over it with foreach (x in &h.v), or move it out for the loop and back (replace then overwrite), each of which is as fast as a local. Confinement is decided per function: one function's use of a vector does not slow another function down.

A function that takes a struct by reference and loops over the struct's vectors (the shape of most programs: fn restock(ref<Inventory, exclusive> inv, …)) runs as plain code too, wherever the struct lives, when that reference is the function's only one, its other parameters are plain values, and it uses each such vector only in the ways listed above (no &inv.counts handed to another function, no element borrowed and kept). The function then checks once, as it starts, that no element of those vectors is borrowed elsewhere, and otherwise runs its checked code, which faults where §08 says it must. Its other fields (inv.owner, inv.total) are used freely. Over 1,000,000 elements through such a reference, a write v[i] = v[i] + 1 went from 546 to 1.8 ns an element, pop from 280 to 8, push from 41 to 9: the speed of a local.

Functions over a struct that owns vectorsruns
import std;

struct Inventory
{
    String owner;
    Vec<i64> counts;
    Vec<u8> flags;
    i64 total;
}

// One reference, to a struct whose vectors it only indexes, pushes,
// swaps and measures: checked once as it starts, then plain code.
fn restock(ref<Inventory, exclusive> inv, i64 amount)
{
    for (usize i = 0; i < Vec::len(&inv.counts); i += 1)
    {
        if (inv.counts[i] < 3)
        {
            inv.counts[i] += amount;
            inv.flags[i] = 1;
        }
        inv.total += inv.counts[i];
    }
    Vec::push(&mut inv.counts, amount);
    Vec::push(&mut inv.flags, 0);
    Vec::swap(&mut inv.counts, 0, Vec::len(&inv.counts) - 1);
}

fn main()
{
    Inventory inv = Inventory { .owner = String::from_str("shop"), .counts = Vec::new(), .flags = Vec::new(), .total = 0 };
    foreach (i in 0..8: i64)
    {
        Vec::push(&mut inv.counts, i % 5);
        Vec::push(&mut inv.flags, 0);
    }
    restock(&mut inv, 10);
    printf("%s: %v items, total %v, first %v\n", &inv.owner, Vec::len(&inv.counts), inv.total, inv.counts[0]);
}
output
shop: 9 items, total 73, first 10
The same sum of 20,000,000 elements, ten timesTime, relative
A local Vec, indexed1
The same Vec, passed as &v to a helper that indexes it1
A Vec field of a local struct, indexed in the loop (h.v[i])1
A Vec field of a struct passed to that helper (sum_helper(&h.v)), or moved out for the loop and back1
A local Vec while a shared reference to one element is held1
A local Vec that is moved to another binding after the loop10

Predicates. Vec::retain, Vec::position and Vec::binary_search_by run as plain loops when the function they are given is a closure literal written at the call, a named function, or a fn value holding a named function, provided it only reads the elements it is given (Vec::sort_by does so for a closure literal): 1,000,000 elements through retain took 3 to 9 ns each. A closure stored in a variable first (fn(…) : bool p = [lim](…) { … };) is tested when the call starts: one that only reads what it is given is called with bare element addresses, about 150 ns an element (the cost of calling through the variable); one that may keep what it is given is lent each element through the runtime, about a microsecond. Writing the closure at the call is fastest.

Updating elements

Every way of updating the elements of a Vec of structs compiles to the element's address and a few plain instructions, as long as the vector is confined: v[i].x += v[i].vx, foreach (p in &mut v) { p.x += p.vx; }, a call that receives one element, step(&mut v[i]), and an element named for a block, ref<P, exclusive> p = &mut v[i]; (provided the rest of that block uses p and not v). Over 100,000 elements updated 100 times, all four took the same time. Choose whichever reads best:

Updating a Vec of structsruns
import std;

struct Particle
{
    i64 x;
    i64 vx;
}

fn main()
{
    Vec<Particle> ps = Vec::new();
    foreach (k in 0..1000: i64)
    {
        Vec::push(&mut ps, Particle { .x = k, .vx = k % 7 - 3 });
    }
    foreach (t in 0..100)
    {
        foreach (p in &mut ps)              // plain code: the loop holds ps for its length
        {
            p.x += p.vx;
            if (p.x < 0 || p.x > 1000)
            {
                p.vx = -p.vx;
            }
        }
    }
    printf("%v %v\n", ps[0].x, ps[999].x);
}
output
294 803

Functions that take references

A function that takes a reference (ref<T, …>) or a slice runs as plain code, with no tracking inside it, when cobc can see that nothing else can clash with that reference while the call runs. It can see it in three cases: the reference is the function's only one; all its references are shared; or, the usual shape of code that builds something from its input, some are exclusive and the rest shared and every one is to plain data. In the third case the function checks once, as it starts, that its arguments do not overlap, and then runs unchecked; where they do overlap (count(&mut c, &c.buf[0..4])), it runs the checked code instead, which faults where §08 says it must. Plain data is numbers, bool and char, and arrays, structs and enums of them; not a String, a Vec, a Box or a reference. And in every case the body must use each reference only directly:

  • through it: reads and writes of the value, its fields and its elements, and slice_len;
  • never handed to another function, kept in a variable or a struct, or borrowed from (an only reference may still be handed whole to a function that returns plain data or nothing);
  • beside parameters that are plain values, in the third case.
10,000,000 bytes summed into a struct of two countersTime, relative
fn feed(ref<Acc, exclusive> a, slice<u8, shared> data)1
fn feed(Acc a, slice<u8, shared> data) : Acc, the counters passed in and returned0.7
The first, each byte read through a helper, a.sum += byte_at(data, i)80
The first, with Acc also holding a Vec90

So keep the loop over the input in the function itself, and keep the state it updates in a struct of plain fields. Where that state belongs to something larger (a struct that also holds a String or a Vec), pass the plain part alone, as count(&mut r.counts, …) does here:

State and inputruns
import std;

struct Counts
{
    u64 bytes;
    u64 newlines;
    u64 sum;
}

struct Report
{
    String name;
    Counts counts;
}

// One exclusive reference to plain data beside a shared slice, each used
// only directly: checked once as the call starts, then plain code.
fn count(ref<Counts, exclusive> c, slice<u8, shared> data)
{
    for (usize i = 0; i < slice_len(data); i += 1)
    {
        c.bytes += 1;
        c.sum = wrapping_add(c.sum, widen<u64>(data[i]));
        if (data[i] == 10)
        {
            c.newlines += 1;
        }
    }
}

fn main()
{
    Vec<u8> data = Vec::new();
    foreach (k in 0..1000)
    {
        Vec::push(&mut data, narrow_wrapping<u8>(k * 7));
    }
    Report r = Report { .name = String::from_str("input"), .counts = Counts { .bytes = 0, .newlines = 0, .sum = 0 } };
    count(&mut r.counts, &data[0..$]);    // the struct holds a String: pass only its plain part
    count(&mut r.counts, &data[0..10]);
    printf("%v: %v bytes, %v newlines, sum %v\n", &r.name, r.counts.bytes, r.counts.newlines, r.counts.sum);
}
output
input: 1010 bytes, 4 newlines, sum 126831

Text in hot code

std takes text it only reads as a StringView (D-0134), and for most code that is the right choice. In compiled code a view is not quite free, though: making one from a String (String::as_view) costs about 100 nanoseconds for its checks, and every piece split returns holds its own borrow of the text. For a function of your own that a hot loop calls millions of times:

1,000,000 calls of a function that reads a 60-byte textTime, relative
fn f(StringView s), one view made before the loop and passed each time0.1
fn f(ref<String, shared> s), called f(&text)1
fn f(StringView s), called f(String::as_view(&text))1.8
fn f(String s), called f(String::clone(&text))14
  • A function that only reads its StringView (indexes it, takes its length, hands it to std's readers) costs nothing to call: the view is passed as its address and length. Make the view once, outside a hot loop: each String::as_view costs about 100 nanoseconds.
  • To process input line by line, use a read_line loop: read_all followed by String::split took about nine times as long over the same 200,000 lines, the price of keeping every line's view at once.
  • Calls to std that take a view cost per call, not per byte (a 620-byte text cost the same as a 60-byte one).

Building and writing text

Building one String from 2,000,000 numbersTime, relative
String::append(&mut out, i) then String::append(&mut out, ",")1
sprintf("%v,", i), then append that9
A Vec<String> of pieces (each made by sprintf), then String::join19

String::append formats a number or a bool straight into the String, with no intermediate text, so build output with it; sprintf makes a new String each time, about two-thirds of a microsecond a call. Standard output is buffered (§21), so printf per line is a fast way to write many lines: 2,000,000 lines to a file took 0.35 s with one printf per line and 0.24 s building one String with String::append and printing it once. Printing as you go is fast enough for most programs and keeps no text in memory; building one String saves a little more where all the text fits.

Choose the collection by what you do most

OperationFastSlowMeasured
Take elements from the front, in orderQueue::pop_front, or Vec::reverse once then Vec::popVec::remove(&mut v, 0), which moves every remaining element: the cost grows with the square of the length10,000 elements: 2.4 times; 100,000: 51 times
Test membership in a large setVec::sort once then Vec::binary_search, or HashSet::containsVec::contains, which compares element by element20,000 keys, 5,000 queries: binary_search 5 to 6 times as fast, HashSet 1.2 times, growing with the size
Fill a Vec whose size you knowVec::filled(n, x), then write the elementspushing one by one (with or without Vec::reserve first, which no longer saves anything measurable)50,000,000 elements: 5 to 6 times

Counting with a HashMap<String, …>: *HashMap::entry(&mut counts, String::clone(w), 0) += 1 clones the key on every call; looking the key up first, match (HashMap::get_mut(&mut counts, w)) { Some(c) : *c += 1, None : … }, clones only for a new key. Over 1,000,000 words the second was about 8% faster, but both are a few hundred nanoseconds a word: write whichever reads better.

Threads

Each running thread keeps its own share of the compiled program's runtime checks, so threads doing their own work run in parallel (§19). Measured with four threads each counting the words of the same text: 0.30 s, against 0.16 s for one thread doing a quarter of the work, and 0.24 s for four separate programs. To keep a thread's work its own:

  • Move what a thread works on into it (spawn(count, chunk) with chunk a Vec you no longer need): it then belongs to the thread. A thread that reads the spawner's data through a reference shares it. Read it through a shared reference *parameter* the function never rebinds or borrows (fn band(ref<Vec<Row>, shared> rows, …)), and hand its fields and elements on only to functions that keep nothing of them: cobc then reads through it with bounds checks alone, since nothing can change what it reaches while the call runs, and several threads read one structure at once (four threads aggregating 20,000 rows: 30 ms, against 48 ms for one). A reference kept past the call, or stored in a struct, is tracked as usual, and threads that form such references into one structure take turns on its records.
  • Hand results back as the thread's result, through join, or through a Channel, rather than writing into a structure every thread locks.
  • Keep a lock's block to reads and writes through its guard ({ auto g = lock(&m); *g += 1; }): such a block is a lock and an unlock and nothing more. Four threads each taking one shared mutex 200,000 times, with a little work between, took 0.6 s; giving the guard to a function, or returning from inside the block, makes each lock cost about ten times as much.
  • A thread per connection is a sound design for a server. Each thread is an operating-system thread with a reserved, mostly uncommitted stack, and a finished thread is reused by the next spawn. A test server held 10,000 connections at once, each its own supervised thread answering two queries, in 250 MB, about 25 kB per idle connection, with no failures, and a round trip cost the same 0.13–0.16 ms with 1,000 connections open as with 10,000. An idle connection costs nothing while it waits: on Linux its thread sleeps in the kernel until data arrives or it is cancelled.

Arithmetic and plain values

  • Every +, - and * on integers is checked for overflow (§06). Where wrapping around is what you mean (hashes, checksums, random number generators), say so with wrapping_add, wrapping_mul and their relatives: a tight sum took a third less time, and Clang executed 40% of the instructions. Never use them to avoid a check on arithmetic that should not overflow: the check is what turns a wrong result into a diagnostic.
  • Pass a struct by whichever reads best. A 64-byte struct of eight i64 passed to a function that reads it took the same time copied as through a ref<…, shared>.

When nothing else is enough

A kernel that must run at the speed of hand-written C can be hand-written C: extern "./kernel.c"; links it and unsafe extern fn declares it (§20). That code is outside every guarantee CobaltC makes: its memory safety, its aliasing and its arithmetic are yours to get right, and its declaration is an unchecked claim. Measure first.

Rules of thumb
  • Measure the -O2 build, never coby or -O0.
  • Keep a hot Vec a local (or a parameter it owns) that only its own name reaches; across a loop, hold shared element references only, never an exclusive one.
  • Index a struct's Vec field (h.v[i]) where the function only indexes it; where it also pushes to it or hands it on, loop with foreach or a helper.
  • Give a function over a struct's vectors the struct as its only reference, and keep its uses of the vectors to indexing and the Vec calls listed above.
  • In a function that takes references, use them only directly: loop over a slice in the function itself, and update plain state through one exclusive reference (or pass it by value).
  • Make a StringView once, outside a hot loop; passing it on is free.
  • Build text with String::append; printf per line is fine.
  • Write a child process's input in large pieces (ChildInput::write, Child::write_input): each write goes straight to the pipe and wakes the child, so a write per line of 100,000 short lines cost 11 s of system time, and 64 KB at a time 0.06 s. Nothing is buffered, so a program that writes a line and then waits for the child's answer works as written.
  • Never Vec::remove(&mut v, 0) in a loop over a long Vec; for membership in a large set, sort once and Vec::binary_search, or use a HashSet.
  • Make a Vec of a known size with Vec::filled, then write its elements.
  • Say wrapping_ only where wrapping is the meaning.

AAppendix: diagnostics

Every named diagnostic, what it means, and the usual fix.

Every diagnostic has a stable name. Static means the program is rejected before it runs; dynamic means a checked fault terminates the program; both means the same rule is applied statically where the checker can see the problem and dynamically otherwise.

DiagnosticPhaseMeaningUsual fix
diag.syntax-errorstaticthe text is not a CobaltC program by the grammarcorrect the text there; the message names what was expected and what was found
diag.unbound-namestatica name resolves to nothingdeclare it, fix the spelling, or import it
diag.ambiguous-namestatictwo imports or two enums supply the same namequalify it
diag.name-not-visiblestaticthe item is private to another modulemark it export or move the use
diag.module-file-not-foundstaticmodule m "…"; names no readable filefix the path (relative to the declaring file) or create the file
diag.module-cyclestatica file's module body loads itself again, directly or through other filesmove the shared items into a third file both name
diag.module-file-duplicatestaticone file is named by two module declarationsdeclare it once; reach it by qualified path or import
diag.const-not-constantstatica const's initializer is not a constant expression, or a constant depends on itselfuse only constants in it (a local const helps), or compute the value in a function
diag.static-assert-failedstatica static_assert condition is false, for the program or for one instantiationfix what the assertion guards, or the assertion
diag.static-assert-not-constantstatica static_assert condition (or message argument) is not a bool constant expressioncheck run-time values with assert; name compile-time values with const
diag.duplicate-itemstatictwo declarations add the same qualified namerename one, or move it into a module
diag.duplicate-localstatica name declared twice in one block (a function's parameters and a loop's or pattern's names count as declared in the block they govern)assign to the existing variable, or choose another name; an inner block may shadow it
diag.foreign-associated-fnstaticfn T::name in a module that does not declare T (for example fn Vec::first_or outside std)declare a plain function, or put it in T's module
diag.no-mainstaticno fn main() at the root module (a main inside a module is just m::main)declare fn main() at the root, or run the file that has one
diag.div-overflowbothmin / -1 or min % -1 for a signed typechecked_div, or widen first
diag.transfer-without-authoritybotha move from a binding that no longer holds its resource (already moved or destroyed)move from the current holder
diag.type-mismatchstatican operand or argument has the wrong typeconvert explicitly; fix the types
diag.unbounded-type-parameterstaticarithmetic, comparison, field access or a call on a bare Tuse a concrete type
diag.cannot-infer-type-parameterstatica generic argument is fixed by nothingwrite name<T>(…) or declare the variable's type
diag.break-outside-loop, diag.return-outside-fnstaticmisplaced control statementmove it
diag.capture-list-mismatchstaticthe closure's […] does not equal its free variablesadd or remove names
diag.stale-bindingbothuse of a moved-from, dropped, or dead binding or referenceuse the current owner; re-fetch after a move or reallocation
diag.use-of-uninitializedstatica read on a path with no prior writeinitialise on every path; initialise aggregates whole
diag.reference-escapes-scopestatica reference stored or returned beyond its referent's blockreturn an owned value
diag.lifetime-elision-ambiguousstatica reference returned from a function with 0 or 2+ reference parametersone reference parameter, or return owned
diag.aliasing-conflictbotha read or write while a conflicting reference is liveend the earlier borrow first
diag.borrow-exceeds-sourcestatic&mut taken through a shared referencetake ref<T, exclusive>
diag.write-through-sharedstaticassignment through a shared referenceas above
diag.destroy-while-aliasedbothdrop or scope exit while a reference or guard is livelet the reference's holder end first
diag.move-while-aliasedbotha move while a reference is liveend the reference before moving
diag.move-out-of-fieldstaticmoving a resource out of a live struct's field or through a referencemove the whole container, or match a reference (match (&e))
diag.borrow-of-non-place, diag.borrow-of-temporarystatic& applied to a value or temporarybind it first
diag.no-destroy-authority, diag.transfer-without-authoritybothdestroying or moving something this thread does not owndestroy in the owning thread, once
diag.overwrite-of-live-resourcebothassigning over a resource that is still owned and is not quiet (its end runs a destructor or releases something)overwrite(&mut x, v), or drop first
diag.unwrap-faileddynamicunwrap or expect of a None or an Errmatch, or unwrap_or with a default
diag.assert-faileddynamican assert condition is false (the formatted message, if any, is shown)the condition names what was assumed; find why it did not hold
diag.read-of-resourcestatica resource used where a copy would be made (==, a value read)move it or take a reference
diag.bad-destructor-signature, diag.direct-destructor-callstaticdrop declared wrongly or called by handfn T::drop(ref<T, exclusive> self); use drop(x)
diag.destructor-on-plain-typestaticT::drop declared for a type that is not a resourcedeclare the type resource struct T (or resource enum T)
diag.arith-overflowboth+ - * or negation left the type's rangewiden, or use wrapping_/saturating_/checked_
diag.div-by-zero, diag.div-overflowbothdivision by zero; MIN / -1test first or checked_div
diag.shift-amount-out-of-rangebothshift by the bit width or moremask the amount
diag.narrowing-overflowbothnarrow or to_int of a value that does not fit, or a value too wide for a bitstruct fieldchecked_narrow or narrow_wrapping, or widen the target
diag.result-discardedstatican expression statement whose value is a Result, so its error would vanishhandle it (?, match, unwrap), or write _ = e; to ignore it on purpose
diag.literal-out-of-rangestatica literal that does not fit its typefix the literal or its type
diag.recursive-typestatica struct or enum contains itself by valueput the recursive part behind Box<T>, Rc<T> or Vec<T>
diag.index-out-of-boundsbotharray or Vec index, or str_byte position, at or past the lengthcheck the index first
diag.not-asciidynamicString::push_ascii given a byte of 128 or morebuild the bytes in a Vec<u8> and check them with String::from_utf8
diag.not-char-boundarydynamica StringView bound inside a character's UTF-8 encodingmove the bound to a character's start, e.g. with StringView::find
diag.non-exhaustive-matchstatica variant with no arm and no _add arms
diag.unreachable-armstatican arm whose every value an earlier arm already takes (arms are tried in order)remove it, or move it before the arm that shadows it
diag.propagate-outside-fallible-contextstatic? in a function whose return type is not Result<_, E> with the same Echange the return type or map_err
diag.format-invalidstatica printf-family format that is not a string literal, or a malformed % specifierfix the specifier; write %% for a literal %
diag.spawn-borrow-closurestaticspawn given a closure that borrowsadd move
diag.mutex-reentrant-lockdynamiclocking a mutex this thread already holdsrelease the guard first
diag.channel-zero-capacitydynamicChannel::new given a capacity of 0give the number of values it may hold; 1 for a hand-over
diag.channel-deadlockdynamicthe program's only running thread waits on a full or empty channel that is not closedsend or receive from another thread, or close the channel first
diag.stack-exhausteddynamiccalls nested deeper than the stack the implementation providesrecurse less deeply: a loop, or an explicit stack in a Vec
diag.trusted-outside-unsafestatica raw or extern operation outside unsafewrap it in unsafe after checking the precondition
diag.extern-non-ffi-typestatican unsafe extern fn with a non-FFI parameter or resultpass scalars or rawptr
diag.alloc-failuredynamicVec or Rc could not allocatecall allocate yourself if you need to recover