# CeeSafe > CeeSafe is a memory-safe systems programming language with C#- and TypeScript-style syntax, compiled ahead of > time to portable C11. Ownership and borrowing are checked at compile time; there is no garbage collector. Its > compiler, ceesafec, is written in CeeSafe. Agents writing CeeSafe: read the agent guide first, and use `ceesafec mcp` (an MCP server with a checker, fixes, a sandboxed interpreter and this documentation) or `ceesafec check --diagnostics-format=json` and `ceesafec fix`. Before concluding that a function or method does not exist, look it up: `ceesafe_api` (MCP) with its name, or the built-in types page and the lib/core and lib/std API pages below. # What CeeSafe is CeeSafe is a memory-safe systems programming language. Its syntax will look familiar if you know C# or TypeScript; its safety comes from ownership and borrowing, as in Rust. It is compiled ahead of time to portable C11, so it runs wherever a C compiler does: on Linux, macOS and Windows, on bare metal, and inside an operating system kernel. ```ceesafe struct Account { id: u64; balance: i64; public fn deposit(borrow mut this, amount: i64) { this.balance += amount; } drop { Console.write("closing account "); Console.writeLine(this.id); } } fn main() { var account = Account { id: 7, balance: 0 }; account.deposit(100); Console.writeLine(account.balance); } ``` ```text 100 closing account 7 ``` ## What it promises In safe code, which is all code outside an `unsafe` block, CeeSafe rules out: - **use after free and double free:** every value has one owner, and it is destroyed exactly once; - **dangling references:** a reference cannot outlive what it refers to, which the compiler checks without lifetime annotations in most code; - **data races between a reference and a write:** while something is borrowed it cannot be changed, and while it is borrowed mutably nothing else can read it; - **out-of-bounds access and reading uninitialised memory:** indexes are checked, and a variable must be assigned before it is read; - **silent integer overflow:** arithmetic that overflows stops the program, in every build. Where wrapping is meant, the code says so (`x.wrappingAdd(y)`). It does this **without a garbage collector or reference counting.** Destruction is deterministic: a value is destroyed at the end of its scope, or when its owner is, in an order the language defines. ## What makes it different - **Lifetimes without annotations, mostly.** A function that takes and returns references names the parameters its result borrows from (`-> borrow(a, b) T`). Only types that store references, and functions over them, declare region parameters. - **Allocation is explicit and pluggable.** Each allocation chooses its allocator: the system heap, an arena, a pool, a fixed buffer, a scratch frame, or your own. Allocation failure is a value (`Result`), handled or propagated with `?`. - **The compiler shows its work.** It can explain which boxes it placed on the stack and which bounds and overflow checks it removed, and why. - **C you can read.** The generated C is structured and warning-free, maps back to CeeSafe lines for debuggers, and has no undefined behaviour by design. A reference interpreter runs the same programs and stops on any memory error, which makes it a second implementation to test the compiler against. - **Built for kernels too.** A freestanding profile without a C library, context effects that reject sleeping while a spin lock is held, C structs imported from the C codebase's own headers, and lock guards and reference-counted kernel objects. All of this is exercised inside [CosmoOS](https://github.com/vkuttyp/CosmoOS), an operating system written in C. ## Status CeeSafe is **pre-1.0**. Its compiler, `ceesafec`, is written in CeeSafe and compiles itself. The language and its tools work end to end, and are tested against a corpus of about 550 programs. But nobody has used it in production. What is done and what is next are in the repository's `ROADMAP.md`. Some things it does not have yet: trigonometry and logarithms, iterator adapters (`map`, `filter` and the like; loops do their work). Each is planned, or recorded as an open decision. ## Where to go next - [Install the compiler](installation.md) and [write a first program](getting-started.md). - Read [the language guide](basics.md), chapter by chapter. - Coming from another language? Start with [CeeSafe for Rust, Zig, C, C# and Go programmers](coming-from/index.md). - The full rules are in the [specification](../spec/README.md): the grammar, the type system and the ownership rules. # Installation ## Install ceesafec On macOS and Linux: ```sh curl -fsSL https://ceesafe.github.io/CeeSafe/install.sh | sh ``` On Windows, in PowerShell: ```powershell irm https://ceesafe.github.io/CeeSafe/install.ps1 | iex ``` The script downloads the latest release of `ceesafec` for your machine from [GitHub](https://github.com/CeeSafe/CeeSafe/releases), checks its SHA-256, and installs it in `~/.ceesafe/bin` (`%USERPROFILE%\.ceesafe\bin`). On macOS and Linux it then tells you how to add that directory to your `PATH`; on Windows it adds it for you. `CEESAFE_VERSION=v0.1.0` installs a given release instead. Run the same line again to update. Releases are built for macOS (arm64 and x64, signed and notarized by Apple), Linux (x64 and arm64; glibc 2.35 or newer: Ubuntu 22.04, Debian 12, Fedora 36 and later) and Windows (x64 and arm64). You can also download an archive from the [releases page](https://github.com/CeeSafe/CeeSafe/releases) yourself: each holds `ceesafec` (`ceesafec.exe`), and `SHA256SUMS` lists their checksums. `ceesafec` is one native executable: its runtime, its libraries and this specification are inside it, and it needs nothing else to run. ```sh ceesafec --version ``` ## Requirements | | For | Notes | |---|---|---| | A C compiler: clang, gcc or MSVC | `ceesafec build`, and building `ceesafec` from source | clang and gcc are fully supported; MSVC is best-effort (`/std:c11`). `check`, `run --interp`, `fmt`, `lsp` and `mcp` need none | | git | dependencies from git repositories | only for [packages](packages.md) that use them | | Node.js and [Vite+](https://viteplus.dev/) | the VS Code extension | optional | ## Build from source To work on CeeSafe itself, or on a platform without a release, build `ceesafec` from the repository. It is written in CeeSafe and builds itself, so the first build of a checkout needs a `ceesafec`: a release's, or one built from a release's C seed with a C compiler alone. With a released `ceesafec` (any platform's archive from the [releases](https://github.com/CeeSafe/CeeSafe/releases), or the one `install.sh` put in `~/.ceesafe/bin`): ```sh git clone https://github.com/CeeSafe/CeeSafe.git cd CeeSafe sh tools/bootstrap_release.sh ~/.ceesafe/bin/ceesafec HEAD out/ceesafec out/ceesafec --version ``` `tools/bootstrap_release.sh` builds, with the release, the commits recorded for it in the script (a compiler cannot always build a tree whose own sources use library code added after it), then the tree you checked out. On a platform with no release, build a `ceesafec` from `ceesafec--seed.tar.gz`, attached to every release: it is ceesafec's own C, and its `README.md` gives the one `cc` (or `cl`) command. That `ceesafec` builds the tree of its release's tag directly. After that, `ceesafec` rebuilds itself: ```sh out/ceesafec build -o out/ceesafec.new && mv out/ceesafec.new out/ceesafec ``` `ceesafec` is one native executable: its runtime, its libraries and this specification are inside it, so it can be copied anywhere, for example onto your `PATH` as `~/.local/bin/ceesafec`. To check everything, run the contract suite (the repository's `spec/testing.md`; it compiles and runs the corpus many times, checks the fixed point, and sweeps allocation failures): ```sh python3 tools/contract_tests.py --ceesafec out/ceesafec ``` ## Check that it works ```sh cat > hello.cee <<'EOF' fn main() { Console.writeLine("Hello, CeeSafe"); } EOF ceesafec build hello.cee -o hello && ./hello # through C, to a native program ceesafec run --interp hello.cee # or in the reference interpreter ``` `ceesafec build` uses the C compiler in `$CC`, or `cc`; `--cc clang` picks one. On Windows it finds one itself: clang (`winget install LLVM.LLVM`), MinGW's gcc, or Visual Studio's `cl`, and it sets up Visual Studio's headers and libraries for clang and `cl` when you are not in a developer prompt. So with LLVM and Visual Studio's "Desktop development with C++" workload (or its Build Tools) installed, `ceesafec build` works from any terminal and makes `out\hello.exe` ([tooling](../spec/tooling.md) §8). ## Editor support The repository has a VS Code extension in `editors/vscode`: syntax highlighting, and through `ceesafec lsp` errors as you type, hover, go to definition, the outline and formatting. See [Editors](editors.md) to build and install it, and to use the language server from other editors. # Getting started This page makes a package, builds and runs it, and shows the commands you will use every day. It assumes `ceesafec` is [installed](installation.md) and on your `PATH`. ## A new package ```sh ceesafec new hello cd hello ``` `ceesafec new` writes a manifest, a first source file, and instructions for AI coding agents: ```text hello/ ├── ceesafe.toml ├── src/ │ └── main.cee ├── AGENTS.md └── .gitignore ``` `AGENTS.md` tells an agent working in the package what CeeSafe is and how to check its work. With `ceesafec new hello --agent claude` (or `gemini`, `copilot`, `cursor`) it also writes that agent's own file and configures CeeSafe's MCP server for it; `--no-agent` writes none. See [CeeSafe for AI agents](for-agents.md). `ceesafe.toml` names the package and says what it builds: ```toml [package] name = "hello" version = "0.1.0" kind = "executable" [dependencies] ``` and `src/main.cee` is a program: ```ceesafe fn main() { Console.writeLine("Hello from hello"); } ``` ```text Hello from hello ``` ## Build and run Inside a package, the commands need no file names: ```sh ceesafec build # compiles to C, then to out/hello ./out/hello ceesafec run --interp # runs the program in the reference interpreter instead ceesafec check # type and borrow checks only: the fastest way to find errors ceesafec fmt # formats the package's sources in the canonical style ``` Every `.cee` file under `src/` belongs to the package, and all of them share one scope: a function declared in one file can be called from another without an import. ## A slightly larger program Replace `src/main.cee` with: ```ceesafe /// The number of steps the Collatz sequence takes from `n` to 1. fn steps(n: u64) -> u32 { var value = n; var count: u32 = 0; while (value != 1) { value = if (value % 2 == 0) value / 2 else 3 * value + 1; count += 1; } return count; } fn main() { var longest: u64 = 1; for (n in 1..1000u64) { if (steps(n) > steps(longest)) { longest = n; } } Console.write("longest below 1000: "); Console.write(longest); Console.write(", "); Console.write(steps(longest)); Console.writeLine(" steps"); } ``` ```text longest below 1000: 871, 178 steps ``` `3 * value + 1` is checked: if it overflowed a `u64`, the program would stop with a message naming the line, in every build. CeeSafe never wraps silently. ## When the compiler says no Errors name a code, the place, and usually the fix. This program keeps a reference to a vector's element and then grows the vector, which could move the element: ```ceesafe fn main() { var numbers = Vector.create(); numbers.push(1)!; let first = borrow numbers[0]; numbers.push(2)!; Console.writeLine(first); } ``` ```text main.cee:5:5: error[N0241]: cannot borrow 'numbers' as mutable while 'numbers[0]' is borrowed | 5 | numbers.push(2)!; | ^^^^^^^ = note: 'numbers[0]' is borrowed here (4:5) = note: the borrow is used later here (6:5) ``` The fix is to finish with `first` before the push, or to copy the value (`let first = numbers[0];`). Every code is explained in [Diagnostics](../spec/diagnostics.md). ## Files without a package For a quick experiment, a package is not needed: give the files to the command. ```sh ceesafec build tool.cee helpers.cee -o tool ceesafec run --interp tool.cee ``` ## Next - [Basics](basics.md): values, types, functions and control flow. - [Packages](packages.md): dependencies, the lock file, and sharing code. - [The ceesafec command](ceesafec.md): every command and option. # CeeSafe for AI agents This page is for AI agents that write CeeSafe, and for the people who set them up. CeeSafe is new, so a model has seen little of it: it will write C#, TypeScript or Rust that looks like CeeSafe and is not. The compiler is built to correct that quickly. It says exactly what is wrong, fixes what it is sure of, and serves its own specification. The guide and the specification are inside `ceesafec`: an agent reads them through `ceesafec explain` and the MCP server's resources (`ceesafe://docs/…`, below), so it needs no network access to read the rules. On the web they are also in machine-readable form: [llms.txt](https://ceesafe.github.io/CeeSafe/llms.txt) (an index) and [llms-full.txt](https://ceesafe.github.io/CeeSafe/llms-full.txt) (the guide, the specification and the library reference in one file), and every page is available as Markdown at its address with `.md` added. The library reference is [lib/core](../spec/api/core.md) and [lib/std](../spec/api/std.md), and the compiler's own types and functions (`Console`, `File`, `Vector`, `StringView`, integer methods, …) are on [built-in types and functions](builtins.md). ## In a new package `ceesafec new` writes `AGENTS.md`, which most coding agents read: what CeeSafe is, the package's layout, the commands that check the work, how to use the MCP server, and the common mistakes below. Name your agent to get its own file and its MCP configuration too: ```sh ceesafec new hello --agent claude # also CLAUDE.md (importing AGENTS.md) and .mcp.json ``` | `--agent` | Writes | |---|---| | `agents` (the default) | `AGENTS.md` | | `claude` | `CLAUDE.md`, `.mcp.json` | | `gemini` | `GEMINI.md`, `.gemini/settings.json` | | `copilot` | `.github/copilot-instructions.md`, `.vscode/mcp.json` | | `cursor` | `.cursor/rules/ceesafe.mdc`, `.cursor/mcp.json` | CeeSafe's part of `AGENTS.md` sits between `` and ``. After updating `ceesafec`, run `ceesafec agents` in the package to refresh that part; whatever you wrote outside it stays as it is. ## Set up the MCP server `ceesafec mcp` is a [Model Context Protocol](https://modelcontextprotocol.io) server. Add it to your agent once: ```sh claude mcp add ceesafe -- ceesafec mcp # Claude Code ``` or, for any MCP client: ```json { "mcpServers": { "ceesafe": { "command": "ceesafec", "args": ["mcp"] } } } ``` Its tools: | Tool | What it does | |---|---| | `ceesafe_check` | type-checks code and returns its diagnostics as JSON, with fixes | | `ceesafe_fix` | applies every fix the compiler is sure of, and returns the files and what remains | | `ceesafe_format` | formats a file in the one canonical style | | `ceesafe_run` | runs a program in a sandboxed interpreter: no files, sockets or processes, and a step and memory budget | | `ceesafe_emit_c` | shows the generated C, and where allocations, bounds checks and overflow checks went | | `ceesafe_explain` | what a diagnostic code means | | `ceesafe_spec_search`, `ceesafe_spec_section` | search and read the specification, which is embedded in `ceesafec` | | `ceesafe_symbol` | the type and declaration of what is at a position | | `ceesafe_api` | a library or built-in name looked up (`TextWriter.decimal`, `Process.argument`): its signature and documentation, or the closest names | The prompt `write-ceesafe` gives an agent this page and the loop below. The specification and this guide are resources (`ceesafe://docs/…`). The [tooling reference](../spec/tooling.md) §6 has the details. ## The loop 1. Write the code. 2. Check it: `ceesafe_check`, or `ceesafec check --diagnostics-format=json`. 3. Apply the fixes: `ceesafe_fix`, or `ceesafec fix`. A fix is offered only where the compiler is sure of it: `mut` before an argument, `let` to `var`, `truncate(…)` for a narrowing cast, a missing `;`. 4. Fix the rest yourself. Each diagnostic has a code; `ceesafe_explain` (or `ceesafec explain N0249`) says what it means, and the message often names the specification section to read. For a function or method you are not sure of, look it up with `ceesafe_api` before writing it, rather than guessing a name from another language. 5. Repeat until there are no errors. Then run it with `ceesafe_run`, or build it with `ceesafec build`. Without MCP, the same loop works from a shell: `ceesafec check --diagnostics-format=json main.cee` prints one JSON document with every diagnostic, its position and its fixes, and `ceesafec fix main.cee` applies them. ## Common mistakes These are the mistakes models make most often in CeeSafe, each with the compiler's answer and what to write instead. ### There is no `~` ```ceesafe fn invert(x: u32) -> u32 => ~x; ``` ```text main.cee:1:29: error[N0102]: expected expression, found '~' | 1 | fn invert(x: u32) -> u32 => ~x; | ^ ``` Complement with an exclusive or of all ones: ```ceesafe fn invert(x: u32) -> u32 => x ^ 0xFFFFFFFF; ``` ### There are no type aliases ```ceesafe type Count = u32; ``` ```text main.cee:1:1: error[N0101]: expected 'fn', 'const', 'struct' or 'enum', found 'type' | 1 | type Count = u32; | ^^^^ ``` Write the type itself, or wrap it in a struct when it deserves a name of its own. ### There are no tuples ```ceesafe fn main() { let (a, b) = (1, 2); } ``` ```text main.cee:2:9: error[N0100]: expected identifier, found '(' | 2 | let (a, b) = (1, 2); | ^ ``` Declare two variables, or return a struct with named fields. ### Arrays of structs are not compared with `==` `==` compares arrays and slices of integers, `bool` and fieldless enums. Arrays of structs are a different matter: ```ceesafe struct P { x: i32; } fn same(a: [P; 2], b: [P; 2]) -> bool => a == b; ``` ```text main.cee:3:42: error[N0207]: '==' compares slices and arrays of integers, bools and fieldless enums, not of 'P' (type-system.md §16.2) | 3 | fn same(a: [P; 2], b: [P; 2]) -> bool => a == b; | ^^^^^^ ``` Compare element by element: ```ceesafe struct P { x: i32; } fn same(a: [P; 2], b: [P; 2]) -> bool { for (i in 0..a.length) { if (a[i].x != b[i].x) { return false; } } return true; } ``` ### Indexes are `usize`, and a range's type comes from its bounds A range of two literals that the body indexes with is a `usize` range, so this works as written: ```ceesafe fn sum(values: Slice) -> u64 { var total: u64 = 0; for (i in 0..4) { total += values[i]; } return total; } ``` The variable then is a `usize` everywhere in the body. Using it as an `i32` as well is an error: ```ceesafe fn fill(values: SliceMut) { for (i in 0..4) { let v: i32 = i; values[i] = v; } } ``` ```text main.cee:3:22: error[N0205]: expected 'i32', found 'usize' | 3 | let v: i32 = i; | ^ ``` A range's type otherwise comes from its bounds: `0..4` with no index use is a range of `i32`, and `0..values.length` is a `usize` range. Convert explicitly where the two meet: ```ceesafe fn fill(values: SliceMut) { for (i in 0..values.length) { values[i] = truncate(i); } } ``` ### A block has a value only as a branch A branch of an `if` or `match` expression may be a block that ends with its value, written without `;`: ```ceesafe fn sign(x: i32) -> i32 { let s = if (x < 0) { let magnitude = 0 - x; magnitude / magnitude * -1 } else 1; return s; } fn main() { Console.writeLine(sign(-4)); } ``` ```text -1 ``` A statement's block has no value: a `match` statement whose arms end in a value is an error. ```ceesafe fn sign(x: i32) { match (x < 0) { true => { -1 } false => { 1 } } } ``` ```text main.cee:3:21: error[N0100]: expected ';', found '}': a block ends in a value only as a branch of an 'if' or 'match' expression (let x = match …) | 3 | true => { -1 } | ^ main.cee:4:21: error[N0100]: expected ';', found '}': a block ends in a value only as a branch of an 'if' or 'match' expression (let x = match …) | 4 | false => { 1 } | ^ ``` Make it an expression, `let s = match (x < 0) { … };`, and return its value. ### A view of a temporary lasts until the end of its statement A call's result can be borrowed, sliced or measured where it is made, `use(make().view())`, but it is destroyed at the end of the statement, so a view of it cannot be kept: ```ceesafe fn make() -> String { var s = String.create(); s.append("hello")!; return s; } fn main() { let v = make().view(); Console.writeLine(v); } ``` ```text main.cee:8:5: error[N0243]: a temporary value is dropped at the end of this statement while still borrowed; bind it to a local first | 8 | let v = make().view(); | ^^^^^^^^^^^^^^^^^^^^^^ = note: a temporary value is borrowed here (8:5) = note: the borrow is used later here (9:5) ``` Bind the value to a local, and keep the view of the local: ```ceesafe fn make() -> String { var s = String.create(); s.append("hello")!; return s; } fn main() { let text = make(); let v = text.view(); Console.writeLine(v); } ``` ```text hello ``` ### There is no string interpolation `{x}` in a string is printed as written. When `x` is a name in scope, the compiler warns (N0400): ```ceesafe fn main() { let x = 3; Console.writeLine("x = {x}"); } ``` ```text main.cee:3:28: warning[N0400]: this string prints '{x}' as written (there is no string interpolation): write the value in a call of its own, or '\{' to mean the brace | 3 | Console.writeLine("x = {x}"); | ^^^ ``` Write the value in a call of its own: ```ceesafe fn main() { let x = 3; Console.write("x = "); Console.writeLine(x); } ``` ```text x = 3 ``` ### A multi-line string is a raw `"""` literal A `"..."` literal ends on its line. For several lines, open with `"""` at the end of a line and close with `"""` on a line of its own. The closing line's indentation is removed from every line, there is no final newline, and there are no escapes: ```ceesafe fn main() { let query = """ SELECT id, name FROM users """; Console.writeLine(query); } ``` ```text SELECT id, name FROM users ``` ### `for` loops over iterators too `for` takes arrays, slices, ranges, and any value with `fn next(borrow mut this) -> T?`, such as a `split`: ```ceesafe fn main() { let text = "a,b,c"; for (part in text.split(",")) { Console.writeLine(part); } } ``` ```text a b c ``` lib/core's `Iterator` names that shape as an interface, so generic code and `dyn` can iterate too: `fn total>(it: I) -> u32 { … for (x in it) { … } … }`, and `for (x in boxed)` over a `Box>` in a `var`. Rust's iterator chains (`map`, `filter`, `collect`, …) become the loop's body: there is no adapter library. ### A function that always exits returns `never` A call of a `never` function ends its branch, so a `match` that gives a value needs no value from that branch: ```ceesafe fn die(message: StringView) -> never { Console.writeLine(message); Process.exit(1); } fn main() { let n: Result = Ok(7); let value = match (n) { Ok(v) => v, Err(_) => { die("no number"); } }; Console.writeLine(value); } ``` ```text 7 ``` ### Converting with a check: `checkedTruncate` and `saturatingTruncate` `as` only allows conversions that never lose information, so `usize` to `i64` is not one. `checkedTruncate(x)` is `null` when the value does not fit, and `saturatingTruncate(x)` clamps it: ```ceesafe fn main() { let n: usize = 300; let wide = checkedTruncate(n) ?? -1; let small = checkedTruncate(n) ?? 0; Console.writeLine(wide); Console.writeLine(small); Console.writeLine(saturatingTruncate(n)); } ``` ```text 300 0 255 ``` ### A function in a field is not called through the field ```ceesafe struct Counter { step: fn(i32) -> i32; } fn advance(c: borrow Counter, n: i32) -> i32 => (c.step)(n); ``` ```text main.cee:5:49: error[N0210]: this expression cannot be called | 5 | fn advance(c: borrow Counter, n: i32) -> i32 => (c.step)(n); | ^^^^^^^^ ``` Pass the function or closure as a parameter, and call the parameter: ```ceesafe fn advance(step: fn(i32) -> i32, n: i32) -> i32 => step(n); ``` ### A borrowed string in an enum needs a region ```ceesafe enum Failure { Refused(StringView), Io, } ``` ```text main.cee:2:13: error[N0270]: a payload of scoped type 'StringView' needs a region parameter on the enum: 'enum Failure' (type-system.md §25.4) | 2 | Refused(StringView), | ^^^^^^^^^^ ``` A `StringView` borrows its text, so a type holding one says from where. For fixed messages, an enum of reasons and a function that words them is simpler: ```ceesafe enum Reason { TooLarge, BadMagic, } enum Failure { Refused(Reason), Io, } fn describe(reason: Reason) -> StringView => match (reason) { Reason.TooLarge => "too large", Reason.BadMagic => "bad magic number", }; ``` ### A method cannot share a field's name ```ceesafe struct Account { balance: i64; fn balance(borrow this) -> i64 => this.balance; } ``` ```text main.cee:4:8: error[N0228]: 'Account' already has a member named 'balance' | 4 | fn balance(borrow this) -> i64 => this.balance; | ^^^^^^^ ``` Name the method for what it does (`current`), or make the field `public`. ### Look a name up before deciding it is missing A model guesses names from the languages it knows (`unsigned`, `args`, `writeLn`). CeeSafe's names are often different, and the compiler says which ones exist: ```ceesafe fn main() { var buffer = [0u8; 32]; var w = TextWriter.create(mut buffer[..]); w.unsigned(42); } ``` ```text main.cee:4:7: error[N0200]: 'TextWriter' has no method named 'unsigned'; did you mean 'signed', 'decimal' or 'hex'? | 4 | w.unsigned(42); | ^^^^^^^^ ``` `decimal` is suggested because its documentation says "Appends an unsigned integer in decimal". Before writing a call you are unsure of, look the name up: `ceesafe_api` with `TextWriter`, `TextWriter.decimal`, `decimal` or `Process.argument` returns the entries, or the closest names. Without MCP, the [built-in types](builtins.md) and the [lib/core](../spec/api/core.md) and [lib/std](../spec/api/std.md) pages list every name. ### Reserved words are not names Some keywords read like ordinary names: `own`, `type`, `move`, `drop`, `where`, `class`, `yield`, `macro`, `using`, `static` and `in` are among them ([grammar](../spec/grammar.md) §1.3 lists them all). ```ceesafe fn main() { let own = 1; } ``` ```text main.cee:2:9: error[N0100]: expected identifier, found 'own', which is a reserved word: choose another name (grammar.md §1.3) | 2 | let own = 1; | ^^^ ``` ### A lookup that may find nothing is an optional, not a `Result` `Process.argument(i)`, `StringView.fromUtf8(bytes)`, `Environment.variable(name)`, `v.pop()` and `x.checkedAdd(y)` give a `T?`: match it with `Some(…)` and `null`, not `Ok` and `Err`. A `!` unwraps an optional as well as a result. ```ceesafe fn main() { match (Process.argument(1)) { Some(path) => { Console.writeLine(path); } null => { Console.writeLine("no file given"); } } let bytes = [104u8, 105]; let text = StringView.fromUtf8(bytes[..])!; // `!` unwraps an optional too, and panics on null Console.writeLine(text); } ``` ```text no file given hi ``` ### `Console` blocks its thread, so async code uses `AsyncConsole` ```ceesafe async fn greet() { Console.writeLine("hello"); } fn main() { Async.run(greet()); } ``` ```text main.cee:2:5: error[N0295]: 'greet' is an async fn, which must not block its thread (type-system.md §40.1), but this may sleep: it does console I/O | 2 | Console.writeLine("hello"); | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ ``` The same holds for `File` and `TcpStream`: async code uses `AsyncFiles`, `AsyncTcpListener` and `AsyncTcpStream` from lib/std. A call through `dyn` counts as blocking unless the interface method is declared `@effects()`. ```ceesafe async fn greet() -> Result { await AsyncConsole.writeLine("hello")?; return Ok(); } fn main() { match (Async.run(greet())) { Ok() => {} Err(_) => { Console.writeLine("failed"); } } } ``` ```text hello ``` ### `mut` at a call, `borrow mut` anywhere else A mutable borrow passed as an argument is written `mut x` (or `mut a[lo..hi]`). The same borrow in a `let` or any other expression is `borrow mut x`. ```ceesafe fn bump(n: borrow mut i32) { n += 1; } fn zero(bytes: SliceMut) { bytes.fill(0); } fn main() { var n = 1; bump(mut n); // an argument: `mut` let r = borrow mut n; // anywhere else: `borrow mut` r += 1; var buffer = [7u8; 4]; zero(mut buffer[0..2]); let tail = borrow mut buffer[2..]; tail[0] = 9; Console.writeLine(n); Console.writeLine(buffer[2]); } ``` ```text 3 9 ``` ### What the compiler fixes for you `ceesafe_fix` and `ceesafec fix` repair these without being asked twice: a `let` that is assigned (`var`), an argument for a `borrow mut` parameter without `mut`, a `mut` where the parameter is a shared `borrow`, `as` between integer types that can lose information (`truncate(…)`, which wraps), and a `;` missing after a statement. ## Where to read more The [guide](introduction.md) teaches the language; [Coming from another language](coming-from/index.md) maps what you know. [Built-in types and functions](builtins.md), [lib/core](../spec/api/core.md) and [lib/std](../spec/api/std.md) list every name. The rules themselves are the [specification](../spec/type-system.md), with the [grammar](../spec/grammar.md) and every [diagnostic](../spec/diagnostics.md). # Built-in types and functions The types and functions below are part of the compiler, not of a library, so they have no page in the library reference. The libraries' own types, such as `TextWriter`, `Timer`, `Environment`, `HashMap` and the async sockets, are in [lib/core](../spec/api/core.md) and [lib/std](../spec/api/std.md). Every example here is compiled and run by the guide's checks, so the names and signatures are the compiler's. With the MCP server, `ceesafe_api` looks any of these names up. ## `Console` | Function | Type | |---|---| | `Console.write(x)`, `Console.writeLine(x)` | `x` an integer, float, `bool`, `StringView` or `String` | | `Console.writeBytes(bytes)` | `bytes: Slice`, raw bytes to standard output, UTF-8 or not | | `Console.writeError(text)`, `Console.writeErrorLine(text)` | `text: StringView`, to standard error | | `Console.read(mut buffer)` | `Result`; `0` at the end of input | `Console` blocks its thread: in an `async fn`, use lib/std's `AsyncConsole` (N0295 otherwise). `Console.write` takes text, which is always UTF-8. Bytes from a file or the network may not be: `StringView.fromUtf8(bytes)!` stops the program on them. To pass them through as they are, write them with `Console.writeBytes(bytes)`. ```ceesafe fn main() { Console.write("answer: "); Console.writeLine(42); let bytes = "raw\n"; Console.writeBytes(bytes.bytes()); Console.writeLine(2.5); Console.writeLine(true); } ``` ```text answer: 42 raw 2.5 true ``` ## `Process` and program arguments | Function | Type | |---|---| | `Process.argumentCount()` | `usize`, including the program name | | `Process.argument(i)` | `StringView?`: `null` out of range or for bytes that are not UTF-8 | | `Process.exit(status)` | `status: i32`; never returns | | `Process.run(program, arguments)` | `Result`; `arguments: borrow Vector` | | `Process.capture(program, arguments, input)` | `Result` (lib/std) | Environment variables are lib/std's `Environment.variable(name)`, a `StringView?`. ```ceesafe fn main() { Console.writeLine(Process.argumentCount() >= 1); match (Process.argument(1000)) { Some(a) => { Console.writeLine(a); } null => { Console.writeLine("no argument 1000"); } } } ``` ```text true no argument 1000 ``` ## `File`, `TcpListener` and `TcpStream` | Function | Type | |---|---| | `File.open(path)`, `File.create(path)` | `Result` (reading; writing, created or truncated) | | `File.remove(path)` | `Result` | | `f.read(mut buffer)` | `Result`, `0` at the end; `buffer: SliceMut` | | `f.write(bytes)` | `Result`: all of them or an error | | `File.openReadWrite(path)`, `File.openOrCreate(path)`, `File.createNew(path)` | `Result` for reading and writing, never truncated: an existing file; created if missing; a new one (`AlreadyExists` otherwise) | | `f.readAt(mut buffer, offset)`, `f.writeAt(bytes, offset)` | positional: `usize` (0 past the end); every byte or an error | | `f.syncData()`, `f.syncAll()` | durable on disk (`F_FULLFSYNC` on Apple systems); after a failed sync the file's durable contents are unknown | | `f.length()`, `f.setLength(n)` | `u64`; truncate or zero-extend | | `File.rename(from, to)`, `Directory.sync(path)` | atomic replace; make a directory's entries durable | | `Directory.space(path)` | the volume's `total`, `free` and `available` bytes (ADR 0077) | | `Console.flush()` | write standard output's buffer now, so it survives a kill | | `File.openAppend(path)` | write-only, created if missing, every write at the end | | `File.modified(path)`, `File.setPermissions(path, 0o640)` | a `UnixTime`; `chmod` | | `File.openRegular(path)` | read-only, only a regular file: never waits on a FIFO; `IsDirectory`, or `InvalidInput` for a device (ADR 0078) | | `f.modified()`, `f.kind()` | the open file's time and `PathKind`, from the file, not its path | | `IoError.StorageFull` | the volume is full (`ENOSPC`, `EDQUOT`) | | `TcpListener.bind(address, port)` | `Result`; then `port()`, `accept()` | | `TcpStream.connect(address, port)` | `Result`; `read`, `write`, `shutdownWrite`, `shutdownRead` | | `e.message()` | the `IoError`'s text, a `StringView` | A `File` closes when it is destroyed. These block their thread: async code uses lib/std's `AsyncFiles`, `AsyncTcpListener` and `AsyncTcpStream`. ```ceesafe fn main() { match (File.open("/no/such/file")) { Ok(_) => { Console.writeLine("opened"); } Err(e) => { Console.writeLine(e.message()); } } } ``` ```text No such file or directory ``` ## `StringView` and `String` | Member | Type | |---|---| | `s.length` | `usize`, bytes | | `s[i]`, `s[lo..hi]`, `s.bytes()` | `u8`; a `StringView` (character boundaries checked); `Slice` | | `StringView.fromUtf8(bytes)` | `StringView?`: `null` for bytes that are not UTF-8 | | `==`, `!=` | compare the bytes | | `String.create()`, `String.withCapacity(n)` | `String`; `Result` | | `t.append(view)`, `t.appendNumber(n)` | `Result` | | `t.appendHex(n, minWidth, upper)`, `t.appendOctal(n)`, `t.appendFixed(f, digits)`, `t.appendPadded(view, width, align, fill)` | `Result`: Rust's `{:08x}`, `{:o}`, `{:.3}`, `{:>8}` (also on a `Vector`, and as `VectorBytes.appendHex(mut v, …)`) | | `t.view()`, `t.clear()`, `t.length`, `t.capacity` | a `StringView` borrowed from `t`; … | | `s.indexOf(t)`, `s.lastIndexOf(t)`, `s.indexOfByte(b)` | `usize?` | | `s.contains(t)`, `s.startsWith(t)`, `s.endsWith(t)`, `s.equalsIgnoreAsciiCase(t)` | `bool` | | `s.trim()`, `s.trimStart()`, `s.trimEnd()` | the `StringView` without ASCII whitespace | | `s.splitOnce(sep)`, `s.split(sep)` | `TextHalves?` (`left`, `right`); a `TextSplit`, whose `next()` gives each piece, then `null` | | `s.parseInt()`, `s.parseIntRadix(radix)`, `s.parseFloat()` | `T?`: `null` for text that is not a `T` | The searches and parsers work on a `String` too, and on bytes: a `Slice` (or a `Vector`) takes and gives `Slice` where a view takes and gives `StringView`. Every view they return stays valid UTF-8. Floats parse correctly rounded, and accept `inf`, `infinity` and `nan` in any case. A `StringView` is a reference: a variable holding one is a `let`, and a struct or enum holding one needs a region parameter. For formatting into a fixed buffer without allocating, use lib/core's `TextWriter` (`text`, `byte`, `bytes`, `decimal` for a `u64`, `signed` for an `i64`, `hex`, then `written`). ```ceesafe fn main() { let s: StringView = "CeeSafe"; Console.writeLine(s.length); Console.writeLine(s[0..3]); var t = String.create(); t.append("x = ")!; t.appendNumber(-7)!; Console.writeLine(t.view()); let bytes = [104u8, 105]; match (StringView.fromUtf8(bytes[..])) { Some(v) => { Console.writeLine(v); } null => { Console.writeLine("not UTF-8"); } } } ``` ```text 7 Cee x = -7 hi ``` ## `Vector`, arrays and slices | Member | Type | |---|---| | `Vector.create()`, `Vector.withCapacity(n)` | `Vector`; `Result, AllocError>` | | `v.push(x)` | `Result` | | `v.pop()` | `T?` | | `v.length`, `v.capacity`, `v[i]` | `usize`; an element place | | `v.clear()`, `v.truncate(n)`, `v.swap(i, j)` | | | `v.insert(at, x)`, `v.remove(at)`, `v.swapRemove(at)` | `Result`; `T`; `T` (the last element fills the gap); the index is checked at the call | | `[]` | the empty `Slice` or `SliceMut`, where one is expected | | `a.writeLE(0, x)` on an array | an array's methods are its slice's: `a[..].writeLE(0, x)` | | `v.view()`, `v.viewMut()` | `Slice`, `SliceMut` | | `[x; N]`, `[a, b, c]`, `a[lo..hi]`, `borrow mut a[lo..hi]` | an array; a `Slice`; a `SliceMut` | | `d.copyFrom(s)`, `d.fill(v)` | bulk writes into a `SliceMut` | | `v.extend(s)`, `v.resize(n, fill)`, `v.reserve(extra)` | `Result`; one growth at most; `extend` and `resize` need Copy elements | | `b.readBE(at)`, `b.readLE(at)` | `T`, an integer from bytes, big- or little-endian | | `b.writeBE(at, x)`, `b.writeLE(at, x)` | into a `SliceMut` (or a mutable `Vector`) | | `v.pushBE(x)`, `v.pushLE(x)` | `Result`: `x`'s bytes appended to a `Vector` | Byte order works on fixed-width integers (`u8` to `u64`, `i8` to `i64`); the value's type gives the width, so a literal needs a suffix (`v.pushBE(5u32)`). A read or write past the end panics at the call, and writes nothing. ```ceesafe fn main() { var message = Vector.create(); message.pushBE(0x51u8)!; // 'Q' message.pushBE(0u32)!; // the length, patched below message.extend("SELECT 1".bytes())!; message.pushBE(0u8)!; let length = truncate(message.length - 1); message.writeBE(1, length); Console.writeLine(message.readBE(1)); var fields = "r=abc,s=xyz,i=4096".split(","); while (true) { let field = match (fields.next()) { Some(f) => f, null => { break; } }; let pair = field.splitOnce("=")!; if (pair.left == "i") { Console.writeLine(pair.right.parseInt()! * 2); } } Console.writeLine(" 2.5e-3 ".trim().parseFloat()!); } ``` ```text 13 8192 0.0025 ``` Indexes are `usize`. A range of two literals that the body indexes with is a `usize` range. Arrays and slices may hold views (`[StringView; 3]`, `Slice`), but not `borrow T` references. Slice a variable, not a call's result: bind it to a local first. ```ceesafe fn total(xs: Slice) -> u32 { var n: u32 = 0; for (x in xs) { n += x; } return n; } fn main() { var v = Vector.create(); for (i in 0u32..4) { v.push(i * 10)!; } Console.writeLine(total(v.view())); var buffer = [0u32; 4]; buffer[1..3].fill(5); Console.writeLine(total(buffer[..])); let names = ["Low", "Medium", "High"]; Console.writeLine(names[1]); match (v.pop()) { Some(last) => { Console.writeLine(last); } null => {} } } ``` ```text 60 10 Medium 30 ``` ## Integers, floats and conversions | Operation | Notes | |---|---| | `+ - * / %`, unary `-` | panic on overflow, and on dividing by zero | | `& \| ^ << >>` | unsigned types only; no `~` (use `x ^ 0xFFFFFFFF`) | | `x.wrappingAdd(y)`, `x.checkedAdd(y)`, `x.saturatingAdd(y)` | also `Sub`, `Mul`; `checked…` is a `T?` | | `x.min(y)`, `x.max(y)` | every integer type | | `x.abs()`, `x.unsignedAbs()` | signed types: `abs` panics at the minimum, `unsignedAbs` is the unsigned type of the width | | `x.rotateLeft(n)`, `x.rotateRight(n)`, `x.wrappingShl(n)`, `x.wrappingShr(n)` | unsigned types; `n: u32`, taken modulo the width | | `x.leadingZeros()`, `x.trailingZeros()`, `x.countOnes()` | unsigned types; a `u32` (the width for 0) | | `x.byteSwap()` | every integer type: the bytes in the other order | | `x as T` | only conversions that never lose information; `a as u64 < b` compares | | `*r` | the Copy value `r` refers to; `*r = v` writes through a `borrow mut` | | `replace(mut place, v)`, `swap(mut a, mut b)` | the old value, which nothing is left without; exchange two places | | `assert(cond)`, `assert(cond, message)`, `panic(message)` | panic when false; always (`never`) | | `debugAssert(cond)`, `debugAssert(cond, message)` | as `assert`, but only in debug builds, tests and the interpreter; may change nothing that exists before it ([Debugging](debugging.md#debug-assertions)) | | `embed("path")`, `embedText("path")` | a file's bytes as a `Slice`, or as UTF-8 text, read when the package is checked; the path is relative to the source file and stays inside the package ([Constants](basics.md#embedded-files)) | | `r.isOk()`, `r.isErr()`, `r.ok()`, `r.err()`, `o.okOr(e)` | a `Result`'s tests and conversions; an optional as a `Result` | | `enum E { @from Io(IoError), … }` | `?` turns an `IoError` into `E.Io(e)` in a function returning `Result<_, E>` | | `truncate(x)` | wraps an integer; a float to an integer rounds toward zero and panics out of range or on NaN | | `checkedTruncate(f)`, `saturatingTruncate(f)` | a float to an integer: `T?` (`null` where `truncate` would panic); the nearest bound (NaN gives 0) | | `round(x)` | to a float type, rounding to nearest | | `f.sqrt()`, `f.abs()`, `f.floor()`, `f.ceil()`, `f.min(g)`, `f.max(g)`, `f.bits()` | floats | ```ceesafe fn main() { let big: u8 = 250; Console.writeLine(big.wrappingAdd(10)); Console.writeLine(big.saturatingAdd(10)); let n: i32 = -7; Console.writeLine(n.abs()); Console.writeLine(n.max(3)); let wide: i64 = 300; Console.writeLine(truncate(wide)); let f: f64 = 2.75; Console.writeLine(truncate(f)); Console.writeLine(round(wide)); Console.writeLine(f.floor()); } ``` ```text 4 255 7 3 44 2 300 2 ``` ## `Box` and memory | Type or function | Notes | |---|---| | `Box.create(value)` | `Result, AllocError>`, heap-owned; `Box` for dynamic dispatch | | `FixedBufferAllocator.over(mut bytes)` | an allocator over a caller's buffer, for freestanding code | | `Arena`, `Scratch`, `Pool`, `SlotMap`, `StableVector` | see [memory](memory.md) and [collections](collections.md) | | `Thread.spawn`, `Thread.both`, `Async.run`, `Async.scope` | see [threads](threads.md) and [async](async.md) | ```ceesafe fn main() { let b = Box.create(41)!; Console.writeLine(b + 1); } ``` ```text 42 ``` ## Every signature above This block is type-checked, not run: each call is annotated with the type the tables give, so a table that drifts from the compiler fails the guide's checks. ```ceesafe fn everything() -> Result { let created: Result = File.create("/tmp/x"); var f = created?; let wrote: Result = f.write("hi".bytes()); var buffer = [0u8; 8]; let got: Result = f.read(mut buffer[..]); let removed: Result = File.remove("/tmp/x"); let input: Result = Console.read(mut buffer[..]); Console.writeError("e"); Console.writeErrorLine("e"); var listener = TcpListener.bind("127.0.0.1", 0)?; let port: Result = listener.port(); let accepted: Result = listener.accept(); var stream = TcpStream.connect("127.0.0.1", 1)?; let n: Result = stream.read(mut buffer[..]); let w: Result = stream.write(buffer[..]); let sw: Result = stream.shutdownWrite(); let sr: Result = stream.shutdownRead(); var arguments = Vector.create(); let status: Result = Process.run("true", borrow arguments); let captured: Result = Process.capture("true", borrow arguments, "".bytes()); let home: StringView? = Environment.variable("HOME"); return Ok(); } fn collections() -> Result { var v: Vector = Vector.withCapacity(4)?; v.push(1)?; v.push(2)?; let c: usize = v.capacity; v.swap(0, 1); v.truncate(1); let m: SliceMut = v.viewMut(); v.clear(); var t: String = String.withCapacity(8)?; t.clear(); let tc: usize = t.capacity; var dst = [0u8; 2]; let src = [1u8, 2]; dst[..].copyFrom(src[..]); var bytes = Vector.create(); bytes.extend(src[..])?; bytes.resize(8, 0)?; bytes.reserve(8)?; bytes.pushBE(1u16)?; bytes.pushLE(-1i32)?; let word: u32 = bytes.readBE(0); let back: i16 = bytes.view().readLE(2); bytes.writeLE(0, 7u64); dst[..].writeBE(0, 3u16); return Ok(); } fn text() { let s: StringView = " a,b "; let at: usize? = s.indexOf(","); let last: usize? = s.lastIndexOf("b"); let byte: usize? = s.indexOfByte(','); let has: bool = s.contains("a") && s.startsWith(" ") && s.endsWith(" ") && s.equalsIgnoreAsciiCase(" A,B "); let t: StringView = s.trim().trimStart().trimEnd(); let halves: TextHalves? = t.splitOnce(","); var pieces: TextSplit = t.split(","); let piece: StringView? = pieces.next(); let n: i64? = "-12".parseInt(); let h: u8? = "ff".parseIntRadix(16); let f: f32? = "1.5".parseFloat(); let raw: Slice = s.bytes(); let rawAt: usize? = raw.indexOf(",".bytes()); let rawTrim: Slice = raw.trim(); let rawHalves: ByteHalves? = raw.splitOnce(",".bytes()); var rawPieces: ByteSplit = raw.split(",".bytes()); let rawN: u32? = raw.parseInt(); } fn numbers() { let a: u8 = 250; let s: u8 = a.wrappingSub(1); let m: u8? = a.checkedMul(2); let x: i64 = -5; let u: u64 = x.unsignedAbs(); let f: f64 = 2.0; let ct: u8? = checkedTruncate(f); let st: u8 = saturatingTruncate(f); let r: f64 = f.sqrt().abs().ceil().min(1.0).max(0.5); let bits: u64 = f.bits(); let w: u32 = 0x80000001; let rotated: u32 = w.rotateLeft(1).rotateRight(2).wrappingShl(33).wrappingShr(1); let counts: u32 = w.leadingZeros() + w.trailingZeros() + w.countOnes(); let swapped: i16 = (-2i16).byteSwap(); } fn memory() { var storage = [0u8; 64]; let arena = FixedBufferAllocator.over(mut storage); } fn main() {} ``` # Basics This chapter covers what every program uses: values and types, variables, functions, operators, and control flow. Each example is a whole program you can run with `ceesafec run --interp file.cee`. ## A program A program is one or more `.cee` files with a `main` function. `main` takes nothing and returns nothing or an `i32` exit status. ```ceesafe fn main() -> i32 { Console.writeLine("Hello, CeeSafe"); return 0; } ``` ```text Hello, CeeSafe ``` `Console.write` and `Console.writeLine` print integers, booleans, floats and text. ## Types | Type | Values | |---|---| | `i8` `i16` `i32` `i64` | signed integers, two's complement | | `u8` `u16` `u32` `u64` | unsigned integers | | `isize` `usize` | the target's pointer width; lengths and indexes are `usize` | | `f32` `f64` | IEEE 754 floating point | | `bool` | `true`, `false` | | `StringView` | a read-only view of UTF-8 text; string literals have this type | Later chapters add structs, enums, arrays, references, optionals (`T?`), results (`Result`), boxes and collections. There is no `char`: text is UTF-8 bytes, and a character literal such as `'k'` is an integer. ## Variables `let` declares a variable that cannot be assigned again; `var` one that can. The type is inferred from the initialiser, or written after a colon. ```ceesafe fn main() { let name = "CeeSafe"; // StringView let year: u16 = 2026; var count = 0; // i32: an integer literal's default type count += 1; var total: u64; // declared now, assigned before it is read total = 40 + 2; Console.writeLine(name); Console.writeLine(year); Console.writeLine(count); Console.writeLine(total); } ``` ```text CeeSafe 2026 1 42 ``` Reading a variable that might not be assigned yet is a compile error, and so is assigning to a `let`. Two locals in scope cannot have the same name: there is no shadowing. `let _ = f();` evaluates `f()` and discards its value. ### Reserved words and built-in names Some words that look like ordinary names are reserved, so they can't name a variable, parameter or field: `move`, `own`, `type`, `namespace`, `macro`, `yield`, `class` and `internal`, among others. The full list is in [grammar.md §1.3](../spec/grammar.md). The compiler says so when one is used as a name. The built-in types' names are taken in every program, so a program can't declare its own `Handle` (a `SlotMap`'s handle), `Check` (the test helpers), `TestFailure`, or the lib/core names listed in [the library API](builtins.md). ## Integer literals and the expected type An integer literal takes its type from where it is used. In `let year: u16 = 2026;` it is a `u16`; in `x + 1` it is `x`'s type; with nothing to go by it is an `i32`. A suffix fixes it: `7u8`, `100_000u64`. Literals can be written in hex, binary and octal (`0xFF`, `0b1010`, `0o17`), with `_` between digits. A value that does not fit its type is a compile error. A character literal is the character's Unicode value, typed like any other literal: ```ceesafe fn isDigit(c: u8) -> bool => c >= '0' && c <= '9'; fn main() { Console.writeLine(isDigit('7')); Console.writeLine(isDigit('x')); Console.writeLine('\n' as u32); } ``` ```text true false 10 ``` ## Functions ```ceesafe fn add(a: i64, b: i64) -> i64 { return a + b; } fn square(x: i64) -> i64 => x * x; // an expression body fn greet(name: StringView) { // no return type: returns nothing Console.write("Hello, "); Console.writeLine(name); } fn main() { Console.writeLine(add(2, square(3))); greet("reader"); } ``` ```text 11 Hello, reader ``` Parameters are immutable. Arguments are evaluated left to right. A function that returns a value must return on every path, which the compiler checks. ## Operators Both operands of an operator must have the same type: there are **no implicit conversions**, not even between `i32` and `i64`. | Operators | On | Notes | |---|---|---| | `+ - * / %` | integers, floats | integer overflow stops the program; so does dividing by zero | | `& \| ^ << >>` | unsigned integers | a shift by the width or more stops the program | | `== != < <= > >=` | integers, floats, `bool` (`==`, `!=`), text (`==`, `!=`) | | | `== !=` | enums without payloads; arrays and slices of integers, `bool` or such enums | compare the elements | | `&& \|\| !` | `bool` | `&&` and `\|\|` short-circuit | | `??` | an optional and a fallback | `o ?? d`: `o`'s value, or `d` when `o` is `null` | When overflow is expected, say what should happen: ```ceesafe fn main() { let big: u8 = 250; Console.writeLine(big.wrappingAdd(10)); // 4: modulo 256 Console.writeLine(big.saturatingAdd(10)); // 255: clamped match (big.checkedAdd(10)) { // u8?: null on overflow Some(sum) => { Console.writeLine(sum); } null => { Console.writeLine("overflow"); } } } ``` ```text 4 255 overflow ``` The same three families exist for `Sub` and `Mul`, on every integer type. ## Conversions `as` converts only when no value can be lost: `u8 as u32`, `i32 as i64`, `u32 as i64`, `bool as u8`, `i32 as f64`. Anything that could lose information is spelled out: ```ceesafe fn main() { let big: u32 = 300; Console.writeLine(truncate(big)); // 44: the low 8 bits let x: f64 = 2.75; Console.writeLine(truncate(x)); // 2: toward zero; out of range stops the program Console.writeLine(round(1234567890123u64)); // the nearest f64 let n: i32 = -1; Console.writeLine(n as i64); } ``` ```text 44 2 1234567890123 -1 ``` ## Control flow `if`, `while`, `for` and `match` put their condition in parentheses, and their bodies are blocks. ```ceesafe fn main() { var n = 0; while (n < 3) { n += 1; } for (i in 0..3) { // 0, 1, 2 if (i == 1) { continue; } Console.write(i); } Console.writeLine(); var tries = 0; loop { // until a break tries += 1; if (tries == 5) { break; } } Console.writeLine(n + tries); } ``` ```text 02 8 ``` `for (x in values)` walks an array, a slice or a vector (see [Collections](collections.md)). ### `if` and `match` are expressions too ```ceesafe fn describe(n: i32) -> StringView => match (n) { 0 => "zero", 1 => "one", _ => if (n < 0) "negative" else "many", }; fn main() { Console.writeLine(describe(0)); Console.writeLine(describe(7)); Console.writeLine(describe(-2)); let sign = if (-2 < 0) -1 else 1; Console.writeLine(sign); } ``` ```text zero many negative -1 ``` An `if` expression needs its `else`. A branch can also be a block. It either ends with its value, written without `;`, or it leaves, such as `{ return; }`, which is how an early exit fits inside an expression: ```ceesafe fn parse(text: StringView) -> i32 { let n = match (text.parseInt()) { Some(v) => { let doubled = v * 2; doubled } null => { return -1; } }; return n; } fn main() { Console.writeLine(parse("21")); Console.writeLine(parse("x")); } ``` ```text 42 -1 ``` ## Constants ```ceesafe const SECONDS_PER_DAY: u32 = 24 * 60 * 60; fn main() { Console.writeLine(SECONDS_PER_DAY); } ``` ```text 86400 ``` A constant is evaluated at compile time. An overflow there is a compile error rather than a run-time stop. A constant may be text, and in a constant or a static `+` joins constant text, so a prefix is written once: ```ceesafe const TOOL: StringView = "bun"; const USAGE: StringView = TOOL + " [options]"; const VERSION_LINE: StringView = TOOL + " 1.4.4"; fn main() { Console.writeLine(USAGE); Console.writeLine(VERSION_LINE); } ``` ```text bun [options] bun 1.4.4 ``` The joined text is made when the program is compiled. Anywhere else `+` on views is an error (N0207): build text at run time by appending to a `String`. With a `c"…"` on the right, `TOOL + c"rc"` is a C string (a `CStr`, [C interop](unsafe-and-c.md)). ## Embedded files `embed("path")` puts a file's bytes in the program, as a `Slice`; `embedText("path")` puts them as text, a `StringView`, and refuses a file that is not UTF-8 (N0361). The file is read when the package is checked, so the program needs nothing beside it at run time: ```ceesafe static ROOT_CERTS: Slice = embed("certs/roots.der"); const HELP: StringView = embedText("help.txt"); const BANNER: StringView = "bun: " + embedText("banner.txt"); fn main() { Console.writeLine(ROOT_CERTS.length); Console.write(HELP); } ``` The path is relative to the source file that names it, with `/` between directories, and nothing in it is expanded: no absolute paths, no `~`, no environment variables. Wherever its symbolic links lead, the file must be inside the package's directory, or inside a directory its manifest lists in `[embed] roots` ([Packages](packages.md)); a file checked on its own embeds only below its own directory. Anything else, a missing file, a directory or a file over 16 MiB, is N0362, also in the editor and the MCP server, which read the files too. A constant text's `.bytes()` is constant too, so a byte string is written as text: `const MAGIC: Slice = "\u{7f}ELF".bytes();`. A constant or a static may also be a table of them, `[Slice; N]`, embedded files included. ## Comments `//` comments run to the end of the line, `/* … */` comments may span lines, and `///` before an item is its documentation, which [`ceesafec doc`](formatting-and-docs.md) collects. ## Next [Structs, enums and patterns](structs-and-enums.md). # Structs, enums and patterns ## Structs A struct groups named fields. Fields end with `;`. Functions declared inside are its methods (with a `this` receiver) or static functions (without one). ```ceesafe struct Point { x: i64; y: i64; fn origin() -> Point => Point { x: 0, y: 0 }; // static: Point.origin() fn manhattan(this) -> i64 { // `this` by value (Point is Copy) return abs(this.x) + abs(this.y); } fn moveBy(borrow mut this, dx: i64, dy: i64) { // changes the point in place this.x += dx; this.y += dy; } } fn abs(v: i64) -> i64 => if (v < 0) -v else v; fn main() { var p = Point.origin(); p.moveBy(3, -4); Console.writeLine(p.manhattan()); let q = Point { x: 1, y: 2 }; // every field, in any order Console.writeLine(q.x + q.y); } ``` ```text 7 3 ``` - A **struct literal** names every field exactly once. - The **receiver** says how a method gets its struct: `this` (a copy, or a move), `borrow this` (read-only access) or `borrow mut this` (changes the caller's value). Calling a `borrow mut this` method needs a `var` (or a mutable reference); the call borrows it for the duration of the call. - A struct is **Copy**, copied on assignment, when every field is Copy and it has no `drop` block. Otherwise it moves: see [Ownership](ownership.md). - Fields are laid out in declaration order with C's rules, so a struct is the same in C. ### Visibility Items and members are `internal` by default: visible throughout their package. `public` makes them visible to other packages (see [Packages](packages.md)); `private` on a field limits it to the file that declares the struct, which lets a type guard an invariant. ## Enums An enum is one of several variants, each with zero or more positional payloads. Variants are written with the enum's name. ```ceesafe enum Shape { Empty, Circle(i64), Rect(i64, i64), } fn area(s: Shape) -> i64 => match (s) { Shape.Empty => 0, Shape.Circle(r) => 3 * r * r, Shape.Rect(w, h) => w * h, }; fn main() { Console.writeLine(area(Shape.Circle(2))); Console.writeLine(area(Shape.Rect(3, 4))); Console.writeLine(area(Shape.Empty)); } ``` ```text 12 12 0 ``` An enum whose variants have no payloads compares with `==` and `!=`. An enum with payloads compares with `match`, or with `==` once it derives equality (below). ```ceesafe enum Kind { User, Alias, List } fn main() { let kind = Kind.Alias; Console.writeLine(kind == Kind.Alias); Console.writeLine(kind != Kind.User); } ``` ```text true true ``` ### Deriving equality, hashing and ordering Structs and enums have no `==` of their own. `@derive(Equal)` gives one: the fields are compared in declaration order (an enum's variant, then its payloads), and `!=` is the opposite. `@derive(HashKey)` (which needs `Equal`) makes the type a `HashMap` key, and `@derive(OrderKey)` an `OrderedMap` key ordered field by field. ```ceesafe @derive(Equal, HashKey, OrderKey) struct Point { x: i32; y: i32; } @derive(Equal) enum Shape { Dot(Point), Box(Point, Point), } fn main() { let a = Point { x: 1, y: 2 }; let b = Point { x: 1, y: 5 }; Console.writeLine(a == b); Console.writeLine(Shape.Box(a, b) == Shape.Box(a, b)); Console.writeLine(a.compareKey(b)); var seen = HashMap.create(); seen.put(a, 1)!; Console.writeLine(seen.contains(Point { x: 1, y: 2 })); } ``` ```text false true -1 true ``` Every field needs `==` itself: numbers, `bool`, strings and views, fieldless enums, byte vectors and slices, optionals of these (two `null`s are equal), and other types that derive `Equal`, with arrays, slices and vectors of them (`==` works on those anywhere: same length, equal elements). Floats compare as IEEE values, so a `NaN` field makes a value unequal to itself. A field without `==` is an error at the attribute that names it; write a method of your own for such a type. The derived code is ordinary CeeSafe that `ceesafec check --dump-hir` prints: for `Point`, `impl DerivedEqual for Point { fn derivedEqual(borrow this, other: borrow Point) -> bool => this.x == other.x && this.y == other.y; }`, and `a == b` calls it. For `HashKey` and `OrderKey`, a field whose type is a fieldless enum needs no derive of its own: its variant's position in the declaration is hashed and compared. ## `match` `match` tries its arms in order. It must be **exhaustive**: every possible value matches some arm, or the compiler names one that does not. An arm that can never be reached is an error too. As a statement, each arm is a block; as an expression (above), each arm is a value, separated by commas. ```ceesafe enum Command { Move(i32, i32), Wait(u32), Quit, } fn run(command: Command) { match (command) { Command.Move(0, 0) => { Console.writeLine("stay"); } Command.Move(x, _) => { Console.write("move x by "); Console.writeLine(x); } Command.Wait(seconds) => { Console.write("wait "); Console.writeLine(seconds); } Command.Quit => {} } } fn main() { run(Command.Move(0, 0)); run(Command.Move(5, 1)); run(Command.Wait(3)); run(Command.Quit); } ``` ```text stay move x by 5 wait 3 ``` Patterns can be nested: a variant inside a variant (`Ok(Shape.Circle(r))`), a literal in a payload's position (`Command.Move(0, 0)`), `null` or `Some(…)` for optionals, and `_` for anything. Integers and booleans can be matched directly, with `_` for the rest. ```ceesafe fn kind(n: u8) -> StringView => match (n) { 0 => "zero", 'a' => "the letter a", _ => "something else", }; fn main() { Console.writeLine(kind(97)); Console.writeLine(kind(3)); } ``` ```text the letter a something else ``` ### Alternatives `A | B` matches when either pattern does, at the top of an arm or inside a payload. When the alternatives bind names, each binds the same ones, with the same types. ```ceesafe enum Token { Word(u32), Number(u32), End, } fn isSpace(c: u8) -> bool => match (c) { ' ' | '\t' | '\n' => true, _ => false, }; fn length(t: Token) -> u32 => match (t) { Token.Word(n) | Token.Number(n) => n, Token.End => 0, }; fn main() { Console.writeLine(isSpace('\t')); Console.writeLine(length(Token.Number(3))); } ``` ```text true 3 ``` ### Constants and names A bare name in a pattern is a constant when a `const` of that name is in scope, and otherwise binds the value at that position, at the top of an arm too. `Type.NAME` names an associated constant. ```ceesafe const LF: u8 = 10; fn describe(c: u8) -> StringView => match (c) { LF => "newline", other => if (other < 32) "control" else "text", }; fn main() { Console.writeLine(describe(10)); Console.writeLine(describe(65)); } ``` ```text newline text ``` A misspelt constant binds instead and matches everything; the compiler then reports the arms after it as unreachable. ### Ranges `lo..=hi` matches the integers from `lo` to `hi`, both included; `lo..hi` leaves `hi` out. The bounds are integer or character literals. Literals and ranges that together hold every value of the type need no `_`. ```ceesafe fn kind(c: u8) -> StringView => match (c) { 'a'..='z' | 'A'..='Z' | '_' => "letter", '0'..='9' => "digit", _ => "other", }; fn half(b: u8) -> u32 => match (b) { 0x00..=0x7F => 1, 0x80..=0xFF => 2, }; fn main() { Console.writeLine(kind('q')); Console.writeLine(kind('7')); Console.writeLine(half(200)); } ``` ```text letter digit 2 ``` A match on literals and ranges compiles to a C `switch`. ### Naming what matched `name @ pattern` tests the pattern and binds the value it matched: ```ceesafe fn digit(c: u8) -> u32 => match (c) { d @ '0'..='9' => (d - '0') as u32, q @ ('"' | '\'') => 100 + q as u32, _ => 999, }; fn main() { Console.writeLine(digit('7')); } ``` ```text 7 ``` A value moves at most once: in a match that moves payloads out, `whole @ Slot.Full(inner)` cannot move the value into `whole` and its payload into `inner` too (N0250); bind one of them. ### Guards An arm can add a condition after its pattern, `if (…)`: the arm is taken only when the pattern matches and the condition holds; otherwise the next arm is tried. The guard sees the arm's bindings. ```ceesafe fn size(x: u32?) -> StringView => match (x) { Some(n) if (n > 100) => "big", Some(0) => "zero", Some(_) => "small", null => "none", }; fn main() { Console.writeLine(size(Some(500))); Console.writeLine(size(Some(7))); } ``` ```text big small ``` A guarded arm never counts toward exhaustiveness: the compiler cannot know when its condition holds, so the other arms must still cover every value. A guard only reads: it cannot move, assign or lend as `mut` the matched value or the arm's bindings (N0355). In a match that moves a payload into its binding, the move happens only once the guard holds. ### Strings and slices A string literal is a pattern on a `StringView`, and on a `Slice`, where it matches the text's bytes. A slice pattern, `[…]`, matches a slice or an array by its elements: `..` stands for any number of them, and `rest @ ..` binds those as a slice. ```ceesafe fn command(word: StringView) -> u32 => match (word) { "get" | "head" => 1, "put" => 2, _ => 0, }; fn ends(s: Slice) -> i32 => match (s) { [] => 0, [only] => only, [first, .., last] => first + last, }; fn unquote(text: Slice) -> Slice => match (text) { ['"', inner @ .., '"'] => inner, _ => text, }; fn main() { Console.writeLine(command("head")); let numbers = [3, 5, 8]; Console.writeLine(ends(numbers[..])); Console.writeLine(unquote("\"quoted\"".bytes()).length); } ``` ```text 1 11 6 ``` Strings never cover their type, so a match on a `StringView` ends with `_` or a binding. Slice patterns cover by length: `[]` and `[_, ..]` together match every slice, and a missing case is reported with one: ```ceesafe fn size(s: Slice) -> u32 => match (s) { [] => 0, [_] => 1, }; fn main() {} ``` ```text main.cee:1:41: error[N0256]: this match does not cover [_, _, ..]; add the missing arms or a '_' arm | 1 | fn size(s: Slice) -> u32 => match (s) { | ^ ``` A slice pattern never moves an element: one of a Copy type is copied, any other is bound as a `borrow T`. ### Patterns as conditions `e is P` tests a value against a pattern and gives a `bool`. In an `if` or `while` condition, the names the pattern binds are in scope in the rest of the `&&` chain and in the then-branch or the loop body: ```ceesafe fn find(key: u32) -> u32? => if (key > 2) Some(key * 10) else null; fn main() { let c = 'q'; let letter = c is 'a'..='z' | 'A'..='Z'; Console.writeLine(letter); if (find(5) is Some(found) && found > 20) { Console.writeLine(found); } var stack = Vector.create(); stack.push(1)!; stack.push(2)!; while (stack.pop() is Some(top)) { Console.writeLine(top); } } ``` ```text true 50 2 1 ``` Nowhere else is the test known to be true, so a pattern that binds under `!` or `||`, or as a whole `let` initialiser, is an error (N0359). In a chain outside a condition, `let big = find(5) is Some(v) && v > 20;`, the names end with the statement, so a `RefCell` guard bound that way is released before the next one. A test that binds a payload that is not Copy moves the value only when it succeeds: after `if (t is Token.Word(w)) { … } else { … }`, the `else` branch still has `t`. ## Optionals `T?` is a `T` or `null`. A plain `T` converts to `T?` wherever one is expected. ```ceesafe fn find(values: Slice, wanted: i32) -> usize? { for (i in 0..values.length) { if (values[i] == wanted) { return i; // converts to usize? } } return null; } fn firstOfTwo(values: Slice) -> usize? { let a = find(values, 2)?; // `?`: if null, return null return a; } fn main() { let numbers = [5, 2, 9]; match (find(numbers[..], 9)) { Some(i) => { Console.writeLine(i); } null => { Console.writeLine("absent"); } } Console.writeLine(find(numbers[..], 7) == null); Console.writeLine(firstOfTwo(numbers[..])!); // `!`: the value, or stop the program if null Console.writeLine(find(numbers[..], 7) ?? 99); // `??`: the value, or a fallback Console.writeLine(find(numbers[..], 5).isSome()); } ``` ```text 2 true 1 99 true ``` - `o == null` and `o != null` test for absence; so do `o.isNull()` and `o.isSome()`. - `o ?? fallback` is the value, or `fallback` when `o` is `null`. The fallback is evaluated only then, and a chain `a ?? b ?? c` takes the first value present. - `o!` is the value; on `null` it stops the program with "unwrapped null". - `o?` is the value; on `null` the enclosing function returns `null` (it must return an optional). - A reference can be optional (`borrow T?`), and so can a box (`Box?`); both are one pointer. ## Results `Result` is the other built-in enum: `Ok(value)` or `Err(error)`. It is how functions report failure; see [Errors](errors.md). ## Next [Ownership and borrowing](ownership.md): what happens when a value is assigned, passed and destroyed. # Ownership and borrowing CeeSafe manages memory without a garbage collector by tracking, at compile time, who owns each value and who is looking at it. This chapter explains the rules, why they hold, and what the compiler says when a program breaks them. The formal rules are in [Ownership](../spec/ownership.md) and [References and the borrow checker](../spec/lifetimes.md). ## Copy types and move types Integers, floats, `bool`, references and string views are **Copy**: assigning one copies it, and both copies stay usable. A struct or enum is Copy when everything in it is Copy and it has no `drop` block. Everything else is a **move type**: boxes, vectors, strings, files, and any struct that owns one or has a `drop` block. Assigning a move-type value, passing it, returning it or storing it **moves** it. The old place is then empty, and using it is a compile error. ```ceesafe struct Buffer { size: u32; drop { Console.writeLine("freed"); } } fn consume(b: Buffer) {} fn main() { let b = Buffer { size: 64 }; consume(b); Console.writeLine(b.size); } ``` ```text main.cee:11:5: error[N0250]: 'b.size' is used here after it was moved (on at least one path) | 11 | Console.writeLine(b.size); | ^^^^^^^^^^^^^^^^^^^^^^^^^^ ``` Moves are what make destruction safe: a value has exactly one owner, so it is destroyed exactly once. ## Destruction A value is destroyed when its owner goes away: at the end of the variable's scope, when a temporary's statement ends, or when an assignment overwrites it. Destroying a value runs its `drop` block, if it has one, and then destroys its fields. Locals are destroyed in the reverse order of their declaration. ```ceesafe struct Noisy { id: u32; drop { Console.write("drop "); Console.writeLine(this.id); } } fn main() { let first = Noisy { id: 1 }; let second = Noisy { id: 2 }; { let inner = Noisy { id: 3 }; } // inner is destroyed here var replaced = Noisy { id: 4 }; replaced = Noisy { id: 5 }; // 4 is destroyed by the assignment let moved = first; // first is moved: it is destroyed once, as `moved` Console.writeLine("end of main"); } ``` ```text drop 3 drop 4 end of main drop 1 drop 5 drop 2 ``` At the end of `main`, the locals are destroyed in reverse order of declaration: `moved` (holding 1), `replaced` (holding 5), then `second`. `first` was moved, so it is not destroyed again. A value moved on only some paths gets a hidden flag, so it is still destroyed exactly once. A **panic** or `Process.exit` does not run `drop` blocks: a panic stops the program at once (there is no unwinding). ### `defer` and `using` `defer s;` runs `s` when the enclosing block exits by any path: its end, `break`, `continue` or `return`. `using x = e;` declares a local that cannot be moved away: it lives exactly as long as its block, which documents that its lifetime *is* the scope (a lock guard, an arena, a file). ```ceesafe fn work(fail: bool) -> i32 { defer Console.writeLine("cleanup"); if (fail) { return 1; } Console.writeLine("working"); return 0; } fn main() { Console.writeLine(work(false)); Console.writeLine(work(true)); } ``` ```text working cleanup 0 cleanup 1 ``` ## Borrowing Most code does not want to take ownership; it wants to look at a value or change it in place. A **reference** does that without moving: - `borrow T` is a shared reference: it can read, and any number can exist at once; - `borrow mut T` is an exclusive reference: it can read and write, and while it is in use nothing else can touch the value. ```ceesafe struct Counter { hits: u64; } fn show(c: borrow Counter) { // looks at the counter Console.writeLine(c.hits); } fn record(c: borrow mut Counter) { // changes it in place c.hits += 1; } fn main() { var counter = Counter { hits: 0 }; record(mut counter); // `mut` at the call: this call changes `counter` record(mut counter); show(counter); // borrowed for the call; no marker needed } ``` ```text 2 ``` A `borrow T` parameter accepts the value itself, borrowed implicitly. A `borrow mut T` parameter needs `mut` before the argument, so that every place where a value may change is visible where it is called, as C#'s `ref` is. ### The rule While a borrow is **live**, meaning the reference or anything made from it is still used later, the borrowed value may not be: - changed, mutably borrowed or destroyed, if the borrow is shared; - used in any way, if the borrow is mutable. The compiler checks this within each function, on every path. A borrow ends at its last use, not at the end of its scope. ```ceesafe fn main() { var total = 0; let r = borrow total; total += 1; Console.writeLine(r); } ``` ```text main.cee:4:5: error[N0240]: cannot assign to 'total' while 'total' is borrowed | 4 | total += 1; | ^^^^^^^^^^^ = note: 'total' is borrowed here (3:5) = note: the borrow is used later here (5:5) ``` Moving the `Console.writeLine(r)` above the `+=` fixes it: the borrow is then dead before the write. The same rule is what stops a vector from growing while an element is borrowed, since growing can move its elements. ### Returning references A function that returns a reference must return something borrowed from its parameters, never from its own locals. With one reference parameter, that is assumed. With several, the result names the ones it may borrow from: ```ceesafe fn larger(a: borrow i64, b: borrow i64) -> borrow(a, b) i64 => if (a > b) a else b; fn firstPositive(values: Slice) -> borrow i64 { for (i in 0..values.length) { if (values[i] > 0) { return borrow values[i]; } } return borrow values[0]; } fn main() { let x: i64 = 3; let y: i64 = 7; Console.writeLine(larger(x, y)); let numbers = [-1i64, 4, 9]; Console.writeLine(firstPositive(numbers[..])); } ``` ```text 7 4 ``` The caller then knows how long the result stays valid: as long as the arguments it borrows from. Returning a reference to a local is rejected, since the local is gone when the function returns: ```ceesafe fn pick(a: borrow i32) -> borrow i32 { let copy = a + 1; return borrow copy; } fn main() { Console.writeLine(pick(1)); } ``` ```text main.cee:3:5: error[N0244]: cannot return a reference to 'copy', which is local to 'pick' | 3 | return borrow copy; | ^^^^^^^^^^^^^^^^^^^ = note: borrowed here (3:12) ``` ### Two mutable borrows Two `borrow mut` references to one value can never be live together, even as two arguments of one call: ```ceesafe fn addTo(target: borrow mut i32, amount: borrow mut i32) { target += amount; } fn main() { var x = 1; addTo(mut x, mut x); } ``` ```text main.cee:7:18: error[N0241]: cannot borrow 'x' as mutable while 'x' is mutably borrowed | 7 | addTo(mut x, mut x); | ^^^^^ = note: 'x' is mutably borrowed here (7:11) = note: the borrow is used later here (7:5) ``` ## References in structs: regions A struct that holds a reference or a view must say so, with a **region parameter**: the region is the lifetime of what the reference points into. Values of such a type cannot outlive that region. ```ceesafe struct Word { text: StringView; start: usize; } fn firstWord(line: StringView) -> Word { var end: usize = 0; while (end < line.length && line[end] != ' ') { end += 1; } return Word { text: line[0..end], start: 0 }; } fn main() { let line = "hello wide world"; let word = firstWord(line); Console.writeLine(word.text); } ``` ```text hello ``` Plain functions that take and return references never need region parameters; only types that *store* references, and functions that pass such types through, do. Regions are a compile-time idea: they cost nothing at run time. ## What the checker does not do The borrow checker works within one function at a time, using the signatures of the functions it calls. It is deliberately simpler than Rust's: some correct programs are rejected. When that happens, the usual fixes are to end a borrow earlier, copy a value out, split a function, or use an index or a [slot map handle](collections.md) instead of a stored reference. ## Next [Errors](errors.md): `Result`, `?`, `!` and panics. # Errors CeeSafe has two kinds of failure, and keeps them apart: - **Recoverable errors** are values: a function that can fail returns `Result`, and the caller decides what to do. Allocation failure and I/O errors are of this kind. - **Bugs** stop the program: an index out of bounds, an integer overflow, dividing by zero, unwrapping an error or a `null`. This is a **panic**: the program prints the reason and where it happened, and exits at once. There is no unwinding and no catching. ## `Result` ```ceesafe enum ParseError { Empty, BadDigit(u8), TooLarge, } fn parse(text: StringView) -> Result { if (text.length == 0) { return Err(ParseError.Empty); } var value: u32 = 0; for (c in text.bytes()) { if (c < '0' || c > '9') { return Err(ParseError.BadDigit(c)); } let next = value.checkedMul(10); if (next == null) { return Err(ParseError.TooLarge); } value = next! + (c - '0') as u32; } return Ok(value); } fn main() { match (parse("2026")) { Ok(n) => { Console.writeLine(n); } Err(ParseError.BadDigit(c)) => { Console.write("not a digit: "); Console.writeLine(c); } Err(_) => { Console.writeLine("no number"); } } match (parse("20x6")) { Ok(n) => { Console.writeLine(n); } Err(ParseError.BadDigit(c)) => { Console.write("not a digit: "); Console.writeLine(c); } Err(_) => { Console.writeLine("no number"); } } } ``` ```text 2026 not a digit: 120 ``` `Result` is an enum with two variants, `Ok(T)` and `Err(E)`, matched without a type prefix. `T` may be `void` (`Result`, with `Ok()`); `E` is usually an enum of the ways the function can fail. ## `?`: pass the error on In a function that returns a `Result` with the same error type, `e?` is the value inside `Ok`, or returns the `Err` to the caller at once. Every local is destroyed on the way out, as for any `return`. ```ceesafe enum ParseError { Empty, BadDigit(u8), } fn digit(c: u8) -> Result { if (c < '0' || c > '9') { return Err(ParseError.BadDigit(c)); } return Ok((c - '0') as u32); } fn twoDigits(text: StringView) -> Result { if (text.length != 2) { return Err(ParseError.Empty); } return Ok(digit(text[0])? * 10 + digit(text[1])?); } fn main() { Console.writeLine(twoDigits("42")!); match (twoDigits("4x")) { Ok(n) => { Console.writeLine(n); } Err(_) => { Console.writeLine("not two digits"); } } } ``` ```text 42 not two digits ``` The same works on optionals: `o?` returns `null` from a function that returns an optional. ## `!`: this cannot fail `e!` is the value inside `Ok` (or a present optional's value). If it is an `Err` (or `null`), the program panics. Use it where failure would be a bug, or in small programs and tests where stopping is the right answer: ```ceesafe fn main() { var names = Vector.create(); names.push("ada")!; // allocation failure stops this program names.push("grace")!; Console.writeLine(names.length); } ``` ```text 2 ``` ## Allocation failure is a value Every operation that allocates returns a `Result<…, AllocError>`: `Box.create`, `Vector.push`, `String.append`, `Arena.alloc` and the rest. A program that must survive running out of memory, such as a kernel, handles it; a program that need not, writes `!` or passes it on with `?`: ```ceesafe struct Node { value: i32; next: Box?; } fn push(head: Box?, value: i32) -> Result, AllocError> => Box.create(Node { value: value, next: head }); fn build(n: i32) -> Result?, AllocError> { var head: Box? = null; for (i in 0..n) { head = push(move head, i)?; } return Ok(head); } fn main() { match (build(3)) { Ok(list) => { Console.writeLine(list!.value); } Err(AllocError.OutOfMemory) => { Console.writeLine("out of memory"); } } } ``` ```text 2 ``` The test suite runs every allocating program again with each allocation failing in turn, to check that every failure path cleans up after itself. ## Panics A panic prints the reason and the source location, and exits with status 134: ```ceesafe fn main() { let values = [1, 2, 3]; var i: usize = 0; while (true) { Console.writeLine(values[i]); i += 1; } } ``` ```text 1 2 3 ``` Here the fourth read stops the program with `panic: index out of bounds` and the file and line of `values[i]`. Panics never run `drop` blocks or deferred statements: the program is in a state its author did not expect, so nothing more of it runs. ## I/O errors Files, sockets and processes report `IoError`, a built-in enum (`NotFound`, `PermissionDenied`, `AlreadyExists`, …, `Other(code)`), whose `message()` is the C library's text: ```ceesafe fn main() -> i32 { match (File.open("/no/such/file")) { Ok(file) => { Console.writeLine("opened"); return 0; } Err(e) => { Console.writeLine(e.message()); return 1; } } } ``` ```text No such file or directory ``` ## Next [Memory and allocation](memory.md). # Memory and allocation CeeSafe has no hidden allocation. Values live on the stack unless the program asks for memory, and every request names where the memory comes from and returns an error when there is none. This chapter goes from the simplest owner of heap memory, `Box`, to arenas, pools and scratch frames, and ends with how the compiler moves boxes to the stack when it can prove that is safe. The rules are in [Memory model](../spec/memory-model.md) and the type system's sections on [boxes](../spec/type-system.md#_18-boxes-and-allocators-m3b) and [regions](../spec/type-system.md#_19-regions-m4). ## `Box` `Box` owns one `T` on the heap. It is one pointer, it moves (never copies), and destroying it destroys the `T` and frees the memory. Fields and methods are reached through it as if it were the value. ```ceesafe struct Account { id: u64; balance: i64; fn deposit(borrow mut this, amount: i64) { this.balance += amount; } } fn open(id: u64) -> Result, AllocError> => Box.create(Account { id: id, balance: 0 }); fn main() { var account = open(7)!; account.deposit(100); // through the box Console.writeLine(account.balance); } // the Account is destroyed and its memory freed here ``` ```text 100 ``` Boxes are how recursive types are built, since a type cannot contain itself directly: ```ceesafe enum Tree { Leaf, Node(Box, i32, Box), } fn sum(t: borrow Tree) -> i32 => match (t) { Tree.Leaf => 0, Tree.Node(left, value, right) => sum(left) + value + sum(right), }; fn leaf() -> Box => Box.create(Tree.Leaf)!; fn main() { let tree = Tree.Node(Box.create(Tree.Node(leaf(), 1, leaf()))!, 2, Box.create(Tree.Node(leaf(), 3, leaf()))!); Console.writeLine(sum(tree)); } ``` ```text 6 ``` A long list of boxes is destroyed with a loop, not recursion, so it cannot overflow the stack. ## Allocators `Box` is `Box`: memory from the platform's `malloc`. The allocator is part of the type, and `Box.createIn(allocator, value)` chooses another one: | Allocator | Gives | Frees | |---|---|---| | `SystemHeap` | the platform heap | each value when its owner is destroyed | | `Arena` | chunks of one region | everything at once, when the arena is destroyed or reset | | `FixedBufferAllocator` | a buffer you provide (an array on the stack, say) | nothing: the buffer is reused when the allocator ends | | `Scratch` frames | a runtime stack of chunks, last in first out | everything in a frame when the frame ends | | `Pool` | slots for up to *n* values of one type | each slot when its box is destroyed | | your own | any struct that implements `Allocator` | as you implement it | `FailingAllocator` is a `SystemHeap` that can be told to fail after *n* allocations, for testing failure paths. ## Arenas An arena hands out memory quickly and frees it all at once. Every allocation **borrows the arena**, so none can outlive it: the compiler rejects any attempt to keep one after the arena is reset or destroyed. ```ceesafe fn histogram(input: Slice) -> Result { using arena = Arena.create(SystemHeap {}, 65536u64); let counts = arena.allocArray(256usize, 0u32)?; // a SliceMut in the arena for (b in input) { counts[b as usize] += 1; } var distinct: u64 = 0; for (c in counts) { if (c != 0) { distinct += 1; } } return Ok(distinct); // a number, not arena memory } // the arena releases everything here fn main() { Console.writeLine(histogram("hello".bytes())!); } ``` ```text 4 ``` Values in an arena must not need destruction (no `drop` blocks, no boxes inside), since the arena never runs their destructors. Returning `counts` itself would be a compile error: it borrows an arena that ends with the function. ## A fixed buffer ```ceesafe struct Span { start: u32; length: u32; } fn main() { var storage = [0u8; 1024]; let fba = FixedBufferAllocator.over(mut storage); // borrows `storage` mutably let spans = fba.allocArray(16usize, Span { start: 0, length: 0 })!; spans[3].length = 5; Console.writeLine(spans[3].length); Console.writeLine(spans.length); } ``` ```text 5 16 ``` When the buffer is full, an allocation returns `Err(AllocError.OutOfMemory)`; nothing is ever taken from the heap. ## Scratch frames Scratch memory is a stack of frames: a function enters a frame, allocates temporaries from it, and they are all released when the frame ends. Nested frames borrow their parent, so frames always end in order. ```ceesafe fn render(n: usize) -> Result { using frame = Scratch.enter(); let buffer = frame.allocArray(n, 0u8)?; { using inner = frame.enter(); let temporary = inner.allocArray(n * 2, 0u16)?; temporary[0] = 1; } // `inner`'s memory is released here return Ok(buffer.length); } // `frame`'s memory is released here fn main() { Console.writeLine(render(64)!); } ``` ```text 64 ``` ## Pools A pool holds up to a fixed number of values of one type. `take` returns a box-like `PoolBox` that returns its slot when destroyed, so pool values may have `drop` blocks. ```ceesafe struct Particle { x: i32; v: i32; } fn simulate() -> Result { using pool: Pool = Pool.create(SystemHeap {}, 64u32); var a = pool.take(Particle { x: 0, v: 2 })?; let b = pool.take(Particle { x: 10, v: -1 })?; a.x += a.v; return Ok(a.x + b.x); } fn main() { Console.writeLine(simulate()!); } ``` ```text 12 ``` ## Your own allocator An allocator is a struct with no fields that implements `lib/core`'s `Allocator` interface: `allocate` returns memory for a `Layout` (size and alignment) or an `AllocError`, and `deallocate`, which is `unsafe` because it must be given exactly what `allocate` returned, gives it back. `Box.createIn(MyAllocator {}, value)` then uses it. The CosmoOS kernel's `KernelHeap` over `kmalloc` is written this way; see [Kernel code](kernel.md) and the [lib/core reference](../spec/api/core.md). ## Placement: boxes on the stack The compiler looks for boxes that provably never outlive their function, or one loop iteration, and puts them on the stack instead. The program cannot tell the difference: destruction happens at the same points and in the same order, and allocation failure can no longer happen for those boxes. `ceesafec build --explain-allocations` lists each allocation and the decision; `--no-placement` turns it off. The rules are in [Optimisations](../spec/optimisations.md). ## Next [Arrays, slices and collections](collections.md). # Arrays, slices and collections ## Arrays `[T; N]` is `N` values of `T`, inline: on the stack, or inside the struct that holds it. Arrays are values (copied when assigned) and their element type must be Copy. ```ceesafe fn main() { var squares = [0u32; 5]; // five zeros for (i in 0..squares.length) { squares[i] = truncate(i * i); // usize to u32 could lose bits: say so } let primes = [2, 3, 5, 7]; var sum = 0; for (p in primes) { sum += p; } Console.writeLine(squares[4]); Console.writeLine(sum); } ``` ```text 16 17 ``` Every index is checked: `a[i]` with `i >= a.length` stops the program. The compiler removes the check where it can prove the index is in range, as in both loops above. ## Slices `Slice` is a borrowed view of consecutive elements, and `SliceMut` a borrowed view that can change them. They are a pointer and a length, they follow every rule for references, and they are how functions take "some elements": ```ceesafe fn total(values: Slice) -> i64 { var sum: i64 = 0; for (v in values) { sum += v; } return sum; } fn double(values: SliceMut) { for (i in 0..values.length) { values[i] *= 2; } } fn main() { var numbers = [1i64, 2, 3, 4, 5]; double(mut numbers[1..3]); // elements 1 and 2 Console.writeLine(total(numbers[..])); Console.writeLine(total(numbers[3..])); } ``` ```text 20 9 ``` `a[lo..hi]`, `a[lo..]`, `a[..hi]` and `a[..]` take elements `lo` up to `hi`; the range is checked. A slice borrows the whole array it came from, so the array cannot change while the slice is used. Slices also copy and fill in one checked operation (`d.copyFrom(s)`, `d.fill(v)`), swap elements, and sort and search with closures (see [Functions and closures](closures.md)). ## Vectors `Vector` owns a growable list on the heap. Pushing can fail, so `push` returns a `Result`. ```ceesafe fn main() { var v = Vector.create(); // allocates nothing yet for (i in 0..5) { v.push(i * 10)!; } Console.writeLine(v.length); Console.writeLine(v[2]); match (v.pop()) { // i32?: null when empty Some(last) => { Console.writeLine(last); } null => {} } v[0] = 99; var sum = 0; for (x in v) { sum += x; } Console.writeLine(sum); } ``` ```text 5 20 40 159 ``` A vector can be passed where a `Slice` is expected (it lends its elements), and `mut v` where a `SliceMut` is. `v.view()` and `v.viewMut()` give the slices explicitly. Other members: `capacity`, `clear()`, `truncate(n)`, `swap(i, j)`, and `Vector.withCapacity(n)`. **Invalidation is a compile error.** Growing a vector may move its elements, so while any element or view of it is borrowed, `push`, `pop`, `clear` and writes are rejected. This is the most common thing the borrow checker catches in code that would crash in C++. ## Text: `StringView` and `String` `StringView` is a read-only view of UTF-8 text: string literals are views, and so are slices of strings. `String` owns growable text. ```ceesafe fn greet(name: StringView) -> Result { var s = String.withCapacity(name.length + 7)?; s.append("Hello, ")?; s.append(name)?; s.append("! You are visitor ")?; s.appendNumber(42)?; return Ok(s); } fn main() { let message = greet("Ada")!; Console.writeLine(message); let word: StringView = "CeeSafe language"; Console.writeLine(word[0..7]); Console.writeLine(word.length); Console.writeLine(word == "CeeSafe language"); } ``` ```text Hello, Ada! You are visitor 42 CeeSafe 16 true ``` - `length` is in bytes; `s[i]` is a byte (`u8`); `s.bytes()` is a `Slice`. - Slicing (`s[lo..hi]`) checks that both ends fall on character boundaries. - A `String` can only receive valid UTF-8 (`append` takes a view, which always is), so its contents are always valid text. - `==` compares bytes, on views and on `String`s. Numbers in other forms go through lib/core's helpers, which a `String` and a `Vector` both have (and `VectorBytes` as static functions): `appendHex(n, minWidth, upper)`, `appendOctal(n)`, `appendFixed(f, digits)`, rounded half to even as Rust's `{:.3}` is, and `appendPadded(text, width, align, fill)` for a field of `width` characters. ```ceesafe fn main() { var s = String.create(); s.appendHex(48879, 8, true)!; s.append(" ")!; s.appendOctal(8)!; s.append(" ")!; s.appendFixed(2.675, 2)!; s.append(" [")!; s.appendPadded("id", 6, TextAlign.Right, ' ')!; s.append("]")!; Console.writeLine(s.view()); } ``` ```text 0000BEEF 10 2.67 [ id] ``` `2.675` is `2.67499999…` in binary, so it rounds down, as it does in Rust and C. ## Stable references: `SlotMap` and `StableVector` Sometimes values must refer to each other, as in a graph. A reference stored in a value would borrow the collection for as long as the value lives. A `SlotMap` gives out **handles** instead: plain values that name an element, can be stored anywhere, and are checked on every use, so a handle to a removed element finds nothing instead of something else. ```ceesafe struct Node { value: i32; next: Handle?; } fn main() { var nodes = SlotMap.create(); let a = nodes.insert(Node { value: 1, next: null })!; let b = nodes.insert(Node { value: 2, next: a })!; match (nodes.get(b)) { Some(node) => { Console.writeLine(node.value); } null => {} } let removed = nodes.remove(a); Console.writeLine(nodes.get(a) == null); // the handle is stale now Console.writeLine(nodes.length); } ``` ```text 2 true 1 ``` `StableVector` is a grow-only sequence whose elements never move, so an element can stay borrowed while more are pushed. ## Maps: `HashMap` ```ceesafe fn key(text: StringView) -> String { var s = String.create(); s.append(text)!; return s; } fn main() { var ages = HashMap.create(); ages.put(key("ada"), 36)!; ages.put(key("alan"), 41)!; let previous = ages.put(key("ada"), 37)!; // the old value: Some(36) let ada = key("ada"); match (ages.get(borrow ada)) { Some(age) => { Console.writeLine(age); } null => { Console.writeLine("unknown"); } } for (i in 0..ages.length()) { let name = ages.keyAt(i); Console.writeLine(name.view()); } } ``` ```text 37 ada alan ``` - **Keys.** A key type implements `HashKey`; lib/std does for integers, `bool`, `String` and `Vector`. - **The hash.** It is keyed by a random value chosen once per process, so a client cannot send keys that all collide. - **Order.** Iteration follows insertion, so a program's output never depends on the hash. A removal moves the last entry into the removed one's place. ## Next [Generics and interfaces](generics.md). # Generics and interfaces ## Generic functions and types Functions, structs and enums can take type parameters. Each use with different types compiles to its own copy (monomorphisation), so generic code is as fast as hand-written code for each type. ```ceesafe struct Pair { first: T; second: T; fn swapped(this) -> Pair => Pair { first: this.second, second: this.first }; } enum Maybe { Nothing, Just(T), } fn firstOf(pair: Pair) -> T => pair.first; fn main() { let p = Pair { first: 1, second: 2 }; // Pair, inferred let q = p.swapped(); Console.writeLine(firstOf(q)); // T = i32, inferred let names = Pair { first: "x", second: "y" }; Console.writeLine(firstOf(names)); // written out let m: Maybe = Maybe.Just(7); match (m) { Maybe.Just(v) => { Console.writeLine(v); } Maybe.Nothing => {} } } ``` ```text 2 x 7 ``` Type arguments are inferred from the arguments where possible, and written otherwise: `Pair { … }`, `Vector.create()`, `firstOf(names)`. **Generic code is checked once,** for every possible type argument. Inside it, a value of type `T` can be moved, borrowed, stored and returned, but nothing else is known about it: no arithmetic, no fields, no `==`. What it can do comes from its **bounds**. ## Interfaces An interface lists methods. A type implements it with an `impl` block, and a generic function bound by it can call those methods. ```ceesafe interface Shape { fn area(borrow this) -> u64; fn name(borrow this) -> StringView; } struct Square { side: u64; } struct Rect { w: u64; h: u64; } impl Shape for Square { fn area(borrow this) -> u64 => this.side * this.side; fn name(borrow this) -> StringView => "square"; } impl Shape for Rect { fn area(borrow this) -> u64 => this.w * this.h; fn name(borrow this) -> StringView => "rect"; } fn total(shapes: Slice) -> u64 { var sum: u64 = 0; for (shape in shapes) { sum += shape.area(); } return sum; } fn describe(shape: borrow S) { Console.write(shape.name()); Console.write(": "); Console.writeLine(shape.area()); } fn main() { let squares = [Square { side: 2 }, Square { side: 3 }]; var rects = Vector.create(); rects.push(Rect { w: 4, h: 5 })!; Console.writeLine(total(squares[..]) + total(rects.view())); describe(squares[1]); } ``` ```text 33 square: 9 ``` - Calls through a bound are resolved at compile time: there is no virtual dispatch and no cost. - `Copy` is a built-in bound: a `T: Copy` can be used twice and stored in arrays. - Several bounds are joined with `+`: ``. - An interface can itself be generic: `interface Comparable { fn compare(borrow this, other: borrow T) -> i32; }`. ## Default methods and `Self` An interface method can have a body: its **default**. An impl that leaves the method out inherits the default; one that writes it overrides it. Inside an interface, `Self` is the implementing type. ```ceesafe interface Watcher { fn onError(borrow mut this, code: u32); fn onWatchError(borrow mut this, code: u32) { this.onError(code + 100); } fn label(borrow this) -> StringView => "watcher"; fn copyOf(borrow this) -> Self; } struct Log { total: u32; } impl Watcher for Log { fn onError(borrow mut this, code: u32) { this.total += code; } fn label(borrow this) -> StringView => "log"; fn copyOf(borrow this) -> Self => Self { total: this.total }; } fn report(w: borrow mut W) { w.onWatchError(1); Console.writeLine(w.label()); } fn main() { var log = Log { total: 0 }; log.onWatchError(5); report(mut log); Console.writeLine(log.copyOf().total); } ``` ```text log 206 ``` - A default is checked once, where the interface is, with `Self` unknown: it can call the interface's methods but not use fields. An error in it is reported there, even if no impl inherits it. - An impl that inherits a default gets its own copy, compiled for the target type: a call on the type, through a bound or through `dyn` calls it directly or through the table, as for a method the impl writes. - A type has one method of each name: an inherited default whose name the type already has is an error at the impl. - In an `impl` block and in a struct's or enum's body, `Self` is the type itself, with its parameters: `Self { … }`, `Self.make()` and `Self.MAX` work as with the type's name. - A method without a receiver is a static function: `fn create(options: borrow Options) -> Self;` is called as `T.create(o)` through a bound `T`, as `Self.create(o)` in a default and as `Rule.create(o)` on a type. A method may also have type parameters of its own, `fn visit(borrow mut this, value: T)`, instantiated per call. ## Constants and associated types An interface can declare constants, with or without a default, and associated types, which each impl chooses: ```ceesafe interface Encoding { const NAME: StringView; const WIDTH: usize = 1; type Unit: Copy; fn decode(borrow this, bytes: Slice) -> Self.Unit?; } struct Latin1 { unused: u8; } impl Encoding for Latin1 { const NAME: StringView = "latin-1"; type Unit = u8; fn decode(borrow this, bytes: Slice) -> u8? => if (bytes.length > 0) bytes[0] else null; } struct Ucs2 { unused: u8; } impl Encoding for Ucs2 { const NAME: StringView = "ucs-2"; const WIDTH: usize = 2; type Unit = u16; fn decode(borrow this, bytes: Slice) -> u16? { if (bytes.length < 2) { return null; } return (bytes[0] as u16) * 256 + (bytes[1] as u16); } } fn first(e: borrow E, bytes: Slice) -> E.Unit? { Console.write(E.NAME); Console.write(" reads "); Console.write(E.WIDTH); Console.writeLine(" byte(s)"); return e.decode(bytes); } fn main() { let bytes = [1u8, 2u8]; Console.writeLine(first(Latin1 { unused: 0 }, bytes[..]) ?? 0); Console.writeLine(first(Ucs2 { unused: 0 }, bytes[..]) ?? 0); Console.writeLine(Ucs2.WIDTH); } ``` ```text latin-1 reads 1 byte(s) 1 ucs-2 reads 2 byte(s) 258 2 ``` - `E.NAME` and `E.Unit` name the constant and the type of whatever `E` is; each instance reads its impl's, so a test of a constant such as `if (H.HAS_ON_DATA)` costs nothing at run time. - An associated type's bounds (`Copy` here) are checked at each impl, and are all that generic code knows of it. - When callers need to fix a type (an iterator's item, say), make it an interface parameter instead: `Iterator` rather than an associated type. - An interface with constants, associated types or static functions has no `dyn` type. An interface can build on others, its **supertraits**: `interface Shape: Named { … }`. Every `impl Shape` needs an `impl Named` for the same type, and a bound `S: Shape` gives `Named`'s methods too: inside a generic function over `S: Shape`, `s.name()` works, and `s` can be passed where a `Named` is wanted. `dyn Shape` holds both interfaces' methods. `Copy` can be a supertrait as well, making every bound by the interface a Copy one. ## Generic impls An impl can have type parameters of its own. A wrapper then implements an interface for every inner type that has it: ```ceesafe interface Source { fn next(borrow mut this) -> u32; } struct Counter { value: u32; } impl Source for Counter { fn next(borrow mut this) -> u32 { this.value += 1; return this.value; } } /// Any source, with the calls of `next` counted. struct Counted { inner: T; calls: u32; } impl Source for Counted { fn next(borrow mut this) -> u32 { this.calls += 1; return this.inner.next(); } } fn take(s: borrow mut S) -> u32 => s.next() + s.next(); fn main() { var counted = Counted { inner: Counted { inner: Counter { value: 0 }, calls: 0 }, calls: 0 }; Console.writeLine(take(mut counted)); Console.writeLine(counted.calls + counted.inner.calls); } ``` ```text 3 4 ``` - The impl is checked once, then compiled for each instance it applies to: `Counted` and `Counted>` here. An instance whose argument does not meet the bound (`Counted`) does not implement `Source`. - Each of the impl's type parameters must appear in its target, and the target is a generic struct or enum. - Beside a generic impl, no other impl of the same interface may be written for the same struct or enum: no `impl Source for Counted` as well. - An impl lives in the package that declares the interface or the type (the orphan rule), so two packages never both provide one. - A struct with region parameters implements an interface with its regions named, `impl Body for RequestBody { … }`, and is used through bounds; it does not convert to `dyn Body`, since a `dyn` value holds no loans. ## Const parameters A generic item may also take **values**: `const N: T`, with `T` a `bool`, an integer type or a fieldless enum. Each distinct value compiles to its own copy, so a flag or a size known at compile time costs nothing at run time. ```ceesafe enum Mode { Plain, Shout } struct Buffer { bytes: [u8; N]; used: usize; } fn render(c: u8) -> u8 { if (UPPER && c >= 'a' && c <= 'z') { return c - 32; } return c; } fn greet() -> StringView => match (M) { Mode.Plain => "hello", Mode.Shout => "HELLO" }; fn sum(values: [u32; N]) -> u32 { var total: u32 = 0; for (v in values) { total += v; } return total; } fn main() { let b = Buffer<16> { bytes: [0u8; 16], used: 0 }; Console.writeLine(b.bytes.length); let shout = true; let c = if (shout) render('q') else render('q'); // the instance is chosen at run time Console.writeLine(c); Console.writeLine(greet()); Console.writeLine(sum([1u32, 2, 3])); // N = 3, inferred from the argument } ``` ```text 16 81 HELLO 6 ``` - Inside the generic item, a const parameter is a value of its type: `if (UPPER)`, `match (M)`, `[u8; N]`. Both branches of `if (UPPER)` are checked, and each copy keeps only its own. - An argument is `true`, `false`, an integer literal, a constant (`Buffer`), an enum variant (`greet()`) or the caller's own const parameter. A run-time value is not one: choose the instance with an `if`, as above. - An array length may also name a `usize` constant: `const SIZE: usize = 64;` then `[u8; SIZE]`. - Expressions over a parameter (`[u8; N + 1]`, `render`) are refused; name a constant, or add a parameter. ## Dynamic dispatch A bound is resolved at compile time, so a `Slice` holds shapes of one type. For a collection of different types that implement the same interface, use `dyn Shape`: a value of some type implementing `Shape`, whose methods are called through a table at run time. Its size is not known, so it lives behind a pointer: `borrow dyn Shape`, `borrow mut dyn Shape`, or an owning `Box`. ```ceesafe interface Shape { fn area(borrow this) -> u64; fn name(borrow this) -> StringView; } struct Square { side: u64; } struct Rect { w: u64; h: u64; } impl Shape for Square { fn area(borrow this) -> u64 => this.side * this.side; fn name(borrow this) -> StringView => "square"; } impl Shape for Rect { fn area(borrow this) -> u64 => this.w * this.h; fn name(borrow this) -> StringView => "rect"; } fn describe(shape: borrow dyn Shape) { Console.write(shape.name()); Console.write(": "); Console.writeLine(shape.area()); } fn main() { var shapes = Vector>.create(); shapes.push(Box.create(Square { side: 3 })!)!; // a Box becomes a Box shapes.push(Box.create(Rect { w: 2, h: 5 })!)!; for (shape in shapes) { describe(shape); } let sq = Square { side: 4 }; describe(sq); // a Square, lent as a borrow dyn Shape } ``` ```text square: 9 rect: 10 square: 16 ``` - A `borrow T` or a `Box` converts to the `dyn` type where one is expected, if `T` implements the interface. There is no way back to `T`. - Every method of the interface must take `borrow this` or `borrow mut this`, have no type or const parameters of its own and not name `Self` except as its receiver: a static function or a generic method has no place in a table. Defaults are fine. Async methods are allowed: `await shape.load()` through `dyn` waits for the value's own method, in room the compiler makes for the largest of the types converted to that `dyn` type. - A call through `dyn` cannot be inlined. Prefer a bound when one type is enough. - An interface method may declare its effects, `@effects() fn visit(borrow mut this, x: u64);`, which every `impl` must keep: kernel code in an atomic context can then call it through `dyn`. ## Next [Functions and closures](closures.md). # Functions and closures ## Functions are values A function can be stored in a variable and passed as an argument. Its type is written `fn(parameter types) -> result`: ```ceesafe fn double(x: i32) -> i32 => x * 2; fn negate(x: i32) -> i32 => -x; fn applyTwice(f: fn(i32) -> i32, value: i32) -> i32 => f(f(value)); fn main() { Console.writeLine(applyTwice(double, 3)); var op: fn(i32) -> i32 = double; op = negate; Console.writeLine(op(5)); } ``` ```text 12 -5 ``` A function value is one pointer, and calling it is an indirect call. ## Closures A closure is a function written inline, `(parameters) => body`, that can use the variables around it. It borrows what it captures, so it is a kind of reference: its type is `borrow fn(…) -> R` if it only reads what it captures, and `borrow mut fn(…) -> R` if it changes something. ```ceesafe fn countIf(values: Slice, keep: borrow fn(i32) -> bool) -> i32 { var n = 0; for (v in values) { if (keep(v)) { n += 1; } } return n; } fn forEach(values: Slice, visit: borrow mut fn(i32)) { for (v in values) { visit(v); } } fn main() { let values = [3, 8, 1, 9, 4]; let limit = 4; Console.writeLine(countIf(values[..], (v) => v > limit)); // captures `limit` var total = 0; forEach(values[..], (v) => { total += v; }); // changes `total` Console.writeLine(total); let twice = (x: i32) => x * 2; // captures nothing: a plain fn Console.writeLine(twice(21)); } ``` ```text 2 25 42 ``` - Parameter types can be left out where the expected type gives them, as in the calls above. - The body is an expression, or a block: `(v) => { total += v; }`. - A closure that captures nothing is an ordinary function value, and can be stored anywhere a function can. ### Closures do not outlive their block Because a closure borrows its captures, the borrow checker treats it like any reference: it cannot be returned from the function that made it, stored in a struct, or kept after a captured variable changes. That makes closures free of allocation and safe to use in kernels. ### Owning closures A `move` literal moves what it captures into the closure, so the closure can outlive the function that made it. It is boxed, with an allocation you can see and handle, as a `Box R>`. A closure that changes what it captured is a `Box R>`, and calling it needs mutable access. ```ceesafe fn adder(n: u64) -> Box u64> { return Box.create(move (x) => x + n)!; } fn counter() -> Box u64> { var count: u64 = 0; return Box.create(move () => { count += 1; return count; })!; } fn main() { let add2 = adder(2); Console.writeLine(add2(3)); var next = counter(); next(); Console.writeLine(next()); var steps = Vector u64>>.create(); steps.push(adder(1))!; steps.push(adder(10))!; var x: u64 = 0; for (step in steps) { x = step(x); } Console.writeLine(x); } ``` ```text 5 2 11 ``` - The literal goes straight into `Box.create(…)` where a `Box` is expected; its parameter types come from there. - A captured variable is moved in, and cannot be used afterwards (a Copy one is copied). It cannot hold a reference. - The body changes the closure's own copy of what it captured, and cannot move it out. ## Methods that take closures Slices, arrays (through `a[..]`) and vectors have `sortBy`, `forEach`, `any`, `all`, and vectors `retain`: ```ceesafe struct Reading { sensor: u32; celsius: f64; } fn main() { var readings = Vector.create(); for (i in 0..8u32) { readings.push(Reading { sensor: i % 3, celsius: 18.5 + (i as f64) * 0.75 })!; } let limit = 20.0; readings.retain((r) => r.celsius > limit); readings.sortBy((a, b) => if (a.celsius > b.celsius) -1 else if (a.celsius < b.celsius) 1 else 0); var sum = 0.0; readings.forEach((r) => { sum += r.celsius; }); Console.writeLine(readings.length); Console.writeLine(readings[0].celsius); Console.writeLine(readings.any((r) => r.sensor == 0)); } ``` ```text 5 23.75 true ``` `sortBy` is an introsort: it allocates nothing and runs in O(n log n), but it is not stable. Sorted, reversed and duplicate-heavy input take about one pass. Its comparator returns a negative number, zero or a positive number. These methods are written in CeeSafe, in `lib/core/slices.cee`. In an async fn, they take async closures too: `await readings.forEach(async (r) => await upload(r))` calls the closure on each element and waits for each call. Such a call is a task, so it needs its `await`. The same holds for your own functions: a function marked `@effectsOf(f)` for its closure parameter `f` waits for an async `f`. It must then only call `f`, or pass it on to another such function. It must not store it. ## Effect lists In kernel code, a callback may run where sleeping or allocating is forbidden. A function type can say what its functions may do: `@effects() fn(i32)` may neither sleep nor allocate, and `@effects(alloc) fn(i32)` may allocate but not sleep. The compiler checks every function and closure given for it. See [Kernel code](kernel.md). ```ceesafe fn onEach(values: Slice, f: borrow mut @effects() fn(u32)) { for (v in values) { f(v); } } fn main() { var largest: u32 = 0; let values = [4u32, 11, 7]; onEach(values[..], (v) => { if (v > largest) { largest = v; } }); Console.writeLine(largest); } ``` ```text 11 ``` ## Floats `f32` and `f64` follow IEEE 754: no operation on them panics, and NaN and the infinities behave as specified. They print as the shortest text that reads back as the same value. ```ceesafe fn mean(values: Slice) -> f64 { var sum = 0.0; for (v in values) { sum += v; } return sum / round(values.length); } fn main() { let samples = [1.5, 2.25, 4.0]; let m = mean(samples[..]); Console.writeLine(m); Console.writeLine(m.sqrt()); Console.writeLine(truncate(m * 1000.0)); Console.writeLine(0.1 + 0.2); } ``` ```text 2.5833333333333335 1.6072751268321592 2583 0.30000000000000004 ``` Integers become floats with `as` when exact (`i32 as f64`) or `round(n)`; floats become integers with `truncate(x)` (toward zero; out of range stops the program), `checkedTruncate` or `saturatingTruncate`. The math methods are `sqrt`, `abs`, `floor`, `ceil`, `min` and `max`. Kernel code compiles with `--no-float`, which rejects float types altogether. ## Next [Threads](threads.md). # Threads A program can run work on several threads. The compiler checks that safe code never has a data race: a value moves to another thread only if it is safe there, and threads share memory only through types made for it. ## Spawning and joining `Thread.spawn` starts a thread running a `move` closure, which owns what it captures. It returns a `Thread`, and `join` waits for the thread and gives back its result. ```ceesafe fn main() { var numbers = Vector.create(); for (i in 1..101u64) { numbers.push(i)!; } let worker = Thread.spawn(move () => { var sum: u64 = 0; for (n in numbers[..]) { sum += n; } return sum; })!; Console.writeLine(worker.join()); } ``` ```text 5050 ``` - The vector is moved into the thread. The spawner cannot use it afterwards, so nothing is shared. - Spawning can fail (no memory, no thread): `Thread.spawn` returns a `Result`, here unwrapped with `!`. - A `Thread` that is dropped without `join` is joined then: every thread ends before `main` returns. - A panic in any thread ends the whole program. ## What may cross: `Send` A value may move to another thread if it is `Send`. Plain data, strings, vectors and boxes are. References are not, nor anything that holds one, nor raw pointers. The compiler infers it, and reports N0334 where it fails: ```ceesafe fn main() { var n: u64 = 5; let address = borrow n as *u64; let t = Thread.spawn(move () => address.isNull())!; } ``` ```text main.cee:4:26: error[N0334]: this thread's closure captures 'address', a '*u64', which is not Send: it cannot move to another thread (type-system.md §38.1) | 4 | let t = Thread.spawn(move () => address.isNull())!; | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ ``` A value that two threads *read* at once must also be `Share`. Most `Send` types are, but not a `StableVector`, whose `push` changes it through a shared reference, nor a `Box`, whose contents the compiler cannot see, nor anything that holds one. Put such a value behind a `Mutex` to share it. `Send` and `Share` can also bound a generic function: `fn run(value: T)`. ## Sharing: `Arc`, `Mutex` and atomics To share a value, give each thread an owner of it: `Arc` counts its owners, and the last one to go frees it. An `Arc` gives only shared access, so a value that threads change goes in a `Mutex`, whose guard lends it to one thread at a time. ```ceesafe struct Tally { count: u64; } fn main() { let tally = Arc>.create(Mutex.create(Tally { count: 0 })!)!; var threads = Vector>.create(); for (t in 0..4u64) { let mine = tally.clone(); threads.push(Thread.spawn(move () => { for (k in 0..250u64) { var guard = mine.get().lock(); let data = guard.data(); data.count += 1; } })!)!; } threads.clear(); // dropping the threads joins them var guard = tally.get().lock(); Console.writeLine(guard.data().count); } ``` ```text 1000 ``` For a counter or a flag, an atomic is simpler: `AtomicU64`, `AtomicU32`, `AtomicUSize` and `AtomicBool` change through a shared reference with `load`, `store`, `swap`, `compareExchange`, `fetchAdd` and `fetchSub`, and the integer ones with `fetchOr` and `fetchAnd`. Those are sequentially consistent: every thread sees every atomic operation in one order. That is the simple rule, and on x86-64 it costs a full barrier on each store. When one thread publishes data and another reads it, release and acquire are enough, and `storeWith` and `loadWith` say so: ```ceesafe struct Message { data: AtomicU64; ready: AtomicBool; } fn main() { let m = Message { data: AtomicU64.create(0), ready: AtomicBool.create(false) }; var got: u64 = 0; Thread.both(() => { while (!m.ready.loadWith(LoadOrdering.Acquire)) {} got = m.data.loadWith(LoadOrdering.Relaxed); }, () => { m.data.storeWith(42, StoreOrdering.Relaxed); m.ready.storeWith(true, StoreOrdering.Release); }); Console.writeLine(got); } ``` ```text 42 ``` The release store makes the data written before it visible to a thread whose acquire load reads `true`. The `…With` methods take C11's orderings (`Relaxed`, `Acquire`, `Release`, `AcquireRelease`, `SequentiallyConsistent`). A weaker ordering never makes a data race, but a wrong one can make a thread read a stale value, and neither `ceesafec run --interp` nor ThreadSanitizer will notice. Use the plain methods unless a measurement says otherwise. `benchmarks/atomics` has one: a ring between two threads, 5 to 9 times faster on x86-64 with release and acquire. ## Borrowing: `Thread.both` A spawned thread cannot borrow, since it may outlive what it borrowed. `Thread.both(a, b)` can: it runs `a` on a new thread and `b` on this one, and returns only when both have finished. So the two closures may use the caller's data, for example the two halves of a slice: ```ceesafe fn sum(values: Slice) -> u64 { var total: u64 = 0; for (v in values) { total = total + v; } return total; } fn main() { let values: [u64; 6] = [1, 2, 3, 4, 5, 6]; let halves = values[..].splitAt(3); var low: u64 = 0; var high: u64 = 0; Thread.both(() => { low = sum(halves.left); }, () => { high = sum(halves.right); }); Console.writeLine(low + high); } ``` ```text 21 ``` The usual borrowing rules hold between the two closures: both may read a value, but if one changes it the other may not use it. A value both read must be `Share`, and one that a closure changes must be `Send`; a reference counts as its referent here. `splitAtMut` gives two halves that each closure may change. If a thread cannot be started, `both` runs `a` and then `b` on this thread, so it never fails. ## Async tasks on several threads An `Async.run` uses one thread. To spread a scope's children over several, run it with `Async.runWorkers(4, task)` and spawn them with `s.spawnSend(async () => …)` instead of `s.spawn`. Another worker may take such a child before it starts. Once started, the child stays on that worker. What the child captures follows the rule of `Thread.both`: a value it reads must be `Share`, and one it changes must be `Send`. Two children cannot change the same value, so they report results through atomics, or through a value per child. Under a plain `Async.run`, `spawnSend` is `spawn`. ## Where threads run Threads are part of the hosted profile: Linux, macOS, Windows and CosmoOS user programs. Kernel code uses the kernel's own locks instead ([Kernel code](kernel.md)). The interpreter (`ceesafec run --interp`) runs threads one at a time, in a fixed order, so its output is always the same. ## Next [Async tasks](async.md): many waits on one thread. # Unsafe code and C Safe CeeSafe cannot express everything a systems program needs: talking to hardware, calling C, implementing an allocator. For those there is `unsafe`, kept small and visible. The contract unsafe code must keep is in [Unsafe code](../spec/unsafe.md), and the C boundary in the type system's [C interoperability](../spec/type-system.md#_28-c-interoperability-m8b) section. ## Raw pointers `*T` and `*mut T` are raw pointers: plain addresses, Copy, with no borrow attached. **Making** a pointer is safe; **using** one (`read`, `write`, `add`, turning it back into a reference) is allowed only inside `unsafe`. ```ceesafe /// Safety: `p` points to `n` initialised `u32`s that nothing else writes while this runs. unsafe fn sum(p: *u32, n: usize) -> u64 { var total: u64 = 0; for (i in 0..n) { total += p.add(i).read() as u64; } return total; } /// Safety: the pointer comes from a live array and stays inside it. fn main() { var values = [1u32, 2, 3, 4]; let p = borrow mut values[0] as *mut u32; unsafe { p.add(3).write(10); } Console.writeLine(unsafe { sum(p, values.length) }); } ``` ```text 16 ``` ## The `Safety:` rule Every `unsafe fn`, and every function that contains an `unsafe` block, must say in its doc comment why it is sound, on a line starting with `Safety:`. The compiler enforces it: ```ceesafe fn first(p: *u8) -> u8 { return unsafe { p.read() }; } ``` ```text main.cee:1:4: error[N0283]: function 'first' uses 'unsafe' and needs a '/// Safety:' doc comment saying why it is sound (type-system.md §27.4) | 1 | fn first(p: *u8) -> u8 { | ^^^^^ ``` For an `unsafe fn`, the line states what callers must guarantee; for a safe function with an `unsafe` block inside, why the block cannot go wrong. Reviewers read these lines; the compiler checks that they exist. `unsafe` permits only the pointer operations, calls of unsafe functions and C functions, and volatile accesses. Everything else in an unsafe block is checked exactly as in safe code. ## Calling C C functions are declared in an `extern "C"` block and called inside `unsafe`, since CeeSafe cannot check what C does. Their types are C's: integers, `bool`, raw pointers, `*CChar` for C strings, `CChar` for a `char`, and `@repr(C)` structs. ```ceesafe extern "C" { fn strlen(s: *CChar) -> usize; fn puts(s: *CChar) -> i32; } /// Safety: a `c"…"` ends in a zero byte, as `puts` and `strlen` need. fn main() -> i32 { let text = c"hello from C"; let p = text.ptr(); unsafe { puts(p); } return truncate(unsafe { strlen(p) }); } ``` `c"…"` is a C string: the compiler adds the zero byte that ends it, and refuses a `\0` inside. Its type is lib/core's `CStr`, a view with `ptr()` for C, `view()` and `length()` for CeeSafe (without the zero byte). It can be a constant or a static, and there `PREFIX + c"…"` joins constant text before it (`const RC: CStr = HOME + c"/.bunrc";`). `CStr.fromBytesWithNul(bytes)` makes one from bytes that end in their only zero byte, or gives `null`; like any view, it cannot outlive those bytes. `"…\0".ptr as *CChar` also works, but nothing checks the `\0` is there. C files are built with the program: `ceesafec build main.cee helpers.c -o program`, or the `c` list of a package's manifest. CeeSafe can't see what a C function does, so a call of one counts as possibly sleeping and allocating (type-system.md §32). Async code may not sleep, so calling such a function from an `async fn` is N0295. When the C function returns promptly, say so on its declaration: `@effects()` for none, or `@effects(alloc)` if it allocates. ```ceesafe extern "C" { @effects() fn crc32_update(crc: u32, data: *u8, length: usize) -> u32; } ``` ## Sharing C declarations between packages A C function has one declaration in a program, so its CeeSafe types can't drift apart: a second declaration with the same C name is N0201, even in another package, and so is an `@export` with that name. One package owns the declaration and makes it `public`, and the others call it through that package ([ADR 0081](../spec/decisions/0081-c-declarations-shared-between-packages.md)): ```ceesafe // sys/src/lib.cee, a library extern "C" { public fn strlen(s: *CChar) -> usize; } ``` ```ceesafe // app/src/main.cee, which depends on sys /// Safety: the literal ends in a zero byte, as strlen needs. fn main() { let text = "hello\0"; Console.writeLine(unsafe { sys.strlen(text.ptr as *CChar) }); } ``` If `app` declares `strlen` too, N0201 is reported at `app`'s declaration, with a note at the one `sys` owns. The message says what to do: ``use `sys.strlen`, which package 'sys' declares; remove this declaration``, or, when the declaration in `sys` isn't `public`, to make it `public` there. When the two declarations are identical in CeeSafe (the same parameter and result types, variadic-ness and attributes), `ceesafec fix` removes the redundant one. It doesn't change the calls, which still need `sys.` in front. To give a C function a stronger type, such as a `borrow` instead of a raw pointer, write a wrapper in the package that wants it, rather than a second declaration. The wrapper calls the owner's declaration and says in its `Safety:` line why that call is sound. Within one package, two declarations with one C name are N0201 too, reported at the later one in the package's file order: ```ceesafe extern "C" { fn strlen(s: *CChar) -> usize; @cname("strlen") fn length(s: *CChar) -> usize; } ``` ```text main.cee:4:8: error[N0201]: the C name 'strlen' of C function 'strlen' is already used by another C function or export (type-system.md §28.2) | 4 | fn length(s: *CChar) -> usize; | ^^^^^^ = note: the other declaration is here (2:8) ``` ## Being called from C `@export` makes a CeeSafe function callable from C, and `@repr(C)` gives a struct C's layout. `ceesafec` writes `ceesafe_exports.h` with their declarations for the C side to include. ```ceesafe @repr(C) struct Point { x: i32; y: i32; } /// The Manhattan length of a point, callable from C as `geo_length(struct Point)`. @export("geo_length") fn length(p: Point) -> i32 => abs(p.x) + abs(p.y); fn abs(v: i32) -> i32 => if (v < 0) -v else v; fn main() { Console.writeLine(length(Point { x: 3, y: -4 })); } ``` In C, a `@repr(C)` struct or `@repr` enum keeps the name it is declared with, even when it comes from a dependency: package `geo`'s `Point` is `struct Point` in `ceesafe_exports.h`, not `geo.Point`. So, like a C function, a `@repr` type's name belongs to one package in a program: a second `@repr(C) struct Point` in another package is N0201 (type-system.md §28.5). Ordinary structs and enums have no such limit. `@callback` gives a function a C address without exporting a symbol, for C APIs that take a function pointer and a `void *` argument, as kernels' timer and thread APIs do. `extern "C" fn(…) -> R` is the type of such a pointer. ## C structs from the C code's headers A C codebase's own structs are not re-declared in CeeSafe, where the two copies could drift apart. They are imported from its headers: CeeSafe uses them by pointer or embedded by value, names only the fields it uses, and the C compiler checks each one at build time. ```ceesafe @header("kernel/timer.h") extern "C" { struct timer; @effects() fn timer_start(t: *mut timer, delay_ns: u64); } ``` See [C structs from headers](../spec/decisions/0008-c-struct-bindings-from-headers.md) and [Kernel code](kernel.md). ### Arrays, keyword names and tables A C array member is declared as a CeeSafe array (`uint8_t uuid[16]` is `uuid: [u8; 16]`) and used like one: indexed with bounds checks, sliced, copied. A member whose name is a CeeSafe keyword takes `@cname`. An array of C structs is indexed in place. A constant table can be exported to C as data with `@export`. ```ceesafe @header("kernel/gpt.h") extern "C" { struct gpt_part { first_lba: u64; @cname("type") kind: [u8; 16]; uuid: [u8; 16]; } struct gpt_table { nparts: u32; parts: [gpt_part; 32]; } } /// `const uint8_t gpt_type_esp[16]` in C. @export("gpt_type_esp") const TYPE_ESP: [u8; 16] = [0x28, 0x73, 0x2a, 0xc1, 0x1f, 0xf8, 0xd2, 0x11, 0xba, 0x4b, 0x00, 0xa0, 0xc9, 0x3e, 0xc9, 0x3b]; /// Is partition `i` an EFI system partition? fn isEsp(table: borrow gpt_table, i: usize) -> bool { let kind = table.parts[i].kind; // a copy of the C array for (k in 0..kind.length) { if (kind[k] != TYPE_ESP[k]) { return false; } } return true; } ``` This is how CosmoOS's GPT parser is written (`integrations/cosmoos/c9.py` checks it; the rules are [ADR 0014](../spec/decisions/0014-c9-c-arrays-and-exported-constants.md)). ### C enums A header that types a parameter or a member with an enum is imported with `enum name: I;`, where `I` is the integer the C compiler gives the enum. The type is an integer of its own: `as` converts it to and from `I`, and its values are constants of the type. CeeSafe spells it `enum name` (or a typedef's name, with `@ctype`) everywhere C sees it, so a function pointer type whose C type names the enum, such as an operation table's entry, is the same C type as the CeeSafe function's. With `u32` in its place, C would accept the call, but UBSan's `-fsanitize=function` refuses a call through a pointer of another type. ```ceesafe @header("kernel/vfs.h") extern "C" { enum vnode_type: u32; struct vnode { @cname("type") kind: vnode_type; } } const VNODE_DIR: vnode_type = 2; fn isDirectory(vn: borrow vnode) -> bool => vn.kind == VNODE_DIR; /// A readdir callback: its type, `vfs_dirent_cb`, takes the enum. @callback fn emit(arg: *mut void, name: *CChar, len: usize, ino: u64, kind: vnode_type) -> i32 => if (kind == VNODE_DIR) 1 else 0; ``` The C compiler checks that the enum is the integer declared. That integer depends on the target: GCC and Clang on Linux and macOS make an enum without negative values `unsigned int` (`u32`), as this kernel's is, and one with a negative value `int` (`i32`), while on Windows every enum is `int`. A header meant for every target gives its enum a negative value, so that it is `i32` everywhere. The rules are [ADR 0079](../spec/decisions/0079-imported-c-enums.md) and type-system.md §31.6. ## C constants, C data and configured code A port of a C file keeps using what the C side defines: its constants, its globals, its build switches. The [C data rules](../spec/decisions/0080-the-kernel-floors.md) let CeeSafe name each of them without a C file of its own. **Constants C checks.** A constant written in an `extern "C"` block is an ordinary CeeSafe constant, and the C compiler asserts that the header agrees. `@cvalue` asserts a C expression instead, such as a size or an offset. `const NAME: T;` with no value reads C's own value at run time, for a string macro: ```ceesafe @header("kernel/vfs.h") extern "C" { const EINVAL: i32 = 22; @cvalue("sizeof(struct cosmo_stat)") const STAT_SIZE: usize = 144; const COSMO_VERSION: *CChar; } fn invalid() -> i32 => -EINVAL; ``` **Data C names, and data C defines.** `@export` on a static makes it C data under that symbol: a plain `static` of a C type is C's `const` object, such as an operation table a header declares `extern const`, and a `static mut` is a variable C may write, such as a sysctl's. In an `extern "C"` block, `static NAME: T;` reads data C defines: ```ceesafe @header("kernel/storage.h") extern "C" { struct fs_type { flags: u32; } static procnet_names: [*CChar; 8]; } /// `extern const struct fs_type pool_fs_type;` in C. @export("pool_fs_type") static POOL_FS: fs_type = fs_type { flags: 1 }; /// `extern uint32_t g_pool_rate;`: the sysctl writes it. @export("g_pool_rate") static mut RATE: u32 = 100; ``` **C locals.** `var s: T;` of a C type is C's zero-filled object on the stack, for state a C API keeps in the caller's frame, as a hash state is. It is used by its address and never moved: ```ceesafe @header("kernel/blake2b.h") extern "C" { struct blake2b; fn blake2b_init(s: *mut blake2b, outlen: usize); fn blake2b_final(s: *mut blake2b, out: *mut u8); } /// Safety: the state is a live C local for each call. fn digest(out: borrow mut [u8; 32]) { var s: blake2b; unsafe { blake2b_init(borrow mut s as *mut blake2b, 32); blake2b_final(borrow mut s as *mut blake2b, borrow mut out[0] as *mut u8); } } ``` **Code only some builds have.** `if (cfg("CONFIG_DEBUG")) { … } else { … }` is C's `#if CONFIG_DEBUG`. A member or a C function that only some builds have is declared `@cfg("CONFIG_DEBUG")` and used only inside such a block, which the compiler checks (N0350). Both blocks are checked in every build: ```ceesafe @header("kernel/cosmofs.h") extern "C" { struct cfs { @cfg("CONFIG_DEBUG") test_armed: bool; } @cfg("CONFIG_DEBUG") fn cfs_debug_note(fs: *mut cfs); } /// Safety: `fs` is a live filesystem. @export("cfs_test_arm") unsafe fn arm(fs: *mut cfs) { if (cfg("CONFIG_DEBUG")) { let f = Ptr.toBorrowMut(fs); f.test_armed = true; cfs_debug_note(fs); } } ``` `CChar` is C's `char` by value as well as behind a pointer (`b as CChar`, `c as u8`), `CLong` and `CULong` are C's `long` and `unsigned long`, an imported struct's member may be an array of pointers, and `AtomicU8` is a byte member C changes atomically. ## C code that allocates A package's C files can allocate through CeeSafe's allocator, so the allocation-failure sweep reaches their out-of-memory paths too. `ceesafe/foreign.h` is on every C file's include path: ```c #include "ceesafe/foreign.h" void *block = ceesafe_alloc(size, CEE_ALLOC_FOREIGN); /* malloc */ block = ceesafe_realloc(block, bigger, CEE_ALLOC_FOREIGN); /* realloc; NULL allocates */ ceesafe_free(block, CEE_ALLOC_FOREIGN); /* free; NULL does nothing */ ``` These allocations are counted and can be failed (`CEESAFE_FAIL_ALLOC_AT`, `ceesafec test --sweep alloc`) like CeeSafe's own, but a block C still holds at exit is not a leak, since C libraries keep global state until the process ends. The allocation report shows those blocks on their own line, `ceesafe alloc: foreign held=`. A library with allocation hooks can be pointed at them in test builds; OpenSSL's are `CRYPTO_set_mem_functions` ([ADR 0060](../spec/decisions/0060-web-random-tryrecv-foreign-host.md)). Some libraries cannot survive a failed allocation during their one-time setup: OpenSSL's initialisation re-enters itself when one fails. Allocate during that setup with `CEE_ALLOC_FOREIGN_INIT` instead: those allocations are never failed and do not count as attempts, so the sweep tests the program's own out-of-memory paths, and the blocks they keep are reported as held, like `CEE_ALLOC_FOREIGN`'s. Either number may reallocate or free a block made under the other. Use it for the setup only: an allocation made under it is never tested ([ADR 0061](../spec/decisions/0061-foreign-allocations-for-initialisation.md)). ## Embedding in a host program A program whose `main` is not CeeSafe's (a C, C++ or Rust program calling CeeSafe exports) links CeeSafe as a static library. `ceesafec build --emit lib -o build/libtext.a` writes one archive holding the generated code and the runtime, one member per C file; with `--cflag -flto=thin` the members are bitcode and join the host's ThinLTO. Link one archive per host program. Beside the archive it writes the headers: `build/ceesafe_exports.h` declares the exports, and `build/ceesafe/embed.h` is the runtime's interface for a host. A package with a CeeSafe `main` gets no C `main` in the archive, since the host has its own (`--lib-main` keeps it). ```ceesafe /// The number of bytes of `text` that are ASCII letters. /// Safety: the host passes `length` readable bytes at `text`. @export("text_letters") fn letters(text: *u8, length: usize) -> u64 { let bytes = unsafe { Slice.fromRaw(text, length) }; var count: u64 = 0; for (b in bytes) { if ((b >= 'a' && b <= 'z') || (b >= 'A' && b <= 'Z')) { count += 1; } } return count; } ``` The host calls `ceesafe_init` once, before any thread calls CeeSafe; after it, exports may be called from any of its threads at once. Its config gives the program arguments (copied), the allocation report and failure injection, and a panic hook, and `ceesafe_shutdown` prints the report and returns the blocks still live: ```c #include "ceesafe/embed.h" #include "ceesafe_exports.h" #include #include static void on_panic(void *context, const ceesafe_panic_info *info) { (void)context; fprintf(stderr, "panic: %s at %s:%u\n", info->reason, info->file ? info->file : "(runtime)", info->line); abort(); /* a hook must end the process or never return */ } int main(int argc, char **argv) { ceesafe_config config = {0}; config.size = sizeof config; config.flags = CEESAFE_INIT_ENVIRONMENT; /* CEESAFE_ALLOC_REPORT and CEESAFE_FAIL_ALLOC_AT */ config.argc = argc; config.argv = (const char *const *)argv; config.panic_hook = on_panic; if (ceesafe_init(&config) != 0) return 1; printf("%llu\n", (unsigned long long)text_letters((const uint8_t *)"a1b2", 4)); return ceesafe_shutdown() == 0 ? 0 : 1; /* 1 if blocks leaked under a report */ } ``` `cc -I build host.c build/libtext.a -o host` links it. A panic calls the hook on the thread that panicked, with the reason, the message and the CeeSafe location; threads that panic at once are in the hook at once, so a hook that writes a report takes a lock first. The hook must not return into CeeSafe, call an export, `longjmp` out or throw: CeeSafe's frames have no unwinding, so after a panic that thread may not call CeeSafe again, and an export called on it prints the panic and aborts. Without `ceesafe_init` everything still works, but the panic is only printed, the environment's settings are ignored and `Process.argument` has no arguments ([ADR 0088](../spec/decisions/0088-embedding-ceesafe-in-a-host-program.md)). ## Next [Kernel code](kernel.md). # Kernel code CeeSafe is designed to run inside an operating system kernel as well as in user programs. Its kernel support was developed against [CosmoOS](https://github.com/vkuttyp/CosmoOS), a kernel written in C, where the language runs as kernel components and as loadable modules on x86-64 and AArch64 (CosmoOS builds those with the ceesafec it pins). Nothing in it is specific to CosmoOS: the same mechanisms fit any C kernel or firmware. ## The freestanding profile `--profile freestanding` (or `profile = "freestanding"` in a package's manifest) compiles for an environment without a C library or an operating system: - there is no `main`: the entry points are `@export` functions that the environment calls; - everything that needs a hosted runtime is unavailable, and naming it is an error: `Console`, files, sockets, `SystemHeap` and the collections built on it; - memory comes from an allocator the environment provides, written in CeeSafe over the kernel's C functions; - `--no-float` (or `float = false`) rejects floating point, for code compiled without FP registers. `ceesafec build --emit obj` then writes one relocatable object, with only the undefined symbols the environment provides, ready for the kernel's own build to link. ```ceesafe /// A CRC-32C over `n` bytes at `data`, for C callers. /// Safety: `data` points to `n` readable bytes, as the C prototype's contract says. @export("crc32c_sum") unsafe fn crc32c(data: *u8, n: usize) -> u32 { return checksum(Slice.fromRaw(data, n)); } fn checksum(bytes: Slice) -> u32 { var crc: u32 = 0xFFFFFFFF; for (b in bytes) { crc = crc ^ (b as u32); for (bit in 0..8) { let mask = if ((crc & 1) != 0) 0x82F63B78u32 else 0u32; crc = (crc >> 1) ^ mask; } } return crc ^ 0xFFFFFFFF; } ``` The unsafe part is confined to the boundary: the exported function turns C's pointer and length into a slice under a stated contract, and everything behind it is safe, checked CeeSafe. ## Context effects Kernel code runs in contexts with rules: while a spin lock is held it must not sleep; in an interrupt handler it must not sleep or allocate. CeeSafe checks these rules at compile time, across function calls: - an operation's **effects** are `sleep` (it may block) and `alloc` (it may allocate), inferred for every function from what it does and calls, its destructors included; - `@effects(…)` declares what a C function may do, or bounds what a CeeSafe function may do; - `@context(atomic)` on a function forbids `sleep` in it; `@context(irq)` forbids `sleep` and `alloc`; - `@context(atomic)` on a **struct** makes holding one an atomic context: a spin-lock guard. ```ceesafe @header("kernel/sched.h") extern "C" { @effects(sleep) fn cosmo_sleep_ms(ms: u32); } /// Holding a spin-lock guard makes the code that runs meanwhile atomic: it must not sleep. @context(atomic) struct SpinGuard { cpu: u32; drop {} } /// Safety: cosmo_sleep_ms has no preconditions; it may sleep. fn backOff() { unsafe { cosmo_sleep_ms(1); } } fn update(counter: borrow mut u64) { let guard = SpinGuard { cpu: 0 }; counter += 1; backOff(); // sleeps while the guard is held } ``` ```text main.cee:17:5: error[N0295]: a 'SpinGuard' is live here, which makes this an atomic context (type-system.md §32.3), but this may sleep: it calls 'backOff', which calls 'cosmo_sleep_ms', declared @effects(sleep) | 17 | backOff(); // sleeps while the guard is held | ^^^^^^^^^^ ``` This proves that CeeSafe code does nothing its context forbids, given that the `@effects` of C functions are declared correctly. It does not prove the absence of deadlocks or lock-order inversions: the kernel's own lockdep stays the check for those. ## Hardware registers `lib/core` provides `Volatile`, a cell read and written only by volatile accesses, and `MmioPtr`, the address of memory-mapped registers. Making an `MmioPtr` is the one unsafe step; register access through it is safe. ```ceesafe @repr(C) struct UartRegs { data: Volatile; status: Volatile; } struct Uart { regs: MmioPtr; /// Safety: `base` is the mapped address of a UART that no other `Uart` aliases. public unsafe fn attach(base: usize) -> Uart => Uart { regs: MmioPtr.at(base) }; public fn putByte(borrow mut this, b: u8) { while ((this.regs.get().status.read() & 0x20) != 0) { Cpu.spinHint(); } this.regs.getMut().data.write(b as u32); } } ``` ## Kernel allocators, locks and objects The CosmoOS integration (the repository's `integrations/cosmoos/`) builds the rest from these pieces: - `KernelHeap`, an allocator over `kmalloc`/`kfree`, so `Box.createIn(KernelHeap {}, value)` allocates kernel memory and returns `Err` when the kernel has none; - `SpinLock`, `IrqSpinLock` and `Mutex` that hold the data they protect: the data is reachable only through a guard, the guard unlocks when it is destroyed, and a spin guard is an atomic context; - `KRef`, a reference-counted kernel object whose final release may sleep, so dropping one under a spin lock does not compile; - C kernel state (lists, locks, kernel objects) embedded by value in CeeSafe structs, built in place and never moved; - a loadable module whose state, `init` and teardown are CeeSafe. How these were built and tested inside CosmoOS, including a stress test under lockdep, is in the CosmoOS reports and the [profiles](../spec/profiles.md). ## Async tasks in kernel code Async tasks run in the freestanding profile too. The environment gives the executor four functions: - the time; - a way to idle until a deadline; - a wake that is safe in an interrupt; - a slot for each thread's executor. A scope's children come from an allocator you name, `await Async.scopeIn(KernelHeap {}, oom, (s) => { … })`, since there is no system heap. Timers, `race`, `all`, `shield` and cancellation work as in a hosted program. An interrupt handler resumes a task through an `AsyncSignal`. The task waits with `await signal.wait()`. The handler, an `@context(irq)` function, calls `signal.raise()`, which has no effects. A raise that comes before the wait is kept. `Async.blocking`, `Async.runWorkers`, `spawnSend` and async I/O need threads or descriptors, so they stay hosted. CosmoOS's boot self-test `cee-async` runs these over the kernel's clock, a wait queue and a timer's interrupt. ## Next [The ceesafec command](ceesafec.md). # The ceesafec command `ceesafec` is the compiler and every tool around it. `ceesafec --help` prints the same summary as this page. Inside a [package](packages.md), commands take no file names: they use the nearest `ceesafe.toml` in the current directory or above. Outside one, give the files. ## Commands | Command | Does | |---|---| | `ceesafec new [--lib] [--agent ] [--no-agent]` | make a package: `ceesafe.toml`, `src/main.cee` (or `src/lib.cee`), and `AGENTS.md` for [AI agents](for-agents.md) | | `ceesafec agents [--agent ]` | write or refresh the package's agent instructions | | `ceesafec fetch [--offline]` | resolve the package's dependencies and write `ceesafe.lock` | | `ceesafec check` | parse, type-check and borrow-check; the fastest way to find errors | | `ceesafec build` | compile to C, then with the C compiler to `out/` (or `-o`) | | `ceesafec run --interp` | run the program in the reference interpreter | | `ceesafec test` | run the package's `@test` functions, each as its own process ([Testing](testing.md)) | | `ceesafec parse` | parse only, reporting syntax errors | | `ceesafec fmt [--check \| --stdout]` | format in the canonical style ([Formatting](formatting-and-docs.md)) | | `ceesafec doc [-o file] [--title t] [--missing]` | a Markdown page of the public items ([Documentation](formatting-and-docs.md#documentation)) | | `ceesafec lsp` | the language server, for [editors](editors.md) | | `ceesafec fix [--dry-run]` | apply the fixes the compiler is sure of, and check again until none applies | | `ceesafec explain ` | what a diagnostic code (`N0249`) means | | `ceesafec mcp` | the MCP server, for [AI agents](for-agents.md) | ## Building | Option | Does | |---|---| | `-o ` | the output file (or directory, with `--emit c`) | | `--emit exe` | an executable (the default) | | `--emit c` | the generated C sources, to look at or to build yourself | | `--emit obj` | one relocatable object, for a kernel or another program's link (`--ld ld.lld` joins objects for ELF cross builds) | | `--cc ` | the C compiler; default `$CC`, else `cc`, or on Windows clang, gcc or Visual Studio's `cl`, found and set up ([tooling](../spec/tooling.md) §8) | | `--cflag ` | an extra C compiler flag (repeatable); the default is `-O2` | | `-g` | a debug build: `#line` directives and `-g -O0` (see [Debugging](debugging.md)) | | `--line-directives` | `#line` directives without changing the optimisation level | | `--split` | the C in one file per CeeSafe source, `c//.c`, so it compiles in parallel ([C backend](../spec/c-backend.md) §2.4) | | `--jobs ` | with `--split` (without it, a usage error): compile at most `n` C files at once; the default is the number of CPUs, and the output is the same for any `n` | | `--profile hosted \| freestanding` | the [profile](kernel.md#the-freestanding-profile) | | `--no-float` | reject every float type and literal | | `--offline` | in a package: never run `git` | C files given with the CeeSafe files (`ceesafec build main.cee shim.c`) are compiled and linked with the program. ## Seeing what the compiler decided | Option | Prints | |---|---| | `--explain-allocations` | each allocation site, and whether its box lives on the stack or the heap, and why | | `--explain-bounds-checks` | each index check, and whether it was removed | | `--explain-overflow-checks` | each checked `+`, `-`, `*` and negation, and whether its check was removed | | `--no-placement`, `--no-bce`, `--no-oce` | turn off box placement, bounds-check elimination, overflow-check elimination | | `--dump-ast`, `--dump-hir`, `--dump-nir` | the compiler's intermediate forms (`parse`, `check`, `build`) | The optimisations never remove a check that could fail; their preconditions are in [Optimisations](../spec/optimisations.md). ## Exit status and errors `ceesafec` exits with 0 on success, 1 when the program has errors, and 2 for a usage error. Diagnostics go to standard error as `file:line:column: error[code]: message`, followed by the source line, notes, and a hint where there is one. The codes are listed in [Diagnostics](../spec/diagnostics.md). With `--diagnostics-format=json` (on `parse`, `check`, `build`, `run`, `fmt` and `fix`), they go to standard output instead, as one JSON document with each diagnostic's position, message and fixes, for programs and [AI agents](for-agents.md) ([tooling](../spec/tooling.md) §5). ## The interpreter `ceesafec run --interp` runs a program without a C compiler, in an interpreter that executes the compiler's own intermediate form. It stops with an exit status of 70 on any use of freed, out-of-bounds or uninitialised memory, or a bad free, which the C build would not detect. The test suite runs every program both ways and compares the results. # Packages A package is a directory with a `ceesafe.toml`: a program or a library, its sources, and the packages it depends on. Dependencies come from local paths or git repositories, pinned to exact commits in `ceesafe.lock`. The full rules are in [Tooling §4](../spec/tooling.md#_4-packages-t6-adr-0013) and [ADR 0013](../spec/decisions/0013-m10-modules-and-packages.md). ## The manifest ```toml [package] name = "app" # an identifier: what other packages call this one version = "0.1.0" kind = "executable" # "executable", "library", or "object" (one .o for a kernel's build) profile = "hosted" # or "freestanding" float = true # false is --no-float sources = ["src"] # the default c = ["src/shim.c"] # C files built with an executable ceesafec = ">=0.20.0" # the compiler this package needs: an older one stops here, naming both versions [dependencies] geometry = { path = "../geometry" } lz4 = { git = "https://example.org/lz4-cee.git", tag = "v1.2.0" } # or rev = "", or branch = "main" ``` Only `name` is required. `ceesafec` takes a version requirement like a dependency's (`>=0.20.0, <0.21.0`); a compiler older than the field reports it as unknown, at the same line. A dependency's key must be the name its own manifest gives it, and a dependency must be a library. ## C libraries A package that wraps a C library names it, and every program that depends on the package links it: ```toml [package] name = "tls" kind = "library" c = ["c/shim.c"] links = ["ssl", "crypto"] # -lssl -lcrypto (ssl.lib crypto.lib with cl) lib-dirs = ["$OPENSSL_DIR/lib"] # an environment variable, or a literal path include-dirs = ["$OPENSSL_DIR/include"] # for this package's own C files [target.windows] # added on Windows; also [target.macos], [target.linux] links = ["libssl", "libcrypto"] ``` Nothing runs to find the library: no pkg-config, no scripts. A directory is literal, or starts with an environment variable, which must be set. `ceesafec build --emit c` writes the link line to `ceesafe_link.txt`, for building by hand. ## Embedded files `embed("path")` and `embedText("path")` ([Basics](basics.md#embedded-files)) read files inside the package's directory. To embed what a build generates elsewhere, list the directory: ```toml [embed] roots = ["../build/codegen"] # relative to the package; no absolute paths or environment variables ``` A root may be outside the package in your own package and in a path dependency you reach through path dependencies alone. A git or registry dependency, and every package reached through one, embeds only from that checkout, whatever its roots say: a symbolic link it holds, or a path dependency it names, that leads outside is refused (N0362). So is any package in the package cache, even when you open its files in the editor or run `check` there. A missing file and one outside get the same message, which names the path only as you wrote it. ## Using a dependency All files of one package share one scope. Another package's items are reached through its name, and only its `public` items are visible: ```ceesafe // geometry/src/lib.cee public struct Point { public x: i64; public y: i64; public fn create(x: i64, y: i64) -> Point => Point { x: x, y: y }; public fn norm(this) -> i64 => this.x * this.x + this.y * this.y; } public fn origin() -> Point => Point.create(0, 0); fn helper() -> i64 => 1; // internal: only geometry can call it ``` ```ceesafe // app/src/main.cee import geometry.Point; // one item; `import geometry;` brings all its public items fn main() { let p = Point.create(3, 4); // imported let q = geometry.origin(); // qualified Console.writeLine(p.norm() + q.norm()); } ``` - `geometry.X` names a dependency's public item, in types, expressions and patterns. - `import geometry.X;` brings one item into the file's scope, and `import geometry;` all its public items. Imports apply to their file only. A name two imports provide is an error where it is used. - Within a package, the name of a dependency is reserved: nothing else in the package may be called `geometry`. - A public function may not expose a type its users cannot see (a public function returning an internal struct). - Two packages may declare the same name: in the generated C, a dependency's items carry its package's name. ## The lock file ```sh ceesafec fetch # resolves dependencies and writes ceesafe.lock ceesafec build # uses ceesafe.lock; fails if it does not match ceesafe.toml ``` `ceesafe.lock` records every dependency, direct and indirect: where it comes from, and for a git dependency the exact commit and a SHA-256 of its files. Commit it with `ceesafe.toml`, so that every build of the package uses the same code. - A `tag` or `branch` is resolved to a commit by `ceesafec fetch` and stays that commit until the next fetch, even if the branch moves. - Every build checks each git dependency's files against the lock's hash, so a checkout changed behind your back is an error. - Git repositories and checkouts are cached in `~/.ceesafe` (or `$CEESAFE_HOME`). `--offline` builds never touch the network. - Nothing runs during a fetch or a build except `git` and the C compiler: packages have no build scripts. - Two path or git dependencies that need the same package must agree on its source and commit. ## The registry A dependency can also be a version requirement, looked up in the package index: ```toml [dependencies] shapes = "2.1" # any 2.x from 2.1.0 on geometry = "=1.4.0" # exactly json = { version = "0.9", index = "https://example.org/my-index.git" } ``` `ceesafec fetch` chooses the newest version of each package that satisfies every requirement on it, and records its commit and hash in `ceesafe.lock`, so builds stay pinned until the next fetch. A version already locked is kept while it still fits; `ceesafec fetch --update` (or `--update shapes`) chooses afresh. When no choice works, the error says which requirements clash and where each came from. To publish a library, give its manifest a `version`, commit and push it, and run `ceesafec publish`. It checks the package and prints the entry to add to the index by pull request. ## Building ```sh ceesafec check # the package and its dependencies, checked together ceesafec build # out/ ceesafec run --interp ceesafec fmt # the package's own sources ceesafec doc -o api.md # its public items ``` A build compiles the package and all its dependencies together into one C program, so generic code from a dependency is specialised for its users and checked like the rest. ## Files without a package Everything also works on files: `ceesafec build a.cee b.cee -o tool`. Files given together are one package with no dependencies. # Formatting and documentation ## Formatting `ceesafec fmt` rewrites CeeSafe source in one canonical style. It has no options, so code formatted by anyone looks the same. The style is specified in [Formatting](../spec/formatting.md). ```sh ceesafec fmt # the package's sources (or: ceesafec fmt file.cee dir/) ceesafec fmt --check # change nothing; list the files that would change; exit 1 if any ceesafec fmt --stdout file.cee # print one file formatted ``` The style, briefly: - 4-space indentation, no tabs, at most one blank line in a row, lines up to 120 columns; - a block or list you wrote on one line stays on one line if it fits; one you broke across lines is laid out one element per line, with a trailing comma; - a line that is too long is broken at the outermost place that helps: an expression body, then a list, then an initialiser, then an operator; - `} else {` on one line, and the blocks of an `if`/`else` chain are broken together; - trailing comments on consecutive lines are aligned. It is safe by construction: the formatter only changes whitespace and trailing commas, and before returning it parses its own output and checks that the program and its comments are unchanged. A file with a syntax error is left alone. ## Documentation `ceesafec doc` writes one Markdown page for a package's public items: each item's signature, as the formatter prints it, and its `///` doc comment, which is Markdown. ```sh ceesafec doc -o api.md --title "geometry" ceesafec doc --missing # list the public items without a doc comment; exit 1 if any ``` ```ceesafe /// A point in the plane. /// - coordinates are exact integers; /// - `norm` is the squared length. public struct Point { public x: i64; public y: i64; /// The squared distance from the origin. public fn norm(this) -> i64 => this.x * this.x + this.y * this.y; } ``` becomes a section with `public struct Point { public x: i64; public y: i64; }`, the list, and a subsection for `Point.norm`. The [lib/core reference](../spec/api/core.md) on this site is generated this way. # Editors ## VS Code The extension in `editors/vscode` gives `.cee` files syntax highlighting, and through the language server: errors as you type, the type or signature on hover, go to definition (F12), the outline, and formatting (Shift+Alt+F). Install it from the [Visual Studio Marketplace](https://marketplace.visualstudio.com/items?itemName=ceesafe.ceesafe-language): search for **CeeSafe** in VS Code's Extensions view, or run ```sh code --install-extension ceesafe.ceesafe-language ``` Every [release](https://github.com/CeeSafe/CeeSafe/releases) also has it as `ceesafe-language-.vsix`, built and tested with that release's `ceesafec` (its checksum is in `SHA256SUMS`), for **Extensions: Install from VSIX…**. To build it from the repository instead, with [Vite+](https://viteplus.dev/): ```sh cd editors/vscode vp install vp test # the grammar, and ceesafec lsp through VS Code's own client library vp run package # makes ceesafe-language-.vsix code --install-extension ceesafe-language-*.vsix ``` The extension starts `ceesafec lsp`. It finds `ceesafec` on your `PATH`, or where the setting `ceesafe.server.path` says. ## Other editors `ceesafec lsp` is a standard Language Server Protocol server on standard input and output, so any editor with an LSP client can use it: Neovim, Helix, Emacs, Zed, Sublime Text. Configure it to run `ceesafec lsp` for `.cee` files. The TextMate grammar in `editors/vscode/syntaxes/ceesafe.tmLanguage.json` works in editors that accept TextMate grammars. ## What the server does | | | |---|---| | Diagnostics | every error and note of `ceesafec check`, as you type | | Hover | the type of an expression; `let`/`var` and the type of a variable; a function's signature; a field's type | | Go to definition | locals, parameters, functions, methods, fields, structs, enums and interfaces in the file | | Document symbols | the outline: functions, structs with their fields and methods, enums, interfaces, constants | | Formatting | the whole file, in `ceesafec fmt`'s style | A file that belongs to a package is checked with the package and its locked dependencies; any other file is checked on its own. The details are in [Tooling §2](../spec/tooling.md#_2-language-server-t3). # Debugging A CeeSafe program is debugged as the C it compiles to, with the C mapped back to CeeSafe lines, so lldb and gdb work without knowing anything about CeeSafe. ## A debug build ```sh ceesafec build -g main.cee -o program lldb ./program # or: gdb ./program ``` `-g` writes `#line` directives into the C and compiles it with `-g -O0`. Breakpoints then go on CeeSafe lines, and stepping moves through CeeSafe statements. | | lldb | gdb | |---|---|---| | Stop at a line | `b main.cee:12` | `break main.cee:12` | | Stop when a check fails, before the program exits | `b ceesafe_panic` | `break ceesafe_panic` | | Where am I | `bt` | `bt` | ## Names The C names follow fixed rules, so a CeeSafe name is easy to find: | CeeSafe | C | |---|---| | function `length` | `ceeF_length` | | method `Counter.bump` | `ceeM_7Counter_bump` (the type's name, prefixed by its length) | | local or parameter `sum` | `v_sum_1` | | a temporary the compiler made | `t_3` | | field `x` | `f_x` | | struct `Point` | `ceeT_Point` | | an item of a dependency `geometry` | `ceeF_8geometry_area`, `ceeT_8geometry_Point`, `ceeM_08geometry5Point_norm` (the package's name after its length) | ## CeeSafe names in the debugger `editors/debug` adds two commands that print with CeeSafe names: ```sh lldb ./program -o "command script import editors/debug/ceesafe_lldb.py" gdb ./program -ex "source editors/debug/ceesafe_gdb.py" ``` - `ceesafe-locals`: the current frame's locals and parameters, without the compiler's temporaries, with struct fields by their CeeSafe names: `p = { x: 3, y: 4 }`. - `ceesafe-bt`: the backtrace as `Counter.bump at main.cee:6`. - `ceesafe-tasks`: in an async program, the thread's tasks and the line each one waits at, with a scope's children indented under the task that awaits the scope. ```text (lldb) b main.cee:12 (lldb) run (lldb) ceesafe-locals p = { x: 3, y: 4 } sum = 7 (lldb) ceesafe-bt length at main.cee:12 main at main.cee:18 ``` ## Debug assertions `debugAssert(condition)` and `debugAssert(condition, message)` check what should always hold, at no cost in a release build: they are evaluated only in a debug build (`-g`), under `ceesafec test` and in the interpreter, and a release build leaves them out entirely. `--debug-assertions` and `--no-debug-assertions` turn them on or off in any build. A false one panics, as `assert` does. ```ceesafe fn isSorted(values: Slice) -> bool { for (k in 1..values.length) { if (values[k - 1] > values[k]) { return false; } } return true; } fn main() { var scores = Vector.create(); scores.push(3)!; scores.push(8)!; debugAssert(isSorted(scores.view()), "scores out of order"); Console.writeLine(scores.length); } ``` ```text 2 ``` Since a release build leaves the call out, the condition and message must not change anything that exists before them, or the two builds would behave differently. The compiler checks this, through every function they call: no assignment, `mut` argument, move or drop of an outer variable, no I/O, no C call, no call through a closure or `dyn`, and no way out but their end: no `?`, no `break` or `continue` to a loop outside them, no `Process.exit`. They may read, allocate, and change their own locals. To assert on something that changes, do the change first: ```ceesafe fn main() { var queue = Vector.create(); queue.push(3)!; debugAssert(queue.pop() != null); } ``` ```text main.cee:4:5: error[N0360]: 'debugAssert' must not change what exists before it, since release builds leave it out (ADR 0087 §2.5): this lends as 'mut' 'queue', which exists before the call | 4 | debugAssert(queue.pop() != null); | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ``` Here, `let last = queue.pop(); debugAssert(last != null);` pops in every build. `assert` is always evaluated, so it has no such rule. ## When the program panics A failed check prints the reason and the CeeSafe location, and exits with status 134. Under a debugger, a breakpoint on `ceesafe_panic` stops at the moment of the panic, with the whole call stack to inspect: ```text (lldb) b ceesafe_panic (lldb) run (lldb) ceesafe-bt ceesafe_panic at ceesafe_rt_core.c:207 cee_add_u8 at rt.h:190 grow at main.cee:2 main at main.cee:7 ``` In an async program, stopped in a child task of a scope: ```text (lldb) ceesafe-tasks gather at main.cee:18 (waiting for its scope) async closure at main.cee:20 (running) async closure at main.cee:25 (waiting for a deadline) ``` Each line is the task's own async fn and the line where it last suspended. The running task is at the `await` it resumed from, and `ceesafe-bt` shows where it is now. For memory errors in `unsafe` code, `ceesafec run --interp` is often faster: the interpreter stops at the first invalid access and names it. # Coming from another language CeeSafe borrows deliberately from several languages, and each will feel partly familiar: - its **syntax** from C# and TypeScript: braces, `fn name(x: T) -> R`, `let` and `var`, generics in `<…>`; - its **ownership and borrowing** from Rust, with less annotation and a simpler checker; - its **explicit allocators** from Zig, with use-after-free made a compile error; - its **regions** from Cyclone, and its **compile-through-C** design and compiler-managed memory from Goose; - its **deterministic destruction** from C++'s RAII, without the undefined behaviour. These pages map what you know to CeeSafe, show the same program side by side, and point out the differences that will trip you up. | If you know | Read | The biggest changes | |---|---|---| | Rust | [CeeSafe for Rust programmers](rust.md) | no lifetime syntax on plain borrows; allocators per allocation; checks in every build; no macros or traits-as-types | | Zig | [CeeSafe for Zig programmers](zig.md) | use-after-free and dangling references are compile errors; checks never turn off; generics instead of `comptime` | | C or C++ | [CeeSafe for C and C++ programmers](c-and-cpp.md) | the compiler tracks ownership and borrows; no undefined behaviour; no preprocessor | | C#, Java or TypeScript | [CeeSafe for C#, Java and TypeScript programmers](csharp-java-typescript.md) | no garbage collector, no classes or inheritance, no exceptions, no null references | | Go | [CeeSafe for Go programmers](go.md) | no garbage collector; errors propagate with `?`; interfaces are explicit and static; no goroutines yet | Why CeeSafe is built the way it is, against those languages, is on the repository's [README](https://github.com/CeeSafe/CeeSafe#why-ceesafe). # CeeSafe for Rust programmers CeeSafe's safety model is Rust's: single ownership, moves, shared and exclusive borrows, deterministic destruction, no garbage collector. What differs is how much you write down, where memory comes from, and how much the language does for you. This page assumes you know Rust well. ## At a glance | Rust | CeeSafe | Notes | |---|---|---| | `let x = 1;` / `let mut x = 1;` | `let x = 1;` / `var x = 1;` | | | shadowing (`let x = x + 1;`) | not allowed | two locals in scope never share a name | | `&T`, `&mut T` | `borrow T`, `borrow mut T` | | | `f(&x)`, `f(&mut x)` | `f(x)`, `f(mut x)` | a shared borrow is implicit at the call; a mutable one is marked | | `fn f<'a>(a: &'a T, b: &T) -> &'a T` | `fn f(a: borrow T, b: borrow T) -> borrow(a) T` | the result names its sources; no lifetime parameters | | `struct S<'a> { s: &'a str }` | `struct S { s: StringView; }` | only types that store references declare regions | | `Box`, `Vec`, `String`, `&str` | `Box`, `Vector`, `String`, `StringView` | | | `Option`: `Some(x)`, `None` | `T?`: `Some(x)`, `null` | a `T` converts to `T?` where one is expected | | `Result`, `?` | `Result`, `?` | the same | | `.unwrap()` | `!` (postfix) | `r!`, `o!` | | `panic!` (unwinds by default) | panics always abort | no unwinding, no `catch_unwind` | | `impl Drop for S` | `drop { … }` inside the struct | | | `trait`, `impl Trait for T`, `T: Trait` | `interface`, `impl Interface for T`, `T: Interface` | default methods, `Self`, associated types and constants, supertraits | | `Box`, `&dyn Trait` | `Box`, `borrow dyn I` | | | `fn f()`, `f::<3>()`, `f::<{ X + 1 }>()` | `fn f()`, `f<3>()`, `f()` with `const Y: usize = X + 1;` | `bool`, integer and fieldless-enum values; a `const fn` is an ordinary `fn` | | `#[derive(Clone, Copy)]` | automatic | a struct is Copy when all its fields are and it has no `drop` | | `x as u8` (truncates) | `truncate(x)` | `as` only converts where nothing can be lost | | `wrapping_add`, `checked_add`, `saturating_add` | `wrappingAdd`, `checkedAdd`, `saturatingAdd` | | | `\|x\| x + 1` | `(x) => x + 1` | a closure literal borrows its captures and cannot escape | | `Box::new(move \|x\| x + n)` | `Box.create(move (x) => x + n)!` | a `Box u64>`, which owns its captures | | `match x { A => …, }` | `match (x) { E.A => { … } }` | variants are qualified; statement arms are blocks | | `Some(n) if n > 3 =>`, `A \| B`, `0..=9`, `x @ P` | `Some(n) if (n > 3) =>`, `A \| B`, `0..=9`, `x @ P` | the guard is parenthesised | | `"get" =>`, `b"cjs" =>`, `[first, rest @ ..] =>` | `"get" =>`, `"cjs" =>`, `[first, rest @ ..] =>` | a string pattern is typed by the value: a `StringView` or a `Slice` | | `if let Some(v) = e`, `while let …`, `matches!(e, P)` | `if (e is Some(v))`, `while (e is …)`, `e is P` | one operator; it chains with `&&` | | `std::thread::spawn(move \|\| …)` | `Thread.spawn(move () => …)!` | with `Arc`, `Mutex` and atomics | | `Send`, `Sync` | `Send`, `Share` | | | `println!("{}", x)` | `Console.writeLine(x)` | no macros, no format strings yet | | `#![no_std]` | `--profile freestanding` | | | `unsafe { }` with a `// SAFETY:` convention | `unsafe { }` with a required `/// Safety:` doc line | the compiler enforces it | | Cargo, `Cargo.toml`, `Cargo.lock` | `ceesafec`, `ceesafe.toml`, `ceesafe.lock` | path, git and registry dependencies | | `rustfmt`, `rustdoc` | `ceesafec fmt`, `ceesafec doc` | | ## The same program ```rust fn longest_word(text: &str) -> Option<&str> { let mut best: Option<&str> = None; for word in text.split(' ') { if best.map_or(true, |b| word.len() > b.len()) { best = Some(word); } } best } fn main() { match longest_word("the quick brownish fox") { Some(w) => println!("{}", w), None => println!("empty"), } } ``` ```ceesafe fn longestWord(text: StringView) -> StringView? { var best: StringView? = null; for (word in text.split(" ")) { match (best) { Some(b) if (b.length >= word.length) => {} _ => { best = word; } } } return best; } fn main() { match (longestWord("the quick brownish fox")) { Some(w) => { Console.writeLine(w); } null => { Console.writeLine("empty"); } } } ``` ```text brownish ``` The CeeSafe version loops over the `split` iterator where Rust would call `map_or` (there are no adapters), tests with a guard, returns explicitly, and its signature says the result views the input through the region `r`. With a single reference parameter, as here, the region could also be left out: `fn longestWord(text: StringView) -> StringView?` means the same. ## Ownership and borrowing: what is the same Moves, Copy types, "one writer or many readers", borrows ending at their last use, two-phase borrows for `v.push(v.length)`, references that cannot outlive their referent, and destruction at scope end in reverse order: all as in Rust. Error messages name the borrow, where it was made and where it is used later. ## What is different **Less annotation.** A function that returns a reference names the parameters it may borrow from, `-> borrow(a, b) T`, instead of declaring lifetime parameters. Types that store references declare `region` parameters, and so do functions that pass such types through; plain functions over references never do. Regions are erased: `Token` and `Token` are the same type. **A simpler checker.** CeeSafe's borrow checker is intraprocedural and keeps one set of loans per value, where Rust reasons about lifetimes. Borrows end at their last use, as with NLL, but some programs Rust accepts are rejected; the usual fixes are to end a borrow earlier, copy a value out, or use an index or a slot-map handle. Reference locals are `let`, never `var`: assigning to a reference writes through it. **Allocators per allocation.** Every allocating type takes its allocator as a type parameter (`Box`, `Vector`), and every allocation returns a `Result`: there is no infallible `push`. Arenas, pools, fixed buffers and scratch frames are built in, and allocations from a region borrow it, so they cannot outlive it. **Checks in every build.** Integer overflow panics in release builds too; there is no `--release` that turns it off. Bounds and overflow checks the compiler proves unnecessary are removed, and `--explain-bounds-checks` shows which. **Interfaces are traits without the corners.** Interfaces have default methods, `Self`, associated types and constants, and supertraits, and `impl I for Wrapper` implements one for a generic type; there are no blanket `impl B for T` and no `impl Trait` in argument or result position. A collection of different types is an enum or holds `Box`. **Closures borrow, unless they are boxed.** A closure literal borrows what it captures and has a reference type (`borrow fn(i32) -> bool`, `borrow mut fn(i32)`), so it cannot be returned or stored. A `move` closure owns its captures and is boxed where it is made: `Box.create(move (x: u64) => x + n)!` is a `Box u64>`, and one that changes its captures a `Box`. There is no `Fn`/`FnMut`/`FnOnce` trait or `impl Fn`: a parameter takes `borrow fn(…)` or `borrow mut fn(…)`. **Threads and tasks.** `Thread.spawn(move () => …)` and `Thread.both` run threads; `Arc`, `Mutex` and lib/core's atomics share state, and `Send` and `Share` are Rust's `Send` and `Sync`. `async fn` and `await` run tasks on lib/std's executor, on one thread or several (type-system.md §38, §40). **No macros, no iterator chains.** `for` walks ranges, arrays, slices, vectors and iterators: lib/core's `Iterator` (`fn next(borrow mut this) -> T?`), through a bound `I: Iterator` or a `dyn Iterator`, and any struct with such a `next`. The item is an interface parameter, not an associated type: `Iterator`, not `Iterator`. There are no adapters: a chain of `map`, `filter` and `collect` is the loop's body, and slices and vectors have `sortBy`, `forEach`, `any`, `all` and `retain`. **Through C.** CeeSafe compiles to C11, which any C toolchain or kernel build can take, including MSVC and compilers for targets LLVM does not support. The generated C is readable and maps back to CeeSafe lines. **Kernel checking.** Context effects (`@context(atomic)`, `@effects(sleep)`) reject sleeping under a spin lock across calls, at compile time. Rust has no equivalent in the language. ## Things you will miss `Rc` (CeeSafe has `Arc`, and lib/core's `Cell` and `RefCell`, whose `read()` and `write()` are Rust's `borrow()` and `borrow_mut()`: type-system.md §41), macros, iterator adapters, blanket impls, `impl Trait`, shadowing, the standard library's breadth, and crates.io. Several are planned or recorded as open decisions. # CeeSafe for Zig programmers Zig and CeeSafe agree on a lot: no hidden allocation, allocators as explicit values, errors as values, `defer`, no garbage collector, first-class C interop, and code that can run in a kernel. They differ on what guarantees safety. Zig checks bounds, overflow and null at run time in its safe build modes, and leaves use-after-free and dangling pointers to the programmer. CeeSafe makes those compile errors, through ownership and borrowing, and keeps its run-time checks on in every build. ## At a glance | Zig | CeeSafe | Notes | |---|---|---| | `const x = 1;` / `var x: u32 = 1;` | `let x = 1;` / `var x: u32 = 1;` | | | `[4]u8`, `[]u8`, `[]const u8` | `[u8; 4]`, `SliceMut`, `Slice` | slices are borrows: they cannot outlive their array | | `?T`, `null`, `orelse`, `.?` | `T?`, `null`, `match`, `!` | | | `!T`, `error.X`, `try`, `catch` | `Result`, `Err(E.X)`, `?`, `match` | errors are ordinary enums | | `catch unreachable` | `!` (postfix) | | | `defer`, `errdefer` | `defer` | locals are destroyed automatically: most `defer`s are unnecessary | | `allocator.create(T)`, `allocator.destroy(p)` | `Box.createIn(allocator, value)`; destroyed automatically | the allocator is part of the box's type | | `std.ArrayList(T)` with an allocator | `Vector` | | | `std.heap.ArenaAllocator` | `Arena` | allocations borrow the arena: none survives it | | `std.heap.FixedBufferAllocator` | `FixedBufferAllocator` | | | `+%`, `+\|`, `@addWithOverflow` | `wrappingAdd`, `saturatingAdd`, `checkedAdd` | | | `@intCast`, `@truncate` | `as` (lossless only), `truncate` | | | `comptime` generics | `` generics with interface bounds | no compile-time execution | | `comptime` flags and sizes (`comptime ts: bool`) | const parameters: `fn parse()`, `[u8; N]` | `bool`, integer and enum values, one copy per value | | `@cImport` | `extern "C"` declarations; C structs imported by `@header` | | | `export fn`, `callconv(.C)` | `@export`, `@callback` | | | Debug / ReleaseSafe / ReleaseFast | one mode, always checked | proved checks are removed | | `std.debug.print` | `Console.writeLine` | | ## The same program ```zig // Zig 0.13 const std = @import("std"); fn sumAll(allocator: std.mem.Allocator, n: usize) !u64 { var list = std.ArrayList(u64).init(allocator); defer list.deinit(); var i: u64 = 0; while (i < n) : (i += 1) try list.append(i * i); var total: u64 = 0; for (list.items) |x| total += x; return total; } pub fn main() !void { const total = try sumAll(std.heap.page_allocator, 10); std.debug.print("{}\n", .{total}); } ``` ```ceesafe fn sumAll(n: u64) -> Result { var list = Vector.create(); // freed at the end of the function, by its owner for (i in 0..n) { list.push(i * i)?; } var total: u64 = 0; for (x in list) { total += x; } return Ok(total); } fn main() { Console.writeLine(sumAll(10)!); } ``` ```text 285 ``` There is no `deinit`: the vector is destroyed when its owner goes out of scope, on every path, including the early return of `?`. Another allocator goes in the type, `Vector` for an allocator type `A`. An arena works the Zig way instead: `arena.allocArray(n, 0u64)` returns a slice that borrows the arena, and everything is freed with it. ## What changes **Lifetimes are checked.** Returning a slice of a local array, keeping a pointer into an `ArrayList` across an `append`, or using an arena allocation after `arena.deinit()`: each is a compile error in CeeSafe, with a message naming the borrow. In Zig these compile and fail at run time, if they are noticed at all. **Destruction is automatic.** A struct with a `drop` block, or that owns a box, vector or file, is destroyed exactly once when its owner goes away. Ownership moves on assignment, so there is no double free to avoid by convention. **Errors carry types.** An error is any type, usually an enum with payloads (`ParseError.BadDigit(c)`), not a global error set. `?` requires the function's error type to match; there are no implicit error-set unions. **Generics, not `comptime`.** CeeSafe's generics are type parameters checked once, for every type argument, against the bounds you declare. There is no compile-time evaluation of arbitrary code, no `@typeInfo` and no reflection. Constants are evaluated at compile time; functions are not. A `comptime` flag or size becomes a const parameter, `fn scan(…)` or `struct Buffer`, and `if (TS)` in it is folded in each copy. **Safety is not a build mode.** There is no ReleaseFast. Overflow and bounds checks are always on, and the compiler removes those it proves cannot fail. ## Things you will miss `comptime`, `@cImport` of whole headers, packed structs and arbitrary-width integers (`u3`), SIMD vectors, inline assembly, the cross-compiling toolchain in one binary, and Zig's build system. CeeSafe relies on a C compiler for code generation and cross compilation. # CeeSafe for C and C++ programmers CeeSafe compiles to C, calls C directly, and is designed to live inside C codebases, kernels included. What it adds is a compiler that tracks who owns each piece of memory and who is looking at it, and rejects the programs that would use it wrongly. What it removes is undefined behaviour. ## At a glance | C / C++ | CeeSafe | Notes | |---|---|---| | `int32_t x = 1;` | `let x: i32 = 1;` (or `var` to change it) | | | `T *`, `const T *` | `borrow mut T`, `borrow T` in safe code; `*mut T`, `*T` in `unsafe` | references are never null and never dangle | | `malloc` / `free`, `new` / `delete` | `Box.create(v)`; freed by its owner | allocation failure is a `Result`, not `NULL` or an exception | | `std::unique_ptr` | `Box` | | | `std::vector` | `Vector` | growing it while an element is referenced is a compile error | | `std::string`, `std::string_view` | `String`, `StringView` | UTF-8 only | | `std::optional` | `T?` | | | destructors, RAII | `drop { … }` blocks; destruction at scope end | moves leave nothing behind to destroy twice | | `std::move(x)` | assignment moves by default; `move x` to end a use early | using a moved value is a compile error | | exceptions, `errno`, return codes | `Result` and `?` | | | `assert`, `abort` | panics: bounds, overflow, unwrap | always on | | templates | generics with interface bounds | checked once, before use; no SFINAE | | `#include`, the preprocessor | packages and `import` | no macros, no conditional compilation | | `struct` layout | the same as C's, in declaration order | `@repr(C)` to cross the boundary | | `volatile` | `Volatile`, `p.readVolatile()` | | | `-fsanitize=address,undefined` | needed only for `unsafe` code; `ceesafec run --interp` checks it | | ## The same program ```cpp #include #include int main() { std::vector v = {1, 2, 3}; int &first = v[0]; v.push_back(4); // may reallocate: `first` now dangles std::printf("%d\n", first); } ``` In C++ this compiles and has undefined behaviour. In CeeSafe it does not compile: ```ceesafe fn main() { var v = Vector.create(); v.push(1)!; let first = borrow v[0]; v.push(4)!; Console.writeLine(first); } ``` ```text main.cee:5:5: error[N0241]: cannot borrow 'v' as mutable while 'v[0]' is borrowed | 5 | v.push(4)!; | ^ = note: 'v[0]' is borrowed here (4:5) = note: the borrow is used later here (6:5) ``` The fixes are the ones you would reach for anyway: copy the value (`let first = v[0];`), or take the reference after the last push. ## No undefined behaviour Every operation that is undefined in C is defined in CeeSafe, by a check or by a rule: | Undefined in C | In CeeSafe | |---|---| | signed overflow | stops the program (`wrappingAdd` and friends where wrapping is meant) | | out-of-bounds access | stops the program | | use after free, double free | compile errors | | reading uninitialised memory | a compile error | | null dereference | there are no null references; optionals must be checked | | shift by the width or more | stops the program | | unsequenced modifications | evaluation order is left to right | | strict aliasing violations | the generated C never relies on them | The generated C is written so that it has no undefined behaviour itself, and is warning-free under strict flags, so the C compiler's optimiser has nothing to exploit. ## Working with an existing C codebase - Call C functions from an `extern "C"` block, inside `unsafe`. - Export CeeSafe functions with `@export`; `ceesafec` writes `ceesafe_exports.h` for your C code to include. - Use your codebase's own structs from its headers (`@header`, `@ctype`) instead of re-declaring them: the C compiler checks every field CeeSafe uses. - Build a freestanding object with `--profile freestanding --emit obj` and link it with your build. See [Unsafe code and C](../unsafe-and-c.md) and [Kernel code](../kernel.md). ## Things you will miss The preprocessor, inline assembly, unions (enums with payloads replace tagged unions), bit fields, pointer arithmetic in safe code, varargs, goto, and in C++, classes with inheritance and virtual functions, operator overloading, overloads, exceptions, and `constexpr` evaluation. # CeeSafe for C#, Java and TypeScript programmers CeeSafe's syntax was chosen to look like C# and TypeScript: braces, `fn name(x: T) -> R`, `let` and `var`, generics in angle brackets, `public` and `internal`, `using`, interfaces. Underneath, it works very differently: there is no garbage collector, no classes, no inheritance, no exceptions and no null references. Memory is managed at compile time, by tracking ownership. ## At a glance | C# / Java / TypeScript | CeeSafe | Notes | |---|---|---| | `var x = 1;` / `const x = 1;` | `var x = 1;` / `let x = 1;` | | | `class` | `struct` with methods | no inheritance; values, not references | | `interface`, `class C : I` | `interface`, `impl I for C` | interfaces are generic bounds, not types | | `new Point(1, 2)` | `Point { x: 1, y: 2 }` | on the stack, or `Box.create(…)` on the heap | | garbage collection | ownership: destroyed when the owner goes away | deterministic, at a point the language defines | | `IDisposable`, `using` | `drop { … }`, `using` | every owner runs `drop`, not only `using` | | `null` references | `T?`, which must be checked | a plain `T` is never null | | exceptions, `try`/`catch` | `Result`, `?`, `match` | panics are bugs and cannot be caught | | `ref`, `out` | `borrow mut T` parameters, `mut x` at the call | | | `List`, `string` | `Vector`, `String` / `StringView` | | | LINQ, lambdas | `sortBy`, `forEach`, `any`, `all`, `retain`; `(x) => x > 2` | lambdas borrow and cannot escape | | `async` / `await`, `Task.WhenAll`, `Task.WhenAny` | `async fn`, `await`; `Async.scope`, `Async.all`, `Async.race` | a task is awaited where it starts, never stored; see [Async tasks](../async.md) | | threads | `Thread.spawn`, `Thread.both`, `Arc`, `Mutex`, atomics | see [Threads](../threads.md) | | NuGet, Maven, npm | `ceesafe.toml`, path and git dependencies | | ## The same program ```csharp class Account { public long Balance { get; private set; } public void Deposit(long amount) => Balance += amount; } var a = new Account(); a.Deposit(100); Console.WriteLine(a.Balance); ``` ```ceesafe struct Account { private balance: i64; public fn create() -> Account => Account { balance: 0 }; public fn deposit(borrow mut this, amount: i64) { this.balance += amount; } public fn current(borrow this) -> i64 => this.balance; // a method cannot share a field's name } fn main() { var a = Account.create(); a.deposit(100); Console.writeLine(a.current()); } ``` ```text 100 ``` The receiver says how a method uses its object: `borrow this` reads it, `borrow mut this` changes it, and `this` takes it by value. Calling `deposit` needs `a` to be a `var`. ## What changes **Values, not references.** Assigning a struct copies it (if every field is copyable) or moves it, never shares it. Two variables never point to the same object unless one *borrows* the other, and a borrow is checked: while something is borrowed mutably, nothing else can use it. **Destruction is deterministic.** An object is destroyed when its owner goes out of scope, by assignment, or when the object that owns it is destroyed. A `drop` block runs then, exactly once: closing a file or releasing a lock does not depend on a collector or a `finally`. **Errors are return values.** A function that can fail returns `Result`; the caller matches it, or passes it on with `?`. Out of memory is an error value too. Bugs (an index out of range, an overflow) stop the program instead of throwing. **No null.** `StringView` can never be null; `StringView?` can, and the compiler requires a check before use. **Generics are compiled per type,** like C++ templates and unlike Java's erasure, and checked once, like C#'s. Interfaces constrain type parameters (``), but there is no `List` holding different shapes: use an enum. ## Things you will miss Classes and inheritance, interfaces as types, reflection, attributes on everything, properties, events, LINQ's query syntax, string interpolation, exceptions, a task you can store and await later, and a large standard library. # CeeSafe for Go programmers Go and CeeSafe share some taste: a small language, errors as values, `defer`, a canonical formatter, packages as directories, and fast feedback. They differ in how memory is managed. Go has a garbage collector and a runtime; CeeSafe has neither, and tracks ownership at compile time instead, which is what lets it run in a kernel. ## At a glance | Go | CeeSafe | Notes | |---|---|---| | `x := 1`, `var x int32` | `let x = 1;`, `var x: i32 = 0;` | `let` cannot be reassigned | | `func f(a int) (int, error)` | `fn f(a: i32) -> Result` | | | `if err != nil { return err }` | `?` | | | `panic`, `recover` | panics abort; no recovery | | | `defer` | `defer` | runs at the end of the block, not the function | | slices `[]T` (growable) | `Vector` (owns, grows) and `Slice` (borrows) | a slice cannot outlive what it views | | maps | not built in yet | `SlotMap` for handles | | `nil` | `null`, only for optionals (`T?`) | | | interfaces, satisfied implicitly | interfaces, implemented explicitly with `impl` | generic bounds, resolved at compile time | | generics `[T any]` | `` | | | goroutines, channels | async tasks in a scope, `Channel` (`sendAsync`, `recvAsync`; `send`, `recv` between threads) | a task never outlives its scope; see [Async tasks](../async.md) | | `gofmt` | `ceesafec fmt` | one canonical style, no options | | `go.mod`, `go.sum` | `ceesafe.toml`, `ceesafe.lock` | path and git dependencies, pinned by commit and hash | | `go vet`, the race detector | the compiler: ownership, borrows, context effects | | ## The same program ```go package main import ( "errors" "fmt" ) func divide(a, b int) (int, error) { if b == 0 { return 0, errors.New("division by zero") } return a / b, nil } func average(xs []int) (int, error) { sum := 0 for _, x := range xs { sum += x } return divide(sum, len(xs)) } func main() { avg, err := average([]int{3, 5, 10}) if err != nil { fmt.Println(err) return } fmt.Println(avg) } ``` ```ceesafe enum MathError { DivisionByZero, } fn divide(a: i64, b: i64) -> Result { if (b == 0) { return Err(MathError.DivisionByZero); } return Ok(a / b); } fn average(xs: Slice) -> Result { var sum: i64 = 0; for (x in xs) { sum += x; } return divide(sum, truncate(xs.length)); // usize to i64 could lose bits: say so } fn main() { let values = [3i64, 5, 10]; match (average(values[..])) { Ok(avg) => { Console.writeLine(avg); } Err(MathError.DivisionByZero) => { Console.writeLine("division by zero"); } } } ``` ```text 6 ``` `xs.length` is a `usize`, and CeeSafe has no implicit conversions: `as` converts only where nothing can be lost, so a `usize` becomes an `i64` through `truncate`. ## What changes **No garbage collector.** Every value has one owner and is destroyed when the owner goes away. Where Go lets any number of pointers share an object and keeps it alive, CeeSafe lets one owner keep it and others *borrow* it for a while, and checks that no borrow outlives the object. **Allocation is visible and fallible.** Values live on the stack unless the code asks for a box, a vector or a string, and each of those returns a `Result`: out of memory is an error, not a crash. **Errors propagate with `?`** instead of `if err != nil`. The error type is part of the signature, usually an enum. **Interfaces are explicit and static.** A type implements an interface with an `impl` block, and interfaces are used as generic bounds, resolved at compile time. There is no interface value holding "some type", and no type switch. **Overflow is checked.** Integer arithmetic that overflows stops the program; Go wraps silently. ## Things you will miss Goroutines that outlive their caller, `select` over many channels (`Async.race` takes two tasks), maps, interface values and type switches, struct embedding, the standard library, and Go's fast tooling for large codebases. # Clay: a UI toolkit in CeeSafe [Clay](https://github.com/CeeSafe/Clay) is ClayWind ported to CeeSafe, with no C code and no dependencies. ClayWind is a Tailwind-flavoured immediate-mode UI toolkit built on the layout model of [Clay by Nic Barker](https://github.com/nicbarker/clay). The port is about 18,000 lines. It is a working example of what CeeSafe is for: a program that does everything itself, from layout to pixels, and still runs on every desktop. ![The ClayWind demo's dashboard, drawn by the CeeSafe renderer](https://raw.githubusercontent.com/CeeSafe/Clay/main/docs/dashboard.png) ## What it does - **Layout:** a port of Clay 0.14's layout engine, checked against the C original by a differential fuzzer. Flex rows and columns, grow and fit sizing, wrapping, floating elements, scrolling and transforms. - **Rendering:** a software renderer written to match ThorVG's output. - Anti-aliased paths, gradients, images, shadows, blur, filters and every CSS blend mode. - TrueType text, read and rasterized by the toolkit itself. - **Speed:** - **Retina:** frames are drawn at the display's pixel ratio. - **Incremental:** only what changed between frames is redrawn, and the result is byte-identical to a full redraw. A 2× frame costs 2–4 ms instead of 64. - **Controls:** buttons, text input, sliders, tabs, dropdowns, lists, a context menu and tooltips, with focus and transitions. ## What it uses in CeeSafe - **Native windows:** lib/std's `Window` ([type-system.md §26.13](../spec/type-system.md)), with AppKit on macOS, Win32 on Windows and X11 on Linux. ClayWind's desktop host presents only the boxes that changed (`Window.presentChanged`). The program itself has no `extern "C"`. - **Regions:** the renderer and the UI borrow their memory from a `FixedBufferAllocator`, and the borrow checker tracks those loans across the whole frame. - **Generic methods and `dyn` interfaces:** `Ui.endFrame` takes any text measurer, including the renderer's own font engine, and every host's `Platform` is passed as a `dyn Platform`. - **Hosts on the standard library:** sockets and async I/O for a browser host (HTTP and WebSocket servers written in CeeSafe), and the console for a terminal host. Several language changes came from this port, or from work it started: - **The port's own findings:** generic methods, arrays of views and `dyn I` as a bound. - **Follow-ups:** static view tables, and, found while testing them, a soundness fix for `static mut` and stricter rules for `static` in signatures. - **What the hosts needed:** native windows and `presentChanged`. Each has a decision record, ADRs 0063 to 0070. ## Running it With [ceesafec 0.14.0 or later](./installation.md): ```sh git clone https://github.com/CeeSafe/Clay.git cd Clay/claywind-hosts/desktop ceesafec build ./out/desktop ``` The same demo also runs headless (for screenshots and tests), in a browser (`claywind-hosts/browser`) and in a terminal (`claywind-hosts/terminal`). The repository's README describes each host and the test suite. # Async tasks A program that waits for many things at once (timers, sockets, files, other tasks) can run them as **tasks** on one thread instead of one thread each. An `async fn` is a function whose call is a task; `await` runs a task and gives its result; while it waits, other tasks of the same thread run. CeeSafe's tasks are **structured**: every task a program starts has returned before `Async.run` returns, so a task may borrow its caller's locals, just as a function call may. ## Async functions and `await` ```ceesafe async fn add(a: i32, b: i32) -> i32 => a + b; async fn checked(x: i32) -> Result { let y = await add(x, x); if (y > 100) { return Err("too big"); } return Ok(y); } async fn total(values: Slice) -> i32 { var sum = 0; for (v in values) { sum = await add(sum, v); } return sum; } fn main() { let values = [1, 2, 3]; Console.writeLine(Async.run(total(values[..]))); match (Async.run(checked(20))) { Ok(v) => { Console.writeLine(v); } Err(e) => { Console.writeLine(e); } } match (Async.run(checked(60))) { Ok(v) => { Console.writeLine(v); } Err(e) => { Console.writeLine(e); } } } ``` ```text 6 40 too big ``` `Async.run(task)` runs a task on this thread until it returns, from ordinary (sync) code. Inside an async fn, `await` does the same, and `?` works on an awaited `Result` as on any other. A call of an async fn does nothing by itself: it must be awaited, run or spawned. There is no `Task` or `Future` value to store or return, so a task never outlives the code that started it: ```ceesafe async fn add(a: i32, b: i32) -> i32 => a + b; fn main() { let pending = add(1, 2); } ``` ```text main.cee:4:19: error[N0342]: a call of the async fn 'add' is a task: await it, or pass it to 'Async.run' (type-system.md §40.1) | 4 | let pending = add(1, 2); | ^^^^^^^^^ ``` `await` is allowed only in an async fn or an async closure (N0341). An async fn may await itself, directly, through other async fns, or through `dyn` (middleware wrapping middleware): each await on such a cycle takes its callee's frame from the task's own frame stack, reused from task to task, so recursion is written as in sync code. Running out of memory for it stops the program, as a deep sync recursion does; `Async.boxed(alloc, f(…))` gives a call a frame from an allocator instead, with the failure as a value. In freestanding code, which has no frame stack, such recursion is N0343 and `Async.boxed` is the way. ## Running tasks at once: scopes `Async.scope` starts **children** that run while the scope waits, and returns once every child has. A child is an async closure, and it may borrow the awaiting function's locals, because the scope cannot return before it does: ```ceesafe enum Failure { OutOfMemory, Bad, } async fn pause(ms: u64) -> Result { match (await Timer.sleep(ms)) { Ok() => { return Ok(); } Err(_) => { return Err(Failure.Bad); } } } async fn say(text: StringView) -> Result { match (await AsyncConsole.writeLine(text)) { Ok() => { return Ok(); } Err(_) => { return Err(Failure.Bad); } } } async fn gather() -> Result { var slow: u32 = 0; var fast: u32 = 0; await Async.scope(Failure.OutOfMemory, (s) => { s.spawn(async () => { await pause(30)?; await say("slow done")?; slow = 100; return Ok(); }); s.spawn(async () => { await pause(10)?; await say("fast done")?; fast = 20; return Ok(); }); })?; return Ok(slow + fast); // both children have returned } fn main() { match (Async.run(gather())) { Ok(v) => { Console.writeLine(v); } Err(_) => { Console.writeLine("failed"); } } } ``` ```text fast done slow done 120 ``` The two children wait at the same time, so the whole takes about 30 ms, not 40. They write with `AsyncConsole`: `Console.writeLine` blocks its thread, which async code may not do (N0295, below). The rules that keep this safe are the borrow checker's: - two children cannot both capture a local that either changes (N0241), and the scope's body cannot use a local a child has captured until the scope returns; - a child cannot borrow a local of the scope's body, which ends before the child may (N0344); a loop that spawns a child per item copies what each child reads, or moves it in with `async move () => …`; - every child returns `Result` with one `E`: the first `Err` cancels the other children and is the scope's result, after all of them have returned. `oom`, the scope's first argument, is the error if a child's frame cannot be allocated. `Async.scopeIn(alloc, oom, body)` takes the children's frames from an allocator of your choice; `Async.scope` uses the system heap. ## Starting tasks as you go: task sets A scope's children are started in its body, from the variables in reach. A server starts a task per connection as connections come, from a loop that may be in another function. That is a **task set**: ```ceesafe enum WorkError { Failed(u32), NoMemory, } async fn work(n: u32) -> Result { match (await Timer.sleep(1)) { Ok() => {} Err(_) => { return Err(WorkError.Failed(n)); } } return if (n == 3) Err(WorkError.Failed(n)) else Ok(); } /// Starts five tasks through the set it is given; at most two run at once. async fn startAll(set: borrow TaskSet) -> Result { for (n in 0..5u32) { await set.spawn(async move () => await work(n))?; } return Ok(); } async fn run() -> Result { return await Async.tasks(2, WorkError.NoMemory, async (set) => await startAll(set)); } fn main() { match (Async.run(run())) { Ok() => { Console.writeLine("all done"); } Err(WorkError.Failed(n)) => { Console.write("task "); Console.write(n); Console.writeLine(" failed; the others ran to the end"); } Err(WorkError.NoMemory) => { Console.writeLine("no memory"); } } } ``` ```text task 3 failed; the others ran to the end ``` - `Async.tasks(limit, oom, body)` runs `body` with the set, then waits for every task it started. The set's error type is `oom`'s, and must be `Copy`. - `await set.spawn(async move () => …)` waits while `limit` tasks run; `set.trySpawn(…)` is `false` instead. - One task's error does not stop the others: the set reports the first. The body's error cancels them all. - A task may outlive the function that started it, so it owns what it captures: no references or views (N0349). Share through `Arc` or a `Channel`, or pass values in. ## Racing and joining two tasks `Async.race(a, b)` runs two async closures and keeps the first to finish; `Async.all(a, b)` runs both and fails with the first error. Neither allocates: their children's frames are in the caller's. ```ceesafe async fn after(ms: u64, v: u32) -> Result { await Timer.sleep(ms)?; return Ok(v); } async fn fastest() -> Result { let r: Result, AsyncError> = await Async.race( async () => await after(30, 1), async () => await after(5, 2), ); let raced = r?; return Ok(raced.winner * 10 + raced.value); // the second child (winner 1) returned 2 } fn main() { match (Async.run(fastest())) { Ok(v) => { Console.writeLine(v); } Err(_) => { Console.writeLine("failed"); } } } ``` ```text 12 ``` The loser is cancelled and awaited before `race` returns; if it also returned `Ok`, `raced.other` holds its value. ## Cancellation A task is cancelled when a sibling fails, when it loses a race, or when the task that owns its scope is cancelled. Cancelling only sets a flag: the task's next waiting operation (a sleep, a read, a channel receive) returns `Err(AsyncError.Cancelled)` at once, and the task returns through its own `?`s, dropping its locals as any function does. Code between two `await`s is never interrupted. A long computation can look with `Async.isCancelled()` or `Async.checkpoint(error)`. ```ceesafe async fn work() -> Result { var saw: u32 = 0; let outcome = await Async.scope(AsyncError.Cancelled, (s) => { s.spawn(async () => { await Timer.sleep(5)?; return Err(AsyncError.TimedOut); // fails first, so its sibling is cancelled }); s.spawn(async () => { match (await Timer.sleep(1000)) { Ok() => { saw = 1; } Err(AsyncError.Cancelled) => { saw = 2; } Err(_) => { saw = 3; } } return Ok(); }); }); match (outcome) { Err(AsyncError.TimedOut) => { return Ok(saw); } _ => { return Ok(0); } } } fn main() { match (Async.run(work())) { Ok(v) => { Console.writeLine(v); } Err(_) => { Console.writeLine("failed"); } } } ``` ```text 2 ``` `Async.shield(ms, body)` runs cleanup that must finish: inside it the flag is ignored, for at most `ms` milliseconds. ## Deadlines `Async.timeout(ms, timedOut, body)` runs `body` with a deadline. If the body has not returned after `ms` milliseconds, it is cancelled and awaited, and the result is `Err(timedOut)`, an error of the body's own type. `Async.deadline(at, timedOut, body)` is the same with a time on `Timer.now()`'s clock. Neither allocates. ```ceesafe enum FetchError { TimedOut, Async(AsyncError), } async fn after(ms: u64, v: u32) -> Result { match (await Timer.sleep(ms)) { Ok() => { return Ok(v); } Err(e) => { return Err(FetchError.Async(e)); } } } async fn fetch() -> Result { let quick = await Async.timeout(50, FetchError.TimedOut, async () => await after(5, 1))?; match (await Async.timeout(5, FetchError.TimedOut, async () => await after(50, 2))) { Ok(v) => { return Ok(quick * 10 + v); } Err(FetchError.TimedOut) => { return Ok(quick * 10 + 9); } // the slow one ran out of time Err(e) => { return Err(e); } } } fn main() { match (Async.run(fetch())) { Ok(v) => { Console.writeLine(v); } Err(_) => { Console.writeLine("failed"); } } } ``` ```text 19 ``` A body that returned `Ok` before it saw the cancellation keeps its value. If the caller itself is cancelled, the result is the body's own error, never `timedOut`: a timeout reports only its own deadline. ## Waiting for the outside world The library's waiting operations are async functions, in the hosted profile: | What | Operations | |---|---| | Time | `Timer.sleep(ms)`; `Timer.now()` (not async), a monotonic clock for waits and measurements; `Clock.now()`, the date as a `UnixTime` (seconds and nanoseconds since 1970), which may jump | | Random bytes | `Random.fill(bytes)` (not async, and cheap enough per message): bytes from the system's cryptographic source, for keys, masks and tokens | | Console | `AsyncConsole.write(text)`, `writeLine(text)`, `writeError(text)` | | Files | `AsyncFiles.open`, `create`, `read`, `write` (each on a worker thread while the task waits) | | Sockets | `AsyncTcpListener.bind` or `bindWith(address, options)` (not async), `accept`; `AsyncTcpStream.connect` or `connectTo(address)`, `read`, `write`, `writeVectored(parts)` and `writeAllVectored(parts)` (an `IoParts`, up to eight byte parts sent in one call); `sendFile(file, offset, length)` and `sendFileAll(file, offset, length)`, a file's bytes sent by the kernel where it can (`sendfile`, `TransmitFile`), the file's position untouched; `AsyncUdpSocket.bind(address, options)`, `sendTo`, `recvFrom`, and `sendMsg`/`recvMsg` with the local address, ECN bits, truncation and segments; `AsyncDns.resolve(host, port)`, a name's addresses, cancellable; the options (`setNoDelay`, `setKeepAlive`, buffers, `setLinger`) and `peerAddress`/`localAddress`, not async | | Tasks to tasks | `Channel`: `sendAsync`, `recvAsync`, `close` (`send` and `recv` block, for threads); `trySend` and `tryRecv`, which never wait (a full or empty channel answers at once), and `isClosed()`; `AsyncMutex`: `lock`, whose guard may be held across `await` | | Blocking calls | `Async.blocking(() => …)`: any sync code on a worker thread while the task waits | | Signals | `AsyncSignal`: one task waits, any thread or interrupt handler raises | Each returns `Result`, whose cases are `Cancelled`, `TimedOut` and `Io(IoError)`. A socket's address is a `SocketAddress`, `V4` or `V6`, made from text by `SocketAddresses.parse("[::1]:8080")` or `SocketAddresses.parseIp("::1", 8080)`, and written back with `a.write(mut w)`. A listener on `SocketAddresses.v6Any(port)` also accepts IPv4 clients, whose `peerAddress()` is `V4`. Network failures have their own `IoError` kinds, `ConnectionReset`, `TimedOut`, `HostUnreachable` and the rest, the same on every platform. A blocking call made directly in async code would stop every task of the thread, so it is a compile error (N0295): a `File` read or a `Mutex` lock belongs in `Async.blocking`. A static file goes to a socket with `await stream.sendFileAll(borrow file, offset, length)`: the kernel copies it from the page cache (`sendfile`, or `TransmitFile` on Windows), and where it cannot, lib/std reads 64 KiB at a time on a worker thread and writes them, with the same result. The offset is explicit and the file's position stays where it was, so one open file can be sent on many connections at once. If the file is shorter than `offset + length`, the result is `Io(InvalidInput)`. `sendFile` is one step and returns how many bytes went. ## How it runs Each async fn has a **frame** holding its locals and what it is waiting for. Its size is known at compile time (`ceesafec build --explain-frames` prints it), and it never moves, which is why a reference to a local may be held across an `await`. An awaited call's frame is inside its caller's, so `await` itself allocates nothing; children of a scope get theirs from the scope's allocator. The executor waits with kqueue on macOS and the BSDs, epoll on Linux (io_uring when asked for), and the console and file APIs on Windows. `Async.run` uses one thread; `Async.runWorkers(n, task)` and `s.spawnSend(…)` spread a scope's children over `n` threads ([Threads](threads.md#async-tasks-on-several-threads)). In the interpreter tasks run in a fixed order with a virtual clock, so the examples above print the same every time, and quickly; there `Clock.now()` is 2000-01-01T00:00:00Z plus that clock, and `Random.fill` gives the same bytes every run, which are not secret. Async code also runs in CosmoOS user programs (without async sockets) and in kernel code, over an executor the kernel provides ([Kernel code](kernel.md#async-tasks-in-kernel-code)). The debugger lists a program's tasks and where each waits ([Debugging](debugging.md)). The rules in full are the specification's §40 ([type-system.md](../spec/type-system.md)) and ADRs 0031 to 0040. ## Next [Unsafe code and C](unsafe-and-c.md). # Performance CeeSafe compiles through C, with every safety check on: array bounds, integer overflow, lossy conversions. This page measures what that costs against Rust and plain C on sixteen workloads, and says where CeeSafe is ahead or behind and why. ## The result Over the sixteen workloads CeeSafe takes **0.96× Rust's time** (geometric mean), and **0.88×** that of Rust built with overflow checks, which is the comparison that checks the same things CeeSafe does. Hand-written C without any check takes 0.94×. | Workload | CeeSafe | Rust | Rust, overflow checks | C | CeeSafe (ms) | Rust (ms) | |---|---:|---:|---:|---:|---:|---:| | `heap-alloc` | **0.28** | 1.00 | 0.92 | 0.95 | 121 | 428 | | `nbody` | **0.82** | 1.00 | 1.08 | 0.82 | 500 | 611 | | `stack-alloc` | **0.84** | 1.00 | 1.70 | 0.85 | 244 | 289 | | `vector-growth` | **0.87** | 1.00 | 1.03 | 0.80 | 385 | 443 | | `arena` | **0.95** | 1.00 | 0.91 | 0.98 | 241 | 254 | | `memory-copy` | 0.99 | 1.00 | 1.06 | 1.13 | 595 | 602 | | `linked-list` | 1.01 | 1.00 | 0.99 | 0.90 | 366 | 361 | | `large-objects` | 1.02 | 1.00 | 0.97 | 1.02 | 254 | 250 | | `small-objects` | 1.03 | 1.00 | 1.00 | 0.89 | 510 | 493 | | `string-building` | 1.03 | 1.00 | 1.31 | 0.78 | 299 | 292 | | `numeric` | 1.12 | 1.00 | 1.00 | 1.14 | 240 | 214 | | `binary-trees` | 1.13 | 1.00 | 1.02 | 0.91 | 561 | 494 | | `parsing` | 1.16 | 1.00 | 1.10 | 1.05 | 161 | 139 | | `data-oriented` | 1.18 | 1.00 | 1.05 | 1.00 | 381 | 323 | | `recursion` | 1.26 | 1.00 | 1.26 | 1.00 | 407 | 323 | | `hash-map` | 1.41 | 1.00 | 1.17 | 0.94 | 453 | 321 | | **geometric mean** | **0.96** | 1.00 | 1.09 | 0.94 | | | Times are each language's median wall time over five runs (after one discarded run) divided by Rust's; lower is faster. Measured 2026-10-31 on an Apple M-series Mac (arm64, macOS), with ceesafec 0.2.0-dev, rustc 1.99.1 and Apple clang 21. A run-to-run spread of a few percent is normal: compare ratios, not single milliseconds. Peak memory is the same as Rust's or lower everywhere (`arena`: 7.8 MiB against 17.7 MiB). Binaries are about 40–56 KiB against Rust's 460 KiB. ceesafec compiles each program in about a quarter of a second, roughly twice rustc's time on these single-file programs, since it runs the C compiler after its own work. ## Where CeeSafe is ahead - **`heap-alloc`, 3.5× faster than Rust.** ceesafec's placement optimisation keeps a box that never escapes its function on the stack, so the program's per-iteration heap array costs no allocation at all. It beats hand-written C, which calls `malloc` and `free` each time. - **`stack-alloc`, `nbody`, `vector-growth`.** CeeSafe matches C. Its overflow and bounds checks are removed where ceesafec proves they cannot fail (loop indexes within a length, sums of bounded values), which Rust with overflow checks does not manage on `stack-alloc` (1.70×). - **`arena`.** A region's bump allocation is inlined into the program (Decision 0007), and the arena's chunks take less than half the memory of Rust's. ## Where CeeSafe is behind, and why - **`hash-map` (1.41×).** Rust builds its tables with `vec![0; n]`, so LLVM knows their length and removes the bounds checks on `keys[s & MASK]`. A CeeSafe `Vector` can only be filled by `push`, so the length is unknown and every probe keeps its check: with the checks compiled out the program runs at Rust's speed. A filled-vector constructor would close this gap. - **`recursion` (1.26×) and `parsing` (1.16×).** The overflow check on `fib(n - 1) + fib(n - 2)` and on accumulating digits cannot be proven unnecessary; Rust pays the same when its overflow checks are on (1.26× and 1.10×). - **`data-oriented` (1.18×).** As for `hash-map`, the particle arrays are filled by `push`; the bounds checks cost about 11%. - **`binary-trees` (1.13×) and `small-objects` (1.03×).** Every heap allocation goes through the runtime's `ceesafe_alloc`, which counts allocations (for `CEESAFE_ALLOC_REPORT` and fault injection) before calling `malloc`. Calling `malloc` directly measured 12% faster on both. ## Const parameters `const-flags` (ADR 0083 §1.3) is a recursive scanner over 10 MB of pseudo-random bytes, run 100 times, with three flag tests per byte; its flags are const parameters (`fn scan`), so each instance keeps only its own tests. It is a workload of `run.py`, measured on its own and not in the table above, whose sixteen workloads were measured with an older ceesafec. Measured 2026-11-09 on the same kind of machine (Apple arm64, Apple clang 21, rustc 1.99.1, ceesafec 0.26.0-dev), on a machine shared with other work, so the spread is wider than usual: | Version | Time | |---|---:| | CeeSafe, const parameters (`main.cee`) | 1.05× Rust (2.67 s against 2.55 s, `run.py`, median of 5) | | Rust, const generics (`main.rs`) | 1.00 | | CeeSafe, the same flags as `bool` parameters (`runtime.cee`) | 1.19× CeeSafe's const version (3.68 s against 3.10 s user time, 3 runs each) | At `-O2` clang does not clone a recursive function for a constant argument, so a run-time flag keeps every test; the const parameter removes them, as Rust's const generics do. `main.c` takes the flags as arguments (C has no const parameters), and in the same three runs took 3.27–3.67 s. ## Run it yourself ```sh python3 benchmarks/run.py # every workload: cee, rust, rust-checked, c; 5 runs python3 benchmarks/run.py --workloads hash-map,nbody --runs 3 python3 benchmarks/run.py --cee-flag --no-bce # with bounds-check elimination off ``` Each workload is the same algorithm with the same data structures in every language, and prints one checksum, which the runner checks. `benchmarks/README.md` describes the workloads and the build flags. To see which checks ceesafec kept in a program, build it with `--explain-bounds-checks`, `--explain-overflow-checks` or `--explain-allocations` ([The ceesafec command](ceesafec.md)). # Testing A test is a function marked `@test`, and `ceesafec test` runs a package's tests, each as its own process. ## Writing tests ```ceesafe /// The bytes 0..n, or the allocation failure. fn numbers(n: u64) -> Result, AllocError> { var v = Vector.create(); for (k in 0..n) { v.push(truncate(k))?; } return Ok(v); } @test fn counts() -> Result { let made = numbers(3); CheckResult, AllocError>.isOk(borrow made, "three bytes fit")?; match (made) { Ok(v) => { Check.equalU64(3, v.length as u64)?; let expected = [0u8, 1, 2]; Check.equalBytes(expected[..], v.view())?; } Err(_) => { return Check.fail("unreachable after isOk"); } } return Ok(); } @test async fn waits() -> Result { await Timer.sleep(1)?; return Ok(); } ``` - **Shape.** A test takes no parameters and returns `void` or `Result`. An `Err` is its failure, and so is a panic. It may be `async`. - **Checks.** `Check`'s functions (`isTrue`, `equalU64`, `equalText`, `equalBytes`, `fail`, …) return `Result`, so a failed check returns from the test with `?`, dropping what it holds as any return does. - **Where.** Tests go beside the code, or in the package's `tests/` directory (`tests = [...]` in the manifest). They see the package's private items. - **`build` leaves them out**, so calling a test from other code is an error (N0348). ## Running them ```text ceesafec test every test ceesafec test --filter parse --jobs 4 those whose name contains "parse", four at a time ceesafec test --interp in the interpreter: a virtual clock, a fixed run order ceesafec test --sweep alloc each passing test again, once per allocation, that allocation failing ceesafec test --sweep cancel each passing async test again, cancelled at each of its suspensions ceesafec test --format json one JSON document, for CI ``` Each test runs as its own process, so a panic or a crash ends only that test. Tests run in the order of their names. Each is reported as passed, failed, panicked, crashed (a signal), exited (it called `Process.exit` itself), timed out (`--timeout`, 60 seconds by default) or leaked, natively and with `--interp`. A leak fails a test: CeeSafe has no garbage collector, so memory still allocated at the end is a bug. A failing test's output is shown under its name, and a test failing with its own enum names the variant. A `--filter` that matches no test is an error, and `--sweep` runs natively only. `ceesafec test --help` lists the options. # CeeSafe Grammar — M1 subset Status: normative for Milestone 1 (increment 1a implements the lexer and parser for exactly this grammar). Later milestones extend it. Any syntax not listed here is rejected by the parser, even if `language-spec.md` shows it for a later milestone. Notation: EBNF. `'x'` is a literal token, `X?` optional, `X*` zero or more, `X+` one or more. ## 1. Lexical grammar ### 1.1 Source text - UTF-8. A leading BOM is ignored. Line terminators: `\n`, `\r\n`, `\r`. - Columns in diagnostics are 1-based and count UTF-16 code units. Lines are 1-based. - Non-ASCII characters may appear only inside comments, string literals and char literals. ### 1.2 Whitespace and comments - Whitespace: space, tab, line terminators, form feed. - `// ...` to end of line. `/* ... */` block comments do **not** nest. An unterminated block comment is error N0003. - Comments and whitespace are discarded by the parser, except doc comments (M8a). The lexer records every comment's span for the formatter (M10 T1, formatting.md). - **Doc comments** (M8a): a line comment that starts with exactly `///` (not `////`). A run of doc comment lines, with only whitespace between them, is attached to the declaration whose first token follows the run directly. Its text is each line after `///` with leading spaces removed. Doc comments elsewhere are ordinary comments. Used by the `Safety:` rule (type-system.md §27.4). ### 1.3 Identifiers and keywords ``` Identifier = IdentStart IdentContinue* (not a keyword) IdentStart = 'A'..'Z' | 'a'..'z' | '_' IdentContinue = IdentStart | '0'..'9' ``` A lone `_` is the token `Underscore` (reserved; no meaning in M1). Keywords (all reserved, whether or not M1 uses them): ``` as borrow break const continue defer drop else enum extern false fn for if impl import in interface internal let loop match move mut namespace null own private public return Self static struct this true type unsafe using var where while class async await yield macro ``` Primitive type names (`i32`, `u64`, `bool`, `void`, …) are **identifiers**, resolved by name resolution, not keywords. `is` (ADR 0085 §2.8) is **contextual**: it is the operator of `e is P` (§2.1) only where a complete operand is followed by the identifier `is`; everywhere else it is an identifier, so `c.types.is(t, k)` and a local named `is` keep working. `Self` (ADR 0086 §2.2) names a type: in an interface, the implementing type; in an `impl` block and in a struct's or enum's body, the target type with its parameters. It is written where a type name is, as the first part of a `QualifiedName` in a type (`Self`, `Self.Item`, also as a type argument: `Vector.create()`), as a primary expression (`Self { … }`, `Self.create(…)`, `Self.MAX`) and as the first part of a pattern's qualified name (`Self.Idle`, `Self.Busy(n)`, `Self.TAB`). Declaring anything named `Self` is N0100, as for every keyword. ### 1.4 Integer literals ``` IntLiteral = (Decimal | '0x' HexDigits | '0b' BinDigits | '0o' OctDigits) IntSuffix? IntSuffix = 'i8'|'i16'|'i32'|'i64'|'isize'|'u8'|'u16'|'u32'|'u64'|'usize' ``` - `_` separators are allowed only *between* digits (`1_000`, `0xFF_FF`). A separator at the start or end of the digit sequence, or doubled, is error N0010. - The value must fit in 64 bits (unsigned), otherwise N0004. Whether it fits the *type* is checked later (type checking). - A digit invalid for the radix is N0008. A missing digit after a prefix is N0008. An unknown suffix is N0009. - Decimal literals have no leading-zero restriction, and there is no octal-by-leading-zero: `010` is ten. ### 1.5 Float literals (typed since M11 G7: type-system.md §35.2) ``` FloatLiteral = Decimal '.' Decimal Exponent? FloatSuffix? | Decimal Exponent FloatSuffix? Exponent = ('e'|'E') ('+'|'-')? Decimal FloatSuffix = 'f32' | 'f64' ``` `1.` and `.5` are not float literals. `1..2` lexes as `1` `..` `2`. ### 1.6 String and char literals - `"..."` on one line. Escapes: `\n \r \t \\ \" \' \0 \{ \} \u{H..H}` (1–6 hex digits, a Unicode scalar value). `\{` and `\}` are the braces themselves; `\{` marks braces that are meant as text, so that N0400 does not warn (ADR 0073 §2.4). Other escapes are N0006. A newline or EOF before the closing quote is N0005. - `"""` begins a raw literal, which may span lines (ADR 0074 §2.6): ```ceesafe let query = """ SELECT id, name FROM users """; ``` - The opening `"""` ends its line: anything but spaces and tabs after it is N0011. - The content is the lines that follow, up to the first line holding only spaces or tabs and then `"""`. That closing line's `"""` ends the literal; tokens may follow it on the line. A content line may hold `"""` among other text. - The closing line's indentation is removed from every content line. A content line that is not blank and does not start with that indentation is N0011. A blank line (only spaces and tabs) is empty. - Content lines are joined by `\n`, also in a CRLF file. There is no final newline: the value above is `SELECT id, name\nFROM users`. - There are no escapes: `\` is a backslash, and `{` is a brace (N0400 does not apply). - EOF before the closing line is N0005. - The token is a string literal, the same as `"..."` after the lexer. - `c"..."` is a C string (ADR 0087 §2.4): a `"..."` literal with the same escapes, whose value is a lib/core `CStr` (`type-system.md` §22.3). The compiler appends the NUL byte that ends it, so a `\0` in it is N0006. `c` must touch the `"`: `c "x"` is the name `c` followed by a literal. - **Literal prefixes.** A single letter immediately before `"`, not part of a longer identifier, is a reserved literal prefix: N0100 for any but `c`, and for `c` before a raw literal (`c"""`). `b"…"` is not defined: string patterns are typed by the scrutinee (ADR 0085 §2.7). - `'c'`: exactly one Unicode scalar value or one escape. Otherwise N0007. Its value is the scalar value, as an integer literal (`type-system.md` §4, M11); it may also be a pattern. ### 1.7 Punctuation and operators ``` ( ) { } [ ] , ; : . .. ..= @ ? ?? -> => + - * / % ! & | ^ ~ && || << = == != < <= > >= += -= *= /= %= &= |= ^= <<= ``` The lexer **never** produces `>>` or `>>=`. It emits single `>` (and `>=`) tokens. The parser forms the shift operator `>>` from two *adjacent* `>` tokens, and `>>=` from `>` immediately followed by `>=`. That keeps nested generic arguments (`A>`) unambiguous in later milestones. `..=` is one token (ADR 0085 §2.4): the inclusive range of a pattern, `'a'..='z'`, and only that. In an expression it is N0100 with the hint that ranges there are half-open (`0..10`). `??` is one token (ADR 0072): the fallback operator in an expression (§2.1), and two `?` in a type (`T??`, an optional of an optional). The expression `x??`, `?` applied twice, is written `(x?)?`: a `??` that no operand follows is N0100 with that hint. Any other character is N0001 (unexpected character). ## 2. Syntactic grammar (M1) ``` CompilationUnit = NamespaceDecl? ImportDecl* Item* EOF NamespaceDecl = 'namespace' QualifiedName ';' (* retired by ADR 0013: accepted, no effect *) ImportDecl = 'import' Identifier ('.' Identifier)? ';' (* M10 T5: a dependency, or one of its items; type-system.md §2 *) QualifiedName = Identifier ('.' Identifier)* Item = Attribute* Visibility? (FunctionDecl | ConstDecl | StaticDecl | StructDecl | EnumDecl | ExternBlock) Attribute = '@' Identifier ('(' AttributeArg (',' AttributeArg)* ')')? (* M8b; type-system.md §28.1 *) AttributeArg = Identifier | StringLiteral | IntLiteral ExternBlock = 'extern' StringLiteral '{' (ExternFn | ExternStruct | ExternUnion | ExternEnum | ExternConst | ExternStatic)* '}' (* M8b; §28.2 *) ExternStruct = Attribute* Visibility? 'struct' Identifier ('{' (ExternField | AnonymousUnion)* '}' | ';') (* C6b; type-system.md §31 *) ExternUnion = Attribute* Visibility? 'union' Identifier ('{' ExternField+ '}' | ';') (* ADR 0041; type-system.md §31.1 *) ExternEnum = Attribute* Visibility? 'enum' Identifier ':' Type ';' (* ADR 0079; type-system.md §31.6 *) ExternConst = Attribute* Visibility? 'const' Identifier ':' Type ('=' Expr)? ';' (* ADR 0080; type-system.md §28.2 *) ExternStatic = Attribute* Visibility? 'static' 'mut'? Identifier ':' Type ';' (* ADR 0080; type-system.md §39.6 *) AnonymousUnion = 'union' '{' ExternField+ '}' (* ADR 0041: a C11 anonymous union member *) ExternField = Attribute* FieldDecl (* C9: @cname; type-system.md §31.1 *) ExternFn = Attribute* Visibility? 'fn' Identifier '(' (Param (',' Param)* (',' '...')? ','?)? ')' ('->' Type)? ';' (* `...`: ADR 0041 *) TypeParams = '<' TypeParam (',' TypeParam)* '>' (* M6a; type-system.md §20 *) InterfaceDecl = 'interface' Identifier TypeParams? (':' Type ('+' Type)*)? '{' (MethodSig | InterfaceConst | AssocTypeDecl)* '}' (* M6b; §21; supertraits: ADR 0086 §2.6 *) InterfaceConst = 'const' Identifier ':' Type ('=' Expr)? ';' (* ADR 0086 §2.4: the value is a default *) AssocTypeDecl = 'type' Identifier (':' Type ('+' Type)*)? ';' (* ADR 0086 §2.5 *) MethodSig = Attribute* 'fn' Identifier TypeParams? '(' ParamList? ')' ('->' Type)? (';' | FunctionBody) (* attributes: M13 G2, `@effects`, §37.3; a FunctionBody is a default, ADR 0086 §2.1 *) ImplDecl = 'impl' Type 'for' Type '{' (Attribute* Visibility? FunctionDecl | ConstDecl | AssocTypeDef)* '}' (* M6b; §21; attributes C7a; ConstDecl and AssocTypeDef: ADR 0086 §2.4, §2.5 *) AssocTypeDef = 'type' Identifier '=' Type ';' | 'impl' Type '{' (Attribute* Visibility? FunctionDecl | Visibility? ConstDecl)* '}' (* inherent: ADR 0064 §1; ConstDecl ADR 0074 §2.7 *) | 'impl' TypeParams? Type '{' (Attribute* Visibility? FunctionDecl)* '}' (* inherent: ADR 0064 §1; §21.1 *) MemberCall = Expr '.' Identifier ('<' GenericArg (',' GenericArg)* '>')? '(' Args? ')' (* type arguments: ADR 0064 §1 *) TypeParam = 'region' Identifier (* M6x: a region, before any type parameter; §25.1 *) | 'const' Identifier ':' Type (* ADR 0083: a const parameter; type-system.md §20.5 *) | Identifier (':' Type ('+' Type)*)? (* bounds: M6b *) (* `region` is contextual: only `region` followed by a name declares a region. In a type, a region argument is a region name or `static`, written first in the argument list (`Token`, `StringView`, `Slice`) or in `borrow(r) T`; type-system.md §25.2. *) EnumDecl = 'enum' Identifier TypeParams? '{' (Variant (',' Variant)* ','?)? '}' (* M2d *) Variant = ('@' 'from')? Identifier ('(' Type (',' Type)* ','? ')')? (* `@from`: ADR 0075 §2.3 *) Visibility = 'public' | 'internal' | 'private' FunctionDecl = 'unsafe'? 'async'? 'fn' Identifier TypeParams? '(' ParamList? ')' ('->' Type)? FunctionBody (* 'unsafe': M8a; 'async': L1, type-system.md §40, ADR 0031 *) ParamList = (Receiver | Param) (',' Param)* ','? Receiver = ('borrow' 'mut'?)? 'this' (* M2a: by value; M2b: reference receivers *) Param = Identifier ':' Type StructDecl = 'struct' Identifier TypeParams? '{' Member* '}' (* M2a; TypeParams M6a *) Member = Visibility? FieldDecl | Attribute* Visibility? FunctionDecl | DropBlock (* method attributes: C7a, type-system.md §32 *) | Visibility? ConstDecl (* ADR 0074 §2.7: an associated constant, type-system.md §10 *) DropBlock = 'drop' Block (* M2c *) FieldDecl = Identifier ':' Type ';' FunctionBody = Block | '=>' Expr ';' ConstDecl = 'const' Identifier ':' Type '=' Expr ';' StaticDecl = 'static' 'mut'? Identifier ':' Type '=' Expr ';' (* M14 K1; type-system.md §39 *) Type = QualifiedName | 'borrow' 'mut'? Sources? Type (* M2b references; Sources only in return types *) | '[' Type ';' Expr ']' (* M2d fixed array; the length: type-system.md §16.1, ADR 0083 §2.7 *) | ('Slice' | 'SliceMut') '<' Type '>' (* M2d slices *) | 'Result' '<' Type ',' Type '>' (* M3a; type-system.md §17 *) | Type '?' (* M6c: optional; `borrow T?` is an optional reference *) | '(' Type ')' (* M11 (G6): `borrow (T?)` is a reference to an optional *) | Attribute* 'fn' '(' (Type (',' Type)*)? ')' ('->' Type)? (* M11 (G8a): a function value, type-system.md §36.1; G8b′: `@effects(…)` before `fn`, §36.3 *) | Identifier '<' GenericArg (',' GenericArg)* '>' (* M6a: an instantiation of a generic struct or enum; also Box, Pool, … *) | '*' 'mut'? Type (* M8a: raw pointer; type-system.md §27 *) | 'dyn' Type (* M13 G2: an interface instantiation; only behind `borrow` or in `Box<…>`; type-system.md §37. `dyn` is a keyword only here *) | 'dyn' 'mut'? Attribute* 'fn' '(' (Type (',' Type)*)? ')' ('->' Type)? (* M13 G3: an owning closure's type, §36.5 *) Sources = '(' Identifier (',' Identifier)* ')' (* only if a type follows the ')'; else '(' Type ')' *) GenericArg = Type | ConstArg (* ADR 0083; type-system.md §20.5 *) ConstArg = 'true' | 'false' | IntLiteral | '(' Expr ')' (* a name or `E.V` parses as a Type; the parameter's kind decides (N0354). `'(' Expr ')'` only in a type: a `(` that a type follows (`(borrow T)`, `(*T)`, `(fn …)`, `(dyn I)`, `(T?)`, `(T)?`) is a Type *) Block = '{' Statement* '}' Statement = Block | LocalDecl | IfStmt | 'while' '(' Expr ')' Block | 'loop' Block | 'break' ';' | 'continue' ';' | 'return' Expr? ';' | 'using' Identifier (':' Type)? '=' Expr ';' (* M2c *) | 'defer' Statement (* M2c *) | MatchStmt (* M2d; type-system.md §15.2 *) | 'for' '(' Identifier 'in' Expr ('..' Expr)? ')' Block (* M2d; type-system.md §16.4 *) | 'unsafe' Block (* M8a; type-system.md §27.4 *) | Expr AssignOp Expr ';' (* assignment: a statement, not an expression *) | Expr ';' LocalDecl = ('let' | 'var') Identifier (':' Type)? ('=' Expr)? ';' | 'let' '_' '=' Expr ';' (* ADR 0072: evaluates Expr and destroys its value, as `Expr;` *) IfStmt = 'if' '(' Expr ')' Block ('else' (IfStmt | Block))? AssignOp = '=' | '+=' | '-=' | '*=' | '/=' | '%=' | '&=' | '|=' | '^=' | '<<=' | '>>=' ``` Notes: - Braces are **mandatory** after `if`, `else`, `while` and `loop`, so there is no dangling-else ambiguity. - Assignment is a statement, so `if (x = 1)` is a syntax error. The assignment target must be a name or member access (N0104 otherwise). Whether it is *assignable* (a `var`) is checked later. - A `LocalDecl` without an initialiser must have a type annotation (enforced by the checker, not the parser). ### 2.1 Expressions and precedence From lowest to highest binding. All binary levels are left-associative except comparisons, which are **non-associative**: `a < b < c` is error N0103, and `??`, which is right-associative. | Level | Operators | Notes | |---|---|---| | 0 | `??` | right-assoc, short-circuit (ADR 0072; type-system.md §22.1). An unparenthesised `\|\|` or `&&` operand beside it is N0100 | | 1 | `\|\|` | short-circuit | | 2 | `&&` | short-circuit | | 3 | `== != < <= > >=`, `is` | non-associative. `e is P` (ADR 0085 §2.8; type-system.md §15.4): after `is` the pattern grammar reads as far as it can, so a `\|` there is an or-pattern: `(c is 'a') \| flag` needs the parentheses | | 4 | `\|` | | | 5 | `^` | | | 6 | `&` | | | 7 | `<< >>` | | | 8 | `+ -` | | | 9 | `* / %` | | | 10 | `as` | `Expr 'as' Type`, left-assoc (`x as u32 as u64`). A primitive type name (`u8` … `u64`, `i8` … `i64`, `usize`, `isize`, `f32`, `f64`, `bool`, `char`) takes no type arguments, so `a as u64 < b` is `(a as u64) < b` (ADR 0075 §2.13) | | 11 | unary `- !`, `*` (ADR 0075 §2.14: reading through a reference), `borrow`, `borrow mut`, `move`; `await` (L1) | prefix, right-assoc (`- -x`); `await`'s operand: below | | 12 | postfix: call `(...)`, member `.name` | | The ordering puts bitwise operators **above** comparisons (as in Rust and Swift), so `x & 1 == 0` parses as `(x & 1) == 0`. This avoids a well-known C/C# pitfall. ``` Primary = IntLiteral | FloatLiteral | StringLiteral | CharLiteral | 'true' | 'false' | Identifier GenericArgs? | '(' Expr ')' | 'this' | 'unsafe' '{' Expr '}' (* M8a: an unsafe expression *) | IfExpr | MatchExpr (* M11 (G5): type-system.md §15.3 *) | '(' (ClosureParam (',' ClosureParam)*)? ')' '=>' (Block | Expr) (* M11 (G8b): a closure, §36.2 *) | 'move' '(' (ClosureParam (',' ClosureParam)*)? ')' '=>' (Block | Expr) (* M13 G3: a move literal, §36.5 *) | 'async' 'move'? '(' (ClosureParam (',' ClosureParam)*)? ')' '=>' (Block | Expr) (* L1: an async closure, §40.3; `move`: a child that owns what it captures, §40.5, ADR 0033 *) | Identifier GenericArgs? '{' FieldInits? '}' (* struct literal, M2a; GenericArgs M6a *) | '[' Expr (',' Expr)* ','? ']' | '[' Expr ';' IntLiteral ']' (* array literals, M2d *) | '[' ']' (* the empty slice, ADR 0075 §2.9 *) IsTest = Operand 'is' Pattern (* level 3; Pattern: type-system.md §15.2, §15.4 *) ClosureParam = Identifier (':' Type)? (* a '(' … ')' followed by '=>' is a closure *) IfExpr = 'if' '(' Expr ')' Branch 'else' (IfExpr | Branch) MatchExpr = 'match' '(' Expr ')' '{' (ValueArm (',' ValueArm)* ','?)? '}' ValueArm = Pattern Guard? '=>' Branch (* Guard: type-system.md §15.2 *) Guard = 'if' '(' Expr ')' (* ADR 0085 §2.2; an `Arm` of a MatchStmt takes one too *) Branch = Expr | Block | ValueBlock (* a Block branch must not complete *) ValueBlock = '{' Statement* Expr '}' (* its last Expr has no ';' (ADR 0073 §2.3) *) FieldInits = FieldInit (',' FieldInit)* ','? FieldInit = Identifier ':' Expr GenericArgs = '<' GenericArg (',' GenericArg)* '>' (* only when followed by '(', '.' or '{' — see below *) Postfix = Primary ( '(' ArgList? ')' | '.' Identifier | '[' IndexOrRange ']' | '?' | '!' )* (* M3a: propagate / unwrap a Result *) IndexOrRange = Expr | Expr? '..' Expr? (* M2d: element or slice *) ArgList = Arg (',' Arg)* ','? Arg = 'mut' Expr | Expr (* `mut x` for a `borrow mut` parameter, M2b *) Await = 'await' AwaitOperand ( '?' | '!' ) PostfixRest* (* L1; type-system.md §40.2, ADR 0031 §3.1 *) | 'await' AwaitOperand AwaitOperand = Primary ( '(' ArgList? ')' | '.' Identifier | '[' IndexOrRange ']' )* PostfixRest = '(' ArgList? ')' | '.' Identifier | '[' IndexOrRange ']' | '?' | '!' ``` **`await`** (L1, ADR 0031 §3.1): its operand is a primary followed by calls, member accesses and indexes, and the first `?` or `!` ends it. That suffix, and every postfix after it, applies to the awaited value: `await f(x)?` is `(await f(x))?`, and `await f(x)?.len` is `((await f(x))?).len`. A task is never a `Result`, so a `?` that the operand would take could never type. **Generic arguments in expressions** (`truncate(x)`, and since M6a `id(x)`, `Pair.make()` and `Pair { … }`): after an identifier, `<` starts generic arguments if the tokens up to the matching `>` can only form types (identifiers, `.`, `,`, `;`, `*` (M8a), `?` (M9a: optional types, as in `Vector?>.create()`), integer literals, `borrow`, `mut`, and balanced `<…>` and `[…]`), or const arguments (ADR 0083: `true` and `false`; not `( … )`), and the `>` is followed by `(`, `.` or `{`, or (since C7d, for a generic function's instance as a value, `release`, type-system §34.1) by `)`, `,`, `;` or `]`, which cannot follow a comparison's `>` (it needs an operand). Otherwise `<` is the less-than operator. Write `(f < T) > (x)` to mean the comparison. Since 0.26.0 (ADR 0083 §2.1), `f(i < k, true, j > (k))` is the call `i(k)`, an error where `i` is not generic: write `(i < k)`; `f(i < (k), j > (k))` still compares, since `(` cannot be a generic argument in an expression. **Struct literals** (M2a): in expression position, an identifier *immediately followed by `{`* is a struct literal. This is unambiguous in CeeSafe because every `{` that opens a block follows a keyword (`else`, `loop`), a `)` (conditions are parenthesised) or a type (`-> T {`), never an expression. A `Receiver` (`this`) is only meaningful as the first parameter of a function declared inside a `struct`. The parser accepts it as any first parameter; the checker rejects it elsewhere (N0232). ### 2.2 Nesting limit (M15 S3) Nesting is limited to **1,000 levels** per file. Each of these is one level: - a nested block or statement; - a type (each `Box<…>` inside another is one level); - a pattern; - a unary operand; - each link of a left-associative chain, so `1 + 1 + 1` is two levels deep, as are `a.b.c` and `f()()`. The links are binary operators, `as`, calls, members, indexes, `?` and `!`. Every later pass recurses over the tree the parser builds, so its depth is what is bounded. Past the limit the parser reports **N0110** once, at the token where the limit was passed, and stops parsing that file. No other diagnostic in the file follows it. Hand-written code stays far below the limit: none of the repository's 525 files comes near it. Before M15, 100,000 levels overflowed ceesafec's stack, ending with SIGSEGV and no message (#3). ceesafec runs every command on its own 256 MiB stack, so input within the limit compiles whatever the operating system's main-thread stack is (1 MiB on Windows; measured in milestone-15.md S3). ## 3. Error recovery (normative for diagnostics tests) - Expected-token errors (N0100) are reported at the current token. When the missing token is `;`, they are reported at the end of the previous token instead. The missing token is *not* consumed. - After an error, the parser suppresses further diagnostics until it re-synchronises: - in a block: at the next `;` (consumed), `}` (not consumed), or statement keyword (`let var if while loop return break continue`). Balanced `{ }` groups are skipped whole. An item keyword (`fn`, visibility) ends the block, which reports the missing `}`; - inside a `struct` body: at the next member start (identifier followed by `:`, `fn`, or a visibility keyword) or the closing `}`; - at the top level: at the next declaration keyword (`fn const public internal private namespace struct class enum interface impl let var static extern import using type`) outside any skipped `{ }` group, or at EOF. Each misplaced declaration is therefore reported once (N0101). - Every parse loop consumes at least one token per iteration or terminates, so parsing always terminates. - Past the nesting limit (§2.2), N0110 ends the file's parse: no later diagnostic is reported in that file. # Type System and Static Semantics — M1 subset Status: normative for Milestone 1 (increment 1b implements it). Later milestones extend these rules. Where `language-spec.md` describes a later feature, it does not apply until then. Error codes are listed in `diagnostics.md`. ## 1. Types | Type | Values | Notes | |---|---|---| | `i8 i16 i32 i64` | two's complement, −2^(n−1) … 2^(n−1)−1 | | | `u8 u16 u32 u64` | 0 … 2^n−1 | | | `isize` / `usize` | pointer width of the **target** (64 on every M1 target) | from `TargetInfo`, never the host | | `bool` | `true`, `false` | | | `void` | no value | function return type only | | `never` | no value; the expression does not complete | a function's return type only (§15.3, ADR 0074 §2.2) | Not in M1 (error N0216): floats, `char`, strings as values, user-defined types, references. Floats came in M11 (`f32`, `f64`, §35); character literals are integers (§4). In M1 a string literal was allowed **only** as a direct argument of `Console.write`/`writeLine`; since M6c it is a `StringView` value (§22.2). `void` may appear only as a return type (writing `-> void` is the same as omitting the return type). A *never* expression is accepted wherever any type is expected. `never` (ADR 0074 §2.2) names the bottom type, and only as the return type of a function or of a struct's method: a call of one is a call of type *never* (§9), as `Process.exit` is. Anywhere else (a parameter, a field, a local, a type argument, a function type, an `async` function, an interface method, an `extern` function) it is N0226: "'never' can only be a function's return type". A `never` function does not return: the end of its body must be unreachable (N0213), and `return` in it is N0215. A `let` whose initializer has type *never* has no value to bind (N0224). In C, the function is `_Noreturn void` (`c-backend.md`); in NIR, a call of one ends its block with `Unreachable`. `never` is a type name, not a keyword: a value named `never` is unaffected. ## 2. Names and scopes A **package** is the unit of compilation and of naming (ADR 0013, `tooling.md` §4): its files, its dependencies, and `lib/core`. A build checks the package being built together with every package it depends on, directly or not. - **Package scope.** All files of a package share one item scope. Items are referenced by simple name. Two items of one package with the same name are an error (N0201), even in different files. `namespace` declarations are accepted and have no effect (retired by ADR 0013; they will become an error). - **Dependencies** (M10 T5). A package names its dependencies in its manifest (`tooling.md` §4). A dependency's **public** items are reached by qualifying them with the dependency's name: - in a type, `geometry.Point`, `geometry.Pair`; - in an expression, `geometry.area(p)`, `geometry.LIMIT`, `geometry.Shape.Circle(2)`, a struct literal `geometry.Point { x: 1, y: 2 }`; in a pattern, `geometry.Shape.Circle(r)`. A static function of a generic type of a dependency is called through an import (`Pair.make()`), since `geometry.Pair.make()` does not parse (grammar.md §2.1: in an expression, generic arguments follow a simple name only). - **Dependency names and items.** In a package, no item or type parameter may have the name of one of its dependencies (N0315): they share the namespace `geometry.X` is resolved in. A parameter, local, loop variable, closure parameter or pattern binding may (ADR 0074 §2.10): - in its scope, `geometry.x` in an expression is the local's member, and the dependency's values are unreachable; - types and patterns are unaffected (a local is never a type), so `geometry.Point` in a type, a struct literal `geometry.Point { … }` and a pattern `geometry.Shape.Circle(r)` keep naming the package's items; - where `x` is also a public item of the dependency, `geometry.x` is warning N0401, since the item is hidden. Elsewhere `geometry.x` means the dependency's item, and the compiler resolves it before type checking (`PackageNames`), with a syntactic scope walk to find the locals. - **Imports.** `import geometry;` at the top of a file brings every public item of the dependency `geometry` into that file's scope; `import geometry.Point;` brings one (N0311 if it has none of that name, N0312 if it is not public). An import applies to its file only. The name before the `.` must be a dependency of the package (N0310). - **Visibility between packages.** Only `public` items are visible to other packages, and of a public struct only its `public` fields and methods; `internal`, the default, keeps an item within its package, and `private` within its file (§12). Naming another package's non-public item is N0312. A public function, constant, field or struct whose signature names a type its dependents cannot see (a public function returning an internal struct) is N0314. An `impl I for T` applies wherever `I` and `T` are both visible. - **Built-in scope** (outside the package scope, so a user item hides a built-in): `Console.write`, `Console.writeLine`, `Process.exit`, `truncate`, and the primitive type names. - **`lib/core`** (and, hosted, `lib/std`) is in scope in every package, unqualified. A package may declare an item with one of their names (ADR 0075 §5; before it, N0201): the package's item shadows the library's in that package, while the libraries' own code still finds its own. So a name a release adds to a library is not a source change. In C the library's item is then emitted as `ceeT_lib_…`, `ceeF_lib_…` and the like. - **Function scope.** Parameters, then nested block scopes. A local is visible from the end of its declaration to the end of its block. - **No shadowing of locals.** Declaring a local or parameter whose name is already a local or parameter in scope is an error (N0203), as in C#. Shadowing an *item* with a local is allowed. - **Lookup order** for a simple name: locals (innermost first), the package's items, the file's imports, `lib/core`, built-ins. Two imports that both provide a name are an error where the name is used (N0313), not where it is imported. For `a.b`: if `a` is a local or an item of the package or of `lib/core`, `.b` is a member as before; otherwise, `a` being a dependency (and so not an item, N0315), `a.b` is that dependency's item `b`, unless a local `a` is in scope there (ADR 0074 §2.10). `T.NAME`, with `T` a struct or an enum, is its associated constant (§10). - **Messages and C names** write an item of a dependency with its package's name, `geometry.Point`; the C backend gives it a prefixed name, `ceeT_8geometry_Point` (`c-backend.md` §2.1). ## 3. Bindings and mutability - `let x = e;` declares an immutable local and **requires** an initialiser (N0220). - `let _ = e;` (ADR 0072) binds nothing: `e`, any expression, is evaluated and its value destroyed at the end of the statement (a non-Copy value is consumed). `_` may be used any number of times; `var _` and `let _: T` are N0100. - `var x = e;` / `var x: T = e;` / `var x: T;` declare a mutable local. Without an initialiser the type is required (N0221), and the variable must be definitely assigned before every read (§7). - Without an annotation, a local's type is the type of its initialiser. An initialiser of type `void` is an error (N0224). - Parameters and `const` items are immutable. Assigning to them, or to a `let`, is N0211. - Assignment targets in M1 are locals only. Member targets (`a.b = …`) are N0216. ## 4. Integer literals - A suffixed literal (`7u8`) has the suffix's type. - An unsuffixed literal takes the **expected type** from context (§5) if that type is an integer type. With no integer expectation it defaults to `i32`. - The value must be representable in the literal's type (N0206). A literal directly under unary minus is checked as a negative value, so `-128i8` and `-9223372036854775808` (as `i64`) are valid. - **Character literals** (M11, G4a). `'k'` is an integer literal whose value is the character's Unicode scalar value (`grammar.md` §1.6, escapes included: `'\n'` is 10). It takes its type from context like an unsuffixed literal, so `c == 'k'` with `c: u8` compares bytes, and `'0'..'9'` is a range. With no integer expectation it is a `u32`. In an 8-bit type it must be ASCII (at most `0x7F`), since a larger scalar value is more than one byte in UTF-8 and would not equal any byte of the text (N0206). It is a pattern too (§15.2). CeeSafe has no `char` type: text is UTF-8 bytes (§22.2), and a character type is left for when text gains character iteration. ## 5. Expected types (bidirectional checking) Each expression is checked against an optional expected type: - local initialiser → the annotated type; assignment value → the target's type; argument → the parameter type; `return` value → the return type; `const` value → its declared type; `if`/`while` condition → `bool`; each value branch of an `if` or `match` expression → the expression's expected type (§15.3). - An expression's *actual* type must equal the expected type, unless it is *never* (N0205). - **Flexible operands.** An expression is *flexible* if it consists only of unsuffixed integer literals combined with unary `-`, parentheses and the arithmetic/bitwise binary operators, or (M11) if it is an `if` or `match` expression whose expression branches are all flexible (§15.3). Since M11 (G7), unsuffixed float literals combined the same way are flexible too; they take an expected `f32` or `f64` instead (§35.2). For a binary operator whose operands must have the same type: 1. if the operator's result type is the operand type and an integer type is expected, both operands are checked against it; 2. otherwise, if exactly one operand is flexible, the other operand is checked first (without expectation) and its type becomes the flexible operand's expected type; 3. otherwise both are checked without expectation (flexible ones default to `i32`). So `x + 1` with `x: u8` types `1` as `u8`, and `let y: u64 = 2 * 3;` types both literals as `u64`. ## 6. Operators All binary operators require both operands to have the **same type**. There are no implicit conversions. | Operators | Operand types | Result | Runtime behaviour (implemented in 1c) | |---|---|---|---| | `+ - *` | integer | same | overflow → panic | | `/ %` | integer | same | zero divisor → panic. `MIN / -1` → panic (overflow). `/` truncates toward zero, `%` has the sign of the dividend, and `MIN % -1` is `0` | | `& \| ^` | **unsigned** integer | same | | | `<< >>` | left: **unsigned** integer; right: any integer type | left type | amount `< 0` or `≥ bit width` → panic. `>>` is logical | | `== !=` | integer or `bool`; also floats, views, pointers, fieldless enums, slices and arrays (§16.2), `String` (its text), and a type `@derive(Equal)` gave equality (§28.7) | `bool` | | | `< <= > >=` | integer | `bool` | | | `&& \|\|` | `bool` | `bool` | short-circuit | | unary `-` | **signed** integer | same | `-MIN` → panic | | unary `!` | `bool` | `bool` | | **Explicit overflow behaviour** (M11, G4b; OD-6). The operators panic on overflow. Where a program wants another behaviour, every integer type has nine methods. Each takes one argument of the receiver's type, and a flexible literal adapts: | Method | Result | On overflow | |---|---|---| | `x.wrappingAdd(y)`, `x.wrappingSub(y)`, `x.wrappingMul(y)` | `T` | the result modulo 2^bits, in two's complement for signed types | | `x.checkedAdd(y)`, `x.checkedSub(y)`, `x.checkedMul(y)` | `T?` | `null` (otherwise `Some` of the result) | | `x.saturatingAdd(y)`, `x.saturatingSub(y)`, `x.saturatingMul(y)` | `T` | the nearest of `T`'s minimum and maximum | Two more take one argument the same way and cannot overflow (ADR 0063): `x.min(y)` and `x.max(y)`, of result `T`, the smaller and the larger operand (`x` when they are equal). On signed types (ADR 0063 §2.5), `x.abs()` is the magnitude as a `T`. It panics at `T`'s minimum, whose magnitude `T` cannot hold, exactly as `-x` does, reported at the call. `x.unsignedAbs()` is the magnitude in the unsigned type of the same width and never panics. Neither exists on unsigned types (N0234). **Bit methods** (ADR 0071 §2.3). On every unsigned type, `x.rotateLeft(n)`, `x.rotateRight(n)`, `x.wrappingShl(n)` and `x.wrappingShr(n)` take a `u32` amount (a flexible literal adapts), taken modulo the bit width, and are of type `T`; `x.leadingZeros()`, `x.trailingZeros()` and `x.countOnes()` are a `u32` (the width for a zero `x`). On a signed type they are N0234, as the bit operators are restricted to unsigned types. `x.byteSwap()` is on every integer type: the bytes in the other order, of type `T` (two's complement for signed types; a one-byte type is unchanged). None of them panics except `abs`. Their names are reserved on integer types: an `impl` for an integer type cannot define a method with one of these names (N0228). In C they are `static inline` helpers of `rt.h` with no undefined behaviour: a wrapping operation computes in the unsigned type of the same width and converts back with `cee_as_iN` (`c-backend.md` §6). Bitwise operators and shifts are restricted to unsigned types in M1. This avoids the implementation-defined and undefined parts of C's signed bit operations. Reinterpret with `truncate(x)`. Relaxing this later is backward compatible. A shift amount that is a flexible literal is typed as `u32`. Other mismatches are N0207. ## 7. Conversions - `e as T` is allowed only when **every** value of the source type is representable in `T` (N0208 otherwise): - identity; - `uN → uM` and `iN → iM` for M ≥ N; - `uN → iM` for M > N; - `bool →` any integer type (`false`→0, `true`→1). `isize`/`usize` use the target width. Floats (M11, §35.4): `f32 → f64`, and the integers that a float type holds exactly. - `truncate(e)`: `e` of any integer type, `T` any integer type. The result is `e` modulo 2^bits(T), reinterpreted in `T` (two's complement). It is always defined. Use it for narrowing and for signed↔unsigned reinterpretation. - `checkedTruncate(e)` and `saturatingTruncate(e)` (ADR 0074 §2.9): `e` of any integer type, `T` any integer type. `checkedTruncate` gives `T?`: `e`'s value when `T` holds it, else `null`. `saturatingTruncate` gives `T`: `e`'s value when `T` holds it, else `T`'s least value for an `e` below `T`'s range and its greatest for one above. Both are always defined. They also convert floats (§35.4). - The three spellings say what may happen to the value: `as` never loses information; `truncate` wraps; `checkedTruncate` and `saturatingTruncate` check or clamp (`checkedTruncate(n)!` for a `usize` `n`). ## 8. Calls and statements - A callee must be a package function, or one of the built-ins below (N0210 otherwise). Functions are not values in M1 (since M11 they are, §36.1). The argument count must match (N0209), and each argument is checked against its parameter type. - Built-ins: - `Console.write(x)` and `Console.writeLine(x)`: `x` is an integer, a `bool`, or a string literal. `writeLine()` with no argument writes a newline. Both return `void`. - `Process.exit(code: i32)`: has type *never*. - `truncate(e)`: see §7. Exactly one type argument (N0216 for other generic arguments). - `panic(message: StringView)` (ADR 0075 §2.8): has type *never*. It panics with the program's message: `ceesafe panic: at ::`. In a freestanding program the environment's `ceesafe_panic` gets `CEE_PANIC_EXPLICIT` ("explicit panic") and the message is not shown. - `assert(condition)` and `assert(condition, message)`: `void`. When the condition is false, they panic with "assertion failed" or the message. The message is evaluated only then. Assertions are always checked; no build leaves them out (`debugAssert`, below, is the one that release builds leave out). - `debugAssert(condition)` and `debugAssert(condition, message)` (ADR 0087 §2.5): `void`. They are checked in every build, like any call, but evaluated only when debug assertions are on: under `build -g` (unless `--no-debug-assertions`) or `--debug-assertions` (`build`, `run`), under `ceesafec test`, and in the interpreter (unless `run --interp --no-debug-assertions`). When they are off, nothing of the call is compiled: the C is `#if CEE_DEBUG_ASSERTIONS`, which the program defines as 0 or 1. A failure panics as `assert` does. Since the call may vanish, its arguments may change nothing that exists before it (N0360): - no assignment, `mut` argument or `borrow mut this` call on a place not declared inside the arguments, no move of one (`debugAssert(consume(v))`), and no way out of the arguments but their end: no exit from the function (`?`), no `break` or `continue` to a loop outside them, and no process exit (`Process.exit`), directly or through a call. A move of a place used after the call is N0360 at the call as well as N0250 at the use; - no write through a raw pointer or to a `static mut` (reading one is allowed); - no `sleep` or `suspend` effect (§32.1): no I/O, no `await`. Allocating is allowed, and so is freeing what the arguments allocated; - no call of a C function, even one declared `@effects()`, and no call through a closure, a function value or a `dyn` method. lib/core's atomic loads (`AtomicU32.load`, …) and `Cell.get` only read and are allowed; atomic stores, swaps, fetches and compare-exchanges, lock acquisition and `RefCell.read`/`write` are not; - a call is allowed when its callee's body, and everything it calls or drops, keeps these rules, writes through its own `borrow mut` parameters aside (the caller's `mut` argument is what is checked). Drops count: a temporary the arguments make is dropped inside the call, so its `drop` block is checked as a call. The checker infers this per function with §32.1's machinery, as an internal fourth fact that no `@effects` list names. To assert through something the rules refuse, bind the value to a local before the call, or put an `assert` in `if (cfg("NAME")) { … }` (ADR 0080 §3.3). - `replace(mut place, value) -> T` (ADR 0075 §2.11): puts `value` in the place and returns the old value. `swap(mut a, mut b)` exchanges two places of one type (N0205 otherwise). Each argument is `mut` and a place (N0249, N0247). Neither leaves a place moved-from, so both work for any type, a field of a struct with a `drop` block included: `replace(mut this.buffer, Vector.create())` is how a method gives a field up. - A package's own function or item of one of these names shadows the built-in (§2). - An expression statement must be a call (N0225). `1 + 2;` is rejected; `let _ = 1 + 2;` discards any value (§3). - `break`/`continue` must be inside `while` or `loop` (N0214). - `return;` only in a `void` function. `return e;` only in a non-`void` function (N0215), except that `return v;` of a unit local is `return;` (§20.4). Neither is allowed in a `never` function (§1), which does not return (N0215). ## 9. Control flow facts - **Definite assignment.** A local may be read only if it is assigned on every path from its declaration to the read (N0212). Compound assignment (`x += 1`) reads `x`. Code that is unreachable is not checked. M1 has no moves, so the analysis is a forward must-analysis over the structured statements; it moves to NIR in M2. - **Termination.** A statement *may complete* unless it is a `return`, `break` or `continue`, or a call of type *never*. It also may not complete if it is a block containing such a statement, an `if` with an `else` whose two branches both cannot complete, or a `loop` with no `break` that targets it. A `while` loop may always complete, whatever its condition: write `loop` for infinite loops. An `if` or `match` expression (§15.3) cannot complete when none of its branches can: its condition or scrutinee does not complete, or every branch is a block that cannot complete. - **Missing return.** If the end of a non-`void` function body may be reached, that is N0213. For a function returning `never` (§1), the end must be unreachable too: every path ends in a call of type *never* or a `loop` without `break`. ## 10. Constants - `const N: T = e;` where `e` is a constant expression: literals, other constants, and unary, binary, `as` and `truncate` applied to constant expressions. Calls are not constant (N0217), except `embed` and `embedText` (§10.1) and a constant view's `.bytes()` (below). `==` and `!=` on constant views compare their bytes (`"a" + "b" == "ab"` is `true`). - Constants are evaluated at compile time with the runtime semantics of §6. An operation that would panic at runtime (overflow, division by zero, bad shift) is a compile error (N0217). - A constant that depends on itself is N0218. - Uses of a constant are replaced by its value, typed as the constant's declared type. - A constant's type may be a `StringView` (ADR 0064 §2) or a lib/core `CStr` (§22.3, ADR 0087 §2.4), whose value is a string literal or a `c"…"`, or a `Slice` of static bytes (below); other struct types are N0216. - **Constant arrays** (C9, ADR 0014): `T` may be `[E; N]` with `E` an integer type, `bool`, `StringView` or `Slice` (static bytes, below), and `e` an array literal `[e1, …, eN]` or repeat `[v; N]` of constant expressions. A use is the whole array's value, like an array literal: it can be indexed and iterated, its `.length` read (ADR 0072), and bound to a local to be sliced or borrowed. A table to be sliced where it is named is a `static` instead (§39.2, ADR 0066). Arrays of other types, and of arrays, are N0216. - **Constant views** (ADR 0064 §2): `T` may be `StringView` (or an array of them, above), with string literals as the constant expressions. The value is static data, so a constant view holds no loans. The other reference types a constant may have are `CStr`, `Slice` and an array of `Slice`, all holding static data; any other is N0246. - **Constant bytes** (ADR 0087 §2.8, §9). A constant's `Slice` is static bytes: an `embed(path)` (§10.1), or the `.bytes()` of a constant view (a string literal, a `StringView` constant, an `embedText` or a constant `+`), which is CeeSafe's spelling of a byte-string literal (`const MAGIC: Slice = "\u{7f}ELF".bytes();`). Nothing else gives that type a constant value: another call, or `.bytes()` of a view that is not constant, is N0217 (N0338 in a static), and `as` cannot make one (N0208). - **Constant concatenation** (ADR 0087 §2.7). In a constant's value or a static initialiser (§39.4), `a + b` whose operands are both constant views (a string literal, a constant's name, an `embedText`, §10.1, or another such `+`) is a constant view: `a`'s bytes, then `b`'s. `A + B + C` folds left to right, and a constant of another package (`dep.PREFIX`) is a constant view like any. With a `c"…"` (or a `CStr` constant) as the right operand, the result is a `CStr` whose text is `a`'s then the literal's: a NUL byte in `a` is N0006 at the `+`, as inside a `c"…"`. A `CStr` on the left is N0207. An operand that is not constant is N0217 in a constant and N0338 in a static, and so is a `+` whose result is longer than 16 MiB (16,777,216 bytes), `embed`'s limit (§10.1). Anywhere else, `+` on views stays N0207: text is joined at run time by appending to a `String`. - `@export` on a constant makes it C data (§28.4). - **Associated constants** (ADR 0074 §2.7): `const NAME: T = e;`, with a visibility as a method's, may stand in a struct body or an inherent `impl` block (§21.1), so an enum has them too (`impl Mode { const DEFAULT: u32 = 25; }`, a block with constants only). They follow the rules above and are reached as `Type.NAME`, and from a dependent package as `dep.Type.NAME` when public (N0312 otherwise). A bare `NAME` does not reach one, inside the type's methods either. - A constant whose name is a field, method or variant of its type, or another of its constants, is N0228. - A generic struct or `impl` has none yet (N0216). - An associated constant takes no attributes (N0290). - An interface's constants (§21.1, ADR 0086 §2.4) become associated constants of each impl's target. - An array length in a type or a repeat may name a `usize` constant, top-level or associated (§16.1, ADR 0083 §2.7): `[u8; LENGTH]` and `[u8; Oid.LENGTH]` are accepted. ### 10.1 Embedded files (ADR 0087 §2.8) `embed(path)` is a file's bytes as a `Slice`, and `embedText(path)` the same bytes as a `StringView`, read when the package is checked: `static ROOT_CERTS: Slice = embed("certs/root.der");`, `const RUNTIME_JS: StringView = embedText("../js/runtime.js");`. They are built-in functions, which a package's own item of the same name shadows. - **The value** is static data, like a string literal's: it holds no loan, may be a constant's or a static's value (a constant or a non-`mut` static may have the type `Slice` for it, or an array of them; §10, §39.2), is used anywhere a literal is, and `embedText(…)` is a constant view that constant `+` joins (§10). A use of the constant is the same bytes. - **The path** is a string literal or a constant view (N0362 otherwise), relative to the directory of the source file that names it, with `/` separators on every system. Nothing in it is expanded. N0362 for: an empty path; an absolute one (`/…`, `\…`, `X:`); a `\`, a `:`, a `$` or a `%` anywhere; a `~` first. - **Where it may lead.** The compiler resolves the path fully, every symbolic link and `..`, as the system does (`realpath`; on Windows the final path of the opened file), and compares the result byte for byte with the real paths of its package's directory (the one holding `ceesafe.toml`) and of each of its `[embed] roots` (`tooling.md` §4.1). It must be one of them or below one: a symbolic link is followed only when where it leads is inside. A file given on the command line or to a server outside a package embeds only below its own directory. - **Code the user does not work on is confined.** A git or registry dependency, every package reached through one (its path dependencies, at any depth), and every package whose directory lies in the package cache (`$CEESAFE_HOME`, `tooling.md` §4.3), even as the root package (the language server going to a definition in a checkout, or `check` run there, with the path dependencies it reaches), embeds only inside that checkout: its roots, its own directory included, count only where their real paths lie inside it. - **The file read is the file checked.** The compiler opens the path first, without waiting (a FIFO cannot hang a check) and without taking a terminal, then checks the open file: its own real path (`F_GETPATH` on macOS, `/proc/self/fd` on Linux, `GetFinalPathNameByHandleW` on Windows) against the directories above, and that it is a regular file; then it reads at most 16 MiB + 1 bytes from it. A symbolic link swapped in between the checks and the read changes nothing. - **N0362 at the path** for: a path that names no file or leads outside these directories, with one message for both that names the path only as written (neither whether a file exists outside nor where a link leads is told); a directory, or anything but a regular file; a file of more than 16 MiB (16,777,216 bytes). A source with no file on disk (the MCP server's `files`, a library's sources) embeds nothing (N0362). - **Hard links are not detected.** A hard link inside the package to a file outside it is a name of that file inside the package, and its real path is inside: it is read. Making one needs write access to the package's directory and to the file, on the same volume; a package fetched by git or from the registry holds none (git stores no hard links). - **Letter case.** Real paths are compared byte for byte. On a file system that ignores case, a spelling that names the file but differs in case from the directory's real path is refused (it may be outside), never wrongly accepted; macOS and Windows give the real path in the stored case, so a package is unaffected unless it spells its own directories otherwise. - **`embedText`** also requires the bytes to be UTF-8 (N0361 at the path). A leading byte-order mark is kept. - **When files are read.** Every command that checks the package reads them: `check`, `build`, `run`, `test`, `doc`, the language server and the MCP server. Each compilation reads each file once (by real path), so all its uses agree, and keeps no copy afterwards: a file changed between `check` and `build` is read again by `build`, which compiles the new contents. `embed` is how a program depends on a file; what the file depends on is the build's business. ## 11. `main` If a function named `main` exists, it must take no parameters and return `i32` or `void` (N0219). Whether `main` is *required* is decided by `ceesafec build` (increment 1c). ## 12. Structs (M2a) ```ceesafe struct Point { x: i64; y: i64; fn sum(this) -> i64 => this.x + this.y; // method: `this` by value fn origin() -> Point => Point { x: 0, y: 0 }; // static function } ``` - **Declaration.** Top level only. Field names and member names (fields and functions) are unique within the struct (N0228). Field types are any non-`void` M1 type or another struct. A struct must not contain itself by value, directly or through other structs (N0227: infinite size). Recursive data needs indirection (`Box`, M3). Empty structs are allowed. - **Names.** A struct name is a package item (unique with functions and constants, N0201) and a type name. In expressions, `S.f(...)` calls the static function `f` of `S`. - **Copy.** In M2a every struct is a Copy type: its fields are Copy and `drop` does not exist yet. Assignment, argument passing and return copy the value. OD-7's rule ("Copy iff all fields Copy and no `drop`") takes effect in 2c. - **Literals.** `S { f1: e1, …, fn: en }` must initialise **every** field exactly once (missing: N0229, unknown: N0230, duplicate: N0231), in any order. Each `ei` is checked against the field's type. Initialisers are **evaluated in source order** (`intermediate-representation.md`), not in declaration order. - **Field access.** `e.f` where `e` has struct type `S` and `f` is a field of `S` has `f`'s type (unknown field: N0230; `e` is not a struct: N0234). An access chain `a.b.c` is a *place* if `a` is a local or `this`. - **Field assignment.** `a.f = e` and `a.f op= e` require the root local `a` to be a `var` (N0211). `this` is immutable in a by-value method. The root must be definitely assigned *as a whole* first (N0212): M2a has no partial initialisation. - **Methods.** A function in a struct whose first parameter is `this` is a method. `this` has the struct's type and is an immutable binding (a copy for Copy structs). It is called as `e.m(args)`, where `e` is evaluated first and passed as `this`. A static function (no `this`) is called as `S.f(args)`. Calling a method as `S.m()`, or a static function as `e.f()`, is N0233. `this` anywhere other than a method's receiver position or body is N0232. - **Private fields (C7c).** A field declared `private` (`private data: T;`) is accessible only in the source file that declares its struct (`language-spec.md` §5: "visible within the declaring type or file"), and in any instance of a generic one there: read, assigned, borrowed and given in a literal. Anywhere else, naming it is N0296. A struct with private fields can therefore only be made by code in its own file, so types that cooperate, like a lock and its guard, can share their fields. This is the one place where a visibility modifier takes effect within a package (§2). It lets a type keep an invariant that safe code cannot break, such as "the data is reached only through a guard" (`integrations/cosmoos/c7/sync.cee`). `public` and `internal` fields, and the default, stay visible throughout the package. - **Operators.** No operator applies to struct values in M2a (`==` included: N0207). `Console.write` cannot print structs (N0207). - **Layout.** Fields are laid out in declaration order with C's rules (`@repr(C)` by default, `memory-model.md` §2). The compiler computes size, alignment and field offsets from `TargetInfo`. The generated C checks them with `_Static_assert` (`c-backend.md` §2), so a disagreement with the real C compiler fails the build instead of miscompiling. ## 13. References (M2b) The algorithm that enforces these rules is in `lifetimes.md`. ### 13.1 Types - `borrow T` (shared) and `borrow mut T` (exclusive), where `T` is any non-reference, non-`void` type. `borrow borrow T` is rejected (N0246). At runtime a reference is a non-null pointer. - **Where references may appear (v0):** - parameter types; - `let` locals with an initialiser; - return types (with a source list, §13.5). They may **not** appear in struct fields (E3), constants, or `var` locals (N0246). Struct fields of reference type need lifetime parameters (OD-3). - `borrow T` is Copy. A `borrow mut T` value is never copied: every use reborrows it (§13.3). ### 13.2 Creating references - `borrow p` / `borrow mut p`, where `p` is a **place**: a local, `this`, a field path, or a path through a reference such as `r.f` (`(*r).f`). Borrowing anything else is N0247; bind the value to a local first. - `borrow mut p` requires `p` to be mutable: rooted at a `var` local, or reached through a `borrow mut` reference (N0248). A `let` binding, a parameter, a by-value `this`, and anything reached through a shared reference are immutable. That holds through a `borrow mut` or `SliceMut` stored where a shared reference leads: in `h.v` with `h: borrow Holder` and `v: borrow mut Vector`, the vector cannot be changed, as with Rust's `& &mut T`. - **Call sites.** - A `borrow T` parameter accepts a place or value of type `T`, borrowed implicitly. A value becomes a temporary that lives until the end of the statement. - A `borrow T` parameter also accepts `borrow T` or `borrow mut T`; the latter is coerced by a shared reborrow. - A `borrow mut T` parameter accepts `mut p`, where `p` is a mutable place of type `T`, or a `borrow mut T` value (reborrowed). - A `borrow T?` parameter accepts a place of type `T?` or `T`, and a `(borrow mut T)?` parameter `mut p` of one (§22.1, ADR 0074 §2.5). The `mut` marker is required for a plain place and is rejected where the parameter is not `borrow mut` (N0249). It makes mutation visible at the call site, as C#'s `ref` does. - **Receivers.** `fn m(borrow this)` and `fn m(borrow mut this)` declare reference receivers; `this` then has type `borrow S` / `borrow mut S`. In `e.m(…)` the receiver is borrowed implicitly, with no marker. For `borrow mut this`, `e` must be a mutable place or a `borrow mut S` value (N0248). - **A temporary as a receiver** (ADR 0073 §2.1). A receiver of a `borrow this` or `borrow mut this` method that is not a place, `make().view()` or `text.split(",").next()`, is evaluated into a temporary that lives until the end of its statement (§14.5), and the receiver borrows it; so is a field path on one (`holder().name.view()`). A result derived from it may be used within the statement; kept in a `let` used after it, it is N0243. A path through a reference is not copied: it is borrowed as a place. An explicit `borrow e` or `mut e` of a value is still N0247. ### 13.3 Using references - **Auto-deref.** Where a value of type `T` is needed (an operand, a by-value argument, an initialiser of type `T`, a return of type `T`, `Console.write`), a `borrow T` or `borrow mut T` operand is read through: `*r`. Field access `r.f` and method calls `r.m()` also look through references. - **Writes through references.** `r = v`, `r op= v` and `r.f = v` write to the referent. This requires `borrow mut` (N0211: "cannot assign through a shared reference"). A reference local itself can never be re-pointed. - **Reborrow of `borrow mut`.** Using a `borrow mut T` value as a call argument, an initialiser or a return value creates a new mutable loan of `*r`. The original reference is unusable while the new one is live, and usable again afterwards (B4). This holds for a field of type `borrow mut T` or `SliceMut` (`s.left`) as for a variable: it is reborrowed, not moved out of its struct, and inside a closure it is a mutable use of the struct. Its referent must be mutable from where it is used (§13.2): a field reached through a shared reference cannot be reborrowed mutably (N0248). - References support no operators of their own: `==` compares the referents (via auto-deref). - **`*r`** (ADR 0075 §2.14) reads the referent explicitly: `let id = *m.keyAt(0);` declares a `u64`. The referent must be a Copy type (N0251 otherwise: borrow it, or `replace` it); `*` on anything but a reference is N0205, and a raw pointer is read with `p.read()` (§27). `*r = v` and `*r op= v` are `r = v` and `r op= v`. ### 13.4 Borrow rules (summary of `lifetimes.md`) While a loan is live (the reference, or something derived from it, is used later), the borrowed place may not be: - written, mutably borrowed, or go out of scope, if the loan is shared; - accessed at all, if the loan is `borrow mut`. Diagnostics: N0240 (write), N0241 (conflicting borrow), N0242 (read while mutably borrowed), N0243 (out of scope or temporary dropped while borrowed). ### 13.5 Returning references - A function returning `borrow T` / `borrow mut T` with exactly one reference parameter (counting a reference receiver) returns something derived from that parameter. - Otherwise the return type names the sources: `fn longer(a: borrow T, b: borrow T) -> borrow(a, b) T`. Missing, unknown or non-reference sources are N0245, and so is a `borrow mut` result naming a shared parameter. After `borrow`, a `(` begins a source list only when it holds names separated by commas and a type follows the `)`; otherwise it begins a parenthesised type (M11, §22.1), so `borrow (T?)` is a reference to an optional and `borrow(a) (T?)` names a source. - The body may only return references derived from the named parameters (N0245). A reference to a local or temporary of the function is N0244. ## 14. Move types, destruction, `using` and `defer` (M2c) Normative for 2c. It implements `ownership.md` O1–O5 and R3. The NIR analyses are described in `intermediate-representation.md`. ### 14.1 Copy and move types (OD-7) - **Copy:** integers, `bool`, and `borrow T`. A struct is Copy iff it has no `drop` block and every field is Copy. - **Move types:** structs with a `drop` block or a move-type field, and `borrow mut T` (whose uses are reborrows, §13.3, so in practice it is never moved out of a variable). - **Needs drop:** a struct with a `drop` block, or one with a field that needs drop. Every type that needs drop is a move type. The converse does not hold. ### 14.2 `drop` blocks ```ceesafe struct File { fd: i32; drop { Sys.close(this.fd); } // `this` is `borrow mut File` } ``` - At most one `drop` block per struct (N0228). Inside it, `this` has type `borrow mut S`. The block cannot be called explicitly. It runs exactly once when a value of `S` is destroyed (O4), and the fields that need drop are destroyed **after** it, in declaration order. - Moving a field out of a value whose type has a `drop` block is N0252: the `drop` block needs the whole value. Moving out of `this` inside `drop` is impossible anyway (§14.3). ### 14.3 Moves - **Using a move-type place by value moves it**: - an initialiser or right-hand side; - a by-value argument or a by-value `this` receiver; - a struct-literal field; - a `return` value. The place is then uninitialised. Reading, borrowing or moving it again before it is reassigned is **N0250** ("use of moved value"); this is O2 and O3. - **`move p`** explicitly moves out of the place `p`, even for a Copy type. Afterwards `p` is uninitialised, which ends a value's use early. - **Partial moves** (O2): moving `p.f` out of a local is allowed if no struct on the path from `p` to `f` has a `drop` block. `p` is then partially initialised. `p.f` may be reassigned, after which `p` is whole again. Reading `p` as a whole, or borrowing it, needs all of it initialised (N0250). - **Not allowed:** - moving out of a place reached through a reference (N0251), because the referent must stay valid; this includes `this` in `borrow`/`borrow mut` methods; - moving out of a `using` binding (N0253). - **Moves and borrows:** moving a place while it is borrowed is a conflict, treated like a write (N0240). ### 14.4 Initialisation (replaces §9's first bullet) The analysis runs on NIR move paths (`intermediate-representation.md`): - A place must be **definitely initialised** to be read, borrowed or moved. If it may never have been assigned, that is N0212. If it may have been moved, that is N0250. - Assigning a field `p.f` requires the rest of `p` to be initialised (no piecewise construction of an uninitialised struct). Re-initialising a moved-out field is allowed. - If some enclosing struct of `p.f` has a `drop` block, that whole struct must be initialised, `f` included: such a struct is never partially moved (N0252), so a missing field means the whole value was moved. This also covers a struct whose only field is `f` (found by the mutation suite, M2d). ### 14.5 Destruction (O4) - A value that needs drop is destroyed exactly once, at the first of these: - the end of its variable's scope: the block end, a `break`/`continue` leaving the block, or a `return`; - the end of its statement, for a temporary. An `if` or `while` condition is its own statement: its temporaries are destroyed once it is evaluated, before the body runs, on every evaluation (M15 S2; before M15 they were never destroyed, #1); - being **overwritten** by an assignment, in which case the old value is destroyed first. - **Order:** locals in reverse declaration order; then a struct's `drop` block; then its fields in declaration order. By-value parameters are destroyed when the function returns, after the locals. - **Moved values are not destroyed.** A value that has been moved on *some* paths gets a runtime **drop flag**. A partially moved struct destroys only the fields it still has. - **`Process.exit` and panics do not run destructors or deferred statements** (panic = abort, OD-5). ### 14.6 `using` `using x = e;` declares a local that cannot be moved out of (N0253) and is destroyed at the end of its block, like any local (R3). It documents that the value's lifetime *is* the scope: a lock guard, an arena, a file. *Since M4b:* a `using` binding is **pinned, not immutable**. Its contents may be changed and it may be borrowed mutably (`arena.reset()`, `frame.enter()`). It cannot be assigned as a whole (N0211) or moved (N0253). This also lets scoped values that must be mutably borrowed, such as scratch frames, live in a `using` binding, since they cannot be `var`. ### 14.7 `defer` `defer s;` schedules the statement `s` to run when the enclosing block exits on any path: - the block end; - `break`/`continue` out of it; - `return` (after the return value is computed). Deferred statements and local destructions run in reverse order of their position, so a `defer` runs before the locals declared before it are destroyed, and after the locals declared after it. `s` may not contain `return`, `break`, `continue` or another `defer` (N0254). ## 15. Enums and `match` (M2d) ```ceesafe enum Shape { Empty, Circle(i64), Rect(i64, i64), } fn area(s: Shape) -> i64 { match (s) { Shape.Empty => { return 0; } Shape.Circle(r) => { return 3 * r * r; } Shape.Rect(w, h) => { return w * h; } } } ``` ### 15.1 Declarations and values - `enum E { V1, V2(T1, …), … }` at the top level. Variants are unique (N0228) and have zero or more positional payload types: any non-reference, non-`void` type (E3 applies to payloads too). An enum must not contain itself by value (N0227). - **Values.** `E.V` for a variant without payload; `E.V(e1, …)` with exactly the payload arity (N0209). Each `ei` is checked against its payload type. - **Equality** (ADR 0072). For an enum none of whose variants has a payload (a *fieldless* enum), `a == b` and `a != b` compare the variants; both operands have that enum type and are only read. An enum with payloads is N0207 (compare with `match`), and so are `<`, `>` and the other orderings on any enum. - **Copy and drop.** An enum is Copy iff every payload type is Copy. It needs drop iff some payload needs drop; destroying it destroys the payloads of the active variant. Enums have no `drop` block (v0). - **Layout.** - A tag (`u8` for ≤ 256 variants, else `u16`/`u32`), then a union of the variants' payloads, each laid out like a struct. - Size and alignment follow C's rules for `struct { tag; union { … }; }`. The generated C asserts them. - CeeSafe does not reorder or niche-optimise (v0). ### 15.2 `match` ```text MatchStmt = 'match' '(' Expr ')' '{' Arm* '}' Arm = Pattern Guard? '=>' Block ','? Guard = 'if' '(' Expr ')' -- ADR 0085 §2.2 Pattern = '|'? AltPattern ('|' AltPattern)* -- or-patterns: ADR 0085 §2.3 AltPattern = (Identifier '@')? PrimPattern -- `name @ P`: ADR 0085 §2.5 PrimPattern = '_' | Literal | 'true' | 'false' | 'null' | '(' Pattern ')' | 'var'? Identifier -- a constant in scope, else a binding: ADR 0085 §2.6 | Literal '..=' Literal | Literal '..' Literal -- ranges: ADR 0085 §2.4 | QualifiedName ('(' Payload (',' Payload)* ')')? -- E.V, E.V(x, _), Ok(E.A(x)) (M11) | StringLiteral -- on a StringView or Slice: ADR 0085 §2.7 | '[' (SliceItem (',' SliceItem)* ','?)? ']' -- slices and arrays: ADR 0085 §2.7 SliceItem = Pattern | '..' | Identifier '@' '..' -- at most one '..' (N0100) Literal = IntLiteral | '-' IntLiteral | CharLiteral Payload = Binding | Pattern -- a nested pattern: M11 (G1) Binding = Identifier | 'var' Identifier | '_' -- 'var': M3c (§18.4) ``` A payload that is a single identifier is a binding, unless it names a constant (below); `Some(…)`, `Ok(…)`, `Err(…)` and `E.V…` there are patterns. - This section's `match` is a **statement**; §15.3 has the expression form. Arms are tried in order; the first that matches, and whose guard holds, runs its block. - **Guards** (ADR 0085 §2.2). `P if (g) => …`: the guard `g` is a `bool` (N0205 otherwise), parenthesised like every condition (§9). For one arm: the pattern is tested; if it matches, its bindings are made, then the guard is evaluated; if it is `false`, the next arm is tried. - The guard sees the arm's bindings. A binding that copies (a Copy payload) or refers (a reference scrutinee) holds its value in the guard. A binding that will move (a non-Copy payload of a consuming match, §18.4) is read in place: the move happens only once the guard is `true`, so an arm whose guard fails has moved nothing. - From the start of the pattern test to the end of the guard, the scrutinee carries a shared loan. The guard may read the scrutinee and the bindings, call functions, and read and write other places. It may not move, assign or `mut`-lend the scrutinee, a place inside it or a binding, and it may not `await` (the loan would cross a suspension): N0355. This holds anywhere in the guard: in its blocks, loops (over ranges or iterators) and `defer` blocks, and in a closure it makes, which `mut`-lends what it captures mutably (§36.2). - A guarded arm never counts toward exhaustiveness: its pattern is not added to the usefulness matrix below. It is still checked for reachability: if earlier unguarded arms match every value it does, it is N0257. When the values a match misses are matched only by guarded arms, N0256 says so ("arms with a guard do not count"). - **Scrutinee types:** - an enum: variant patterns; - an integer: literal and range patterns of that type; - `bool`: `true`/`false`; - or a reference to one of these; - a `StringView`: string literal patterns; a `Slice` (or `SliceMut`), an array `[T; N]` or a reference to one: slice patterns, and on a `Slice` string literal patterns too (ADR 0085 §2.7). - **Exhaustiveness** (N0256): enum matches must cover every variant or end with `_`. Integer matches must cover every value of the type with their literals and ranges, or end with `_` (ADR 0085 §2.4). `bool` matches need both values or `_`. An arm that can never match (a repeated variant or literal, a value an earlier range holds, or anything after `_`) is N0257. - **Ranges** (ADR 0085 §2.4). `lo..=hi` matches `lo` to `hi`, both included; `lo..hi` excludes `hi`, as range expressions do. Both bounds are integer or character literals, typed by the scrutinee (or payload) as §4 types a literal, so `'a'..='z'` matches bytes of a `u8`. A range that matches no value (`'z'..='a'`, `5..5`) is N0357. Ranges are for integers only: `bool`, floats and other types take none (N0205; a `bool` bound is N0108). - **String literal patterns** (ADR 0085 §2.7). On a `StringView`, `"get"` matches equal bytes, as `==` does (§22.2). On a `Slice`, the same literal matches its UTF-8 bytes: the literal is typed by the value, as `'k'` is, so a byte string is written `"cjs"`. A string pattern on any other type is N0205. String patterns never cover their type (a match on a `StringView` needs `_` or a binding); a repeated one is N0257. - **Slice patterns** (ADR 0085 §2.7). `[]`, `[a, b]`, `[first, ..]`, `[.., last]`, `[x, rest @ ..]`, `['"', inner @ .., '"']` match a `Slice`, an array or a reference to one, and a payload that is a slice or a `StringView` (`Some([a, ..])`, `Some("yes")`; also an or-pattern of these, `Some("x" | "y")`, or `name @` one, `Some(s @ "zz")`, which binds the view itself). Each item is a pattern on one element; `..` matches any number of elements, at most once per pattern (N0100). Without `..`, the pattern matches exactly as many elements as it has items; with it, at least that many. `rest @ ..` binds the elements `..` matched as a `Slice` of the same region. - **Bindings.** A slice is a view, so a slice pattern never moves: an element binding of a Copy type is a copy, and of any other type a `borrow T` (which cannot be `var`). On an array held by value, the elements bind only by copy. - **Arrays.** An array's length is part of its type: a pattern of more items than it has, or without `..` of a different count, is N0205. A `[T; N]` whose N is a const parameter is matched as a slice in generic code, and in an instance an arm of a count N cannot have is never taken, not an error (§20.5). - **Bindings.** A variant pattern binds each named payload to a new immutable local in the arm: - **Scrutinee of type `E` (a value):** the binding is a *copy* of the payload. Since M3c a non-Copy payload is moved out, and the match consumes its scrutinee (§18.4). - **Scrutinee of type `borrow E` / `borrow mut E`:** the binding is a `borrow T` / `borrow mut T` to the payload, and the borrow checker treats it like any reborrow of the scrutinee. - `_` binds nothing. - The scrutinee is evaluated once. When it is a place it is not moved (only Copy payloads are read). A temporary scrutinee lives until the end of the `match` statement. - **Nested patterns** (M11, G1). In a payload's position, a pattern instead of a binding matches the payload itself: a variant pattern on an enum payload (`Ok(E.A(x))`, `Some(Some(_))`), a literal on an integer or `bool` payload (`Some(3)`), or `null` on an optional payload (`Ok(null)`). Each is checked against the payload's type exactly as a whole `match` is checked against the scrutinee's, at any depth. - **Order.** Arms are tried in order. Within an arm, the outer variant is tested first, then each nested pattern, left to right and depth first. Testing only reads tags and values: an arm that does not match moves nothing. - **Bindings** inside nested patterns follow the rules above. On a value scrutinee they copy, or move (§18.4). On a reference scrutinee they are references to the nested payload. - **Or-patterns** (ADR 0085 §2.3). `A | B` matches when either alternative matches; alternatives are tested left to right. A leading `|` is allowed. An or-pattern may stand anywhere a pattern may, `Some('a' | 'b')` included. Every alternative binds the same names, each with the same type and the same `var`-ness (N0356 otherwise); the arm's body sees one binding per name, made by whichever alternative matched. For usefulness each alternative is a row of its own; an alternative that earlier arms or earlier alternatives already match is N0257, at that alternative. With a guard, each alternative that matches, left to right, binds and runs the guard; the first `true` wins. - **Constants and bare names** (ADR 0085 §2.6). A bare identifier (no `.`, no parentheses), at the top of an arm or at any depth, is a *constant pattern* when it names a `const` in scope that no local hides: it matches that value and counts as that literal for coverage. The constant must be an integer or `bool` (N0358 otherwise) of the value's type (N0205). Otherwise it is a *binding* of the value at that position, at the top level too: `n => …` binds the scrutinee (a copy, a move that consumes it, or a reference, as a payload binding is), and `var n` a mutable one. `Some` on an optional, and `Ok` or `Err` on a `Result`, written alone at the top, still name their variant with its payload unlisted. A qualified name without parentheses (`Type.NAME`) is a variant pattern when it names a variant, and a constant pattern when it names an associated constant (ADR 0074 §2.7) or a constant an `impl` of an interface gives the type, its own or the interface's default (ADR 0086 §2.4): `A.CODE =>`. `Self` may start the name (`Self.Idle`, `Self.Busy(n)`, `Self.TAB`) where it names a struct or enum (ADR 0086 §2.2). Read through a type parameter (`T.NAME`, or `Self.NAME` in an interface's default method) it is known only per instance, and is N0358: test it in a guard. So is a bare name that names a const parameter in scope (§20.5, ADR 0083 §2.3), which is not a binding either. A misspelt constant becomes a binding that matches everything, so the arms after it are N0257. - **`name @ P`** (ADR 0085 §2.5) binds the value that `P` matched, as a payload binding would (a copy, a move or a reference, by the rules above), and tests `P`; it does not change coverage. `@` binds tighter than `|`: `d @ ('0' | '1')` needs the parentheses. A value moves at most once: when `name` binds by move (a non-Copy value of a consuming match, §18.4), no binding under `P` may also move, so `e @ Err(inner)` with a non-Copy payload is N0250 with its own message ("moves the value that `inner` also moves"); bindings under it that copy are made first, from the value, and on a reference scrutinee every binding is a reference. - **A payload that is itself a reference** (`borrow E`) takes only a binding or `_`, not a nested pattern. Matching through it is a separate `match` on that reference (N0205 otherwise). A slice or `StringView` payload takes string and slice patterns, which test its elements (ADR 0085 §2.7). - **Exhaustiveness and unreachable arms** are decided over the whole patterns. A match is exhaustive when every value of the scrutinee's type matches some arm: N0256, whose message names one value that matches none, such as `Ok(E.B)`. An arm is unreachable when every value it matches is matched by an earlier arm: N0257. The decision is the usefulness algorithm of pattern-matching compilers (Maranget, *Warnings for pattern matching*, 2007), with each enum's variants, `true`/`false`, `null`/`Some` and `Ok`/`Err` as the constructors. For an integer position the constructors are intervals of its values (ADR 0085 §2.4, amending M11's rule that integer literals never cover their type): a literal is a one-value interval, a range its own, and the type is split into the intervals the arms' bounds define, so literals and ranges that together hold every value cover the type (`0x00..=0x7F` and `0x80..=0xFF` cover a `u8`). N0256's witness is then a value no arm holds: the lowest, written as a character literal for a printable byte of a `u8` (`'{'`) and in decimal otherwise (`128`); a position no arm constrains is `_`. Strings are constructors of which there are infinitely many, so they never cover a `StringView` (ADR 0085 §2.7). The constructors of a slice are its lengths: `[p1, …, pk]` covers length k, and a pattern with `..` and k items every length from k on. The check splits the lengths into 0, 1, …, L - 1 and "L or more", L being past the longest pattern without `..` and at least the longest prefix plus the longest suffix of those with it (Rust's algorithm); an array has the one length N. A string on a `Slice` is the slice of its bytes. N0256's witness names a length and its elements: `[]`, `[_, _, ..]`. ### 15.3 `if` and `match` expressions (M11, G5) ```ceesafe let sign = if (x < 0) -1 else if (x == 0) 0 else 1; let name = match (shape) { Shape.Circle(_) => "circle", Shape.Square(side) => if (side > 10) "big square" else "square", Shape.None => { return; }, }; ``` ```text IfExpr = 'if' '(' Expr ')' Branch 'else' (IfExpr | Branch) MatchExpr = 'match' '(' Expr ')' '{' (ValueArm (',' ValueArm)* ','?)? '}' ValueArm = Pattern Guard? '=>' Branch (* Guard: §15.2, ADR 0085 §2.2 *) Branch = Expr | Block | ValueBlock ValueBlock = '{' Statement* Expr '}' (* the last Expr without ';', ADR 0073 §2.3 *) ``` - **Where.** In expression position (an initialiser, an operand, an argument, a `return` value, …). An `if` or `match` that begins a statement is the statement form (§9, §15.2), as before. An `if` expression needs its `else` (N0100 otherwise). - **Branches.** A branch is an expression, the value of the whole when that branch is taken, or a block that cannot complete (§9): one that ends in `return`, `break`, `continue` or a call of type *never*, such as `{ return; }`, `{ Process.exit(1); }` or `{ die("bad number"); }` with `fn die(m: StringView) -> never` (§1). A block that can complete has no value: N0235. - **Value blocks** (ADR 0073 §2.3). A block branch may end with an expression written without `;`: the branch's value, typed as an expression branch is. An `if` (with its `else`) or `match` that ends a value block, directly before its `}`, is read as an expression. Only branches of `if` and `match` expressions have value blocks: in a function body, a statement-form `if` or `match` or a plain block, a final expression without `;` is N0100. The value is moved out before the block's locals and temporaries are destroyed; a reference to one of them as the value is N0243. - **Extent.** An expression branch extends as far right as an expression can: `if (c) 1 else 2 + 3` is `if (c) 1 else (2 + 3)`. Write parentheses to use an `if` expression inside a larger one: `(if (c) 1 else 2) + 3`. Value arms are separated by commas; a block arm needs none. - **Type.** - With an expected type (§5), every expression branch is checked against it, and the whole has that type. - Without one, the first expression branch that is neither flexible (§5) nor `null` is checked first, and its type is the type of the whole; the other branches are checked against it. If every branch is flexible, the type is its literals' default: `f64` if any is a float literal (§35.2), else `i32`. A branch of type *never* (and every block branch) fits any type. If no branch is an expression, the whole has type *never*. - **Evaluation.** The condition (or scrutinee) is evaluated once, then exactly one branch. `match` expressions follow §15.2 in everything else: the patterns, exhaustiveness (N0256) and unreachable arms (N0257), bindings by copy, move or reference, and consuming a value scrutinee (§18.4). - **Ownership.** A branch's value is moved (or copied) into the result. A move in one branch leaves the place maybe-moved after the whole, as after an `if` statement (§14.4). A reference produced by a branch carries that branch's loans, so the result carries the loans of every branch that can produce it (`lifetimes.md`). - **Temporaries** made while evaluating a branch end when the branch ends, as at the end of a statement (`lifetimes.md` §6). A reference to one cannot be the branch's value (N0243). ### 15.4 `e is P`: patterns as conditions (ADR 0085 §2.8) ```text IsTest = Operand 'is' Pattern -- at the precedence of '==', non-associative; 'is' is contextual (grammar.md §1.3) ``` One `bool` operator stands for what other languages write as `matches!`, `if let`, `while let` and let chains: ```ceesafe let isDigit = c is '0'..='9'; if (cache.find(key) is Some(entry) && entry.fresh) { use(entry); } while (parts.next() is Some(part)) { handle(part); } ``` - **Meaning.** `e is P` is the test of `match (e) { P => true, _ => false }` (§15.2): `e` is evaluated once, any pattern of §15.2 may follow, and testing only reads. After `is` the pattern grammar reads as far as it can, so `|` there is an or-pattern. The scrutinee's type is one a `match` takes (N0207 otherwise). - **Bindings** are in scope only where the test is known true: in the later operands of the same top-level `&&` chain, and in the then-branch of the `if` (statement or expression) or the body of the `while` whose condition is that chain. A pattern with bindings anywhere else (under `||` or `!`, as a whole `let` initialiser or an argument, as the last operand of a chain outside a condition) is N0359. Parentheses around an operand of the chain do not change this. Bindings copy, move or refer as a `match`'s do (§15.2). - **Moves happen only on success.** `is` moves nothing when the test fails. When it succeeds and its pattern binds a non-Copy payload of a value scrutinee, the scrutinee is consumed at that point, as a consuming `match` consumes it (§18.4: unbound payloads go to hidden locals). The move checker sees a place scrutinee moved on the true path only: `if (e is Some(v)) { A } else { B }` leaves `e` whole in `B`. A later `&&` operand that is false does not undo the move. - **Drop points.** The bindings (and hidden locals) live to the end of the then-branch, or of the `while` body for that pass; `break` and `continue` end them. When a later operand of the chain is `false`, those made so far are destroyed at once, in reverse order, before the `else` branch runs or the loop exits: in `x is Some(v) && g`, a false `g` destroys `v` there. - **Temporaries.** The condition's temporaries end with the condition, before the then-branch, the `else` branch or the loop body runs (§14.5), unless a binding borrows from them: a test of a reference or view that is not a place (`c.read().data().view() is [first, ..]`), or `rest @ ..` on an array that is not a place (a field of a temporary, `make().arr`, is the temporary's, not a place). Then they live to the end of the then-branch or of this pass of the body, and still never into the `else` branch or the next test (Rust 2024's `if let` scope, applied to `while` too). So a `RefCell` guard (§41.3) used to copy a value out is gone before the body, and a `.write()` in the body or in `else` does not panic. A `while` evaluates the scrutinee again on each pass. - Outside a condition, a chain's bindings are in scope in its later operands only: `let big = x is Some(v) && v > 3;`. They end at the end of that statement, with its temporaries: in `let ok = c.tryRead() is Some(g) && g.data() == 5;` the guard `g` is released before the next statement, so a `tryWrite()` there succeeds. ## 16. Arrays, slices and `for` (M2d) ### 16.1 Types - `[T; N]` is a fixed array of `N` elements. `N` is an integer literal (no suffix or `usize`), a `usize` constant (`LENGTH`, or an associated one, `Oid.LENGTH`; §10), or a `usize` const parameter in scope (§20.5; ADR 0083 §2.7), with 1 ≤ N ≤ 2^48 − 1 (N0259), and the array is at most 2^31 − 1 bytes. Any other expression is N0259, and one over a const parameter (`[u8; N + 1]`) N0353. It is laid out as `N` consecutive `T`s: size `N × size(T)`, the alignment of `T`. The same holds for the `N` of a repeat `[v; N]` (§16.2). - **v0 restriction:** `T` must be a Copy type, so arrays are Copy and never need drop. Arrays of move types need element-wise move tracking (future). Slices may have move-type elements since M6b (§21.3). - Otherwise arrays may appear anywhere a value may: locals, parameters, results, struct fields, enum payloads and other arrays. Constants of array type are not supported yet (N0216). - `Slice` / `SliceMut` are **borrowed views** `(pointer, length)` of a run of `T`s in an array or another slice. They follow every rule for references (§13), with the same scoping: - only in parameters, `let` locals with an initialiser, and return types with a source list; - never in fields, payloads, array elements, constants or `var` locals (N0246); - `Slice` is Copy, and each use of a `SliceMut` reborrows it; `SliceMut` coerces to `Slice`; - a slice holds a loan of the whole array (or slice) it was taken from (`lifetimes.md`); - a function returning a slice has no `borrow(...)` source list: it must have exactly one reference (or slice) parameter, which the result is derived from (§13.5's default rule); - the element type follows the array rules: Copy, never a reference. - Internally, `Slice` is `borrow [T]` and `SliceMut` is `borrow mut [T]`, with `[T]` an unsized array type that only exists behind a slice. That is why every reference rule applies. ### 16.2 Values - `[e1, …, en]`: all elements have the same type (the expected element type, or the first element's), n ≥ 1. - `[v; N]`: N copies of `v`. - `[]` (ADR 0075 §2.9) is the empty slice, where a `Slice` or `SliceMut` is expected (an initialiser, an argument, a return): no array is behind it, and its address is `T`'s alignment, never null and never read. Elsewhere it is N0259. There is no zero-length array type: C has no zero-sized object, so `[T; 0]` would be laid out differently in C and in CeeSafe (N0259). - **An array's methods are its slice's** (ADR 0075 §2.10): `a.m(…)`, for a method an array does not have, is `a[..].m(…)`, mutably for the methods that write or move elements (`writeLE`, `copyFrom`, `fill`, `swap`, `sortBy`, `splitAtMut`, …), which need a mutable place as `borrow mut a[..]` does. - `a.length` is a `usize`: the constant `N` for arrays, the stored length for slices. For an array, `a` must be a place (a variable, field or element), because the constant would skip evaluating it (N0216), unless there is nothing to evaluate: a `const` array (§10) or a literal of constants (ADR 0072). - **Equality** (ADR 0072). `a == b` and `a != b` compare two arrays of the same type `[T; N]`, or two slices of the same `T` (a `SliceMut`, and a vector's elements, lent as `Slice`): equal lengths and equal elements in order. `T` is an integer type, `bool` or a fieldless enum (§15.1); other element types, floats included (`NaN != NaN`), are N0207, and so are arrays of different lengths. `Slice == StringView` is N0205: compare `s == t.bytes()`. The operands are only read. The comparison is not constant-time. ### 16.3 Indexing and slicing - `a[i]`: - `a` is an array or slice place, or a reference to an array; - `i` is a `usize` (a flexible literal becomes `usize`); - `i ≥ a.length` **panics** ("index out of bounds", reported at the index expression); - `a[i]` is a place: it can be read (a copy), assigned (if mutable: an array rooted at a `var` or reached through `borrow mut`, or a `SliceMut`), and borrowed. - `a[lo..hi]`, `a[lo..]`, `a[..hi]`, `a[..]` give a `Slice` of elements `lo` up to but not including `hi` (default `0` and `a.length`). `lo ≤ hi ≤ a.length` is checked; otherwise the program **panics**. - `borrow mut a[lo..hi]` gives a `SliceMut` and needs `a` to be mutable. As a call argument for a `SliceMut` parameter, `mut a[lo..hi]` means the same. - Slicing a value that is not a place (ADR 0073 §2.1): an array value, or a view or slice value, is held in a temporary until the end of the statement and sliced there (`digest()[0..4]`, `s.view()[1..3]`); a slice of an array temporary cannot be kept after the statement (N0243), a slice of a view keeps the view's loans. A place reached through a reference is sliced as a place. `.length` of an array value is its constant length, the value still evaluated. Anything else that is not a place is N0247. - Evaluation order: the array place (with its own indexes), then `lo`, then `hi`. In an assignment `a[i] = v`, the target's indexes are evaluated and checked before `v`. - The borrow checker treats all elements of an array as **overlapping** (no index reasoning in v0). Borrowing `a[0]` and then writing `a[1]` while the borrow is live is rejected. ### 16.4 `for` The header is parenthesised, like `while` and `match`, so a name before the body is never read as a struct literal. - `for (i in lo..hi) { … }`: `i` takes each value from `lo` up to but not including `hi`. Both bounds are evaluated once, have the same integer type (flexible literals adapt), and `i` is immutable in the body. - **Index ranges** (ADR 0063). When both bounds are flexible literals, the range is `usize` if the body indexes with `i`, and the literals' default (`i32`, §5) otherwise. The body indexes with `i` where `i` is an index (`a[i]`) or a slice bound (`a[lo..i]`), or an operand of `+ - * / %` that is one (`a[i * w + x]`), in the body's statements, nested blocks and closures. Inside a call, cast, comparison or condition in an index it does not count (`a[truncate(i)]` keeps `i32`). The body also indexes with `i` where it passes `i` itself as an argument for a `usize` parameter (ADR 0072), of a callee known before the body is checked: a function named directly, a struct's static function (`Table.at(i)`), or a method on `this`, a parameter or an earlier local (`list.host(i)`). The decision is syntactic and made before the body is checked; locals cannot be shadowed (N0203), so the name means `i` throughout. - `for (x in seq) { … }`: `seq` is an array place, a slice, a reference to an array, or an iterator (below). `x` is a copy of each element from index 0 upward. The element is read when the loop reaches it, and the length is read once at the start. - **Non-Copy elements** (M11, G2): when the element type `T` is not Copy, `x` is a `borrow T` to the element, in place, instead of a copy (which could not be made: N0251). This is the rule of a `match` on a reference (§15.2). The borrow checker treats it as a shared borrow of that element for as long as `x` is used. While it lives, the sequence cannot be changed (§13.4, §23.1). A Copy element is still copied, as before. - `break` and `continue` behave as in `while`. `continue` proceeds to the next element. - **Iterators** (ADR 0074 §2.8). `for (x in e) { … }`, where `e`'s type is a struct, or a `borrow mut` of one, with a method `next(borrow mut this) -> T?` (its own or an impl's, with no other parameter and no type parameter), calls `next` until it returns `null`: - `e` is evaluated once, before the first pass, into a hidden local of the loop's header. That local and `e`'s temporaries live until the loop exits (ADR 0073 §2.1), and are destroyed then, the iterator first. A value `e` moves into the loop; a `borrow mut` `e` is reborrowed, and the iterator keeps its state after the loop. - Each pass borrows the iterator mutably only for the call to `next`, whose temporaries end before the body. The body may use what the iterator borrows as a shared borrow (`text` in `for (part in text.split(",")) { … }`), but not change it while the iterator lives (N0241). - On `Some(v)`, `x` takes `v` (moved: `x` owns a non-Copy `T`), with type `T` and the region of `next`'s result. - `continue` proceeds to the next call of `next`; `break` leaves the loop. - No interface is needed: the method's shape is the rule. A struct without such a `next`, or whose `next` has another shape, is N0205, naming the expected signature. A shared reference to an iterator is N0248. - **`Iterator`** (ADR 0086 §2.7). lib/core declares `public interface Iterator { fn next(borrow mut this) -> T?; }`, and lib/core's iterators (`TextSplit`, `ByteSplit`) implement it. Beyond the structural rule, which stays, `for (x in e)` accepts an `e` of type `I` (or `borrow mut I`), where `I` is a type parameter whose bound declares such a `next` (`I: Iterator`), a `borrow mut dyn Iterator`, and a mutable place of `Box>`, iterated in place through `borrow mut` of its contents (N0248 if the place is not mutable). `x` has type `T`; the header, borrowing and destruction rules are the ones above. The item is a parameter, not an associated type: callers fix it directly (`I: Iterator`), and `dyn Iterator` is dyn-compatible. There is no `IntoIterator` and no adapter library. - The loop variable is immutable. It has the bounds' type for a range, and the element type for a sequence (or `borrow T`, above), and `next`'s payload type for an iterator. Like every local, it cannot shadow another (N0203). ### 16.5 Bulk operations (M9b) Two methods write a whole slice in one operation. A program computes the same thing with them as with the element loop they replace; they exist so that the copy costs one check instead of one per element. ```ceesafe fn copy(out: SliceMut, to: usize, src: Slice, from: usize, n: usize) { out[to..to + n].copyFrom(src[from..from + n]); // two range checks, one length check, one copy } ``` - `d.copyFrom(s)`, with `d: SliceMut` and `s: Slice` (a `SliceMut` coerces): if `s.length != d.length` the program **panics** ("slice lengths differ", reported at the call) and writes nothing. Otherwise each `d[i]` becomes `s[i]`, as if `s` were first copied to a temporary. In safe code the two cannot overlap, because `s`'s loan would conflict with `d`'s (N0241, N0242); if `unsafe` code makes them overlap (§27.3, `Slice.fromRaw`), the result is still defined. - `d.fill(v)`, with `v: T`: every element of `d` becomes `v`. Nothing is checked. - `T` must be Copy (N0259), because both methods copy elements. - The receiver is a `SliceMut` (a parameter, a local or a call result; reborrowed, like any use of a `SliceMut`), or a slicing expression `a[lo..hi]` in any of its forms, which is then taken as `borrow mut a[lo..hi]`. That needs `a` to be mutable (N0248), as `mut a[lo..hi]` does for a `SliceMut` argument. A `Slice` receiver is N0211. Arrays and vectors are written through a slice: `a[..].fill(0)`, `v.viewMut().fill(0)`. - Evaluation is left to right: the receiver (its place, bounds and range check), then the argument, then the length check, then the writes. The receiver's loan starts before the argument is evaluated: these calls are not two-phase (B5 applies only to `borrow mut this` methods of a place). So the argument cannot read the destination: `s.fill(s[0])` is N0242, and `let v = s[0]; s.fill(v);` is the fix. - The result is `void`. - Not provided in M9b: a copy within one slice whose ranges may overlap, comparisons, searches. - **`d.swap(i, j)`** (M11 G8c), with `i, j: usize`: exchanges elements `i` and `j` of a `SliceMut` (or of a vector, which lends its elements mutably). `T` may be any element type, Copy or not: the elements are moved, not copied. Both indices are checked first (panic "index out of bounds", reported at the call), and nothing changes if either is out of range. `i == j` changes nothing. Effects: none. Diagnostics: N0259 (an element type that is not Copy, an invalid length, or a too-large array). The other errors reuse N0205, N0211, N0216, N0230, N0246, N0247 and N0248 (`diagnostics.md`). ### 16.6 Bytes and text (ADR 0071) Methods of byte slices (`Slice`, `SliceMut`, a `Vector`'s elements), of `StringView` and of a `String`'s text, written in lib/core (`bytes.cee`, `text.cee`, `numbers.cee`) and reached like `sortBy` (§36.4). **Byte order** (§2.2), on byte slices and byte vectors only (a `StringView` reads through `s.bytes()`; N0200 otherwise): - `b.readBE(at)` and `b.readLE(at)` (`at: usize`) are the `T` in the `size(T)` bytes at `at`, big- or little-endian, in two's complement for signed `T`. `T` is a fixed-width integer type, `u8` to `u64` or `i8` to `i64` (N0207 otherwise, `usize` and `isize` included); a missing type argument is N0265. - `d.writeBE(at, x)` and `d.writeLE(at, x)` write `x` there. The receiver is a `SliceMut`, a mutable vector's elements, or a slicing expression taken mutably (N0211 for a `Slice`). `x`'s type gives the width: a type argument (`writeBE(at, 5)`) or the value's own type; a literal without a suffix and without a type argument is N0265. So is a character literal without one: it is a `u32` code point, so `'P'` alone would write four bytes (`pushBE('P')` writes one; 0.15.2). They are not two-phase, like `copyFrom` (§16.5). - `v.pushBE(x)` and `v.pushLE(x)` on a `Vector` append `x`'s bytes with one growth: `Result`, the vector unchanged on failure. The width is chosen as for `writeBE`. They are two-phase (B5), like `push`. - A read or write checks `at < length` and `at + size(T) - 1 < length` at the call, before anything is read or written, and **panics** "index out of bounds" there otherwise. **Searching and trimming** (§2.4). On a `StringView` (or a `String`), with `StringView` arguments; on byte slices with `Slice` arguments and results. Results that are views borrow the receiver, like slicing it. - `s.indexOf(t)` and `s.lastIndexOf(t)`: `usize?`, where `t` first or last starts; an empty `t` is at `0`, and at `s.length` for `lastIndexOf`. `s.indexOfByte(b)` (`b: u8`): `usize?`. - `s.contains(t)`, `s.startsWith(t)`, `s.endsWith(t)` and `s.equalsIgnoreAsciiCase(t)`: `bool`; only `A`–`Z` and `a`–`z` fold. - `s.trim()`, `s.trimStart()`, `s.trimEnd()`: the view without leading and trailing ASCII whitespace (space, `\t`, `\n`, `\v`, `\f`, `\r`). - `s.splitOnce(sep)`: `TextHalves?` (`ByteHalves?` on bytes) with the parts before and after the first `sep`, or `null`; an empty `sep` is found at the start. - `s.split(sep)`: a `TextSplit` (`ByteSplit`) whose `next()` is each piece between separators in order, then `null`: `"a,,b"` gives `"a"`, `""`, `"b"`, an empty text one empty piece, and an empty `sep` the text whole. - **UTF-8.** A match of valid UTF-8 inside valid UTF-8 starts and ends on character boundaries, and ASCII whitespace is never part of a longer character, so every `StringView` these return is valid UTF-8 without a further check. - On other element types they are N0200. None of them panics. **Numbers from text** (§2.5), on `StringView`, `String` and byte slices: - `s.parseInt()` and `s.parseIntRadix(radix)` (`radix: u32`, 2 to 36): `T?` for any integer type `T`. The text is an optional `+` (or `-` for a signed `T`), then one or more digits `0`–`9`, `a`–`z`, `A`–`Z` below the radix, and nothing else. A value outside `T`, any other text, and a radix outside 2–36 are `null`. - `s.parseFloat()`, `T` `f32` or `f64`: `T?`. The text is `[+-]digits[.digits][(e|E)[+-]digits]` with a digit on at least one side of the point, or `inf`, `infinity` or `nan` in any case after an optional sign; anything else is `null`. The result is the nearest `T` to the decimal value, ties to even, rounded once (an `f32` is not rounded through an `f64`); past the largest finite value it is ±infinity, and below the smallest subnormal ±0. For every finite `x`, parsing the text `Console.writeLine(x)` prints gives `x` back (§35.7). It does not use the C library, so the locale does not matter, and the interpreter runs the same code. - A missing type argument is N0265, and a type that is not an integer (or float) type N0207. Under `--no-float`, `parseFloat` names a float type, which is N0297 (§35.9). The method names are reserved on these types, as the integer methods are (§6). The lib/core structs that hold them (`ByteOrder`, `VectorBytes`, `TextSearch`, `ByteSearch`, `TextHalves`, `ByteHalves`, `TextSplit`, `ByteSplit`, `NumberParsing`) are lib/core items, which a program may shadow (§2) without changing these methods. ## 17. Results and error propagation (M3a) ```ceesafe enum ParseError { Empty, BadDigit(u8), Overflow } fn digit(c: u8) -> Result { if (c < 48u8 || c > 57u8) { return Err(ParseError.BadDigit(c)); } return Ok((c - 48u8) as u32); } fn twoDigits(a: u8, b: u8) -> Result { return Ok(digit(a)? * 10 + digit(b)?); } ``` `Result`, `Ok`, `Err` and `AllocError` are built in (ADR 0003). User generics come in M6; the names below then become library items with the same meaning. - **Splitting** (M13 G5): `s.splitAt(mid)` gives a `SliceHalves` whose `left` is `s[..mid]` and `right` is `s[mid..]`. `s.splitAtMut(mid)`, on a `SliceMut` or a vector, gives a `SliceHalvesMut` of two `SliceMut` that do not overlap, so both may be written at once, at a split known only at run time. Both panic ("slice range out of bounds") if `mid` is past the end; the panic names lib/core's line, not the caller's. The halves borrow `s` for as long as either lives. They are lib/core code (`SliceSplitting`, `lib/core/slices.cee`). ### 17.1 Types - `Result` is a built-in enum with two variants, in this order: `Ok` with payload `T`, and `Err` with payload `E`. Everything in §15.1 applies: layout, Copy iff `T` and `E` are Copy, needs drop iff either does, and only the active variant's payload is destroyed. - `T` may be `void`, in which case `Ok` has no payload. `E` must not be `void`. Neither may be a reference or slice (N0246, E3); arrays, structs, enums and results are fine. - A type contains `T` and `E` by value: `struct S { r: Result; }` is recursive (N0227). - `AllocError` is a built-in enum `{ OutOfMemory }` (Copy). The names `Result`, `Ok`, `Err` and `AllocError` cannot be redeclared as items (N0201). - Two `Result` types are equal iff their `T` and their `E` are equal. ### 17.2 Values - `Ok(e)`, `Ok()` (for `T = void`) and `Err(e)` need an **expected `Result` type** that fixes `T` and `E`. That type comes from a declared variable type, a parameter type, a return type (`return Ok(x);`), or a field or payload type. Without one, it is N0260 ("write the type"). - The payload is checked against `T` or `E` like a call argument against its parameter (§5). ### 17.3 Matching - Patterns `Ok(x)`, `Ok(_)`, `Ok` (for `T = void`), `Err(x)` and `Err(_)` are written without a type prefix. The bindings and exhaustiveness rules of §15.2 apply unchanged: a non-Copy payload is bound by matching on `borrow r` (N0258 otherwise). ### 17.4 `e?` and `e!` ```text Postfix = Primary ( … | '?' | '!' )* -- '!' is postfix only when it follows an operand ``` - **`e?` (propagate).** - `e` must have a `Result` type (N0262). The enclosing function must return `Result` with the same `E`, or `Result` where `F` is an enum with an `@from` variant whose payload is `E` (N0261 otherwise, naming the `@from` variant that would fix it). - **`@from`** (ADR 0075 §2.3): `enum F { @from Io(IoError), … }` marks a variant with exactly one payload (N0262 otherwise), and an enum has at most one `@from` variant per payload type (N0262). `e?` then returns `Err(F.Io(x))` for `Err(x)`. The conversion is one step, and only `?` makes it: `return Err(x)` still names the variant. - If `e` is `Ok(v)`, the value of `e?` is `v` (of type `T`; `void` if `T` is `void`). - If `e` is `Err(x)`, the function returns `Err(x)` at once, exactly like a `return` statement. The statement's temporaries end, then every scope is left (drops and deferred statements), innermost first. Like `return`, `?` is not allowed in a deferred statement (N0254). - **`e!` (unwrap).** `e` must have a `Result` type (N0262). The value is `v` for `Ok(v)`; `Err(_)` **panics** with "unwrapped an Err value". Panic is abort, so nothing is destroyed. - In both cases `e` is evaluated once and consumed. The payload moves out of it (or is copied); nothing else of `e` remains to be destroyed. - As an expression statement: `f()?;` and `f()!;` are allowed when the operand is a call, like calls themselves (N0225 otherwise). - Precedence: postfix, like a call or member access: `parse(a)?.value` and `-x?` work as expected (`-` applies to `x?`). `!` directly followed by `=` lexes as `!=`. Write `r! == 1` with a space. ### 17.5 Discarded values (correction to §14.5) - An expression statement whose value needs drop (for example a call `make();` returning a `Res`) destroys the value at the end of the statement, like any other temporary. Before M3a the value was never destroyed: an O4 violation, found while specifying 3a. Diagnostics: N0260 (no expected `Result` type for `Ok`/`Err`), N0261 (`?` in a function whose return type is not a `Result` with the same error type or an `@from` variant for it), N0262 (`?` or `!` on a non-`Result`, or a misused `@from`). ### 17.6 Methods of `Result` (ADR 0075 §2.3) - `r.isOk()` and `r.isErr()` (`borrow this`): `bool`. - `r.ok()`: `T?`, and `r.err()`: `E?`. Both consume `r`; the other side is destroyed. - `o.okOr(e)` on a `T?`: `Result`, consuming `o`; `e` is destroyed when there is a value. - They are lib/core's ResultMethods and OptionalResults. `map` and `mapErr` are not provided: they need generic methods of generic structs, which the checker does not have yet. ## 18. Boxes and allocators (M3b) ```ceesafe struct Account { id: u64; balance: i64; fn deposit(borrow mut this, amount: i64) { this.balance += amount; } } fn open(id: u64) -> Result, AllocError> { return Box.create(Account { id: id, balance: 0 }); } fn main() -> i32 { var acct = open(7)!; acct.deposit(100); // through the box return 0; } // the box is dropped: Account destroyed, memory freed ``` `Box`, `SystemHeap` and `FailingAllocator` are built in (ADR 0003). ### 18.1 Allocator types - `SystemHeap` and `FailingAllocator` are built-in, field-less, Copy structs. They are the only **allocator types** in M3; user allocators come with interfaces in M6 (`memory-model.md` §3.2). Their values are written as struct literals: `SystemHeap {}`. - `SystemHeap` is the platform heap (`malloc`/`free` in hosted and CosmoOS userland builds). - `FailingAllocator` allocates from the platform heap, but can be told to fail, for testing error paths in the language itself: - `FailingAllocator.failAfter(n: u64)`: the next `n` allocations through `FailingAllocator` succeed, and every later one fails; - `FailingAllocator.reset()`: allocations never fail (the initial state). The countdown is process-wide; frees are never affected. It is atomic: when several threads allocate through `FailingAllocator`, exactly `n` of the allocations that follow `failAfter(n)` succeed, but which ones depends on the order in which the threads reach the countdown. A test that needs the same allocation to fail on every run arms the countdown on one thread and allocates through `FailingAllocator` from that thread only, or orders the threads' allocations itself (a join, a mutex). - Their names, and `Box`, cannot be declared as items (N0201). ### 18.2 `Box` - `Box` owns exactly one value of type `T` in memory obtained from allocator type `A` (H1). `Box` means `Box`. `A` must be an allocator type (N0263). `T` is any value type except `void` and references or slices (N0246). - A box is a **move type** that needs drop, and is one pointer wide (8 bytes on LP64). Destroying it destroys `T` (its drop glue), then returns the memory to `A`. - A type does **not** contain the target of a `Box` by value, so recursive types are built with boxes: `enum List { Nil, Cons(i32, Box) }` is allowed (no N0227). - **Creation** (built-in functions): - `Box.create(v)`: `Result, AllocError>`; - `Box.createIn(a, v)` with `a` a value of allocator type `A`: `Result, AllocError>`. `T` is the type of `v`. An expected type `Result, _>` provides the type hint for `v` (so `Box.create(5)` can make a `Box`). `v` is moved into the call. If the allocation fails, `v` is destroyed and the result is `Err(AllocError.OutOfMemory)`. - **Access through a box** works like access through a reference (§13.3), with the box's own mutability: - fields `b.f`, elements `b[i]` and method calls `b.m()` reach `T` (auto-deref); - the contents are mutable iff the box is: the box variable is `var`, or the box itself is reached through `borrow mut` or another mutable place. `b.f = 1` needs a `var b`; - a `Box` argument for a `borrow T` parameter borrows the contents, and `mut b` lends them for a `borrow mut T` parameter; - where a Copy `T` is expected, a `Box` is read through (auto-deref); - a `borrow Box` converts to `borrow T`, and a `borrow mut Box` to `borrow mut T` or `borrow T`: a reborrow of the contents (for example, a payload bound by reference in a match); - `borrow b` borrows the box itself (`borrow Box`). - **Borrowing rules:** a place inside a box is part of the box. Moving, overwriting or dropping the box while a loan of its contents is live is rejected (N0240, N0243), as for any place and its fields. (Unlike a reference going out of scope, a box going out of scope destroys what it points to.) - **Moving out of a box's contents**: see §18.3 (M3c). - No null boxes exist. A `Box` is never Copy and has no `==`. Diagnostics: N0263 (an allocator argument that is not an allocator type). ### 18.3 Moving out of a box (M3c) - A box held in an **owned place** owns its contents as a part of that place: a local, or a field path of one, not reached through a reference. Its fields may be moved out (`let l = n.left;`, `move n.left`), exactly like fields of a struct local (§14.3–14.4): - the box cannot be used as a whole again (moved, passed, matched, borrowed) until every moved-out field is re-initialised by assignment (N0250); - a field of a `T` with a `drop` block cannot be moved out (N0252); - through a reference, a box's contents are as immovable as anything else (N0251). - Destroying a partially moved box destroys the fields still initialised (in declaration order), then returns the memory to the allocator. `T`'s drop glue does not run, and `T` has no `drop` block by the rule above. - There is no way to move the whole contents out in v0 (`T` would need an unbox operation, M6). ### 18.4 Moving payloads out in `match` (M3c; replaces N0258) ```text Binding = Identifier | 'var' Identifier | '_' ``` - `var x` makes a mutable binding; `x` an immutable one. Both may bind any payload type. - **Consuming match.** If the scrutinee is a value (not a reference) and some arm binds a non-Copy payload, the match **consumes** its scrutinee: - the scrutinee is moved when the match starts. A place scrutinee is left uninitialised, as by `move`; a `using` binding cannot be consumed (N0253); - in an arm whose pattern lists payloads (`E.V(…)`), every payload of the variant moves out: named ones into their bindings, and `_` ones into hidden locals of the arm. Payloads not bound by name are therefore destroyed when the arm ends, before the bindings' own scope ends. A payload matched by a nested pattern (M11) moves into a hidden local too, and the same rule applies to it: if its pattern lists payloads, they move out of it in turn; - in any other arm (`_`, `E.V` without a list, a variant without payload), the value is destroyed when the match statement ends. - Otherwise (Copy payloads only, or a reference scrutinee) the rules of §15.2 apply unchanged. - N0258 is no longer reported (since M3c). ### 18.5 Destroying deep structures (R-9) - A box's drop glue is recursive in general: destroying a `T` destroys its fields, which may own further boxes. For a recursive type that would use stack in proportion to the structure's depth. - **Tail iteration.** The compiler finds the *tail* of `T`: the last field (in destruction order) that needs drop, followed through structs, and through enum variants' last payloads, until it reaches a `Box` again. If there is one, `Box`'s glue becomes a loop: - destroy everything of the current `T` except the tail, in the normal order; - take the tail box's pointer (null for variants that end elsewhere); - free the current memory, then continue with the tail's contents. - Only the moment the memory is freed changes: the current node is freed before the tail is destroyed instead of after. No program can observe that difference; every `drop` block runs in the order §14.5 defines. - Consequence: lists (`Cons(x, Box)`) are destroyed in constant stack depth. Trees use stack in proportion to the number of non-tail steps on a path (the left children in `{ left, right }`). Balanced trees therefore use logarithmic depth, and a left-degenerate tree still recurses (R-9 records this). ## 19. Regions (M4) `Arena`, and in later increments `FixedBufferAllocator`, `ScratchFrame`, `Pool` and `PoolBox`, are built in (ADR 0004). The safety rules are R1–R4 (`ownership.md`): an allocation borrows its region. ### 19.1 Scoped types (M4a) A type is **scoped** if a value of it may hold loans: - references and slices (§13, §16); - `Result` whose `T` or `E` is scoped (a *scoped result*); - the scoped region values: `FixedBufferAllocator` (§19.4), `ScratchFrame` (§19.5) and `PoolBox` (§19.6). Every rule for references applies to scoped types (N0246): - they may appear only in parameters, locals with an initialiser, and return types; - never in fields, payloads of user enums, array elements, box contents or constants. References and slices must be `let` locals, because assigning to a reference writes through it. *Since M4c,* other scoped values (scoped results and region values) may be `var`: assigning one replaces it, like any value. For the borrow checker, the local then holds the loans of every value it is assigned (a sound over-approximation). A function returning a scoped type needs return sources: the default rule (one reference parameter) or a `borrow(...)` list (§13.5). In this rule, "reference parameter" now means "parameter of a scoped type", and the receiver `this` counts when it is borrowed. The borrow checker treats every local of a scoped type as holding loans (`lifetimes.md` §3). Moving or copying a scoped value moves or copies its loans, as for references. A scoped result's loans reach the `Ok` payload through `?`, `!` and `match`. ### 19.2 `Arena` (M4a) ```ceesafe fn process(input: Slice) -> Result { using arena = Arena.create(SystemHeap {}, 65536u64); let counts = arena.allocArray(256usize, 0u32)?; // SliceMut, a loan of `arena` for (b in input) { counts[b as usize] += 1u32; } var distinct: u64 = 0; for (c in counts) { if (c != 0u32) { distinct += 1; } } return Ok(distinct); // a u64, not arena memory } // the arena frees all its memory here ``` - `Arena` is a built-in move type that needs drop. It is not scoped: it owns its memory, which it obtains from a free-standing allocator. - `Arena.create(a, chunkSize)`, with `a` an allocator value (`SystemHeap {}`, `FailingAllocator {}`) and `chunkSize: u64`, makes an empty arena. It cannot fail: memory is obtained in chunks of `chunkSize` bytes when needed. An allocation larger than a chunk gets a chunk of its own. - **Allocation** (R1). Both methods take `borrow this` and return a scoped result that carries a **shared loan of the arena**: - `arena.alloc(v)`: `Result`, with `v` moved into fresh arena memory; - `arena.allocArray(n, fill)` with `n: usize`: `Result, AllocError>`, `n` copies of `fill`. `T` must be Copy. `n` may be 0 (an empty slice); `n × size(T)` beyond the address space is `Err(OutOfMemory)`. The returned references are mutable: each allocation is fresh memory that nothing else refers to. - **R2:** `T` must not need drop, and must not be scoped (no references or slices in arena memory, E3). Both are N0264. - `arena.reset()` takes `borrow mut this` and releases all memory. Because it is a mutable borrow, it is rejected while any allocation is live (B1/B2). Dropping or moving the arena is likewise rejected while allocations are live. - Destroying an arena releases all its memory at once (bulk release). Since no allocation needs drop (R2), this equals destroying each allocation. - Failure: if a chunk cannot be obtained from the allocator, the allocation returns `Err(AllocError.OutOfMemory)`; the arena stays usable. ### 19.3 What cannot escape (M4a, the R1 consequences) - An allocation cannot be returned from the function that owns the arena: N0244, like any reference to a local. - It cannot be stored in a box, a struct, an enum payload or an array: those cannot hold scoped values (N0246). - It cannot outlive the arena: a `reset`, drop, move or scope end of the arena while the allocation is live is N0240–N0243. Diagnostics: N0264 (a type that a region cannot hold). ### 19.4 `FixedBufferAllocator` (M4b) ```ceesafe fn countWords(text: Slice) -> Result { var storage = [0u8; 4096]; let fba = FixedBufferAllocator.over(mut storage); // a mutable loan of `storage` let spans = fba.allocArray(64usize, Span { start: 0u32, len: 0u32 })?; ... } ``` - `FixedBufferAllocator.over(mut s)` makes an allocator over caller-provided bytes. `s` is a mutable place of type `[u8; N]`, or a `SliceMut`. The allocator holds a **mutable loan** of the storage, so the storage cannot be read or written while the allocator or any of its allocations is live. - `FixedBufferAllocator` is **scoped** (§19.1): a `let` local, a parameter or a return type with sources, never stored. It is a move type without drop; nothing is freed. - `alloc(v)` and `allocArray(n, fill)` work as for `Arena` (§19.2), including R2 (N0264). Each allocation is aligned within the storage. When the remaining bytes are not enough, the result is `Err(AllocError.OutOfMemory)`; the storage is never grown. There is no `reset`: to reuse the storage, let the allocator end and make a new one. - An allocation's loans include the allocator's loan of the storage, so the storage stays borrowed while any allocation is live, even after the allocator's last use. ### 19.5 Scratch frames (M4b; ADR 0004) ```ceesafe fn render(n: usize) -> Result { using frame = Scratch.enter(); // a root frame let buf = frame.allocArray(n, 0u8)?; // scoped to `frame` { using inner = frame.enter(); // a nested frame borrows `frame` mutably let tmp = inner.alloc(1u64)?; ... } // `inner`'s memory is released here return Ok(buf.length as u64); } // `frame`'s memory is released here ``` - Scratch memory is a runtime-owned stack of heap chunks (`SystemHeap`); it is not the native stack. - **Each thread has its own scratch stack.** A frame belongs to the thread that entered it, and its memory comes from that thread's stack. - `Scratch.enter()` makes a **root frame**. Only one root frame may be live at a time on a thread: entering a second one on the same thread **panics** ("a scratch frame is already active"). Two threads may each have a root frame live at once. - Code that needs scratch memory below a frame receives the frame (`borrow ScratchFrame`) and nests. - A frame never reaches another thread. `ScratchFrame` is scoped, so neither `Send` nor `Share` (§38.1), and `Thread.both` refuses a capture of a frame or of a borrow of one (N0334, §38.6). So only the thread that entered a frame allocates from it, nests in it or drops it. - An allocation is a reference into the frame's memory, so it is not `Send` either. It reaches another thread only as a `Thread.both` capture: a `borrow mut T` or `borrow T` whose `T` is `Send` or `Share` (§38.6). The borrow checker keeps the frame live until `both` returns, and the other thread is done with the reference by then. - A thread's body that runs on the calling thread because no thread could be started (`Thread.both`'s first closure, `Async.blocking`'s body; §38.6, §40.8) runs with an empty scratch stack, as on a thread of its own; the caller's stack is restored when it returns. So whether that body may enter a root frame does not depend on whether the thread started. - *Up to 0.27.0 the stack was one for the process and not synchronised:* two threads that each entered a frame raced on it, and one could be handed memory the other was using. - `frame.enter()` makes a **nested frame**. For the borrow checker a frame has two parts: its *memory*, which allocations borrow, and its *allocation right*, which every allocation call uses briefly. The rules: - an allocation result carries a shared loan of the frame's memory; - each `alloc`/`allocArray` call needs a shared borrow of the allocation right for the call only; - `enter()` takes a **mutable** loan of the parent's allocation right, which the nested frame carries. While a nested frame is live, the parent therefore cannot allocate or nest again (B1), but allocations already made from the parent stay usable: they lie below the nested frame's memory. Destroying or moving a frame conflicts with both parts. This makes frames strictly LIFO. - Calling a frame method needs a frame that is a place (`using frame = …`): N0247 otherwise. - `ScratchFrame` is scoped (§19.1) and needs drop. Destroying a frame releases everything allocated from it (and from frames nested in it), and returns chunks to the heap. - `alloc(v)` and `allocArray(n, fill)` work as for `Arena`, including R2. A failure to obtain a chunk is `Err(AllocError.OutOfMemory)`. - Frames are bound with `using` (R3; §14.6): nesting borrows the frame mutably, and a scoped value cannot be a `var`. Since frames are scoped, they cannot be returned from the function that entered them. ### 19.6 `Pool` and `PoolBox` (M4c) ```ceesafe struct Particle { x: i32; v: i32; } fn simulate() -> Result { using pool: Pool = Pool.create(SystemHeap {}, 64u32); var a = pool.take(Particle { x: 0, v: 2 })?; // PoolBox: a loan of `pool` let b = pool.take(Particle { x: 10, v: -1 })?; a.x += a.v; // through the box return Ok(a.x + b.x); } // b, a, then the pool are destroyed ``` - `Pool` is a built-in allocator for up to `capacity` values of one type `T`. - `Pool.create(a, capacity)`, with `a` an allocator value and `capacity: u32`, makes an empty pool. `T` comes from the expected type (`let pool: Pool = …`); without one it is N0260. - It cannot fail: the slots are allocated from `a` at the first `take`. - `Pool` is a move type that needs drop; destroying it returns the slots to `a`. It is not scoped. - `pool.take(v)` takes `borrow this` and returns `Result, AllocError>`: `v` moves into a free slot. The result is `Err(AllocError.OutOfMemory)` when all slots are taken, or when the slots cannot be allocated; `v` is then destroyed. - `PoolBox` owns one slot and its value, like a `Box` (§18.2): - auto-deref, the box's own mutability, borrowing its contents, and moving fields out of it (§18.3) all work as for `Box`; - it is a move type that needs drop: destroying it destroys the `T` (its drop glue), then returns the slot to the pool. Values are destroyed one by one, so **`T` may need drop**: R2 does not apply to pools; - it is **scoped**: it carries the shared loan of the pool from `take`. It cannot be returned past the pool, stored in a field or box, or outlive the pool (§19.3); - `T` must not be scoped (E3; N0264). - Destroying the pool while a `PoolBox` from it is live is rejected (B2), so every slot is free when the pool is destroyed. ## 20. Generics (M6a) ```ceesafe struct Pair { first: T; second: T; fn swap(this) -> Pair => Pair { first: this.second, second: this.first }; } enum Maybe { Nothing, Just(T) } fn largest(p: borrow Pair, pick: bool) -> borrow T { if (pick) { return borrow p.first; } return borrow p.second; } fn main() { let p = Pair { first: 1, second: 2 }; // Pair, inferred from the fields let q = p.swap(); let m: Maybe = Maybe.Just(7u8); let r = largest(p, true); // T = i32, inferred from the argument let s = largest(q, false); // explicit type argument } ``` ### 20.1 Declarations ```text TypeParams = '<' TypeParam (',' TypeParam)* '>' TypeParam = 'region' Identifier | 'const' Identifier ':' Type | Identifier (':' Type ('+' Type)*)? FunctionDecl = 'fn' Identifier TypeParams? '(' … ')' … StructDecl = 'struct' Identifier TypeParams? '{' … '}' EnumDecl = 'enum' Identifier TypeParams? '{' … '}' ``` - A top-level function, struct or enum may declare **type parameters**. Inside the declaration, each parameter is a type. Methods of a generic struct see the struct's parameters. - **Generic methods** (ADR 0064 §1). A struct's method or static function may declare type parameters of its own: `fn add(borrow mut this, s: borrow S)`. It sees the struct's parameters too. It is checked once with its parameters opaque (§20.3) and instantiated per call (§20.4), as a top-level generic function is. Its type arguments are inferred from the arguments after the receiver, or written after the name: `canvas.add(s)`, `Pair.make(…)`. Type arguments on a call of a function without type parameters of its own are N0209. An interface's methods may have type parameters too (ADR 0086 §2.3, §21.1), but then the interface has no `dyn` type (N0331): a `dyn` table holds one function per method. - A **const parameter** `const N: T` (ADR 0083) stands for a value rather than a type: §20.5. - Names must be distinct, and must not shadow items or built-in types (N0201). ### 20.2 Using generic items - **Types:** `Pair`, `Maybe>`. A generic struct or enum must always be given all its type arguments in a type (N0204). - **Calls:** `largest(p, true)` gives the arguments explicitly. `largest(p, true)` infers them from the argument types: each parameter type is matched against its argument's type. An integer literal argument is `i32` unless the parameter type fixes it. A parameter that the arguments do not determine is N0265 ("write the type arguments"). - **Values:** `Pair { first: 1, second: 2 }` infers `T` from the field values, or takes it from the expected type. `Pair { … }` gives it. Likewise `Maybe.Just(7)` infers from the payload, and `Maybe.Nothing` needs an expected type or explicit arguments: `Maybe.Nothing`. - **Static functions:** `Pair.make(…)`. - **Inference from a bound** (ADR 0086 §2.7). A type parameter that the arguments do not determine is inferred from a bound `I: J<…T…>` when `I`'s argument implements exactly one instantiation of `J` (its impls; for a `dyn` type, its interface and supertraits; for a type parameter, its bounds). Otherwise it is N0265, as before. So `fn count>(it: I) -> usize` is called as `count(text.split(","))`. ### 20.3 Checking generic code once - A generic declaration is checked **once, for all type arguments**. Each type parameter is an opaque type, about which only what holds for every type is known: - it is a **move type** that **may need drop**. A value of it can be moved, borrowed, passed, returned, stored in fields of the declaring type, and matched as a payload; - nothing else is allowed: no operators, no fields, no methods, no literals, no `==` (N0207, N0234, N0205). Interfaces (§21) add operations through bounds. - Move checking, borrow checking and reachability run on the generic code too. A body that would be wrong for some type argument is rejected at the definition, **whether or not it is ever instantiated** (for example, using a `T` twice is N0250). - Generic declarations that are never used produce no C code. ### 20.4 Instantiation (monomorphisation) - Every distinct use (`largest`, `Pair`) is an **instantiation**: a copy of the declaration with the type arguments substituted. It is checked again and compiled to C. - An instantiated struct or enum is an ordinary type for every other rule. It is Copy iff its substituted fields or payloads are, and needs drop iff one of them does. `Pair` is Copy; `Pair` is a move type. - The layout is that of the substituted declaration. Recursion is checked per instantiation (N0227). - Instantiation is depth-limited: a chain of more than 64 nested instantiations (polymorphic recursion such as `f` calling `f>`) is N0266. - **Unit payloads** (ADR 0074 §2.1). `Result.Ok` has no payload (§17.1), but a pattern may bind one name to it, and `Ok(e)` may pass one argument of type `void`, so `Ok(v) => Ok(v)` means the same at `T = void` as at any other `T`, in a generic body and written out by hand: - A name bound so is a **unit local**: it has type `void` and holds nothing. `let v = r?;` (or `r!`) with `r: Result` declares one too, as does `let w = v;` of one. A `let` of a call of a `void` function is still N0224, and a `var` cannot hold a unit. - Reading a unit local is a unit value. It may be passed where `void` is expected: as `Ok`'s payload, and as `return v;` in a `void` function, which is `return;`. Elsewhere it is a `void` value of the usual rules: N0205 where another type is expected, N0211 when assigned. - `Ok(e)` with any `void` `e` evaluates `e` for its effects, then builds the payload-less `Ok`. Any other type is N0209. - The checker erases units: a unit local is no HIR local, a unit read no operand, and nothing is captured, moved, borrowed or destroyed for one. NIR, the C backend and the interpreter never see them. - Not included: user generic enums instantiated at `void` (N0226), and `Some(v)` with `void`. - **An instantiation that does not check is an error at its use** (M15 S1). §20.3's checking covers every type argument; a `void` one's payloads are unit payloads (above). A scoped type argument the body cannot take is N0274 (§25.4). Otherwise the copy's errors are usually explained by an error at the use (an unmet bound, N0268; a move of C state, N0294). If, after the ownership checks, no error explains them, the copy is N0222 at the use, naming the type arguments and its first error. Its body is never compiled. Before M15, such a copy reached code generation with its errors unreported, and ceesafec crashed (#2). Diagnostics: N0265 (type arguments that cannot be inferred), N0266 (instantiation too deep), N0222 (the instantiation's body does not check for its type arguments). ### 20.5 Const parameters (ADR 0083) ```ceesafe enum Method { Filter, All } struct Grouped { starts: [u32; ENDS]; } struct Parser { pos: usize; fn step(borrow mut this) { if (TS) { this.pos += 2; } else { this.pos += 1; } } } fn hoist(n: u32) -> u32 => match (M) { Method.Filter => n, Method.All => n + 1 }; fn run() -> usize { var p = Parser { pos: 0 }; p.step(); return p.pos; } fn parse(ts: bool) -> usize => if (ts) run() else run(); ``` - **Declarations.** A function, struct, enum, interface, interface method or impl may declare const parameters, `const N: T`, beside its type parameters: after its region parameters (§25.1), in any order with the type parameters (`fn toJson`). `const` stays a reserved word; this is its one place in a parameter list. `T` is `bool`, an integer type (`i8`–`i64`, `u8`–`u64`, `isize`, `usize`) or a fieldless enum (§15.1) of the program or a dependency; anything else (a float, `StringView`, a struct, an enum with payloads, an imported C enum, a type parameter) is N0352. These are the types whose values `==` compares exactly, so two arguments name the same instance iff they are equal. - **Inside the generic item** (checked once, §20.3), `N` is a value of type `T`, the same in every use: it may be read wherever a run-time value of that type may (`if (TS)`, `match (M)`, operands, arguments). It is not a place: assigning it or lending it `borrow mut` is N0211. Both branches of `if (TS)` are checked, and code after `if (TS) { return 1; }` stays reachable. A closure reads `N` without capturing it. A bare `N` in a pattern is N0358: test it in a guard, `x if (x == N) =>` (§15.2). - **Array lengths.** A `usize` const parameter may be an array length, `[T; N]` and `[v; N]` (§16.1). In the generic item `[T; N]` is equal only to `[T; N]` with the same parameter (`[T; M]` is N0205), and `a.length` is `N`. Its length is unknown there, so slice and array patterns on it (§15.2) are checked as on a slice: covering it needs a `_` or `..` arm (N0256). The body is checked once, so an instance adds no pattern error: an arm whose count its N cannot have (`[x, y, ..]` when N is 1, `[]` when N is 3) is never taken, and an arm it cannot reach (`_` after `[x, ..]`) is not N0257. - **Arguments** are written where type arguments are, and evaluated at compile time (§10) to a value of the parameter's type; out of its range is N0217. An argument is `true`, `false`, an integer literal (typed by the parameter: `Grouped<3>` is a `usize`), the name of a constant (an associated one, `Tags.COUNT`, included), an enum variant (`hoist(n)`), or inside a generic item one of its own const parameters, bare (`Parser`). In a type, a parenthesised constant expression that names no const parameter is one too: `let g: Grouped<(Tags.COUNT + 1)>`. In an expression, `(` cannot start an argument (grammar.md, "Generic arguments in expressions"): name a constant instead, `const ENDS: usize = Tags.COUNT + 1;` then `counted()`. - A name in argument position is resolved by the parameter's kind: a value for a const parameter, a type for a type parameter. A type where a value is expected, or a value where a type is, is N0354. - A run-time value (a local, a parameter, a call) as an argument or an array length is N0353; its message names the dispatch: `if (ts) run() else run()`. So is an expression over a const parameter (`run()`, `[u8; N + 1]`): type equality stays by name. An interface constant read through a type parameter (`H.NAME`, ADR 0086 §2.4) is a value of each instance, not a constant, so it is N0353 there too; on a concrete type, `Ts.NAME` is an associated constant and may be an argument. - **Inference** is as for type parameters (§20.2): from the argument types a const parameter appears in (`fn sum(a: [u8; N])` called with a `[u8; 8]`; `l: borrow Lexer` with a `Lexer`), and from an expected type (`let g: Grouped<4> = Grouped { … }`), never from a run-time value; otherwise N0265. - **Interfaces and impls.** An interface may have const parameters (`interface Pools`), and so may an interface method, under the rule for generic methods (§20.1): it is instantiated per call through a bound or on a concrete type, and the interface then has no `dyn` type (N0331). A generic impl names its parameters in its target (ADR 0054): `impl CharIter for ShellCharIter`; an inherent impl names the struct's, in order: `impl Parser`. A generic impl still has no constants (N0216), so no interface constant is defined from a const parameter; and an impl for one value beside a generic one (`impl I for S` with `impl I for S`) overlaps (§21.1). - **Instantiation** (§20.4). Each distinct argument list is an instance, checked again and compiled to C: `Parser` and `Parser` are different types. An instance that does not check is an error at its use; an array length that is 0 or too large there is N0259 (inside a function's body, N0222 naming the values). The 64-deep limit (N0266) applies. In the instance, `N` is its literal, so `if (TS)` compiles to `if (1)` or `if (0)`, which the C compiler removes at `-O2`. The C names are in c-backend.md, "Generic code". - Not included: default values (``), parameters of other types, expressions over a parameter in arguments or array lengths, a const parameter as a constant pattern, and `const fn` (functions are not constants, §10). Diagnostics: N0352 (a const parameter's type), N0353 (an argument or array length that is not a constant), N0354 (a type where a value is expected, or a value where a type is). ## 21. Interfaces (M6b) ```ceesafe interface Shape { fn area(borrow this) -> i64; } struct Square { side: i64; } impl Shape for Square { fn area(borrow this) -> i64 => this.side * this.side; } fn total(shapes: Slice) -> i64 { var t = 0i64; for (i in 0..shapes.length) { t += shapes[i].area(); } return t; } ``` ### 21.1 Declarations ```text InterfaceDecl = 'interface' Identifier TypeParams? (':' Type ('+' Type)*)? '{' (MethodSig | InterfaceConst | AssocTypeDecl)* '}' MethodSig = Attribute* 'fn' Identifier TypeParams? '(' ParamList? ')' ('->' Type)? (';' | FunctionBody) InterfaceConst = 'const' Identifier ':' Type ('=' Expr)? ';' AssocTypeDecl = 'type' Identifier (':' Type ('+' Type)*)? ';' ImplDecl = 'impl' TypeParams? Type 'for' Type '{' (FunctionDecl | ConstDecl | 'type' Identifier '=' Type ';')* '}' InherentImpl = 'impl' TypeParams? Type '{' (FunctionDecl | ConstDecl)* '}' ``` - **`impl S { … }`** (an inherent impl, ADR 0064 §1) adds methods and static functions to a struct `S`, as if they were written in its body: the same visibility, the same "no member named twice" rule (N0228), and generic methods (§20.1). It is written in the package that declares `S`. Its target names `S` with `S`'s own parameters, in order: `impl S { … }` for `struct S`, `impl V { … }` for `struct V`. Anything else is N0267. It may also hold constants (§10, ADR 0074 §2.7); a block of constants only may name an enum of its package. - An **interface** lists method signatures. An interface may be generic (`interface Comparable`). - **Static functions and generic methods** (ADR 0086 §2.3). A method has a receiver (`this`, `borrow this`, `borrow mut this`), or none: a **static function** (`fn create(options: borrow Options) -> Self;`), with or without a default. Through a bound it is called as `T.create(o)`, in a default as `Self.create(o)`, and on a concrete type as `Rule.create(o)`. A method may have **type parameters of its own** (§20.1): an impl's method has as many, in the same order, and they are compared by position (N0267). Through a bound and on a concrete type, it is instantiated per call, as a struct's generic method is (ADR 0064 §1). - **Default methods** (ADR 0086 §2.1). A signature ends with `;`, or has a body (a block or `=> e;`): the method's **default**. An impl may define the method (an **override**, with the same signature: N0267 otherwise) or leave it out and **inherit** the default; a method without a default that an impl leaves out is N0267. - A default is checked **once, at the interface**, as generic code (§20.3) whose one type parameter is `Self`, the implementing type, bounded by the interface. `this` is `Self`, `borrow Self` or `borrow mut Self`, as the receiver says. The body may call the interface's methods, required or default, but not use fields or other methods of `Self` (N0234). An error in a default is reported there, whether or not an impl inherits it. - An impl that inherits a default gets an **instance** of it with `Self` = the target (once per instance for a generic impl). The instance is an ordinary method of the target: it is called on the concrete type, through a bound and through a `dyn` table (whose slot holds the instance), and dispatch stays static. An instance that does not check for its target is N0222 at the impl. If the target already has a method of the inherited name (its own, or another impl's), that is N0228 at the impl: a type has one method of each name. - The interface's `@effects` list bounds a default as it bounds every impl (§37.3). There is no call from an override to the default it replaces. - **`Self`** (ADR 0086 §2.2). In an interface, `Self` is the implementing type: opaque in its signatures and defaults, and the target in each impl, which writes either the target or `Self` where the interface has `Self`. In an `impl` block (inherent or for an interface) and in a struct's or enum's body, `Self` is the target type with its parameters (`Pair`, `View`), valid wherever that type's name is: a type, a struct literal (`Self { first: a, second: b }`), a static call (`Self.make()`) and a constant (`Self.MAX`). Elsewhere it is N0105 in a type and N0200 in an expression. Declaring an item, a type parameter or a local named `Self` is N0100. - **Constants** (ADR 0086 §2.4). An interface may declare `const NAME: T;` or `const NAME: T = v;` (a default). An impl gives `const NAME: T = v;` with the declared type (N0267 otherwise); a missing constant without a default, or one the interface does not declare, is N0267. The value is a constant expression (§10); a default may not use `Self`. Each becomes an associated constant of the target, read as `Type.NAME`; a name the target already has as a constant is N0228. In generic code and defaults it is read as `T.NAME` or `Self.NAME`: there it is a value, not a constant expression (not an array length or a pattern), since it is known only per instance; each instance reads the constant, so `if (H.HAS_ON_DATA) { … }` costs nothing at run time. A generic impl cannot define one (N0216), but its instances get the interface's defaults. - **Associated types** (ADR 0086 §2.5). An interface may declare `type Name;` or `type Name: B1 + B2;` (bounds as in §21.2). Each impl defines `type Name = T;` (N0267 if it is missing, or the interface has no such type); the impl's type and region parameters may appear in `T` (`impl Source for Words { type Item = StringView; }`), and `T` must meet the bounds (N0268, at the impl). - **Projection.** In a type, `P.Name` names the associated type, where `P` is `Self` inside the interface or a type parameter whose bounds declare exactly one associated type `Name`: none is N0200, two or more N0267 (ambiguous, as for bound methods). In generic code `P.Name` is an opaque type with the declared bounds (§20.3); an instance substitutes the impl's type. Where `P` is a concrete type (`Self` in an impl), `P.Name` is the impl's type. - There are no defaults for associated types, no bounds that fix one (`E: Encoding`) and no associated types with parameters of their own: an interface whose callers fix a type takes it as a parameter, as `Iterator` does (§16.4). - **Supertraits** (ADR 0086 §2.6). `interface B: A + Copy { … }`: every supertrait is an interface instantiation, which may use `B`'s parameters but not `Self`, or `Copy`. `impl B for T` requires `T` to implement each supertrait (N0268, at the impl). A bound `T: B` implies `T: A` (and `T: Copy`): the methods, defaults, constants and associated types of `A` are reachable through it, it meets a bound `A`, and the ambiguity rule of §21.2 applies to the union. A cycle of supertraits is N0227. `dyn B`'s table holds `A`'s methods after `B`'s, in declaration order (each interface once), so they can be called through it, and `dyn B` implements `A` for bounds as well as `B` (§37.2); there is no conversion from `dyn B` to `dyn A`. `B` is dyn-compatible only if its supertraits are, and not if it requires `Copy` (N0331). `Sized`, `Send` and `Sync` are not supertraits. - **`impl I for T { … }`** implements interface `I` (with its type arguments, `I`) for a type `T`: a struct, an enum, or a built-in type such as `i32`. A **generic impl** (ADR 0054), `impl I<…> for C<…T…, …U…> { … }`, implements `I` for every instance of the generic struct or enum `C` that its target matches and whose arguments meet the bounds; each of its type parameters appears in the target (N0267), and the target is a generic struct or enum (N0267). Its methods are checked once, with the parameters opaque, and instantiated per instance like a generic struct's: a call on the concrete type, a call through a bound and a `dyn` table all use the instance. A type with region parameters is implemented with them named, `impl I for S` (§25.3), and no other scoped type implements an interface (N0267). - The block must define the interface's methods that have no default, and may define those that have one, each with the same receiver kind, parameter types and return type, after the interface's type arguments (and `Self`) are substituted (N0267). It may not define other functions. - Each type may implement each interface instantiation at most once (N0267). - **Overlap.** Beside a generic impl, no other impl of the same interface may have a target of the same struct or enum, generic or not (N0267): `impl I for Wrap` and `impl I for Wrap` are refused together. There is no specialisation. - **The orphan rule.** An impl is written in the package that declares the interface or the target's struct or enum (N0267). The built-in types, and everything lib/core and lib/std declare, are the libraries': a program implements its own interfaces for them, and lib/std's interfaces for its own types. - An `impl` block's methods are ordinary methods of `T`. `x.area()` works on any `Square`, and their names must not clash with `T`'s other methods (N0228). - Interfaces are not types: an interface is a bound (§21.2), and since M13 also the `I` of a `dyn I` type behind a pointer, which dispatches at run time (§37; ADR 0015). - A method signature may carry an effect list, `@effects() fn visit(borrow mut this);`, which bounds every `impl` of it (§37.3). ### 21.2 Bounds - A type parameter may have bounds: ``, ` + Copy>`. - **Inside generic code**, a value whose type is `T` (or a reference to one) may call the methods of `T`'s bounds, with the signatures the interfaces give (after substitution). A call that no bound provides is N0234. If two bounds provide the same method name, the call is ambiguous (N0267). - **`Copy`** is a built-in bound: a `T: Copy` is a Copy type in the generic code (it can be used twice and stored in arrays) and never needs drop. Only Copy types satisfy it. - **At every instantiation** (a call, type or value that gives `T`), each bound must hold: the argument implements the interface (for a type parameter argument, its own bounds include it), or is Copy for `Copy`. Otherwise it is N0268, reported where the instantiation is written. - Dispatch is static: an instantiation calls `T`'s `impl` method directly (monomorphisation). ### 21.3 Slices of move types (correction to §16.1) - `Slice` and `SliceMut` may have any non-scoped element type, not just Copy ones. A slice only views elements: reading one by value still needs a Copy type (N0251 otherwise), but borrowing one (`borrow s[i]`, `s[i].method()`) does not. Arrays still need Copy elements (§16.1), since an array owns its elements and v0 tracks no per-element moves. Diagnostics: N0267 (an `impl` that does not match its interface, a duplicate `impl`, or an ambiguous bound method), N0268 (a type argument that does not satisfy a bound). ## 22. Optionals and string views (M6c) ### 22.1 Optionals `T?` ```ceesafe fn find(xs: Slice, wanted: i32) -> usize? { for (i in 0..xs.length) { if (xs[i] == wanted) { return i; } } // i converts to usize? return null; } fn firstTwo(xs: Slice) -> usize? { let a = find(xs, 1)?; // null propagates return a + 1; } fn main() { let numbers = [3, 1, 2]; match (find(numbers[..], 2)) { Some(i) => { Console.writeLine(i); } null => { Console.writeLine("absent"); } } let i = find(numbers[..], 3)!; // panics on null } ``` - `T?` (postfix `?` in a type) is an optional `T`, for any non-`void` `T`. It is a built-in enum with two variants, in this order: `null` (no payload) and `Some(T)`. Everything in §15.1 applies: it is Copy iff `T` is, needs drop iff `T` does, and is scoped iff `T` is (`borrow T?`). - **Optional references and references to optionals** (M11, G6). `?` applies to the whole type before it, so `borrow T?` is an *optional reference*, `(borrow T)?`. A *reference to an optional* is written with parentheses: `borrow (T?)`, `borrow mut (Box?)`. A type may be parenthesised anywhere a type is written (grammar.md §2). Diagnostics print the two as written: `borrow i32?` and `borrow (i32?)`. A reference to an optional is a reference like any other (§13): a match on it binds references to the payload, `r == null` reads the referent, and `r = v` through a `borrow mut (T?)` replaces the optional, as in ```ceesafe fn append(slot: borrow mut (Box?), value: i32) { match (slot) { Some(node) => { append(mut node.next, value); } null => { slot = Some(Box.create(Node { value: value, next: null })!); } } } ``` - **Values.** - `null` needs an expected optional type (N0260). - `Some(e)` makes a present value; with an expected `T?`, `e` is checked against `T`, otherwise `T` is `e`'s type. - Where a `T?` is expected (an initialiser, an argument, a return value, a field), a value of type `T` converts to `Some(value)` implicitly. - **Matching.** Patterns `null`, `Some(x)` and `Some(_)` are written without a type prefix; the rules of §15.2 and §18.4 apply. - **Comparisons.** `o == null` and `o != null` test for absence, for any `T` (the value is only read, not consumed). So do `o.isNull()` and `o.isSome()` (ADR 0072), `bool`s. Other comparisons of optionals are not defined (N0207). - **Optional borrows** (ADR 0074 §2.5). `o.asBorrow()`, on a place of type `T?` or on a `borrow (T?)`, is a `borrow T?`: `Some` borrows the payload, and `null` is `null`. `o.asBorrowMut()`, on a mutable place of type `T?` or on a `borrow mut (T?)`, is a `(borrow mut T)?` (N0248 otherwise). Either is a shared or mutable loan of the place for as long as the result is used, and is lowered as `match` would be: a tag test, then the payload's address or `null`. Any other method on an optional is N0234. - **Optional borrow arguments.** At an argument whose parameter is `borrow T?`, a place of type `T?` is borrowed as by `asBorrow()`, and a place of type `T` as `Some(borrow t)`, as a place of `T` is borrowed for a `borrow T` parameter (§13.2). At a `(borrow mut T)?` parameter the same holds with `mut p` and `asBorrowMut()` (N0249 without `mut`). So `f(relay.tlsConfig)` is accepted for `fn f(c: borrow TlsConfig?)`. A `borrow (T?)` stays a different type: it converts with `asBorrow()`. - **`a ?? b`** (ADR 0072) is `a`'s value, or `b` when `a` is `null`. `a` is a `T?` (N0262 otherwise) and `b` is checked against `T`; the whole is a `T`. If `b` is itself a `T?` of `a`'s type, the whole is that `T?`, so a chain may end in an optional (`find(a) ?? find(b)`). `b` is evaluated only when `a` is `null`, so `?? fail("…")`, a call of type *never*, stops with a message. `a` is consumed, as by `match (a)`: a non-Copy payload moves out. `??` binds less tightly than every other binary operator, `||` included, and is right-associative (grammar.md §2.1); beside an unparenthesised `||` or `&&` it is N0100. - **`o!`** is the value, or **panics** with "unwrapped null". - **`o?`** is the value, or returns `null` from the enclosing function, which must return an optional (N0261). `?` on an optional in a function returning a `Result` is N0261 in v0 (`language-spec.md` Ex11). - **Layout.** In general, like any enum: `{ tag; T }`. Since M9e′, an optional whose payload is a pointer that is never null is that pointer alone (the *niche* layout; `language-spec.md` §2): null is `null`, and any other value is `Some`. That covers `Box?`, except a pool box (two pointers), and `borrow T?` and `borrow mut T?`, except slices and string views (a pointer and a length). Such a pointer is never null: - allocation returns a non-null pointer or fails (`ceesafe_alloc` gives a zero-sized object one byte); - a stack-placed box points to its slot; - a reference points to an object, and one made from a raw pointer must be non-null (`unsafe.md` §2, U2). An optional of such an optional (`Box??`) has the general layout around the niche one. The layout changes no semantics, and optionals do not cross the C boundary (§28.3), so no C code sees it. `Box?` is 8 bytes rather than 16 on a 64-bit target. ### 22.2 `StringView` ```ceesafe fn greeting(loud: bool) -> StringView { if (loud) { return "HELLO"; } return "hello"; } fn main() { let s: StringView = "CeeSafe language"; Console.writeLine(s); Console.writeLine(s.length); // 13 (bytes) let first = s[0..4]; // "CeeSafe": a view of s Console.writeLine(first == "CeeSafe"); for (b in s.bytes()) { … } // Slice } ``` - `StringView` is a **shared, read-only view of UTF-8 text** `(pointer, length in bytes)`. It is scoped like `Slice` (§16.1), and Copy. - **String literals** have type `StringView` (their bytes are static data). A literal's view holds no loans. A function may therefore return a `StringView` without scoped parameters (like `greeting` above). The return check still rejects any view that borrows a local (N0244). - **Operations.** - `s.length` is the length in bytes (`usize`); - `s[i]` is the byte at `i` (`u8`, bounds-checked); - `s.bytes()` is the same bytes as a `Slice`; - `s[lo..hi]` (any bound may be omitted) is a `StringView` of bytes `lo` up to `hi`. Besides the range check of §16.3, both bounds must fall on character boundaries (not inside a UTF-8 multi-byte sequence). Otherwise it **panics** with "string slice not on a character boundary"; - `==` and `!=` compare the bytes; - `Console.write(s)` and `Console.writeLine(s)` print it; - searching, trimming, splitting and parsing numbers: §16.6. - `for (b in s)` is not defined: iterate over `s.bytes()` (characters come with `String` methods later). - **From bytes** (M15 S6): `StringView.fromUtf8(bytes)` (`bytes: Slice`, or a `Vector` lent as one) is a `StringView?`. - If the bytes are well-formed UTF-8 (the Unicode Standard's Table 3-7), it is a view of those same bytes; nothing is copied. - Otherwise it is `null`. That covers overlong forms, surrogates, values above U+10FFFF and truncated sequences. - The view borrows the bytes: it holds the argument's loans, so the bytes cannot change while it is used. - It is how text read with `File.read` or `Console.read` becomes a `StringView`, and through `append` a `String`, so the UTF-8 invariant above still holds. - It is available in every profile, through the core runtime's `ceesafe_utf8_valid`. Diagnostics: none new. `null` without an expected type is N0260; `?` and `!` on non-optional, non-result values are N0262. ### 22.3 `CStr` and `c"…"` (ADR 0087 §2.4) lib/core's `CStr` is a Copy view of UTF-8 text with a NUL byte after its end, valid for region `r` as a `StringView` is (§25): what a C function taking a `const char *` needs. - `c"…"` (`grammar.md` §1.6) is a `CStr`. The compiler appends the NUL; a `\0` inside is N0006. - `s.ptr() -> *CChar`, the first byte's address; `s.view() -> StringView` and `s.length() -> usize`, the text without the NUL; `s.bytesWithNul() -> Slice`, the text and its NUL. - `CStr.fromBytesWithNul(b: Slice) -> CStr?` takes bytes whose last byte is their only NUL and whose text before it is UTF-8 (as `view()` returns a `StringView`); otherwise it is `null`. The `CStr` is a view of `b`: returning one made from a local `Vector`'s bytes is N0244, as for any view. - Its fields are private to lib/core (N0296): only `c"…"` and `fromBytesWithNul` make one, so its NUL is always there. lib/core's `TextWriter`, whose `cText()` relies on the zero after what it wrote, keeps its fields private for the same reason. - A struct field of type `CStr` needs a region parameter, as a `StringView`'s does (N0270). - Like a `StringView`, a `CStr` may be a constant's type (§10) and a static's (§39.2), initialised by a `c"…"`. - `"…\0".ptr as *CChar` stays valid, but nothing checks that its `\0` is there, and its view's length counts it. Diagnostics: none new. ## 23. `Vector` and `String` (M6d) ### 23.1 `Vector` ```ceesafe fn sum(xs: Slice) -> i32 { var total = 0; for (x in xs) { total += x; } return total; } fn main() -> i32 { var v = Vector.create(); // no allocation until the first push v.push(1)!; // Result v.push(v.length as i32)!; // two-phase borrow (B5) let r = borrow v[0]; // v.push(3)!; // error[N0240]: `v` is borrowed by `r` Console.writeLine(r); v.push(3)!; // fine: `r` is dead Console.writeLine(sum(v)); // a vector argument for a slice parameter return 0; } ``` - `Vector` owns a growable, contiguous buffer of `T` values obtained from allocator type `A` (H2). `Vector` means `Vector`. `A` must be an allocator type (N0263). `T` is any value type except `void` and scoped types (N0246; references in aggregates are OD-3). - A vector is a **move type** that needs drop and is never Copy. It is not scoped. Destroying it destroys the elements from index 0 upward, then returns the buffer to `A`. - **Creation** (built-in functions on the type; the type arguments come from the explicit list or, without one, from an expected `Vector` or `Result, _>`; N0260 otherwise): - `Vector.create()`: an empty vector. It allocates nothing; - `Vector.withCapacity(n)` (`n: usize`): `Result, AllocError>`, an empty vector with room for `n` elements (`n == 0` allocates nothing). - **Members:** - `v.length` and `v.capacity` (`usize`); - `v[i]` is element `i`, bounds-checked (panic "index out of bounds"). It is a place, like an element of a slice: it can be read (Copy `T`), borrowed (`borrow v[i]`, `borrow mut v[i]`) and assigned (`v[i] = x` destroys the old element). Writing needs a mutable vector: a `var`, or one reached through `borrow mut`. Moving an element out is rejected (N0251), as for a slice; - `v.push(x)`: `Result`. It appends `x`, growing the buffer when it is full (new capacity: twice the old, at least 4). If the allocation fails, `x` is destroyed, the vector is unchanged, and the result is `Err(AllocError.OutOfMemory)`; - `v.pop()`: `T?`, the last element moved out, or `null` if the vector is empty; - `v.clear()`: destroys every element; the capacity stays; - `v.truncate(n)` (M11 G8c, `n: usize`): if `n < v.length`, destroys the elements from index `n` up, lowest first, and the length becomes `n`; otherwise nothing changes. The capacity stays; - `v.extend(s)` (`s: Slice`), `v.resize(n, fill)` (`n: usize`, `fill: T`) and `v.reserve(extra)` (`extra: usize`), ADR 0071 §2.1: `Result`. Each grows the buffer at most once (as `push` grows it, to at least the length needed) and leaves the vector unchanged if that fails. `extend` appends a copy of `s`; `resize` appends copies of `fill` up to length `n`, or truncates to `n`; `reserve` makes room for `extra` more elements and changes nothing else. `extend` and `resize` need a Copy `T` (N0259). They take `borrow mut this` and are two-phase (B5), so `v.resize(v.length + 1, 0)` is allowed; `v.extend(v.view())` is not (N0241); - `v.insert(at, x)`: `Result`, puts `x` at index `at` and moves the later elements up (`at == length` appends); `v.remove(at)`: `T`, takes the element at `at` out and moves the later ones down; `v.swapRemove(at)`: `T`, takes it out and puts the last element in its place (O(1), the order is not kept) (ADR 0075 §2.12). The index is checked at the call, `at <= length` for `insert` and `at < length` for the others, and panics there ("index out of bounds"). They are lib/core's VectorEditing: the elements move by `swap`, so they may be of any type. If `insert` cannot grow the vector, it is unchanged and `x` is destroyed; - for a `Vector`, byte order, searching and parsing (§16.6), on its elements; - `v.swap(i, j)`, and the methods taking closures (`sortBy`, `forEach`, `any`, `all`, `retain`): §16.5 and §36.4; - `v.view()`: the elements as a `Slice`, and `v.viewMut()` as a `SliceMut`; - a `Vector` argument for a `Slice` parameter is borrowed as `v.view()`, and `mut v` for a `SliceMut` parameter as `v.viewMut()` (like a box's contents, §18.2); - `for (x in v)` is `for (x in v.view())` (§16.4). - **Calls take `this` like methods:** `push`, `pop`, `clear` and `truncate` take `borrow mut this`, and their arguments are evaluated before the mutable loan starts (two-phase, B5, `lifetimes.md` §5). `length`, `capacity`, `view` and element reads take `borrow this`. - **Invalidation (H2, acceptance criterion 1 of M6):** an element borrow, a view, or anything derived from them is a loan of the vector. While one is live, `push`, `pop`, `clear`, element writes, `viewMut`, moving the vector and destroying it are rejected (N0240, N0241, N0243), exactly as for a place and its fields. The vector makes **no address-stability promise**: growth moves the elements. - No `==`, no `Copy`. - **Layout (v0):** `{ T *ptr; u64 length; u64 capacity }` (24 bytes on LP64). An empty vector's pointer is a valid, aligned, never-dereferenced address (not null), so views of it are valid. ### 23.2 `String` ```ceesafe fn greet(name: StringView) -> Result { var s = String.withCapacity(name.length + 7)?; s.append("Hello, ")?; s.append(name)?; return Ok(s); } fn main() -> i32 { let msg = greet("CeeSafe")!; Console.writeLine(msg.view()); // a StringView borrowed from msg return 0; } ``` - `String` owns UTF-8 text in a growable buffer (`String` means `String`). It is a move type that needs drop, like `Vector`, but a distinct type. - **UTF-8 invariant:** text only enters a string through `append` of a `StringView`, which is always valid UTF-8 (§22.2), so a string's bytes are always valid UTF-8. Its bytes cannot be written: `s[i] = b` is rejected (N0211), and `borrow mut s[i]` and mutable slices of a string (N0248). - **Creation:** `String.create()` (no allocation) and `String.withCapacity(n)`: `Result, AllocError>`; `String.create()` for another allocator. - **Members:** - `s.append(text)` (`text: StringView`): `Result`, two-phase like `push`. Growth is as for vectors. On failure the string is unchanged; - `s.appendNumber(n)` (M11, G4c; `n` of any integer type, or a reference to one): appends `n` in decimal, with a `-` before a negative value and no leading zeros or `+`: `0`, `-128`, `18446744073709551615`. It is `Result`, two-phase and unchanged on failure, as for `append`. An integer literal argument has its usual default type (§4). Any other argument type is N0207; - formatting helpers (lib/core, ADR 0087 §2.9), each `Result`: `s.appendHex(n, minWidth, upper)` (`n: u64`; at least `minWidth` hexadecimal digits, zeros before them, uppercase when `upper`; no `0x`), `s.appendOctal(n)`, `s.appendFixed(f, digits)` (`f: f64`; exactly `digits` digits after the point, none and no point for 0, rounded half to even on the exact binary value as Rust's `{:.N}` and C's `%.Nf`; `NaN`, `inf`, `-inf`; a negative zero keeps its `-`), and `s.appendPadded(text, width, align, fill)` (`text` in a field of `width` Unicode scalar values, `align` a `TextAlign` (`Left`, `Right`, `Center`, whose extra fill goes after), `fill` a Unicode scalar value, else a panic). They are lib/core's `AppendFormatted` interface, implemented for `Vector` in lib/core and for `String` in lib/std (hosted), over lib/core's `Formatted`, which allocates nothing of its own; `VectorBytes.appendHex(mut v, …)` and the others are the same for a byte vector; - `s.clear()`, `s.length` (bytes), `s.capacity`; - `s.view()`: the text as a `StringView`, a loan of `s`; a `String` argument for a `StringView` parameter is borrowed the same way; - read-only access as for a view: `s[i]` (`u8`), `s[lo..hi]` (a `StringView`, boundary-checked), `s.bytes()`; - printing: `Console.writeLine(s)` prints the text of a string place. - Appending a view of the string itself (`s.append(s.view())`) is rejected (N0240): the view is a live shared loan of `s` when the call's mutable loan starts. - **Layout (v0):** as `Vector`. Diagnostics: none new. The errors reuse N0205, N0207, N0209, N0211, N0240–N0251, N0260 and N0263. ## 24. `SlotMap`, `Handle` and `StableVector` (M6e) ### 24.1 `SlotMap` and `Handle` ```ceesafe struct Node { value: i32; next: Handle?; } fn main() { var nodes = SlotMap.create(); let a = nodes.insert(Node { value: 1, next: null })!; let b = nodes.insert(Node { value: 2, next: a })!; // a handle is a plain value, not a loan match (nodes.get(b)) { Some(node) => { Console.writeLine(node.value); } null => { } } let removed = nodes.remove(a); // `a` is now stale Console.writeLine(nodes.get(a) == null); // true: generation mismatch } ``` - `SlotMap` owns values of `T` in reusable slots from allocator type `A` (`SlotMap` means `SlotMap`; N0263). `T` is any value type except `void` and scoped types (N0246). A slot map is a move type that needs drop, and is not scoped. Destroying it destroys the values it holds, then frees its storage. - `Handle` names one inserted value: a slot index and the slot's generation. It is a **Copy value, not a loan**: it can be stored in structs, vectors and other slot maps (unlike a reference, E3), and it outlives inserts, removals and moves of the map. `==` and `!=` compare handles. Handles are only made by `insert`. - **Operations:** - `SlotMap.create()`: an empty map; it allocates nothing (type arguments as for vectors, §23.1); - `m.insert(v)`: `Result, AllocError>`, taking `borrow mut this` (two-phase, B5). If the storage cannot grow, `v` is destroyed and the result is `Err(AllocError.OutOfMemory)`; - `m.get(h)`: `borrow T?` (a loan of `m`), `null` if `h` does not name a live value; - `m.getMut(h)`: `borrow mut T?` (a mutable loan of `m`; needs a mutable map); - `m.remove(h)`: `T?`, the value moved out (or `null`), taking `borrow mut this`; - `m.contains(h)`: `bool`; `m.length`: the number of live values (`usize`). - **Validation:** every access compares the handle's generation with the slot's. A removed value's handles never match again: a slot's generation changes on every insert and removal, and a slot whose generation would wrap around is retired instead of reused. A handle used with another map of the same `T` is memory-safe: it yields that map's value in that slot, or `null` (a logic error, as with any index). - **Invalidation:** references from `get`/`getMut` are loans of the map, so `insert` and `remove` are rejected while one is live (N0241). The map may reallocate its slots: it promises stable **identity** (handles), not stable addresses. - **Layout (v0):** `{ slot *ptr; u64 length; u64 capacity; u64 count; u32 free }` (40 bytes), each slot `{ u32 generation; u32 nextFree; T value }`; `Handle` is `{ u32 index; u32 generation }`. ### 24.2 `StableVector` ```ceesafe fn main() { let names = StableVector.create(); // `let`: push needs only `borrow this` names.push(10)!; let first = borrow names[0]; names.push(20)!; // fine: elements never move Console.writeLine(first); } ``` - `StableVector` is a **grow-only** sequence whose elements never move (`memory-model.md` §4): it allocates chunks of 8, 16, 32, … elements and never reallocates them. Ownership, element types, `create()` and destruction are as for `Vector` (§23.1). - `sv.push(x)`: `Result` (the new element's index). It takes **`borrow this`**: appending changes only the length and the chunk table, never an existing element, so element borrows stay valid across it (interior mutability; like region allocation, §19.2). On failure `x` is destroyed and the vector is unchanged. - `sv.length` (`usize`) and `sv[i]` (a bounds-checked place, as for vectors: read, `borrow`, `borrow mut` and assignment, the last two needing a mutable vector). - No `pop`, `clear`, views or `for` (the elements are not contiguous): iterate with `for (i in 0..sv.length)`. A `borrow mut sv[i]` is a mutable loan of `sv`, so `push` is rejected while one is live (N0241). - `Send` with a `Send` element, but never `Share` (§38.1): one shared `StableVector` cannot be pushed to from two threads. Share it behind a `Mutex`. **Layout (v0):** `{ void *chunks[32]; u64 length }` (264 bytes); element `i` is in chunk `⌊log2(i + 8)⌋ − 3`. At most `8 · (2³² − 1)` elements; beyond that `push` fails with `OutOfMemory`. Diagnostics: none new. The errors reuse N0205, N0209, N0240–N0251, N0260 and N0263. ### 24.3 `HashMap` (lib/std, ADR 0052) - **Keys.** `K: HashKey`, a lib/std interface: `hash(borrow this, h: borrow mut Hasher)` feeds what identifies the key, and `sameAs(borrow this, other: borrow K)` compares two keys. lib/std implements it for the integer types, `bool`, `String` and `Vector`; a program implements it for its own types, or derives it (`@derive(Equal, HashKey)`, §28.7, through `deriveHash>(key: borrow K, h: borrow mut Hasher)`). A key type without it is N0268. - **The hash.** `Hasher` is SipHash-1-3 keyed by 128 bits. `HashMap.create()` uses a key read once per process from the system's random source (`getrandom`, `arc4random_buf`, `rand_s`; fixed in the interpreter and CosmoOS userland), so colliding keys cannot be chosen against a map. `HashMap.withSeed(k0, k1)` fixes the key. - **Operations:** - `put(key, value)`: `Result`, the old value if the key was there; - `get(borrow key)`, `getMut`: an optional reference; - `contains`, `remove(borrow key)` (`V?`), `clear`, `length()`; - `keyAt(i)` and `valueAt(i)` for `i` in `0..length()`. - **Order.** Iteration is in insertion order, except that a removal moves the last entry into the removed one's place. So the order never depends on the hash or its key, and a program's output is the same everywhere. - **Failure.** Only `put` allocates. A failed `put` leaves the map as it was (docs/limits.md §2): a new entry's table grows first, and a replaced value goes in at the end before it is swapped into place. - **Methods of `String` and `Vector`.** A call naming no built-in method reaches an impl's method (ADR 0052 §3.3), so `impl I for String` is usable. - **The allocator.** The system heap. ## 25. Region parameters (M6x; OD-3, ADR 0006) ```ceesafe enum Kind { Word, Number } struct Token { kind: Kind; text: StringView; } struct Lexer { input: StringView; position: usize; } fn next(lexer: borrow mut Lexer) -> Token? { … } // the token views the input fn split(line: StringView) -> Result>, AllocError> { … } ``` ### 25.1 Declaring regions - Structs, enums and functions may declare **region parameters** before their type parameters: `struct Lexer`, `fn f(…)`. In a parameter list, `region` followed by a name declares a region; `region` alone is still an ordinary name. - A method may use its struct's regions. `static` is the built-in region of static data (string literals); it outlives everything. - A method of a struct or of an inherent impl may also declare regions of its own (ADR 0074 §2.4), as a free function does: `fn first(borrow this, other: borrow(a) Vector) -> Slice`. Its region names may not repeat the struct's or the impl's (N0202). An interface's methods, and the methods of an `impl` of an interface, may not (N0216): conformance compares the signatures, which would have to match the region lists. ### 25.2 Region arguments - A type with region parameters takes region arguments first, then its type arguments: `Token`, `Pair`. The built-in scoped types take one region optionally: `StringView`, `Slice`, `SliceMut`, and references `borrow(r) T`, `borrow mut(r) T`. - Region arguments may be **omitted** (all of them, never some): in function bodies they are inferred, in signatures elided (§25.6), and in fields they are simply not checked (regions are erased, §25.5). - A written region must be in scope: a region parameter of the enclosing item or function, or `static`. In a function's result, `borrow(p)` may also name a parameter, as in v0 (§13.5). Otherwise, or with the wrong number of arguments: N0273. `static` in a parameter's type is N0273 too (§25.6, ADR 0068). ### 25.3 Scoped types A type is **scoped** (§19.1) iff it is a reference, slice or `StringView`, a scoped region value, a `PoolBox`, an optional or result with a scoped part, a struct or enum that declares region parameters, an instance of a generic struct or enum with a scoped type argument, or a `Vector`, `StableVector` or `SlotMap` with a scoped element. `Handle` is not scoped (it holds no loan). A struct or enum with region parameters implements an interface as `impl I for S { … }` (ADR 0054): the impl's region names are in scope in its target and in its methods' signatures and bodies, as a struct's are in its own methods. Such an impl's methods do not declare regions of their own (N0216, §25.1); an inherent impl's may. Such a type satisfies bounds (`fn f(b: borrow mut B)`) and is used statically; it does not coerce to `dyn I` (§37.2). ### 25.4 Where scoped values may be stored - **Fields and payloads:** a struct field or enum payload may have a scoped type only if the struct or enum declares at least one region parameter, or the type is a type parameter (its instances are scoped then, §25.3). Otherwise: N0270. - **Generic instances:** a user generic type or function may be instantiated with scoped type arguments. If the generic needs an unscoped type there (it boxes `T`, makes an array of `T`, …), the instantiation is rejected where it is written: N0274. - **Collections:** `Vector`, `StableVector` and `SlotMap` elements may be scoped; so may the value passed to a region's `alloc`, except a bare reference (N0264: region memory holds values). - **Assignment:** assigning to a reference *local* still writes through it (§13.3). Assigning to a field or element of reference type replaces the reference stored there (`token.text = view`). - **Arrays and slices** (ADR 0064 §2): their elements may be scoped values that are Copy: views (`StringView`, `Slice`) and structs or enums holding them, but not bare `borrow T` references. All elements share the element type's regions, so `[StringView; N]` and `Slice>` hold loans of `r`. An array of scoped elements is itself scoped, like a `Vector` of them (§19.1): its loans are checked as a whole, as its places are (§16.3). A slice's elements live at least as long as the slice, as a Vector's view's do. - **Still rejected (N0246):** `Box`, `Pool`, statics, constants other than views (§10), `borrow T` array and slice elements, and written references to references. **Region values and pool boxes** (`FixedBufferAllocator`, `ScratchFrame`, `PoolBox`) stay out of every place this section opens up (fields, payloads, collection elements, type arguments of user types): they must stay pinned or LIFO in locals. ### 25.5 Regions are erased Regions do not change types: `Token` and `Token` are the same type, and a value's loans are one set (ADR 0006). Region arguments matter only for the checks of §25.2–§25.4 and for signatures. ### 25.6 Signatures: flows Each parameter and the result of a function have **flow names**: - the regions written in their types; `borrow(p)` in the result contributes `p`'s flow names; - the type parameters their types mention; - for a parameter of scoped type without written regions: a fresh name of its own (elision); - for a result of scoped type without written regions: the flow names of the single scoped parameter (v0 elision); with several scoped parameters and none written, N0245 (as in v0). - for a result `borrow T` or `borrow mut T` (or an optional of one) with no sources or regions written on the reference itself, whose pointee names regions (`-> borrow HeaderList`): the reference's own region is elided as above, when there is a single scoped parameter. So the result is tied to that parameter, and is not a deep result of it (B18). With several scoped parameters it is not elided: write `borrow(p)`. - for a result whose written regions are all `static` (`StringView`, `Slice`, `S`, `borrow(static) T`, or an optional or array of one): none, and no elision (ADR 0068). It borrows from no parameter, so the body may return only static data (N0245 for a parameter's loan, N0244 for a local's), and a caller treats the result as static data: it outlives the arguments and may be stored in a static (§39.3). `static` is never a flow name. It is written only in results and bodies: in a parameter's type (a function's, a method's or an interface method's) it is N0273 (ADR 0068), since a call does not check that an argument is static data, and through a function value, a `dyn` method or a bound it could not. A string literal still passes to a parameter without `static`. A function's **flows** are then: - **result ← parameter `i`** iff they share a flow name: the result may carry `i`'s loans (this generalises v0's return sources, §13.5); - **deep results** (ADR 0074 §2.4). A reference parameter `i` that is not a view (`borrow T`, `borrow mut T`, the receiver included) has an **outer** name of its own: its elided name, or the regions written on the reference (`borrow(a) T`). A result that shares a flow name with `i` but names none of `i`'s outer names, and is not tied to `i` by elision or `borrow(i)`, is a **deep** result of `i`: it names only regions that `i`'s referent holds (`fn token(borrow mut this) -> Slice` in `struct Lexer`). A result whose type is a type parameter (`fn take(v: borrow mut Vector) -> T`) is a deep result of each reference parameter it would be tied to by elision, since `T` cannot name a reference's elided region (ADR 0075 §8). It carries the referent's loans, not the loan of `i` itself: - **callee:** a deep result may carry what the referent holds (a copy of a view field, a slice of one, a borrow through a reference stored in it), but not a loan of the referent's own storage, a place reached through `i` without passing another reference (`this.buf[0..2]` for a `buf: [u8; 4]`), nor `i` itself: N0245. Such a function names `i`'s own region instead (`-> Slice`, elided); - **caller:** for a deep result, an argument `borrow p` contributes `p`'s loans (what `p` holds), not the new loan of `p`; a reference parameter passed on contributes what it refers to. So `let t = lexer.token(); lexer.skipSpace();` is accepted, and `t` is tied to the text the lexer views. An argument that holds the result of a call `f(a)` whose result passes through `a` contributes what `a` would as that argument (B18): a `borrow T` result tied to one reference parameter alone, whose pointee names only regions that parameter's referent holds (none of its own). So `o.inner().first()` with `inner(borrow this) -> borrow Inner` is tied to `r`, not to `o`. A pointee that names no region but may hold loans (`-> borrow Inner`) is not followed. - **parameter `i` ← parameter `j`**, for `i` of type `borrow mut T` whose `T` shares a flow name with `j`: the callee may store `j`'s loans into what `i` refers to. A v0 signature (no regions) has exactly its v0 return sources and no parameter flows. ### 25.7 Borrow checking (`lifetimes.md` §3) With one loan set per value: 1. Writing a scoped value into a place of a local (a field, an element, a payload) adds its loans to that local. 2. Writing it through a reference adds its loans to the reference and to what the reference was borrowed from, followed back through reborrows, calls (by their result flows) and region allocation. If that reaches a `borrow mut` parameter `p` of the current function, the value's loans must all come from parameters that flow into `p` (§25.6): otherwise N0272. What `p`'s own referent held (its deep loans, not the loan of `p` itself) may always go back into it (ADR 0075 §8): `let x = take(mut this.a); this.b.push(x)!;`. 3. `push`, `insert` and `append` write their argument through their receiver (rule 2). 4. At a call, an argument that flows into a `borrow mut` parameter adds its loans to what that argument refers to. 5. In generic code, values whose type mentions a type parameter carry loans like scoped values, unless it mentions it only inside a `dyn` type (an interface's arguments, a `dyn fn`'s signature): a `dyn` value carries no loans of its own (§37.2), so `Box>` is stored where `S` is a type parameter as it is for a concrete `S`. 6. Destroying a value that is scoped and needs drop is a use of its loans: its `drop` may read them, so they must still be live (N0243 otherwise). Diagnostics: N0270, N0272, N0273, N0274 (`diagnostics.md`). ## 26. Program arguments, files and sockets (C3) ```ceesafe fn main() -> i32 { let argument = Process.argument(1); if (argument == null) { Console.writeLine("usage: size FILE"); return 2; } let path = argument!; match (File.open(path)) { Ok(file) => { var buffer = [0u8; 4096]; var total: u64 = 0; loop { match (file.read(mut buffer[..])) { Ok(n) => { if (n == 0) { break; } total += n as u64; } Err(e) => { Console.writeLine(e.message()); return 1; } } } Console.writeLine(total); return 0; } Err(e) => { Console.write(path); Console.write(": "); Console.writeLine(e.message()); return 1; } } } ``` ### 26.1 Program arguments - `Process.argumentCount()`: `usize`, the number of command-line arguments including the program name (argument 0). - `Process.argument(i)`: `StringView?`, argument `i` as a view of static text (no loans), or `null` if `i` is out of range or the argument's bytes are not valid UTF-8 (the `StringView` invariant, §22.2). Arguments are never copied. ### 26.2 `IoError` A built-in enum, like `AllocError`, with the variants `NotFound`, `PermissionDenied`, `AlreadyExists`, `IsDirectory`, `NotDirectory`, `InvalidInput`, `BrokenPipe`, `ConnectionRefused`, `AddressInUse`, `OutOfMemory`, `Other(i32)` (the platform's error number), and after it (ADR 0044, so the earlier variants keep their tags) the network kinds `ConnectionReset`, `ConnectionAborted`, `NotConnected`, `TimedOut`, `HostUnreachable`, `NetworkUnreachable`, `AddressNotAvailable` and `Unsupported`, and after them (ADR 0060) `HostNotFound`, the resolver's "no such host", and (ADR 0077) `StorageFull`: `ENOSPC` or `EDQUOT`, Windows' `ERROR_DISK_FULL`. `e.message()` is a `StringView` of static text describing it, glibc's `strerror` wording on every platform: "No such file or directory", "Permission denied", …, "Connection reset by peer", "Software caused connection abort", "Transport endpoint is not connected", "Connection timed out", "No route to host", "Network is unreachable", "Cannot assign requested address", "Operation not supported", "Host not found", which no errno has, and "No space left on device"; `Other`'s is "Input/output error". | Variant | POSIX | Winsock | |---|---|---| | `ConnectionReset` | `ECONNRESET` | `WSAECONNRESET` | | `ConnectionAborted` | `ECONNABORTED` | `WSAECONNABORTED` | | `NotConnected` | `ENOTCONN` | `WSAENOTCONN` | | `TimedOut` | `ETIMEDOUT` | `WSAETIMEDOUT` | | `HostUnreachable` | `EHOSTUNREACH` | `WSAEHOSTUNREACH` | | `NetworkUnreachable` | `ENETUNREACH` | `WSAENETUNREACH` | | `AddressNotAvailable` | `EADDRNOTAVAIL` | `WSAEADDRNOTAVAIL` | | `Unsupported` | `EOPNOTSUPP`, `ENOTSUP`, `ENOPROTOOPT`, `EAFNOSUPPORT`, `EPROTONOSUPPORT` | their `WSAE` forms | | `HostNotFound` | `getaddrinfo`'s `EAI_NONAME`, `EAI_NODATA`, `EAI_ADDRFAMILY` | `WSAHOST_NOT_FOUND`, `WSANO_DATA` | `IoError.TimedOut` is the kernel's (a connect or keep-alive gave up); `AsyncError.TimedOut` is a deadline of the program's. A `match` that names the first eleven variants and has no `_` arm does not cover the network kinds (N0256): a source change of 0.7.0; one that names the first nineteen does not cover `HostNotFound` (N0256): a source change of ADR 0060's release; one that names the first twenty does not cover `StorageFull`: a source change of ADR 0077's. ### 26.3 Files and the console - `File` owns an open file. It is a **move type that needs drop**: destroying it closes the file (an error from closing is ignored). It is not scoped and not Copy. - `File.open(path)`: `Result` for reading; `File.create(path)`: for writing, created or truncated; `File.remove(path)`: `Result`. `path` is a `StringView`; a path that contains a zero byte or is longer than 4095 bytes is `InvalidInput`. - A file may be removed or renamed while a `File` holds it open, natively on every system: the name goes, and the open `File` keeps reading and writing the file until it is dropped. Windows opens every file sharing delete (`FILE_SHARE_DELETE`), which its C runtime's `_open` does not. - **Durable files** (ADR 0075 §2.1), each `Result<…, IoError>`: - `File.openReadWrite(path)` opens an existing file for reading and writing (`NotFound` if missing); `File.openOrCreate(path)` creates a missing one; `File.createNew(path)` creates one and is `AlreadyExists` if something is there. None truncates. A created file gets mode 0666 & ~umask. - `f.readAt(mut buffer, offset: u64)`: `usize`, one positional read; 0 at or past the end. It does not move the position `read` and `write` use. - `f.writeAt(bytes, offset: u64)`: writes every byte at `offset`, or fails; it does not move the position. - `f.syncData()`: the data, and the metadata needed to read it back, reach stable storage (`fdatasync` on Linux; `fcntl(F_FULLFSYNC)` on Apple systems, whose `fsync` does not flush the drive's cache, falling back to `fsync` only where the file system refuses it; `FlushFileBuffers` on Windows). `f.syncAll()` includes all metadata. **After a failed sync the file's durable contents are unknown**: Linux may already have marked the dirty pages clean, so a later successful sync proves nothing about the earlier writes. A program that needs durability treats the file as failed. - `f.length()`: `u64`; `f.setLength(n: u64)` shrinks the file or extends it with zeros. - Each takes `borrow this` (since ADR 0075 §8; 0.19.0 took `borrow mut this`): none moves the position `read` and `write` use, so a file shared by reference can be read and synced. Interrupted calls are retried. On Windows, an `OVERLAPPED` offset would move the handle's own position, so `readAt` and `writeAt` transfer through a second handle on the same file (`ReOpenFile`), which has its own position: the descriptor's position never moves, even for a `read` or `write` on another thread during the call. The descriptor must still be open for reading (`readAt`) or writing (`writeAt`), as with `pread` and `pwrite`, and each call opens and closes that second handle. - `File.openAppend(path)` (ADR 0077): write-only, created if missing, every write at the end (`O_APPEND`). - `File.modified(path)` (ADR 0077, lib/std): `Result`, the last modification time. - `File.openRegular(path)` (ADR 0078 §3): opens `path` for reading if it is a regular file, never waiting on a FIFO (POSIX `O_NONBLOCK | O_NOCTTY`, then `fstat`, then `O_NONBLOCK` cleared); a directory is `IsDirectory`, anything else that is not a regular file `InvalidInput`. - `f.modified()` and `f.kind()` (ADR 0078 §3, lib/std's `FileMetadata`; `borrow this`): the open file's `Result` and `Result`, from `fstat` (`_fstat64` on Windows), not its path, so they describe the file that is read even if another was renamed into its place. `kind` is `File`, `Directory`, or `Other` for a device, FIFO or socket. The interpreter answers `Unsupported` for `modified`, and takes `kind` and `openRegular`'s check from the path. - `File.setPermissions(path, mode)` (ADR 0077, lib/std): `chmod` of the low 12 bits (`0o640`); on Windows only the read-only attribute, set when the owner may not write. Called on a new, empty file before it is written, it keeps the data private under any umask. The interpreter answers `Unsupported` for this and `File.modified`. - `File.rename(from, to)` (lib/std): `rename(2)`, which atomically replaces `to`; Windows `MoveFileExW` with `MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH`. The new name is durable once its directory is synced. - The interpreter runs these on the host's files, reopening the file for each positional operation (as ADR 0051's read at an offset does); its sync does nothing, and its rename is a copy and a removal. Its sandbox refuses them. - `f.read(mut buffer)` (`buffer: SliceMut`, `f` taken as `borrow mut this`): `Result`, the number of bytes read, `0` at the end of the file. Interrupted reads are retried. - `f.write(bytes)` (`Slice`): `Result`; it writes all the bytes or fails. - `Console.read(mut buffer)`: reads standard input like `f.read`. `Console.writeBytes(bytes)` writes raw bytes to standard output, in order with `Console.write`. - `Console.writeError(text)` and `Console.writeErrorLine(text)` (`text: StringView`) write to standard error; standard output is flushed first, so the two keep their relative order. - `Console.flush()` (ADR 0077) writes what standard output holds buffered, so it survives the process being killed. It is hosted only (lib/std's `HostServices.flush`). Standard error is not buffered. ### 26.4 TCP sockets - `TcpListener.bind(address, port)` (`address: StringView`, IPv4 or IPv6 text without a zone such as `"127.0.0.1"`, `"0.0.0.0"`, `"::1"` or `"::"`, ADR 0044; `port: u16`, `0` for any free port): `Result`, listening, with the platform's largest backlog (`SOMAXCONN`), `SO_REUSEADDR` on POSIX, and an IPv6 socket dual stack (`IPV6_V6ONLY` off). `listener.port()`: `Result`, the port it is bound to. `listener.accept()`: `Result`. - `TcpStream.connect(address, port)`: `Result`. `s.read(mut buffer)` and `s.write(bytes)` as for files; `s.shutdownWrite()`: `Result`, so the peer reads the end of the stream; `s.shutdownRead()` likewise ends this side's reading. - The stream's options (ADR 0044 §3.3), each `Result` and not blocking: `s.setNoDelay(on: bool)` (`TCP_NODELAY`), `s.setKeepAlive(idleSeconds: u32?)` (`SO_KEEPALIVE` and the idle time, `0` the system's; null turns it off), `s.setSendBuffer(bytes: u32)` and `s.setRecvBuffer(bytes: u32)` (`SO_SNDBUF`, `SO_RCVBUF`; the kernel may round, and Linux doubles), `s.setLinger(seconds: u32?)` (`SO_LINGER`: `0` makes the close reset the connection; null restores the default). Where the platform has no such option (CosmoOS): `Unsupported`. - The typed addresses (`SocketAddress`, `bindWith`, `connectTo`, `peerAddress`, `localAddress`) are the async types' (§40.4): a built-in cannot name a library type. - Both are move types that need drop; destroying one closes the socket. ### 26.5 Processes - `Process.run(program, arguments)` (`program: StringView`, found on `PATH`; `arguments: borrow Vector`, passed after the program name): `Result`. It starts the program with the caller's standard input, output and error, waits for it, and returns its exit status, or 128 plus the signal number if a signal ended it. Standard output is flushed first, so output keeps its order. A program that cannot be started is an error (`NotFound`, …); an argument with a zero byte is `InvalidInput`; if copying the arguments for the C library fails, `OutOfMemory`. ### 26.6 Runtime (Profile A and hosted) Implemented in `runtime/hosted/ceesafe_rt_io.c` over the C library's `open`, `read`, `write`, `close`, `unlink`, `socket`, `bind`, `listen`, `accept`, `connect`, `getsockname`, `shutdown`, `waitpid`, and `posix_spawnp` (hosted) or `spawnvp` (CosmoOS) (L1, OD-15: public libc functions only). CosmoOS's `struct sockaddr` (host-order port, 16 address bytes), its spawn API and its wait status encoding differ from POSIX; the runtime selects them at compile time. Generated code only calls functions declared in `ceesafe/rt.h`, so it still compiles freestanding. Diagnostics: none new. ### 26.7 Running a program with its output captured (M15 S7, ADR 0028) - `Process.capture(program, arguments, input)`: `Result`. - `program: StringView` is found on `PATH`, as for `Process.run`; `arguments: borrow Vector`. - `input: Slice?` is the child's standard input (empty if `null`). - It waits for the child. - `ProcessOutput` (lib/std) is a struct with three fields: - `status: i32`: the exit status, or 128 plus the signal that ended it, as `Process.run` reports it; - `stdout: Vector` and `stderr: Vector`: everything the child wrote to each stream. - **How.** The three streams go through files in a private temporary directory (§26.10), which is removed before `capture` returns. A child that writes much to both streams therefore cannot block, without `poll` or threads, which CosmoOS's libc does not offer. - POSIX: `posix_spawn` file actions. - CosmoOS: `spawnvp`'s handle mapping (its `spawn.h`). - Windows: `CreateProcess`, quoting the arguments by the C runtime's rules. - **Errors.** As for `Process.run`: a program that cannot be started is `NotFound` and the like, and a failed allocation is `OutOfMemory`. A mistake in the call's arguments is reported against `HostServices.capture`, lib/std's function behind it. ### 26.8 What is at a path - `File.kind(path)`: `Result`, following symbolic links: `PathKind.File`, `PathKind.Directory` or `PathKind.Other` (lib/std's enum), or `null` if nothing is there. - ADR 0028 called this `Path.kind`. It is `File.kind` because `Path` is a common name in programs (the corpus has one), and before ADR 0075 lib/std's names could not be redeclared in a hosted package (§2 now lets a package shadow them). ### 26.9 The environment - `Environment.variable(name)`: `StringView?`, the variable's value, or `null` if it is not set or not valid UTF-8. - Like `Process.argument` (§26.1), the value is a view of static text with no loans. CeeSafe has no way to change the environment, so it stays valid. C code that calls `setenv` breaks that, as it would in C. ### 26.10 Directories - `Directory.create(path)` makes one directory: `AlreadyExists` if something is there, `NotFound` if its parent is missing. - `Directory.remove(path)` removes an empty directory. - `Directory.list(path)`: `Result, IoError>`. - It gives the entries' names, without `.` and `..`, **sorted by byte value**, so a program's output never depends on the order a file system returns them in. - A name that is not valid UTF-8 is `InvalidInput`, not skipped, so a program cannot miss a file without a word. - `Directory.temporary()`: `Result`, the path of a new, empty directory that only this user can enter. - It is created under the platform's temporary directory (`TMPDIR` or `/tmp` on POSIX, `GetTempPath` on Windows) by trying names until `mkdir` makes one, which is atomic. - The caller removes it. - `Directory.current()`: `Result`, the working directory. - `Directory.space(path)` (ADR 0077): `Result`, the bytes of the volume that holds `path`: `total`, `free`, and `available` to this process, which is at most `free` where the system keeps a reserve (`statvfs`; Windows `GetDiskFreeSpaceExA`). The interpreter answers `Unsupported`, so a run stays reproducible. - `Directory.sync(path)` (ADR 0075 §2.1): makes the directory's entries (files created, renamed or removed in it) durable: `open(O_DIRECTORY)` and `fsync` on POSIX. Windows journals directory entries and cannot flush a directory, so there it only checks that `path` is a directory. - **Common rules for §26.7–§26.10.** - Paths are `StringView`s, under `File.open`'s rules: a zero byte or more than 4,095 bytes is `InvalidInput`. - All of these are hosted-only. In freestanding code, `Process` and `File` are N0293, and `Environment` and `Directory`, being lib/std's, are not defined. - They are lib/std code (`lib/std/host.cee`) over the runtime's `ceesafe_host_*` functions (`runtime/hosted/ceesafe_rt_io.c`). - The interpreter models them, and its sandbox (`ceesafec mcp`, `ceesafe_run`) refuses them all. ### 26.11 The wall clock (ADR 0055) - `Clock.now()` (lib/std) is a `UnixTime`: `seconds: i64` since 1970-01-01T00:00:00Z, leap seconds not counted (POSIX time), negative before it, and `nanoseconds: u32`, 0 to 999,999,999. `t.millis()` is `seconds * 1000 + nanoseconds / 1,000,000`, rounded toward minus infinity; `Clock.unixMillis()` is `Clock.now().millis()`. - It is the system's real time (`clock_gettime(CLOCK_REALTIME)`; Windows `GetSystemTimePreciseAsFileTime`), which may jump backward or forward: waits and measurements use `Timer`'s monotonic clock (§40.4). Where the system has no real-time clock (CosmoOS userland without one) it is 1970. - Hosted only: in freestanding code `Clock` and `UnixTime`, being lib/std's, are not defined (N0200). - The interpreter answers 2000-01-01T00:00:00Z (946,684,800 seconds) plus its virtual clock (§40.8), so a run stays reproducible. Calendar dates, time zones and formatting are not part of the library. - `Clock.monotonicNanos()` (ADR 0075 §2.6): `u64` nanoseconds on the monotonic clock from an arbitrary start, for durations and latencies. It never decreases within a process (`CLOCK_MONOTONIC`; `CLOCK_UPTIME_RAW` on Apple systems; `QueryPerformanceCounter` on Windows). The interpreter answers its virtual clock in nanoseconds. ### 26.12 Random bytes (ADR 0060) - `Random.fill(into)` (lib/std; `into: SliceMut`) is `Result`: it fills `into` from the system's cryptographic random source, read on every call: `getrandom(…, 0)` on Linux (retried on `EINTR` and for short reads; `/dev/urandom` where `getrandom` is missing or `ENOSYS`), `arc4random_buf` on macOS and the BSDs, `BCryptGenRandom(NULL, …, BCRYPT_USE_SYSTEM_PREFERRED_RNG)` on Windows. An empty slice is `Ok`. - Errors: `Unsupported` where there is no source (CosmoOS userland); the source's error otherwise (`IoError` of its errno; `Other(EIO)`'s kind on Windows), with `into` partly written. It never answers predictable bytes. - It has no effect (`@effects()`): it does not allocate, and waits only for the kernel's first seeding at boot. - Hosted only: in freestanding code `Random` is not defined (N0200). - The interpreter answers a fixed sequence (SplitMix64 from the seed `0x2b7e151628aed2a6`, each call taking whole 64-bit words, low byte first, and dropping what it does not use), so a run stays reproducible (§40.8). Its bytes are not secret. ### 26.13 Native windows (ADR 0069) - **`Window.open(title, width, height)`** (lib/std; `width`, `height: u32` in points) is `Result`. - **The window.** It is titled, resizable and visible, and on top of the program's other windows. - **`WindowError`:** - `Unsupported`: no backend for this system, the interpreter, or `width`/`height` 0 or over 16384; - `NoDisplay`: no window server, such as no `DISPLAY` or no X11 library; - `NotMainThread`: called on any thread but the program's first, on every system, so that programs stay portable to macOS, which allows windows only there; - `Resources`: the system refused. - **Ownership.** `Window` is a move type, neither `Send` nor `Share`. Its drop closes the window. - **Events.** `w.nextEvent()` is `WindowEvent?`: the next pending event, or null. It never blocks, and the events of one window come in order. `WindowEvent`: - `Closed`: the user asked to close the window, which stays open until it is dropped; - `Resized(WindowSize)`, in points; - `Focus(bool)`; - `PointerMoved(x, y)` and `PointerButton(WindowButton, down, x, y)`. `WindowButton` is `Primary`, `Secondary` or `Middle`. Positions are `f32` points from the content's top left; a pointer outside the window during a drag gives coordinates outside the size; - `Wheel(dx, dy)`, in lines, positive `dy` away from the user (scrolling up); - `Key(WindowKey, down, repeat, WindowModifiers)`. `WindowModifiers` has `shift`, `control`, `alt` and `command`: `command` is ⌘ on macOS and the Windows key elsewhere; - `Text(u32)`: a Unicode scalar the user typed, after the key's own `Key` event; control characters are not reported as `Text`. - **Keys.** `WindowKey` is one of: - `Tab`, `Enter`, `Escape`, `Backspace`, `Delete`, `Left`, `Right`, `Up`, `Down`, `Home`, `End`, `PageUp`, `PageDown` and `Space`; - `Character(u32)`: a letter, as lowercase, a digit, or the unshifted character of a punctuation key, on the layout in use; - `Function(u8)`: F1 to F12; - `Other(u32)`: the system's code for any other key. - **Size and drawing.** `w.size()` is `WindowSize { width, height }` in points, and `w.scale()` is pixels per point as an `f32`, such as 2.0 on a Retina display. - **`w.present(pixels: Slice, width, height)`** is `Result`. It shows a `width × height` image, rows top to bottom, scaled to fill the window's content. - **Pixels.** Each `u32` is one premultiplied RGBA8 pixel with bytes R, G, B, A in memory order: `r | g << 8 | b << 16 | a << 24`. The window is opaque, so alpha does not show through. - **Errors.** `pixels.length < width × height`, or a zero dimension, is `Resources`. - **Copying.** The pixels are copied during the call. A program draws at `size() × scale()` for one image pixel per screen pixel. - **`w.presentChanged(pixels, width, height, changed: Slice)`** (ADR 0070) is `present` with a promise: only the pixels inside the `changed` boxes differ from the last frame presented. `WindowBox` is `{ x, y, width, height }` in the image's pixels. The window may then show just those pixels. - It is a plain `present` when there is no last frame of this size, when a box is not inside the image, or when there are more than 64 boxes. - An empty `changed` shows nothing new, and a box of zero size is skipped. - Pixels outside the boxes may be shown from `pixels` or from the window's copy of the last frame, so a program that breaks the promise sees either. A scripted window shows the copy. - **Settings and waiting.** - `w.setCursor(WindowCursor)` takes `Default`, `Pointer`, `Text`, `Crosshair`, `Grab`, `ResizeHorizontal` or `ResizeVertical`, shown while the pointer is over the content. - `w.setTitle(title)` sets the title. - `w.waitFrame()` returns when about 1/60 s has passed since the previous `waitFrame`, or earlier when an event arrives, so an idle loop does not spin. - **Effects.** `open`, `present`, `presentChanged`, `waitFrame` and `setTitle` may sleep (`@effects(sleep)`). `nextEvent`, `size`, `scale`, `setCursor` and the drop have no effect. - **Backends** (`runtime/hosted/ceesafe_rt_window.c`): - **macOS:** AppKit through the Objective-C runtime, loaded with `dlopen` at the first `open`, so no program links it; - **Windows:** Win32 and GDI; - **Linux:** X11, `libX11.so.6` loaded with `dlopen`. The runtime file is part of a build only when the program calls a window function. - **Scripted windows** (for tests, ADR 0069 §2.3). When the environment variable `CEESAFE_WINDOW_SCRIPT` names a file, `open` makes a window that exists only in memory, on any system: - **Size and scale.** Its size is the requested one, until a `resize`, and its scale is 1. - **Frames.** Frame *n* is the time after the *n*-th `present`. The script's events for frame *n* are what `nextEvent` returns then, in the script's order. - **Script grammar.** The file holds `;`-separated `FRAME:WHAT` items: - `move:X,Y`; - `down:B,X,Y` and `up:B,X,Y`, with B 0 to 2 for primary, secondary and middle; - `click:X,Y`: a primary down on FRAME and its up on FRAME+1; - `wheel:DX,DY`; - `key:NAME` (down then up), `keydown:NAME` and `keyup:NAME`; - `type:TEXT`: one `Text` per character; - `resize:W,H`, `focus:0|1` and `close`. - **Key names:** `tab`, `enter`, `escape`, `backspace`, `delete`, `left`, `right`, `up`, `down`, `home`, `end`, `pageup`, `pagedown`, `space`, `f1`…`f12`, or one character. - **Frame dump.** When `CEESAFE_WINDOW_DUMP` names a file, each `present` replaces it with the line `ceesafe-window W H`, then the W×H pixels as RGBA bytes. - **Waiting.** `waitFrame` returns at once. - **Errors.** A script that cannot be read or parsed makes `open` `Resources`. - **Hosted only.** In freestanding code these types are not defined (N0200). The interpreter answers `Unsupported` to `open`. ## 27. Raw pointers and `unsafe` (M8a) The contract that unsafe code must keep, and what the compiler and the interpreter check, is in `unsafe.md`. This section gives the typing rules. ```ceesafe /// Safety: `p` points to `n` initialised `u32`s that nothing else writes while this runs. unsafe fn sum(p: *u32, n: usize) -> u64 { var total: u64 = 0; for (i in 0..n) { total += p.add(i).read() as u64; } return total; } /// Safety: the pointer comes from a live array and stays inside it. fn main() -> i32 { var a = [1u32, 2, 3, 4]; let p = borrow mut a[0] as *mut u32; unsafe { p.add(3).write(10); } Console.writeLine(unsafe { sum(p, a.length) }); // 16 return 0; } ``` ### 27.1 Types - `*T` (read-only) and `*mut T` (writable) are **raw pointers** to `T`. `T` is any value type, or `void` for an untyped pointer (`*void`, C's `void *`). It may not be a reference, slice, `StringView` or other scoped type (§19.1, §25.3), or `never` (N0281). Since M8d it may be a type parameter: an instantiation with a scoped type argument is then N0274, as for a box of `T`. - A raw pointer is one machine address: 8 bytes, aligned to 8 on LP64. It is **Copy**, needs no drop, holds no loans and is not scoped (U3). So it may appear anywhere a Copy value may: locals, parameters, results, fields, payloads and array elements. Constants of pointer type are not supported (N0216). - `*mut T` coerces to `*T` wherever a `*T` is expected (like `SliceMut` to `Slice`). - `==` and `!=` compare two pointers of the same type (after that coercion) by address. Ordering comparisons are not defined on pointers (N0207); compare `p as usize` instead. ### 27.2 Making pointers (safe) Making and converting a pointer is safe; only using one is not (§27.3). - `r as *T` for a reference `r: borrow T` or `borrow mut T`, and `r as *mut T` for `r: borrow mut T`. `borrow x as *T` parses as `(borrow x) as *T` (grammar §2.1). The borrow ends at once: the pointer holds no loan, so later uses of `x` are not checked against it. - `s.ptr` for a slice `s`: `*T` for `Slice`, `*mut T` for `SliceMut`; for a `StringView`, `*u8`. It is the address of element 0 (of where it would be, for an empty slice). - `p as *U` and `p as *mut U` for any raw pointer `p`, whatever its pointee and mutability. - `p as usize` for any raw pointer, and `n as *T` / `n as *mut T` for `n: usize` (also `*void`). An integer literal converts too: `0 as *mut T` is the **null pointer** (there is no `null` pointer literal: `null` is the empty optional, §22.1). - `p.isNull()`: `bool`. ### 27.3 Using pointers (unsafe) These are allowed only in an unsafe context (§27.4; N0280 otherwise). `p` is a raw pointer expression, evaluated once. - `p.read()`: the `T` at `p`, moved out (copied, for a Copy `T`). Afterwards the memory holds no `T`: the unsafe code must not read it as one, drop it or let anything else use it as one until it writes a new value there (`unsafe.md` U2). A type holding C state (§33) is N0282, since C state never moves. Since ADR 0027; before it, `T` had to be Copy. - `p.write(v)`: moves `v` to `p` (copies, for a Copy `T`) without reading or destroying what was there. `p` must be a `*mut T` (N0282). It returns `void`. For a type with `@placed` methods (§33.3) the store is a placement: they run at `p` afterwards, as for `Box.create`, and `v` may be the struct literal without its C state that `Box.create` takes (§33.2). - `p.readVolatile()` and `p.writeVolatile(v)` (§29.1) still need a Copy `T`. - `p.add(n)` and `p.sub(n)` (`n: usize`) and `p.offset(n)` (`n: isize`): the pointer `n` elements (`n × size(T)` bytes) after (`sub`: before) `p`, with the type of `p`. The arithmetic wraps modulo 2^64; it never panics. - `Ptr.toBorrow(p)`: a `borrow T` to the `T` at `p`; `Ptr.toBorrowMut(p)`: a `borrow mut T`, for `p: *mut T` (N0282 for `*T`). The reference holds **no loans** (U3): the borrow checker cannot protect it, so the unsafe code that makes it must bound its use, typically by returning it from a function whose signature ties it to a parameter (§13.5). - `Slice.fromRaw(p, n)`: a `Slice` of the `n` elements from `p`; `SliceMut.fromRaw(p, n)`: a `SliceMut`, for `p: *mut T`. Like `Ptr.toBorrow`, the slice holds no loans. - Calling an `unsafe fn` (§27.4). - None of these is defined for `*void`: cast it to a typed pointer first (N0282). ### 27.4 Unsafe contexts - `unsafe { statements }` is a statement; `unsafe { e }` is an expression whose value is `e`'s (grammar §2). In statement position, `unsafe {` always starts the statement form. - `unsafe fn` (a modifier after the visibility): calling the function is an unsafe operation, and its body is an unsafe context. `main` may not be `unsafe` (N0219). Methods may be `unsafe`; since C5, interface methods may be too (§30.1), and an `impl` must match (N0267). - Everything lexically inside an unsafe block, an unsafe expression or the body of an `unsafe fn` is in an unsafe context, including deferred statements. A nested `unsafe` is allowed. - **The Safety rule** (U2): an `unsafe fn`, and every function whose body contains an `unsafe` block or expression, must carry a doc comment (grammar §1.2) with a line that starts with `Safety:`. It states the contract: what callers must guarantee (for an `unsafe fn`) or why the unsafe operations inside are sound (for a safe function). Without one, N0283. - Safe code is checked exactly as before. An unsafe context only *permits* the operations of §27.3; it never relaxes the type, move, borrow or region rules for anything else. ### 27.5 Lowering - **NIR**: `p.read()` and `p.write(v)` are a copy from, and an assignment to, the place `(*t)` of a temporary `t` holding `p`: the same `Deref` projection as references, through a pointer. `Ptr.toBorrow`, `Ptr.toBorrowMut` and the pointer casts are `Cast`s; the arithmetic, `isNull`, `.ptr` and `fromRaw` are built-ins. The move and borrow checkers ignore places through raw pointers (no loans, no moves, no mutability check beyond §27.3). - **C** (c-backend §2): see `unsafe.md` §4. Pointer arithmetic is done on `uintptr_t`, so generated C never computes an out-of-object pointer. - **Placement** (M5, `memory-model.md` §5.1 rule 1c): a function that makes a raw pointer from a reference or calls `Ptr.toBorrow`/`toBorrowMut` or `fromRaw` keeps all its boxes on the heap ("address exposed"). Diagnostics: N0280 (an unsafe operation outside an unsafe context), N0281 (an invalid pointee type), N0282 (a pointer operation the pointer or its pointee does not allow), N0283 (no `Safety:` doc comment). Invalid pointer casts are N0208. ## 28. C interoperability (M8b) ```ceesafe extern "C" { fn strlen(s: *CChar) -> usize; fn puts(s: *CChar) -> i32; } @repr(C) struct Point { x: i32; y: i32; } /// The Manhattan length of a point, callable from C as `geo_length(struct Point)`. @export("geo_length") fn length(p: Point) -> i32 => abs32(p.x) + abs32(p.y); fn abs32(v: i32) -> i32 { if (v < 0) { return -v; } return v; } /// Safety: the literal ends in a zero byte, as `puts` and `strlen` need. fn main() -> i32 { let text = "hello from C\0"; let p = text.ptr as *CChar; unsafe { puts(p); } return truncate(unsafe { strlen(p) }); } ``` ### 28.1 Attributes An item may be preceded by attributes `@name` or `@name(arguments)` (grammar §2). The known ones: | Attribute | On | Meaning | |---|---|---| | `@export`, `@export("symbol")` | a top-level, non-generic function; a constant (C9); a static (ADR 0080) | callable from C under `symbol` (default: the function's name); for a constant or a static, C data under that name, §28.4, §39.6 | | `@cname("name")` | a field of an imported C struct (C9); an `extern "C"` function or static (ADR 0080) | its C member name, when that is not a CeeSafe identifier, §31.1; the C symbol of a function or static, §28.2, §39.6 | | `@cvalue("expression")` | a constant of an `extern "C"` block (ADR 0080) | the C constant expression whose value C asserts, §28.2 | | `@repr(C)` | a non-generic struct | a C-compatible struct, §28.5 | | `@repr(u8)` … `@repr(i64)` | a non-generic enum without payloads | passed to and from C as that integer type, §28.5 | | `@effects(…)` | a function, method or `extern` function | the effects it may have (§32.2) | | `@context(atomic)`, `@context(irq)` | a function or method; `atomic` also a struct | a context that forbids effects (§32.3) | | `@placed` | a method `fn m(borrow mut this)` | the type's placement initialiser (§33.3) | | `@callback` | a top-level function | a C address without a symbol, for C callbacks (§34.1) | | `@align(n)` | a non-generic struct | at least `n`-byte alignment (M8d, §29.4) | | `@noncopy` | a struct | a move type even if every field is Copy (OD-7; M8d) | | `@cfg("NAME")` | an `@export` function | its C symbol only in builds where the C macro `NAME` is non-zero (ADR 0043), §28.4 | | `@test` | a top-level function with no parameters, not generic, not exported, returning `void` or `Result` | a test that `ceesafec test` runs (ADR 0049; tooling.md §10); `build` leaves it out, and only the generated harness may call it (**N0348** otherwise) | | `@derive(Equal, HashKey, OrderKey)` | a struct or enum | one or more of these, each once: `==` and `!=`, a `HashKey` impl, an `OrderKey` impl (ADR 0087 §2.3, §28.7) | An unknown attribute, a known one in the wrong place or with the wrong arguments, or the same attribute twice is N0290. ### 28.2 `extern "C"` blocks - `extern "C" { … }` at the top level declares functions defined in C: `fn name(parameters) -> Result;`, each optionally preceded by attributes and a visibility, with no body, receiver, type parameters or region parameters (N0290). `"C"` is the only ABI (N0290). - Calling an extern function is an unsafe operation (U1, N0280): CeeSafe cannot check what C does. - Its C name is its CeeSafe name. It must be a valid C identifier that is not a C keyword (or `bool`, `true`, `false`, `NULL`, `main`), not reserved by C (starting with `__`, or `_` and an uppercase letter), and not owned by the runtime and generated code (starting with `ceesafe_`, `cee_`, `CEE_`, or `cee` and an uppercase letter; c-backend §2.1): N0292. Two declarations, or a declaration and an `@export`, with one C name are N0201, even in different packages: a C name has one declaration, which other packages call through its package (`sys.strlen`) when it is `public` (ADR 0081 §2.1). Of the declarations of one C name, one owns it and N0201 is reported at each of the others, after the other checks of foreign interfaces. A library's declaration (`lib/core`, `std`, `kernel`) owns it before any package's. Otherwise a declaration may own the name when no other declaration of it is in a package its package depends on, directly or not, so a dependency's owns it before its dependant's; of those that may, the owner is the one whose package's name comes first (bytewise; names are unique in a build), and within one package the first in the package's file order (tooling.md §4), then in the file: the choice does not depend on the order of `[dependencies]` (ADR 0081 §7). A note gives the owner's package and position. When the owner's package is a *direct* dependency of this declaration's (one its `[dependencies]` names, which `sys.strlen` needs) and both are extern declarations, the message says to call `sys.strlen` and remove this declaration, or, when sys's is not `public`, to make it `public` there; and if the two are identical in CeeSafe (the same parameter and result types, none of them in error, variadic-ness and attributes, `@cname` and `@effects` among them; an identical C signature is not enough), a fix removes this declaration: its lines, with its doc comment and a trailing `//` comment, and its `extern "C"` block when it was the block's only declaration. The fix does not change its call sites (ADR 0081 §2.2). - Parameter and result types must be **FFI-safe** (§28.3), or N0291. - **`@cname("symbol")`** on an extern function (ADR 0080 §3.5) gives its C symbol when its CeeSafe name must differ, as when a struct has the name (`struct cosmofs_stats` and `cosmofs_stats()`). The symbol follows the rules above. - **C constants** (ADR 0080 §3.1; grammar.md §2's `ExternConst`). In a block with a `@header`: - `const NAME: T = v;` is an ordinary constant (§10) with that value, which the generated C asserts equal to C's `NAME` (an object-like macro or an enumerator): `_Static_assert((NAME) == v, "…")`, naming the declaration's file and line. `T` is an integer type (an imported enum among them) or `bool` (N0291). `@cvalue("expression")` names a C constant expression in `NAME`'s place: `@cvalue("sizeof(struct stat)") const STAT_SIZE: usize = 144;`, or an `offsetof`. The string holds no quote, backslash or line break (N0290); it appears only in the assertion. `NAME`, without `@cvalue`, is a valid, unreserved C identifier (N0292). - `const NAME: T;` imports C's value: a read-only static (§39) whose C initialiser is `NAME` (or the `@cvalue` expression), so it is read at run time and is not a constant expression. `T` is an integer, `bool` or `*CChar` (a string macro; N0291). The interpreter cannot run a program that has one (a fault at start). - A function that a `@header` defines `static inline` may be declared and called (ADR 0030): the generated prototype has no storage class, so it takes the definition's internal linkage (C11 §6.2.2p4–5). CosmoOS's `list.h` helpers are called so (lib/kernel's `List`). - **Variadic functions** (ADR 0041 §3.3). `fn klog(level: u32, format: *CChar, ...);` declares a C variadic function: `...` ends its parameters, which must not be empty (N0290), and it is allowed in an `extern "C"` block only (N0100). A call passes the parameters as for any C function and then any number of further arguments, each of a type that C's default argument promotions (C11 §6.5.2.2p6–7) leave unchanged: `i32`, `u32`, `i64`, `u64`, `isize`, `usize`, `f64` or a raw pointer, `*CChar` among them, passed as its C type (§28.3). A narrower integer, `bool`, `f32` or anything else is N0347, whose message says how to widen it with `as`. Fewer arguments than parameters is N0209. - Like every C call, it is unsafe (N0280), and here the `/// Safety:` comment carries more: whether the further arguments agree with what the callee reads (a format string's conversions, a count) is not something CeeSafe checks, and C checks only a literal format, while the generated C passes a pointer. - It is called directly, with no wrapper function, so it has no C function pointer type and cannot be one (N0291), and it cannot return a `@repr` enum, which a wrapper would check (N0291). A unit that calls one disables C's warnings about a format that is not a literal (`-Wformat-security`, `-Wformat-nonliteral`), for the reason above. - **Formatting without a variadic call** (ADR 0041 §3.3). lib/core's `TextWriter` (`lib/core/format.cee`, every profile) writes into a caller's `SliceMut` as `snprintf` does, with no allocation: `create(buffer)`, then `text(s)`, `bytes(b)`, `byte(b)`, `decimal(u64)`, `signed(i64)` and `hex(u64, width)`; what does not fit is cut, the text stays zero-terminated in a buffer of at least one byte, and `written()`, `length()`, `wanted()` (every byte asked for, as `snprintf`'s result), `truncated()` and `cText()` (a `*CChar` to it) read it. lib/kernel's `KernelLog` (`lib/kernel/log.cee`, `--kernel cosmoos`) sends one: `send(level, line)` is `klog(level, "%s", text)`, `write(level, message)` a fixed message, and `panic(line)` is CosmoOS's `panic("%s", text)`; `LogLevel` is log.h's levels, `LOG_LINE_MAX` its 256. These are the only variadic calls, with a fixed format, so a port's messages are safe code. ### 28.3 FFI-safe types and their C types | CeeSafe | C, in prototypes and in `ceesafe_exports.h` | |---|---| | `i8`…`i64`, `u8`…`u64` | `int8_t`…`int64_t`, `uint8_t`…`uint64_t` | | `usize`, `isize` | `size_t`, `ptrdiff_t` | | `CLong`, `CULong` (ADR 0080) | `long`, `unsigned long` | | `CChar` (ADR 0080) | `char` | | `bool` | `bool` | | `*T`, `*mut T` | `T const *`, `T *`, where `T` is an integer, `bool`, a `@repr(C)` struct, or (recursively) such a pointer | | `*void`, `*mut void` | `const void *`, `void *` | | `*CChar`, `*mut CChar` | `const char *`, `char *` | | a `@repr(C)` struct `S` | `struct S` | | a `@repr(uN)` / `@repr(iN)` enum | that integer type | | `void` (results only) | `void` | Nothing else crosses: references, slices, string views, boxes, arrays, optionals, results and other structs and enums are N0291. (Arrays and imported C types may be fields of a `@repr(C)` struct, §28.5.) (A slice crosses as its `.ptr` and `.length`; a pointer to any other CeeSafe value crosses as `*void`.) A `@repr` enum crosses by value only, not behind a pointer or as a field of a `@repr(C)` struct: inside CeeSafe it is not a plain integer. `CChar` is C's `char`, whose signedness differs between targets. Since ADR 0080 §3.5 it is also a value: a parameter, result, local or field of C's `char`, 8 bits with no arithmetic. `b as CChar` (from `u8`) and `c as u8` convert it (C's conversion, modulo 256 on every target), `==` and `!=` compare it, and nothing else applies (N0207, N0208); `p as *u8` and `q as *CChar` convert pointers. It exists so that declarations of C library functions match the C library's own prototypes exactly. `CLong` and `CULong` (ADR 0080 §3.5) are C's `long` and `unsigned long`: integer types of their own, of the target's width, spelled `long` and `unsigned long` in C. The width is 32 bits on Windows (LLP64), the target of a hosted build when ceesafec runs there (ADR 0069), and 64 bits on every other target (LP64), freestanding ones included. It is part of the layouts ceesafec computes and asserts, so `[CULong; 3]` is 12 bytes on Windows and 24 elsewhere. A generated C file that uses one asserts `sizeof(long)`, so C generated for one target and compiled for another whose `long` is another width fails to compile. **The width is decided where `check` or `build` runs, not by the C compiler.** ceesafec has no target option, and `check` runs no C compiler, so the width is the rule above applied to the host: the target C compiler's `long` only when the C is compiled for the host's own kind of system (hosted Windows, or anything else LP64). Two consequences: - **A program's validity can depend on the host.** `n as CLong`, with `n` an `i64`, is accepted on macOS and Linux and is N0208 on Windows, where `as` would lose information; a `@repr(C)` struct with a `CLong` field has another size and layout there. Code meant for both converts with `truncate` or `checkedTruncate` (or from `i32`), and a struct's layout is that of the system it is checked on. - **C generated on one kind of host and compiled for the other fails to compile**, at the `_Static_assert(sizeof(long) == …)` every C file that uses `CLong` or `CULong` carries ("C's long is not the width of CeeSafe's CLong on this target"): never a program that runs with the wrong width. Cross-compiling generated C between Windows and an LP64 system is not supported for such programs. - **A freestanding LLP64 target (UEFI) is not supported**: freestanding builds are LP64 on every host, so C that uses `CLong` stops at the same assertion there. ### 28.4 Exports - `@export` makes a top-level, non-generic, non-method function callable from C, with the C types of §28.3 (N0291 otherwise). `main` cannot be exported (N0290). The symbol must be a valid, unreserved C name (N0292) and unique (N0201). - The function stays an ordinary CeeSafe function too. Its body is checked like any other; an exported function is not unsafe to call from CeeSafe. - **Configured exports** (ADR 0043). `@cfg("NAME")` on an `@export` function puts its C definition, its prototype and its line in `ceesafe_exports.h` inside `#if NAME` … `#endif`: the symbol exists in the builds where the C macro `NAME` is non-zero, as C's `#if NAME` around a definition would have it, and a header may define a stub of that name in the others. The CeeSafe function is compiled and checked in every build, and CeeSafe code may call it in any. `NAME` is the macro's name as a string, a valid C identifier (N0292), or `"!NAME"` for the builds where it is zero (ADR 0080); `@cfg` without it is N0290. - **Configured code** (ADR 0080 §3.3). What only some builds' C has is used only where it exists: - `@cfg("NAME")` (or `"!NAME"`) on an `extern "C"` function, for one a header declares only in some builds: its prototype and wrapper are emitted inside `#if NAME`. On a field of an imported struct or union, for a member only some builds' struct has: its `_Generic` and `sizeof` checks (§31.3) are inside `#if NAME`. - `if (cfg("NAME")) { … } else { … }`, whose condition is the call `cfg` of one string literal (`"NAME"` or `"!NAME"`, a C identifier: N0292) while nothing the program declares is named `cfg`, is C's `#if NAME` … `#else` … `#endif`: the blocks are compiled in the builds where the condition holds, or does not. `else if (cfg(…))` chains and `cfg` blocks nest. It is a statement: `cfg` as a value (`let x = if (cfg("X")) 1 else 2;`) is an unknown function (N0200). Both blocks are checked in every build, as one program. - A configured C function or field may be used only inside a `cfg` block of its own condition (N0350), so every build's C compiles. A configured export is not configured CeeSafe code: its CeeSafe function exists in every build. - The interpreter takes every macro as 0: `cfg("NAME")` is false, `cfg("!NAME")` true. - A switch read at run time through a C constant macro (§39.4, `static DEBUG: bool = CONFIG_DEBUG_ON();`) remains for code every build can compile. - **`callerAddress()`** (ADR 0030), valid only in an `@export` function's own body (N0340; not in a closure inside it), is a `usize`: the address the exported symbol's call returns to, as C's `__builtin_return_address(0)` in the export's wrapper (MSVC: `_ReturnAddress()`), passed to the body as a hidden argument so that inlining cannot change it. When CeeSafe calls the function, or the interpreter runs it, it is 0. CosmoOS's lockdep names call sites with it. - A C caller must call it with the declared types. A C value for a `@repr` enum parameter that is not one of its variants panics (`invalid enum value from C`) before the body runs. - **Exported constants** (C9, ADR 0014). `@export` or `@export("symbol")` on a constant (§10) of integer or `bool` type, or of a constant array type `[E; N]`, defines read-only C data with that symbol (default: the constant's name): `const CT symbol = v;` or `const CE symbol[N] = { … };`, declared in `ceesafe_exports.h` as `extern const CT symbol;` / `extern const CE symbol[N];`. The symbol follows the rules for functions (N0292, N0201). In CeeSafe the constant is used as any other constant. ### 28.5 C-compatible structs and enums - `@repr(C)` on a struct fixes its layout to C's rules (fields in declaration order, each aligned, the size rounded up to the alignment; CeeSafe's layout already is this, and M9 may reorder only structs without it). Its fields must be integers, `bool`, raw pointers as in §28.3, `@repr(C)` structs, imported C types by value (§31; ADR 0029), arrays `[E; N]` of any of these (ADR 0029), or `AtomicU32`, `AtomicU64` and `AtomicUSize` (ADR 0041; not `AtomicBool`, a `u32` where C's `bool` is a byte), and their names valid, unreserved C identifiers (N0291, N0292). In C it is `struct S` with members named as in CeeSafe, of the types CeeSafe uses inside its own C (`usize` is `uint64_t`, pointers have no `const`), so that the generated code and C code that includes `ceesafe_exports.h` declare the same struct type. It is otherwise an ordinary struct. - **C names.** A `@repr(C)` struct's C tag, and a `@repr` enum's C type name (`typedef uint8_t Shade;` and its constants `Shade_Light`, …, in `ceesafe_exports.h`), are the name it is declared with, in every package: a dependency `geo`'s `Point`, which CeeSafe calls `geo.Point`, is `struct Point` in C, in prototypes and in the exports header, as in `geo`'s own build, so C code that includes the header names it as declared. (Its internal name in the generated C, `ceeT_3geo_Point`, has the package's, c-backend.md §2.1.) A C name has one declaration, as for extern functions (§28.2; ADR 0081 §2.1): two `@repr(C)` structs with one name are N0201, even in different packages, and so are two `@repr` enums, or a `@repr` enum and a C function, export or exported constant, with one name (a struct tag is in C's other name space, so a struct and a function may share a name). The owner is chosen as in §28.2 (a dependency's before its dependant's, then by package name), and N0201 is reported at each other declaration, with a note at the owner's. An ordinary struct or enum has no such rule: its C name has its package's, so two packages may each declare one with the same name. Up to 0.27.0 a dependency's `@repr` type was spelled with its CeeSafe name in C (`struct geo.Point`), and every build that used one failed in the C compiler. - An atomic field (ADR 0041 §3.1) is its integer member in C: `uint32_t`, `uint64_t`, and `uint64_t` for `AtomicUSize`, as for `usize`. CeeSafe reaches it only through the atomic type's methods, which are the `__atomic_*` operations on the member's address, so C code that includes `ceesafe_exports.h` shares it by applying `__atomic_*` to the member (a non-atomic C access while CeeSafe may access it is a data race in C). - An array field is the C array member `CE f[N]` (nested arrays `CE f[N][M]`); in CeeSafe it is an ordinary array. - **C state** (ADR 0029). A `@repr(C)` struct with a field of an imported C type, directly or in an array or a `@repr(C)` field, is C-laid-out (§33.1), and every rule of §33 applies: it is never moved, copied or assigned in CeeSafe, and crosses to C only behind a pointer (N0294); CeeSafe makes one only in place (`Box.create` and its `@placed` initialiser, or a static). C makes one wherever C makes a struct: embedded in another struct, in a static, on the stack. CeeSafe code that receives a pointer to one relies on C having initialised it, which the exported function's `/// Safety:` comment states (§27.4). It may not have a `drop` (N0291): C makes and discards it without calling CeeSafe, so the drop would never run. - `@repr(uN)` / `@repr(iN)` on an enum without payloads gives each variant its index (0, 1, …) as that integer type, which must hold every index (N0290). Across the boundary the enum is that integer; a value from C that is not a variant's index panics (`invalid enum value from C`). ### 28.6 Building with C - `ceesafec build` accepts C source files (`.c`) besides CeeSafe files. They are compiled with the generated C and linked into the program, with the same C compiler and flags. - Every C file of the build has the runtime's headers on its include path. `ceesafe/foreign.h` (ADR 0060) is CeeSafe's allocator for C code: `ceesafe_alloc(size, CEE_ALLOC_FOREIGN)`, `ceesafe_realloc(p, size, CEE_ALLOC_FOREIGN)` and `ceesafe_free(p, CEE_ALLOC_FOREIGN)`, which follow `malloc`, `realloc` and `free` (`free(NULL)` does nothing, `realloc(NULL, n)` allocates; aligned for any C type; a failed realloc keeps the block). Their attempts are counted and fault-injected in one sequence with CeeSafe's (`CEESAFE_FAIL_ALLOC_AT`), but a block live at exit is not a leak: the report gives such blocks on a line of their own (memory-model.md §3.2). A C library's global state, kept until the process ends, may live there, such as OpenSSL's through `CRYPTO_set_mem_functions` in test builds. `CEE_ALLOC_FOREIGN_INIT` (ADR 0061) is for a C library's one-time initialisation, which cannot survive a failed allocation: never failed and not counted as an attempt, its blocks held with `CEE_ALLOC_FOREIGN`'s (either number may reallocate or free them). With them, a program may have its `main` in C instead of in CeeSafe. - **A program without a CeeSafe `main`** (a C, C++ or Rust host; ADR 0088) is linked from `build --emit lib`'s archive (tooling.md §4.1) or `--emit obj`'s object, and starts the runtime with `ceesafe_init` from `ceesafe/embed.h`, which every build that writes headers writes beside `ceesafe/rt.h`, and `--emit lib` beside its output (with the exports header). The archive has no C `main` unless `--lib-main` asks for it. Its config gives the program arguments (copied), the allocation report and failure injection (`fail_alloc_at`, `alloc_report`, or the environment's variables with `CEESAFE_INIT_ENVIRONMENT`), and a panic hook. `ceesafe_init` must happen before every call into CeeSafe on every thread, once; then exports may be called from any host thread at once. `ceesafe_shutdown` prints the report when one was asked for and returns the live blocks. A host that never calls `ceesafe_init` gets what 0.27.0 gave: no program arguments, the environment's variables ignored, and the printed panic. The generated `main` calls the same two functions, so a CeeSafe `main` and a host start the runtime one way. - **Panics in a host** (ADR 0088 §2.4). The panic hook is called on the panicking thread (the caller's, a thread CeeSafe spawned, an `Async.runWorkers` worker, or one C calls a `@callback` on) with the kind, the reason, the message and the CeeSafe location; the executor's own aborts arrive as `CEE_PANIC_ASYNC` with their message and no file. The hook must end the process or never return, must not call CeeSafe, and may not escape (`longjmp`, an exception, an unwind): panic = abort stays the rule. If it returns, the line is printed and the process aborts. It runs once per panicking thread, so several threads panicking at once are in it at once; a hook serialises its own reports. Every export and every `@callback` entry checks a per-thread panicked flag first: on a thread that has panicked, the call prints that panic's line and aborts. - When a program has `@export` functions or `@repr` types, ceesafec also writes `ceesafe_exports.h`: the `@repr` structs and enums, and a prototype for each export. C files include it as `#include "ceesafe_exports.h"`. - `run --interp` runs no C code. Calls to extern functions are interpreted only for a model of a few C library functions (`intermediate-representation.md` §5.4); any other is an interpreter fault. Diagnostics: N0290 (attribute or extern declaration misuse), N0291 (a type that is not FFI-safe), N0292 (a C name that is invalid, a C keyword, or reserved). Calls to extern functions outside an unsafe context are N0280. ### 28.7 `@derive` (ADR 0087 §2.3) `@derive(…)` on a struct or enum names one or more of `Equal`, `HashKey` and `OrderKey`, each once, and no others (N0290). Each writes an `impl`, ordinary code that is checked as written (`--dump-hir` shows it) and whose diagnostics point at the derive's name in the attribute. A generic type's impl has the type's own parameters; for `HashKey` and `OrderKey`, each type parameter (not a const parameter, §20.5) is also bounded by that interface. - **`Equal`** writes `impl DerivedEqual for S` (lib/core). `derivedEqual(borrow this, other: borrow S) -> bool` compares the fields in declaration order, joined by `&&`; for an enum, the variant, then its payloads. `a == b` on `S` is `a.derivedEqual(b)` and `a != b` its negation, both lending their operands. Each field's type must have `==` (N0207 at the attribute, naming the field): what §6 and §16.2 give `==` (integers, `bool`, fieldless enums, slices, vectors and arrays of them, `StringView`, raw pointers), floats (IEEE: `NaN != NaN`), `String` (its text), another `Equal` type and slices, arrays and vectors of one (the same length and equal elements in order, through lib/core's `DerivedEqualElements`; `==` on them works outside derives too), a cell whose contents have it (§41.2), and through `borrow T` and `Box` the referent. A field written `T?` compares as two `null`s being equal, or two payloads compared through borrows. A generic type has `==` for the instances whose arguments have it, checked at the first `==` on an instance (N0222); in generic code over an opaque `T`, `==` stays N0207, and so does a `[T; 2]` or `Vector` field's, each instance checking its element type. `DerivedEqual` is implemented only by `@derive(Equal)`: an `impl` of it in source is N0290, so `==` keeps one meaning (language-spec.md §6: no operator overloading). - **`HashKey`** writes `impl HashKey for S` (§24.3) and needs `Equal` (N0290 otherwise). `hash` feeds each field in order through lib/std's `deriveHash(field, mut h)`, which needs the field's type to implement `HashKey` (N0268), except for a fieldless enum without one, whose variant index is hashed as a `u32`; an enum's variant index, as a `u32`, comes before its payloads. `sameAs` is `derivedEqual`. - **`OrderKey`** writes `impl OrderKey for S` (lib/std, ADR 0075 §2.4): `compareKey` compares the fields lexicographically, in declaration order, through `deriveCompare(a, b)` (N0268 for a field type without `OrderKey`, but a fieldless enum without one compares its variant index); an enum compares its variant index first. Declaration order is the order the author asks for; there is still no `<` on the type. For a two-field struct, `@derive(Equal, HashKey, OrderKey) struct Point { x: i32; y: i32; }` writes: ```ceesafe impl DerivedEqual for Point { fn derivedEqual(borrow this, other: borrow Point) -> bool => this.x == other.x && this.y == other.y; } impl HashKey for Point { fn hash(borrow this, h: borrow mut Hasher) { deriveHash(this.x, mut h); deriveHash(this.y, mut h); } fn sameAs(borrow this, other: borrow Point) -> bool => this.derivedEqual(other); } impl OrderKey for Point { fn compareKey(borrow this, other: borrow Point) -> i32 { let c0 = deriveCompare(this.x, other.x); if (c0 != 0) { return c0; } let c1 = deriveCompare(this.y, other.y); if (c1 != 0) { return c1; } return 0; } } ``` Diagnostics: N0290 (a derive that is not one of the three, repeated, HashKey without Equal, `@derive` on anything but a struct or enum, a hand-written `impl DerivedEqual`), N0207 (a field without `==`), N0268 (a field type without the interface), N0222 (a generic instance whose field type has no `==`). ## 29. Systems capabilities (M8d) ```ceesafe @repr(C) struct UartRegs { data: Volatile; status: Volatile; } struct Uart { regs: MmioPtr; /// Safety: `base` is the mapped address of a PL011-compatible UART that no other `Uart` aliases. public unsafe fn attach(base: usize) -> Uart => Uart { regs: MmioPtr.at(base) }; public fn putByte(borrow mut this, b: u8) { while ((this.regs.get().status.read() & 0x20) != 0) { Cpu.spinHint(); } this.regs.getMut().data.write(b as u32); } } ``` ### 29.1 Volatile accesses (unsafe) `p.readVolatile()` and `p.writeVolatile(v)` are `read` and `write` (§27.3, same rules: unsafe, Copy pointee, `*mut T` to write) as **volatile** accesses: each is performed exactly once, in program order with every other volatile access and call, with the access width of `T`, never merged, split, cached or removed. In C they are `*(volatile T *)p`. They are for memory-mapped registers and memory shared with hardware; they do not order accesses between processors (that needs atomics, C7). ### 29.2 `lib/core`: CeeSafe sources bundled with the compiler Some core types are written in CeeSafe itself, over raw pointers and `unsafe` (ADR 0007 decision 1). Their sources live in `lib/core/*.cee`, are embedded in `ceesafec`, and are part of every package, in both profiles. A package may shadow one of their names with its own item (§2, ADR 0075 §5). Code from `lib/core` is type-, move- and borrow-checked like user code; functions and types the package does not use are not emitted, and `--dump-hir`/`--dump-nir` leave them out. | Type | What it is | |---|---| | `Volatile` | A cell whose value is read and written only by volatile accesses: `v.read()` (`borrow this`), `v.write(x)` (`borrow mut this`). One field of type `T`, so it has `T`'s layout and may be a `@repr(C)` field | | `MmioPtr` | The address of a memory-mapped `T` (typically a `@repr(C)` struct of `Volatile` registers). `unsafe MmioPtr.at(address: usize)` makes one; `p.get()` is a `borrow T` and `p.getMut()` (`borrow mut this`) a `borrow mut T`, tied to `p` by their signatures (§13.5), so register access needs no `unsafe`; `p.address()` is the `usize` | | `PhysAddr`, `VirtAddr` | Distinct address types (`cosmoos/kernel-memory-and-lifetimes.md`): `PhysAddr.of(bits: u64)`, `a.bits()`, `a.offset(n: u64)` (checked). No conversion between them, or to and from integers, except through `of` and `bits` | The contract of `MmioPtr.at` (its `Safety:` line): the address is mapped, aligned for `T`, and no other `MmioPtr` or reference aliases the registers; the program then reaches them only through this value. ### 29.3 CPU and target queries - `Cpu.spinHint()`: a hint inside a busy-wait loop (`pause` on x86-64, `yield` on AArch64, nothing elsewhere). `void`, safe, allowed in both profiles. - `Target.pointerBits()`: `u32`, the target's pointer width (64 on every target CeeSafe supports). `Target.isX86_64()` and `Target.isAArch64()`: `bool`, the architecture the generated C is compiled for. They are decided by the C compiler, not by `ceesafec` (the generated C is the same for every LP64 target), so they are not constants: use them in ordinary conditions. The interpreter answers for the machine it runs on. ### 29.4 `@align(n)` `@align(n)` on a non-generic struct, with `n` a power of two from 1 to 4096, raises the struct's alignment to at least `n` (its size is rounded up to it). In C, the first member is declared `_Alignas(n)`. It may be combined with `@repr(C)`. N0290 for other `n` or places. ### 29.5 C function pointers - `extern "C" fn(T1, …, Tn) -> R` is the type of a pointer to a C function (grammar §2). Its parameter and result types must be FFI-safe (§28.3) and not `@repr` enums (N0291). It is one address, Copy, holds no loans, and is FFI-safe itself, so C callbacks can be passed to and returned from C. - **Values.** The name of an `extern "C"` function, an `@export` function or a `@callback` function (§34.1) is a value of that type where such a type is expected (a typed `let`, an argument, a field, a return value) and the signatures are equal. Only those functions have a C address; naming another function there is N0291. `0 as extern "C" fn(…) -> R` is the null pointer, `f.isNull()` tests for it, and `f as usize` is its address. Both are static initialisers too (§39.4, ADR 0042): an ops table's entries. - **From an address** (ADR 0043): `n as extern "C" fn(…) -> R` for `n: usize` is the function pointer to that address, as `n as *T` is a raw pointer (§27.2), so a hook can be published in an `AtomicUSize` and read back. Making it is safe; calling through it is unsafe, as for any C function pointer, and a wrong address is that call's obligation. - **Calls.** `f(args)`, with `f` a local, parameter or field of such a type, calls through the pointer. It is unsafe (N0280): the pointer may be null or point to anything. - CeeSafe functions that are neither exported nor `@callback` (§34.1, C7d) have no C address. CeeSafe-level function values (`fn(…) -> R`) and closures are §36 (M11 G8). ### 29.6 Entry points A hosted program's entry point is `main`, called by the generated C `main` (§11). A freestanding package has no `main` (`profiles.md` §4.1): its entry points are its `@export` functions, which the environment calls (a kernel's `init`, a boot stub's `kmain`, a callback). An export's symbol may start with one `_` and a lowercase letter (`_start`), the names platforms use for entry symbols (§28.2 reserves only `__` and `_` with an uppercase letter); other reserved C names stay N0292. Diagnostics: no new codes. Volatile accesses outside an unsafe context are N0280; a function pointer type or value that cannot cross to C is N0291; a misused `@align` is N0290. ## 30. User-defined allocators (C5) ```ceesafe extern "C" { fn kmalloc(size: usize, flags: u32) -> *mut void; fn kfree(p: *mut void); } /// CosmoOS's kernel heap: kmalloc and kfree (integrations/cosmoos/c5/kernel.cee). struct KernelHeap {} impl Allocator for KernelHeap { /// Safety: kmalloc returns null or a block of `layout.size` bytes, 16-byte aligned. fn allocate(borrow this, layout: Layout) -> Result<*mut u8, AllocError> { if (layout.align > 16) { return Err(AllocError.OutOfMemory); } let p = unsafe { kmalloc(layout.size, 0) } as *mut u8; if (p.isNull()) { return Err(AllocError.OutOfMemory); } return Ok(p); } /// Safety: the caller passes a block this allocator returned (Box guarantees it). unsafe fn deallocate(borrow this, p: *mut u8, layout: Layout) { kfree(p as *mut void); } } fn make() -> Result, AllocError> => Box.createIn(KernelHeap {}, 42u64); ``` ### 30.1 `Layout` and `Allocator` (`lib/core/alloc.cee`) - `struct Layout { size: usize; align: usize; }`: a block's size in bytes and its alignment (a power of two). - `interface Allocator`, with two methods: - `fn allocate(borrow this, layout: Layout) -> Result<*mut u8, AllocError>`: a block of at least `layout.size` bytes aligned to `layout.align`, or `Err` (never a null `Ok`); - `unsafe fn deallocate(borrow this, p: *mut u8, layout: Layout)`: returns a block. Its contract: `p` came from `allocate` of this allocator type with the same layout, and is returned once. - Interface methods may be `unsafe` (lifting §27.4's earlier rule); an `impl` must match the interface's `unsafe` (N0267). - **`layoutOf()`** (ADR 0027): `T`'s size and alignment as a `Layout`, from `TargetInfo`. For a type holding C state (§33), whose size only the C compiler knows (§31.2), it is C's `sizeof`/`_Alignof` of `T`'s C type, so `ceesafec run --interp` cannot evaluate it. It takes one type argument and no arguments (N0209), and is a static initialiser (§39.4). ADR 0027 named it `Layout.of()`; CeeSafe has no type arguments on a member call, so it is a function, like `truncate`. - **`offsetOf(field)`** (ADR 0030): the offset in bytes of `field` in the struct type `T`, as `usize`. From `TargetInfo` for a CeeSafe layout; for a C-laid-out or imported `T` (§31, §33), C's `offsetof`, so `ceesafec run --interp` cannot evaluate it. It takes one type argument and a field's name (N0209); a `T` that is not a struct, or has no such field, is N0339. A static initialiser (§39.4). It reads no memory, so it is not unsafe; recovering a struct from a member's address with it is raw-pointer arithmetic, which is (lib/kernel's `containerOf`). ADR 0030 wrote it `offsetOf(T, field)`; a type is no expression in CeeSafe, so it takes `T` as a type argument, like `layoutOf()`. ### 30.2 Allocator types - A **user-defined allocator** is a non-generic struct **with no fields** that implements `Allocator`. It may be the `A` of `Box`, and `Box.createIn(a, v)` takes a value of it. Having no fields, it has no state that a box would need to carry: a `Box` stays one pointer, and dropping it needs no allocator value (the memory-model's free-standing allocators, §3.2 there). State that an allocator needs (counters, a C handle) lives on the C side or in global C state. - The built-in allocators are still `SystemHeap` and `FailingAllocator`. `Vector`, `String`, `SlotMap`, `StableVector`, `Arena` and `Pool` take only those in v0 (N0263): their growth is in the C runtime, which cannot call CeeSafe methods. - A type named as a box's allocator that is not a struct without fields implementing `Allocator` is N0263. ### 30.3 Semantics - `Box.createIn(a, v)` with a user allocator calls `allocate(Layout { size: size(T), align: align(T) })` once. On `Err`, it destroys `v` and returns `Err(AllocError.OutOfMemory)`; on `Ok(p)`, it moves `v` to `p`. Destroying the box destroys the contents, then calls `deallocate(p, the same layout)` once (H1). The compiler only calls `deallocate` with blocks `allocate` returned, so the contract holds for every safe program. - A `size(T)` of 0 is never requested: such boxes still allocate one byte (as `SystemHeap` does). - User allocators are not placement-transparent (M5): their boxes stay on the heap. - They are allowed in the freestanding profile (`profiles.md` §4.1): `Box` with a user allocator needs no hosted runtime. `Box.create` (`SystemHeap`) stays hosted-only. Diagnostics: N0263 (a box allocator that is not one), N0267 (an `impl` whose `unsafe` differs). ## 31. C struct types imported from headers (C6b; ADR 0008) ```ceesafe @header("kernel/bootinfo.h") extern "C" { struct cosmoboot_info { // only the fields CeeSafe uses, by their C names fb_phys: u64; fb_width: u32; } struct bootinfo_framebuffer { phys: u64; width: u32; } struct cosmoboot_mem_entry; // opaque: only pointed to } fn pixels(info: borrow cosmoboot_info) -> u64 => info.fb_width as u64; fn copyOut(info: borrow cosmoboot_info, out: borrow mut bootinfo_framebuffer) { out.phys = info.fb_phys; out.width = info.fb_width; } ``` ### 31.1 Declarations - Inside an `extern "C"` block, `struct name { field: Type; … }` declares an **imported C type**: a type that a C header defines, seen through the fields CeeSafe declares. `struct name;` imports one with no fields (opaque). The grammar is grammar.md §2's `ExternStruct`. - `@header("path", …)` on the block names the headers that define its types, one string per header, as C's `#include "path"` would find them (the build's `-I` paths, then the generated file's directory). A block that declares a struct needs a `@header` (N0290). A header string must be non-empty and must not contain `"`, `\`, or a line break (N0290). - The type's C spelling is `struct name`. With `@ctype("cname")` on the declaration, it is `cname` instead, for a C typedef (`typedef struct {…} cname;`). The name, the `@ctype` name and every field name must be valid, unreserved C identifiers (§28.2, N0292). - Fields are declared with their C member names, in any order, and only those CeeSafe uses. Their types must be integers, `bool`, raw pointers whose pointee C can name (§28.3, including imported types), (since C7b, §33.1) imported types, for embedded members, (since C9) **arrays** `[E; N]` of integers, `bool`, imported types or (ADR 0080 §3.5) raw pointers and `extern "C" fn` types, for C array members `CE name[N]` (`struct dev *spare[4]` is `spare: [*mut dev; 4]`; its elements are read and assigned), or (ADR 0041) `AtomicU32`, `AtomicU64` or `AtomicUSize` for a `uint32_t`, `uint64_t` or `size_t` member, and (ADR 0080 §3.5) `AtomicU8` for a `uint8_t` one; otherwise N0291. An embedded member is checked with `_Generic` only, since CeeSafe has no size for it. A field's declared type must be exactly the member's C type, under §28.3's mapping (`u32` is `uint32_t`, `usize` is `size_t`, `*CChar` is `const char *`, `*mut T` is `T *`, `[u8; 16]` is `uint8_t[16]`). This is checked when the C is compiled (§31.3), not by ceesafec. A C `char`, `signed char` or `unsigned char` member is declared `u8`, and an array of them `[u8; N]` (ADR 0034): C lets any object be read and written as bytes, and a byte's value does not depend on `char`'s signedness. (A `char` parameter or result is `CChar`, §28.3.) The address of a `u8` member, when it is borrowed, converts to `uint8_t *` in the C. - **Unions** (ADR 0041 §3.2). Inside an imported struct, `union { a: T; b: U; }` declares the members of a C11 anonymous union member, reached as `r.a` and `r.b` (C's `p->a`). In the block, `union name { … }` declares an **imported union**, spelled `union name` in C (or its `@ctype`), used as an imported struct is (behind a pointer or reference, or embedded as a field) and its members reached the same way; `union name;` imports one opaque. A union needs a member, and an anonymous union holds no union of its own (N0107; grammar.md §2's `ExternUnion` and `AnonymousUnion`). `union` is a word only there: elsewhere it is an identifier. - The members of one union share storage, so a member's type must be one every bit pattern is a value of: an integer, a raw pointer, an array of these, or an imported type whose declared fields are such (N0346). Reading a member after another was written reinterprets bytes, as C11 allows (C11 §6.5.2.3 note 95), and can never make an invalid value; `bool`, function pointers, atomics and imported types holding them are refused. - The borrow checker treats every member of one union as **one place** (§31.2). - **Atomic fields** (ADR 0041 §3.1). A member that C code reads and changes with `__atomic_*` (a statistic, a reference count, a flag word shared between CPUs) is declared with the atomic type over its integer. The atomic type is laid out exactly as its integer (§38.3), and the `_Generic` check compares the member with that integer type. The field is a place of the atomic type, which CeeSafe reaches only through the type's methods: each is the `__atomic_*` operation on the member's address. That C, too, accesses it only atomically is the C side's obligation, as for any state C shares. `AtomicBool` is refused (N0291): it is a `u32`, and C's `bool` a byte. - **`@cname("name")`** on a field (C9) gives its C member name when that is a CeeSafe keyword (`type`, `in`, `match`, …); CeeSafe code uses the declared name. The C name must be a valid, unreserved C identifier (N0292), and no two fields may have the same C name (N0228). `@cname` anywhere else is N0290. - An imported struct has no methods, `drop` block, type or region parameters, or other attributes (N0290). Its visibility is that of a struct. ### 31.2 Use - CeeSafe never knows an imported type's size, alignment or field offsets: they are the C compiler's, under the build's flags. So an imported type `T` appears only behind a pointer or a reference: `*T`, `*mut T`, `borrow T`, `borrow mut T`, and (since C7b, §33) by value as a struct field or a box's contents. As the type of a local, parameter, result, array element, optional, or type argument, it is N0294. No value of it exists in CeeSafe: it is neither Copy nor movable, and has no struct literal (N0294) and no drop. The one exception is a `static mut` item's initialiser (§39.4, ADR 0042), which is C's designated initialiser for the static's C object, not a value. - **Field places.** Through a reference, `r.f` is a place of the declared field type, as for any struct (§12): read it, assign it through `borrow mut`, or borrow it. The borrow checker treats it like any field place. A field of raw pointer type, `usize` or `isize` can be read and assigned but not borrowed (N0294): its C member type (`const char *`, `size_t`) is not the C type CeeSafe uses for the value (`char *`, `uint64_t`), so a pointer to it would have the wrong type in C. - **Union members** (ADR 0041 §3.2). The members of one union (§31.1) are one place: borrowing, assigning or using any member, or a place inside it, conflicts with a live loan of any other member as it would with a loan of the same member (B1–B2; N0240–N0242). Fields outside the union stay separate places. - **Array fields** (C9) are array places of their declared type, as in any struct (§16): `r.f[i]` is bounds-checked against `N`, `r.f[a..b]` is a slice (mutable through `borrow mut`), and when the element is an integer or `bool` the field can be read and assigned whole. An array of an imported type is C-laid-out (§33.1): its elements are places of the imported type, used through their fields or borrowed, and the array itself is never a value. - **Raw pointers.** Making, casting and comparing pointers to imported types is safe (§27.2), and `Ptr.toBorrow` / `Ptr.toBorrowMut` make references from them (unsafe, §27.3). `add`, `sub` and `offset` (unsafe, §27.3) step by the C type's size (since C9): C does the arithmetic. What would copy a value or make a slice of one is N0294: `read`, `write`, `readVolatile`, `writeVolatile`, and `Slice.fromRaw` / `SliceMut.fromRaw`. - **Across the boundary**, `*T` and `*mut T` are FFI-safe (§28.3): `struct name const *` and `struct name *` (or the `@ctype` name) in prototypes. The type itself is not, by value (N0291). ### 31.3 Lowering - The generated C includes every `@header`, after `ceesafe/rt.h`, once each, in the order the blocks appear (files in command-line order); `ceesafe_exports.h` includes them too. - CeeSafe emits no definition, typedef or layout assertion for an imported type, and names it by its C spelling. A field place is C's member access (`(*p).f`). Pointer fields are converted through `uintptr_t` when read and written, as at exports (c-backend.md §2.3), so that `const` in the header's member type causes no warning. - For every declared field `f` of type `F` (C type `CF`), the generated C asserts both `_Generic(((S *)0)->f, CF: 1, default: 0)` and `sizeof(((S *)0)->f) == size(F)`, with a message naming the struct, the field and the declaration's file and line. Neither expression is evaluated. A header that lacks a field, or has it with another type in some configuration, fails to compile; it cannot miscompile. - A field of an imported enum spelled `enum name` (§31.6) is asserted with `_Generic(&((S *)0)->f, enum name *: 1, default: 0)`: Clang targeting Windows reads `enum name:` in an association as an enum with a fixed underlying type (a Microsoft extension) and refuses it. - An array field `f: [E; N]` (C9) is asserted with `_Generic(&((S *)0)->f, CE (*)[N]: 1, default: 0)`, exact where `_Generic` of the member itself would see a decayed pointer, and, for integer and `bool` elements, with the size of CeeSafe's array. Its place is `(*(A *)(uintptr_t)&(*p).f)` (through `uintptr_t`, as `p` may be a `const` pointer), where `A` is CeeSafe's array struct (`struct { CE e[N]; }`, c-backend.md §2), which has the array's size and alignment: an access through it is an access through an aggregate containing the member's type (C11 6.5p7). No copy is made. ### 31.4 The interpreter `run --interp` has no headers, so it has no layout for an imported type. A function that accesses a field of one is an interpreter fault (exit status 70) when it does so. Passing pointers to imported types along is fine. ### 31.5 Profiles Imported types are platform-neutral (`profiles.md` §5). They are allowed in every profile. The same declarations work with CosmoOS's, Linux's or a C library's headers. Diagnostics: N0290 (a misplaced attribute, a block without `@header`, a bad header string, a member an imported struct cannot have), N0291 (a field type C and CeeSafe do not both spell), N0292 (a C name), N0294 (an imported type used where CeeSafe would need its size, or a borrow of a pointer or size field). ### 31.6 Imported enums (ADR 0079) - Inside an `extern "C"` block, `enum name: I;` imports a **C enum type** (grammar.md §2's `ExternEnum`), where `I` is `u8`, `u16`, `u32`, `u64`, `i8`, `i16`, `i32` or `i64`: the integer type the C compiler gives the enum. Any other type is N0291. Its C spelling is `enum name`, or with `@ctype("cname")` (its one attribute; any other is N0290) `cname`, for a typedef. Both names must be valid, unreserved C identifiers (N0292). The block needs a `@header` (N0290). The name is declared in the item scope as a struct's is; its visibility is a struct's. - **It is an integer type of its own**: `I`'s width and signedness, and every rule of integers (§13: literals, arithmetic, comparison, the checked, wrapping and saturating operations, integer patterns), but a type distinct from `I` and from every other. `as` converts between it and integers under §13's rules, so `v as u32` and `n as vnode_type` are lossless for `I = u32`. It is `Copy` and may appear wherever an integer may. - Enumerators are not imported: a value is an integer constant of the type (`const VNODE_DIR: vnode_type = 2;`). - It is FFI-safe wherever `I` is (§28.3) and is spelled with its C spelling in prototypes, `@export` and `@callback` definitions, `extern "C" fn` types, imported struct fields (whose `_Generic` check names it) and locals. A function pointer type of C's with an enum parameter is therefore the same C type as CeeSafe's, which a call through a pointer needs (UBSan's `-fsanitize=function` refuses a call whose pointer type names `unsigned int` where the function's says `enum`, though C calls the two compatible). - **Lowering.** A generated C file that uses the type asserts, once and before its first use, that C gives the enum `I`'s width and signedness: `_Static_assert(sizeof(T) == sizeof(CI) && (T)-1 < (T)1, "…")` for its C spelling `T` and `I`'s C type `CI` when `I` is signed, with `>` when it is unsigned, and a message naming the type and the declaration's file and line. A header whose enum the C compiler lays out as another integer fails to compile. `_Generic` cannot state it: MSVC makes no enum compatible with any integer type. Its mangled name is its own (`N`, the length, the name). - **The integer is the target's** (C11 §6.7.2.2 ¶4 leaves it to the implementation), and values of the type are C's enum in the generated code, so C's arithmetic and comparisons on them are CeeSafe's only when it has `I`'s width and signedness. GCC and Clang on Linux and macOS give an enum whose enumerators are all non-negative `unsigned int` (`u32`) and one with a negative enumerator `int` (`i32`); MSVC, and Clang targeting Windows, give every enum `int`. So one declaration fits every target only where the header's enum has the same integer on all of them, as one with a negative enumerator does (`i32`). Elsewhere the assertion stops the build on the target whose integer differs; ceesafec never accepts an integer of the right width but the wrong signedness. - The interpreter runs it as an integer of `I`'s width. Diagnostics: N0290 (an attribute other than `@ctype`, a block without `@header`), N0291 (an integer type C does not give an enum), N0292 (a C name). ## 32. Context effects (C7a; ADR 0009) ```ceesafe extern "C" { @effects(sleep) fn mutex_lock(m: *mut mutex); @effects() fn spin_unlock(l: *mut spinlock_t); } /// Holding one is an atomic context: nothing may sleep while it is live. @context(atomic) struct SpinGuard { lock: *mut spinlock_t; drop { unsafe { spin_unlock(this.lock); } } } @context(irq) fn onInterrupt(count: borrow mut u64) { count += 1; } // may neither sleep nor allocate ``` ### 32.1 Effects - There are three effects: **`sleep`** (the code may block the thread), **`alloc`** (it may allocate memory from a heap), and, since L1 (ADR 0031), **`suspend`** (it may suspend the current task: an `await`, §40.2). - The effects of a function are those of every operation its body may perform, including everything it calls and every value it drops (drop glue, `drop` blocks, a box's deallocation). They are inferred over the whole package. Recursion is handled by iterating to a fixed point. - **Across packages** (ADR 0082 §2.7): a dependency's function whose body `check` does not check again has the effects its package's interface record gives it, those the dependency's own check inferred; generic code, async functions and functions with a closure parameter are inferred where they are used, as before. - **Explanations.** A diagnostic about an effect (N0295, N0360) names the chain of operations from where it is reported to where the effect comes from: for each function on the chain, an operation whose chain ends in an operation rather than in a call that may do anything (a C function or a `dyn` method without an effect list, or for a write any of them), then the shortest such chain, then the first in the function's order among those. None of this depends on the order the program's functions or packages are looked at, and a closure is named by its place in its function (`the closure 2 in parse`, `{closure environment 2 in parse}`), not by its line, so a chain through a dependency reads the same whether the dependency comes from its record or is checked with the program (ADR 0082 §2.7, §8.5). - Operations have these effects: | Operation | Effects | |---|---| | a call of a CeeSafe function | that function's effects | | a call of an `extern` function | its `@effects` if declared; otherwise `sleep` and `alloc` | | a call through a C function pointer (§29.5) | `sleep` and `alloc` | | a call through a function value or closure (§36) | its type's effect list (§36.3); without one, `sleep` and `alloc` | | `Box.create`, and `Box.createIn` with a built-in allocator | `alloc` | | `Box.createIn` with a user allocator (§30), dropping such a box | the effects of its `allocate`, or of its `deallocate` | | allocating from an `Arena`, a `Scratch` frame or a `Pool`, making one; `Vector.withCapacity`, `push`; `String.append`, `withCapacity`; `StableVector.push`; `SlotMap.insert` | `alloc` | | allocating from a `FixedBufferAllocator`; every other built-in operation on values, pointers and collections | none | | console, file, socket and process operations (§26) | `sleep` (and `alloc` for `Process.run`) | | dropping a value | the effects of its drop glue: `drop` blocks, the contents' drops, deallocations | | `await e` (§40.2), and the library's `Async.park()` | `suspend`, and the effects of starting the awaited task (§40.6) | ### 32.2 Declaring effects - `@effects(e, …)`, with each `e` one of `sleep`, `alloc` and `suspend`, and `@effects()` for none: - on an `extern` function, it **declares** what the C function may do. CeeSafe trusts it, as it trusts a prototype: a wrong declaration is a wrong binding (R-2); - on a CeeSafe function or method, it is an **upper bound**. The inferred effects must be among those declared (N0295, reported at an operation that has the extra effect). - An unknown effect name, or `@effects` twice, is N0290. ### 32.3 Contexts - `@context(atomic)` on a function forbids `sleep` and `suspend` in its body. `@context(irq)`, code that may run in a hardware interrupt, forbids `sleep`, `alloc` and `suspend` (`cosmoos/kernel-memory-and-lifetimes.md` KQ-1: IRQ handlers do not allocate). An operation with a forbidden effect is N0295. - **An async fn or async closure body (§40) is a context that forbids `sleep`** (ADR 0031 §3.2): blocking would stall every task of its thread. `Async.blocking` (§40.4) is the bridge. - `@context(atomic)` on a struct makes holding a value of it an atomic context. At every point of a function where a local that holds such a value (the struct itself, or a type containing it by value) may be live, `sleep` and `suspend` are forbidden (N0295): a spin guard may not be held across an `await`. A local is live from its initialisation until it is dropped, moved away or its storage ends, and a parameter from the function's entry. "May be live" joins over the control-flow paths. Dropping the holder itself is checked as if it were no longer live. - A context restricts the body it is declared on, not callers: any code may call an `@context(irq)` function. A function called from an atomic context is checked through its inferred effects. - Generic functions and structs are checked in each instantiation. An error is reported once. ### 32.4 What this proves The checker proves that CeeSafe code performs no operation whose declared or inferred effects its context forbids. It relies on the `@effects` of extern functions being right, and proves nothing about C code. It does not prove freedom from deadlock or lock-order inversions. In CosmoOS, `might_sleep()` and lockdep remain the runtime check behind it (ADR 0009). Effects are checked in every profile. Only `@context`, `@effects` bounds on CeeSafe functions, and `@context(atomic)` types create obligations, so code that uses none is unaffected. Diagnostics: N0295 (an operation with an effect its context or `@effects` bound forbids; the message names the chain of calls that leads to the operation), N0290 (misused `@effects` or `@context`, or an effect name other than `sleep`, `alloc` and `suspend`). ### 32.5 Effects of closure arguments: `@effectsOf` (OD-9c; ADR 0020) ```ceesafe @effectsOf(test) fn count(values: Slice, test: borrow fn(i32) -> bool) -> i32 { var n = 0; for (v in values) { if (test(v)) { n += 1; } } // not count's own effect: its caller's return n; } fn isEven(v: i32) -> bool => v % 2 == 0; @context(atomic) fn underLock(values: Slice, limit: i32) -> i32 { return count(values, (v) => v > limit) + count(values, isEven); // allowed: neither closure sleeps } ``` - **The attribute.** `@effectsOf(p, …)` on a function or method names some of its parameters. Each must be of a function or closure type (`fn(…)`, `borrow fn(…)`, `borrow mut fn(…)`, with or without an effect list), or it is N0336. It changes no type. With `@effects(…)`, the bound applies to the function's own effects. - **Inside the function**, a call through such a parameter `p`, or through a local that holds `p` (copied, cast or reborrowed from it and assigned nothing else), is not one of the function's own effects. So is passing `p` as the argument for another function's `@effectsOf` parameter. Every other use is as before: `p` passed to a function without the attribute, stored, or captured has its type's effect list wherever it is called. - **At a call**, the effects are the callee's own, plus, for each of its `@effectsOf` parameters, the argument's: | Argument | Its effects | |---|---| | a closure literal, or a local known to hold one (optimisations.md §5.1: every assignment to it is that literal, or copies or reborrows of a local that holds it) | the literal's body's inferred effects | | a named function (§36.1), adapted to a closure | that function's inferred effects: the adapter calls it directly | | the caller's own `@effectsOf` parameter, or a local that holds it | none: the caller's caller counts it | | anything else | its type's effect list (both effects without one, §36.3) | N0295 names the argument in its chain: "it calls 'count' with the closure 1 in main, which calls …". - **As a value**, an `@effectsOf` function (§36.1) has its own effects plus each named parameter's type list. - **Why this is sound.** A parameter is never assigned (N0211), and a closure or function value cannot be `var` (N0246), so every call through `p` during a call runs the argument the caller passed, whose effects the caller counted. Anything made from `p` other than a copy or reborrow is counted by its type, where it is called. - lib/core's `sortBy`, `forEach`, `any`, `all` and `retain` (§36.4) are `@effectsOf` their closure, and so are `sortBy`'s helpers. - **Async arguments** (ADR 0037). An `@effectsOf` parameter of closure type also takes an async closure literal (§40.3) whose task's result is the closure type's, or a named async fn of that signature (§36.1), unless the type's effect list lacks `suspend` (N0295). The call is then a task (§40.1): `await f(…)`, `Async.run(f(…))`, `Async.boxed(alloc, f(…))` or a spawn's body; N0342 elsewhere. It runs `f`'s **async instance** for those arguments, one per (`f`, the closures bound to its parameters), whether `f` is sync or async: a copy of `f` in which each call through such a parameter, or a local holding it, awaits the closure, and passing it on to another function's `@effectsOf` parameter awaits that function's instance. - The instance is checked as an async fn (§40.1), and its errors are reported at the call that made it, since its code may be the library's: a blocking call is N0295, and an instance that calls itself with its closure is N0343. Any other use of the closure (stored, captured, passed to a parameter without `@effectsOf`) is N0342: it would be called without an await. - The call's effects are `f`'s own, the closure's (`suspend` among them) and the instance's. Cancellation is the closure's (§40.7): `f` sees only what its awaits return. - `f` sees only values, so lib/core's functions work as they are: `await v.forEach(async (x) => …)`, `await v.retain(async (x) => …)`, `await s.sortBy(async (a, b) => …)` (its introsort is iterative, so its instance's frame has a size). ## 33. C state embedded by value (C7b; ADR 0009 §2) ```ceesafe @header("kernel/spinlock.h") extern "C" { @ctype("spinlock_t") struct spinlock_t; @effects() fn spinlock_init(lock: *mut spinlock_t, name: *CChar); } /// A lock and the data it protects; the C compiler lays it out, and it never moves. struct SpinLock { raw: spinlock_t; // by value, zero-filled when made name: *CChar; data: T; /// Runs at the lock's final address, before anything can use it. @placed fn init(borrow mut this) { unsafe { spinlock_init(borrow mut this.raw as *mut spinlock_t, this.name); } } } fn make(name: *CChar) -> Result, KernelHeap>, AllocError> => Box.createIn(KernelHeap {}, SpinLock { name: name, data: 0 }); // `raw` is left out ``` ### 33.1 C-laid-out types - A struct field may have an imported C type (§31) by value. In a CeeSafe struct, it embeds that C state; in an imported struct, it declares an embedded member (`struct mutex { lock: spinlock_t; }`); in a `@repr(C)` struct, it is C state that C code may embed too (§28.5, ADR 0029). - A type is **C-laid-out** if it is an imported type, an array of a C-laid-out type (C9), or a struct with a field of a C-laid-out type. Like an imported type (§31.2), it has no CeeSafe layout. Generated C defines a C-laid-out CeeSafe struct with its real member types, so the C compiler lays it out, and asserts nothing about its size, alignment or offsets. Allocation sizes come from C's `sizeof`, as for every type. - A C-laid-out type appears by value only as a field of another struct, as the contents of a `Box`, or (C9) as the element of an array that itself appears so. As a local, parameter, result, optional, enum payload or any other type argument, it is N0294. ### 33.2 Made in place, never moved - C state may point into itself once initialised (an empty list head points to itself), so a C-laid-out value is never moved, copied or assigned. Every move of one is N0294. - The only way to make one is a **struct literal given directly to `Box.create` or `Box.createIn`**, or nested as a field of such a literal. Fields of imported type are left out of the literal and are **zero-filled** (C's `{0}`). Writing one is N0294: CeeSafe has no values of imported types. - **C locals** (ADR 0080 §3.4). `var s: T;`, with no initialiser, of an imported C type or of a C-laid-out struct, is C's zero-filled object on the stack (`s = (T){0};` where it is declared), at a fixed place for its scope. It is borrowed, its fields used and its address taken (`borrow mut s as *mut T`) as any place; it is never moved, copied or assigned whole (N0294, as above), and not in an `async` function, whose frame moves (N0294). The interpreter cannot make one (a fault, as for any C state, §33.4). - A C-laid-out value lives in its box until the box is dropped. Its contents are borrowed (`borrow`, `borrow mut`, through `Ptr.toBorrow` of a pointer to them) but never moved out. Dropping the box runs the contents' drop glue in place, then frees them. ### 33.3 `@placed` methods - `@placed` on a method `fn m(borrow mut this)` with no other parameters and no result makes it the type's **placement initialiser**. A struct has at most one. Misuse is N0290. - When `Box.create`/`Box.createIn` has placed a C-laid-out value in its box, and before it returns, the compiler calls the `@placed` methods of the value's fields' types, depth first, innermost first, in field order, and then the type's own. No other code can see the value before then. A `@placed` method runs C's initialisers (`spinlock_init`, `mutex_init`, `kobject_init`) at the value's final address. - A box whose allocation fails runs no `@placed` method: the value is dropped, as in §18.2. - The effects of `Box.create`/`Box.createIn` (§32.1) include those of the `@placed` methods they run. ### 33.4 The interpreter `run --interp` has no C layout, so making, allocating or accessing a C-laid-out value is an interpreter fault (exit status 70), as for imported types (§31.4). Diagnostics: N0294 (a C-laid-out type by value outside a field or a box; a move of one; a literal of one outside `Box.create`/`createIn`; an imported field in a literal), N0290 (a misused `@placed`). ## 34. Callbacks into CeeSafe, and raw boxes (C7d) ```ceesafe @header("kernel/object.h") extern "C" { struct kobject; struct kobject_type { name: *CChar; release: extern "C" fn(*mut kobject); } } /// The kernel calls this when the last reference goes; one C function per instance. /// Safety: `obj` is the first field of a `KObject` made by `KRef.create`, and this runs once. @callback unsafe fn release(obj: *mut kobject) { let object: Box, KernelHeap> = Box.fromRaw(obj as *mut KObject); } // dropping `object` drops the data and frees it // … this.ktype.release = release; in KObject's @placed method ``` ### 34.1 `@callback`: C addresses of CeeSafe functions - `@callback` on a top-level function gives it a C address, as `@export` does (§29.5), without a symbol. Where an `extern "C" fn` type is expected, its name is a value of that type if the signatures are equal. A generic function is named with its type arguments (`release` inside generic code, `release` outside), and each instance has its own C function. - Its parameter and result types must be FFI-safe (§28.3) and must not mention its type parameters, so every instance has the same C signature (N0291). It may be an `unsafe fn`: its `Safety:` line states what the C caller guarantees. `@callback` with `@export` is N0290. - Generated C gives each instance used as a value a `static` function with the C types, which converts its arguments and result as an export does (c-backend.md §2.3). Calling through the pointer is unsafe and has the `sleep` and `alloc` effects (§32.1). The callback's own body is checked like any other function's. ### 34.2 `Box.intoRaw` and `Box.fromRaw` - `Box.intoRaw(b)` consumes the box `b: Box` and returns `*mut T`, the address of its contents. Nothing is dropped or freed: the caller owns the memory, and leaks it unless it gives it back. It is safe (a leak is not undefined behaviour). It is for handing ownership to C: a kernel object, a deferred free. - `Box.fromRaw(p)` (unsafe, §27.3) turns `p: *mut T` back into a box. The box type, and with it the allocator, comes from the expected type (`let b: Box = unsafe { Box.fromRaw(p) }`); without one it is N0205. Contract: `p` came from `Box.intoRaw` of a box of exactly that type, and is given back once. - Neither applies to pool boxes (N0205). Neither has an effect. In the interpreter both work as in C. ### 34.3 A struct's first field The fields of a struct, including a C-laid-out one (§33), are laid out in declaration order, so the address of a struct is the address of its first field (C11 6.7.2.1p15). A pointer to the first field converted with `as` is a pointer to the struct, which is how a kernel callback that receives the embedded `struct kobject *` or `struct quiesce_head *` finds its object. Since C7d, a field of an imported struct may also have a C function pointer type (§29.5), such as `kobject_type.release`. Diagnostics: N0291 (a callback whose C signature is not FFI-safe or depends on its type parameters), N0290 (`@callback` misused), N0205 (`Box.fromRaw` without a box type). ## 35. Floating-point types (M11 G7; ADR 0011) ```ceesafe fn mean(values: Slice) -> f64 { var sum = 0.0; for (v in values) { sum += v; } return sum / round(values.length); } fn main() { let samples = [1.5, 2.25, 4.0]; let m = mean(samples[..]); // 2.5833… Console.writeLine(truncate(m * 1000.0)); // 2583: toward zero let x: f32 = 0.1; // the f32 nearest 0.1 Console.writeLine(x.bits()); // 1036831949 } ``` ### 35.1 Types and values - `f32` and `f64` are IEEE 754 binary32 and binary64. Their layout comes from `TargetInfo` (size and alignment 4 and 8 on both supported targets). They are Copy, need no drop, hold no loans, and are FFI-safe (C `float` and `double`, §28.3). - The floating-point environment is the default one, and CeeSafe code never changes it: rounding to nearest, ties to even; no traps; status flags are never read. - Every operation rounds its exact result once, to its type. There is no fused multiply-add and no evaluation in a wider type (§35.6). ### 35.2 Literals - A float literal (grammar.md §1.5: `1.5`, `1e9`, `2.5e-3`, `6.02e23f64`) has type `f32` or `f64`. Its value is the decimal value rounded to nearest (ties to even) in that type, once: `0.1f32` is the `f32` nearest 0.1, not the `f64` nearest 0.1 rounded again. - With a suffix, the type is the suffix's. Without one, the literal is *flexible* (§5): it takes an expected `f32` or `f64` (looking through an optional, as integer literals do) and otherwise defaults to `f64`. A flexible float literal where an integer type is expected, or an integer literal where a float type is expected, is N0205: there are no implicit conversions (write `1.0`, or `round(n)`). - A literal whose value rounds to infinity in its type is N0206 (`1e39f32`). A literal that rounds to zero is zero. - `-` before a literal is the negation operator (§35.3), so `-0.0` is negative zero. ### 35.3 Operators | Operators | Operand types | Result | Behaviour | |---|---|---|---| | `+ - * /` | the same float type | that type | IEEE 754, rounded to nearest even. **Never panics**: overflow gives ±infinity, `x / 0.0` gives ±infinity, and `0.0 / 0.0`, `inf - inf` and `0.0 * inf` give NaN | | unary `-` | float | same | flips the sign, also of zeros and NaNs | | `== != < <= > >=` | the same float type | `bool` | IEEE comparisons: NaN is unequal to everything, itself included, and is neither less nor greater; `-0.0 == 0.0` | | `+= -= *= /=` | | | as `x = x op y` (§6) | - Float arithmetic is the one arithmetic that does not panic on an unrepresentable result (OD-6 is about integers; ADR 0011 Q2). - `%`, the bitwise operators and the shifts are not defined on floats (N0207). - Floats cannot be `match` scrutinees or patterns (N0207, §15.2). - **NaN bits are not portable.** Which NaN an operation produces is not specified: x86-64 and AArch64 differ in its sign (inferred from their default NaNs). A NaN compares, converts and prints alike whatever its bits; only `bits()` (§35.5) shows the difference. ### 35.4 Conversions - **`as`** stays lossless (§7): it converts a value only to a type that holds every value of the source type exactly. For floats, these are: - `f32 as f64`; - `i8`, `i16`, `u8` and `u16` as `f32` or `f64`; - `i32` and `u32` as `f64`. Any other `as` from or to a float type is N0208. - **`round(e)`**: `e` of any integer or float type, `T` a float type. The result is `e` rounded to nearest (ties to even) in `T`. It is always defined: an `f64` too large for `f32` becomes ±infinity, and NaN stays NaN. - **`truncate(e)`** with `e` of a float type and `T` an integer type rounds toward zero. It **panics** with "float out of range" if `e` is NaN or ±infinity, or if the rounded value does not fit in `T`. (`truncate` between integer types is unchanged, §7.) A float `T` is N0207: use `round`. - **`checkedTruncate(e)`**: as `truncate`, but the result is `T?`, `null` where `truncate` would panic. - **`saturatingTruncate(e)`**: as `truncate`, but a value below `T`'s range gives its minimum and one above gives its maximum. NaN gives 0. It never panics. - `checkedTruncate` and `saturatingTruncate` need a float or integer `e` (an integer `e` converts as in §7, ADR 0074 §2.9) and an integer `T`, and `round` a float `T` (N0207 otherwise). All four take exactly one type argument (N0216). ### 35.5 Representation - `f.bits()` is the IEEE representation of `f`: a `u32` for `f32`, a `u64` for `f64`. `f32.fromBits(u)` (`u: u32`) and `f64.fromBits(u)` (`u: u64`) make a float from a representation; every pattern is a value, some of them NaNs. - The names `bits` and `fromBits` are reserved on float types, as the overflow methods are on integers (§6). ### 35.6 C and the interpreter - `f32` and `f64` are C `float` and `double`. A float literal is emitted by its bits (`cee_f64_from_bits`), so its value never depends on the C compiler's decimal conversion. - In a program that uses floats, `rt.h` requires `FLT_EVAL_METHOD == 0` (no wider evaluation) and IEEE formats (`FLT_RADIX == 2`, `FLT_MANT_DIG == 24`, `DBL_MANT_DIG == 53`), and disables contraction with `#pragma STDC FP_CONTRACT OFF` (clang) and `#pragma fp_contract(off)` (MSVC). The generated `main.c` asks for this with `#define CEE_FLOATS` when the program has a float local, parameter, result or constant. A program without floats needs none of it. That matters for kernels built with `-mgeneral-regs-only`: x86-64 then has no SSE, and `FLT_EVAL_METHOD` is not 0. gcc ignores the standard pragma, but does not contract under `-std=c11`, which CeeSafe's builds use. `ceesafec` also passes `-ffp-contract=off` to clang and gcc. Integrations that compile CeeSafe's C must not enable contraction or `-ffast-math` (c-backend.md §1). - Conversions in C have no undefined behaviour: - `truncate` and its variants compare the value with the target's exact bounds before converting (C11 6.3.1.4); - `round` of an `f64` outside `f32`'s range gives the infinity directly, not through a C conversion (C11 6.3.1.5). - The interpreter computes in CeeSafe's own `f32` and `f64` (`ceesafec`'s C `float` and `double`), which round the same way. Compiled and interpreted programs give the same results, NaN bits aside (§35.3). - Not in G7 (ADR 0011 §3.8): `%`, a math library beyond §35.8, and other float formats. Parsing floats from text came with ADR 0071 (§16.6). ### 35.7 Printing (M11 G7b) - `Console.write`, `Console.writeLine` and `String.appendNumber` accept `f32` and `f64` (and references to them). - **The digits** are the shortest decimal that reads back as the same value. Reading back means rounding to nearest, ties to even, in the value's own type, so an `f32` prints with `f32`'s shortest digits: `0.1f32` prints as `0.1`. When several decimals of that length read back, the nearest to the value is printed, and on a tie the one with an even last digit. - **The layout** is that of ECMAScript's Number::toString (ECMA-262 §6.1.6.1.20). Write the decimal as 0.d1…dk × 10^n with no trailing zeros. Then: - if k ≤ n ≤ 21, the digits and n − k zeros: `100`, `100000000000000000000`; - if 0 < n ≤ 21, a point after n digits: `1.5`, `123456.789`; - if −6 < n ≤ 0, `0.`, then −n zeros, then the digits: `0.000001`; - otherwise one digit, a point and the rest if k > 1, `e`, the sign of n − 1 (`+` or `-`), and |n − 1|: `1e+21`, `1.5e-7`, `5e-324`. - `NaN`, `Infinity` and `-Infinity`. Both zeros print as `0`, as in ECMAScript; `bits()` tells them apart. - In C, the digits come from Schubfach (R. Giulietti, 2020; `runtime/hosted/ceesafe_rt_float.c`), with integer arithmetic only and a table of 617 powers of ten generated exactly. It differs from the paper's Java code in one place: a result may have one digit. Java needs at least two, so that code tries one digit fewer only from three digits on, and gives `4.9e-324`. The file is linked only into programs that print a float. The interpreter finds the same digits by an exact search over rationals instead (`src/interpreter/float_format.cee`). ### 35.8 Math methods (M11 G7c; ADR 0011 §5 Q5) | Method | Result | Behaviour | |---|---|---| | `x.sqrt()` | same type | the square root, correctly rounded (IEEE `squareRoot`). `(-0.0).sqrt()` is `-0.0`; a negative operand or NaN gives NaN | | `x.abs()` | same type | `x` with its sign cleared, NaN included | | `x.floor()`, `x.ceil()` | same type | the nearest integral value below or above (IEEE `roundToIntegralTowardNegative`/`Positive`). Integral values, infinities and NaN are unchanged, and a zero result keeps `x`'s sign: `(-0.5).ceil()` is `-0.0` | | `x.min(y)`, `x.max(y)` | same type | IEEE 754-2019 `minimum`/`maximum`: NaN if either is NaN, and `-0.0` is below `0.0`. `y` is checked against `x`'s type, so a flexible literal takes it | - None of them panics, and none calls a C math library: CosmoOS has no libm. - `floor`, `ceil`, `abs`, `min` and `max` are exact portable C over integers and bits in `rt.h`. On baseline x86-64 there is no instruction for `floor` or `ceil`; `roundsd` needs SSE4.1. - `sqrt` is the hardware square root, which is correctly rounded on both targets. It is reached through `__builtin_elementwise_sqrt` with clang and `__builtin_sqrt` with gcc, built with `-fno-math-errno` (ceesafec passes it), so it never sets `errno` or calls `sqrt`. MSVC uses its C runtime's `sqrt`. - The interpreter uses CeeSafe's own methods of the same names, so it computes what compiled code does. - The eight names `bits`, `fromBits`, `sqrt`, `abs`, `floor`, `ceil`, `min` and `max` are reserved on float types: an interface method of one of those names cannot be implemented for a float type (N0228). Wrong argument counts are N0209. ### 35.9 `--no-float` (M11 G7c; ADR 0011 §3.7) - `ceesafec build --no-float` and `ceesafec check --no-float` reject every float type, float literal and `f32.fromBits`/ `f64.fromBits` with N0297. Every float value starts at one of those, so nothing of the program computes with floats. - It is meant for kernels and other code built without FP registers. CosmoOS's kernel builds pass it (`c4.py` to `c8.py`), and `check-fpregs.sh` remains the backstop. Floats in the generic freestanding profile are otherwise allowed (`profiles.md` §1). - The libraries are not checked this way: lib/core's float parser (§16.6) is reached only through `parseFloat` or `parseFloat`, whose type argument the program writes and N0297 rejects, and a function nothing calls is not emitted (ADR 0071 §2.5). - In the corpus, a line `// option: no-float` checks a file this way (testing.md §3). Diagnostics: none new. Mismatched float types, and float literals where an integer is expected, are N0205. A literal that overflows is N0206. Operators that are not defined on floats are N0207, and so are float match scrutinees and wrong argument or target types of the conversions. A lossy `as` is N0208. Wrong type-argument counts are N0216. ## 36. Function values and closures (M11 G8; ADR 0012) ### 36.1 Function types and values (G8a) ```ceesafe fn double(x: i32) -> i32 => x * 2; fn negate(x: i32) -> i32 => -x; fn applyTwice(f: fn(i32) -> i32, x: i32) -> i32 => f(f(x)); fn main() { Console.writeLine(applyTwice(double, 5)); // 20 let pick: fn(i32) -> i32 = if (true) negate else double; Console.writeLine(pick(3)); // -3 let id = identity; // a generic function, named with its type arguments Console.writeLine(id(7)); } fn identity(x: T) -> T => x; ``` - **The type** `fn(T1, …, Tn) -> R` (`fn(T1, …, Tn)` when `R` is `void`) is a plain function value: one code address. It is Copy, needs no drop, holds no loans (it is not scoped), and is not FFI-safe: C function pointers stay `extern "C" fn(…)` (§29.5). Two function types are the same type when their parameter and result types are. - **Values.** The name of a package function is a value of its function type: - wherever a value can be (`let f = double;`), or where a function type is expected, which must then equal the function's; - a generic function is named with its type arguments (`identity`), giving that instance. Methods are not values: they are only named through their type or receiver. These functions are not values either, N0298 (a new code): - `extern` and `unsafe` functions, since a call through a value is safe; - functions with region parameters (§25), since their signatures can store an argument's loans in another argument, which a function type cannot say; - `main`. - **Calls.** `f(args)`, with `f` a local, parameter or field of a function type, calls the function it holds. The arguments are checked against the parameter types as in any call (§8, §13.2). The call is safe. Its effects are the type's effect list (§36.3), else `sleep` and `alloc` (§32.1), as for a C function pointer. - **References.** Parameter and result types may be references or other scoped types. A reference result needs exactly one reference (or scoped) parameter: a function type has no names to write a source list with (N0245, §13.5). The borrow checker gives a call's result the loans of every reference argument. That is sound for every function the value can hold, because such a function's result can only come from its parameters (no region parameters, above). - **Not in G8a:** `==` on function values (N0207). Closures are §36.2, and effect lists on function types §36.3. - **C.** A function type is a C function pointer typedef `ceeN_…` over the functions' C signatures. A value is the function's address, and a call is an indirect call. ### 36.2 Closures (G8b) ```ceesafe fn countIf(values: Slice, keep: borrow fn(i32) -> bool) -> i32 { var n = 0; for (v in values) { if (keep(v)) { n += 1; } } return n; } fn forEach(values: Slice, visit: borrow mut fn(i32)) { for (v in values) { visit(v); } } fn main() { let values = [3, 8, 1, 9, 4]; let limit = 4; Console.writeLine(countIf(values[..], (v) => v > limit)); // 2: captures `limit` var total = 0; forEach(values[..], (v) => { total += v; }); // changes `total`: a borrow mut fn Console.writeLine(total); // 25 let twice = (x: i32) => x * 2; // captures nothing: a plain fn(i32) -> i32 Console.writeLine(twice(21)); } ``` - **Literals** (grammar.md §2): `(x: i32, y) => expression` or `(x) => { statements }`, with `()` for none. A `(` … `)` followed by `=>` is a closure; a parenthesised expression cannot be followed by `=>`. The body is a block or an expression, which extends as far right as an expression can (as for branches, §15.3). - **Parameter types** are written, or taken from an expected function or closure type. The **result type** is the expected type's, else the expression body's type, else `void` for a block. A parameter without a type and without an expected type, or a block body that returns a value without an expected type, is N0236 (a new code). - **Captures.** The body may name the enclosing function's locals and parameters, `this` included, also from nested closures. Each one named is *captured by reference*: - **mutably** if the body assigns to it or to a place rooted at it, passes it as `mut`, borrows it mutably, calls a `borrow mut this` method or mutating collection method on it, or, for a `borrow mut` variable or field, uses it where a `borrow mut` is expected, which reborrows it (§13.3): `() => grow(s)` with `s: borrow mut String` captures `s` mutably, so the closure holds a mutable loan on the string while it is live; - **shared** otherwise. - **Field-precise** (M13 G5): a captured struct local that the body reaches only through its fields (`s.width`, `s.items[s.next]`), never whole and not from a nested closure, is captured as one reference per field it names, so the closure borrows those fields and the enclosing code may use the others. The mode (shared or mutable) is still decided per local. A local behind a reference (`this`) is captured whole. - **Types.** - A literal that captures nothing is a plain function value, `fn(…) -> R` (§36.1). - One that captures is a **closure**: `borrow fn(…) -> R` if every capture is shared, `borrow mut fn(…) -> R` if any is mutable. - A closure type is a reference (§13): `borrow fn` is Copy, and `borrow mut fn` is unique and reborrowed at each call. Both are scoped, and allowed where references are (§13.1, §25): parameters, `let` locals, fields of aggregates with region parameters (`borrow(r) fn(…)`), and elements of scoped collections. - In source, `borrow fn(…)` always means a closure type: a reference to a plain function value cannot be written. - **Loans.** A closure's value carries the loans of its captures, shared or mutable, and a loan of its *environment*. The environment is a hidden local of the block that contains the literal, holding references to the captures. So, by the rules that already apply to references (B1–B5, `lifetimes.md`): - a captured variable cannot be changed, moved or ended while a closure that captures it is live; - nothing else may use a mutably captured one then; - a closure cannot be used after its block, nor returned from its function (N0243, N0244). - **Conversions.** - Where a closure type is expected, a plain function value converts to it (an adapter closure). For a named function the adapter calls it directly; for any other value it holds a copy and calls through it. - A `borrow fn` converts to a `borrow mut fn`: calling a shared closure changes nothing. - A `borrow mut fn` where a `borrow fn` is expected is N0299 (a new code): a shared copy could call it while it is changing what it captures. So is a capturing closure where a plain `fn` is expected. - **Calls** `f(args)` are as for function values (§36.1): safe, with the effects of the type's effect list (§36.3), else `sleep` and `alloc`. - **In the body**, `return` returns from the closure. `break` and `continue` cannot leave it (N0214, as outside a loop). `?` propagates from the closure, whose result type must allow it (N0261). A captured value cannot be moved out: the body reaches it through a reference (N0251). - **C.** The environment is a struct `ceeE_…` whose first member is the code pointer, followed by one pointer per capture. The closure's value is a pointer to that first member (C11 6.7.2.1p15). The body is a `static` function taking `const void *env` first. A call is `(*f)(f, args…)`. The code pointer's member is `f_0code`, and a field-precise capture's `f_0__`: the digit, which starts no identifier, keeps them apart from a captured local's `f_`, whatever its name. ### 36.3 Effect lists on function and closure types (G8b′; ADR 0012 §3.7) ```ceesafe extern "C" { @effects() fn spin_lock(l: *mut spinlock_t); @effects() fn spin_unlock(l: *mut spinlock_t); } @context(atomic) fn countUnderLock(lock: *mut spinlock_t, values: Slice, keep: borrow @effects() fn(i32) -> bool) -> i32 { unsafe { spin_lock(lock); } var n = 0; for (v in values) { if (keep(v)) { n += 1; } } // allowed: `keep` neither sleeps nor allocates unsafe { spin_unlock(lock); } return n; } fn isEven(v: i32) -> bool => v % 2 == 0; fn noisy(v: i32) -> bool { Console.writeLine(v); return true; } fn demo(lock: *mut spinlock_t, values: Slice) { let limit = 4; let quiet: @effects() fn(i32) -> bool = isEven; // checked: `isEven` has no effects countUnderLock(lock, values, (v) => v > limit); // the literal's body is held to @effects() countUnderLock(lock, values, noisy); // N0295: 'noisy' may sleep: it does console I/O } ``` - **Syntax** (grammar.md §2): `@effects(e, …)` written immediately before `fn` in a function type, with each `e` `sleep`, `alloc` or `suspend` as in §32.2: `@effects() fn(i32) -> bool`, and for closures `borrow @effects() fn(i32)` and `borrow mut @effects(alloc) fn(i32)`. No other attribute is allowed there (N0290), and only `fn` may follow it (a syntax error otherwise): C function pointer types (`extern "C" fn`, §29.5) take no effect list. - **The type.** The effect list is part of a function or closure type: `@effects() fn(i32)` and `fn(i32)` are different types. A type without a list may have `sleep` and `alloc`, so `fn(i32)` is the same type as `@effects(sleep, alloc) fn(i32)`. It never has `suspend`: only an async closure's call suspends, and that is a task (§40.3), not an effect of a sync call. A list may name `suspend`: an `@effectsOf` parameter of such a type takes an async closure (§32.5). Everything else (Copy, scoped, loans, C representation) is as in §36.1 and §36.2. - **Calls** through a value of such a type have the type's effects (§32.1). Without a list, that is `sleep` and `alloc`. - **Values.** Every value of a type with an effect list has at most those effects: - a **named function** used where such a type is expected (§36.1) is checked after effect inference: its inferred effects must be among the list (N0295, reported where it is named, with the chain of calls); - a **closure literal** used where such a type is expected has the list as its body's upper bound, as `@effects` is on a function (§32.2; N0295 in the body). So does the adapter closure of a plain function value (§36.2); - otherwise a value converts only to a type whose list includes its own: `@effects() fn(i32)` converts to `@effects(alloc) fn(i32)` and to `fn(i32)`, and `borrow @effects() fn(i32)` to `borrow fn(i32)` (with the kind conversions of §36.2). The reverse is N0299: a value whose effects are not known to be in the list. - **Inference does not narrow types.** A named function or closure literal with no expected type gets the type without a list, whatever its effects: `let f = isEven;` is a `fn(i32) -> bool`, which cannot be passed where an `@effects() fn(i32) -> bool` is expected. Write the type (`let f: @effects() fn(i32) -> bool = isEven;`). - **C.** The list changes nothing in C: the typedefs of two types that differ only in their lists name the same C type, so a conversion between them needs no cast. ### 36.4 Library methods taking closures (G8c; ADR 0012 §3.9) ```ceesafe fn main() { var values = [5, 3, 9, 1, 4]; values[..].sortBy((a, b) => a - b); // 1 3 4 5 9 var total = 0; values[..].forEach((v) => { total += v; }); // 22 Console.writeLine(values[..].any((v) => v > 8)); // true Console.writeLine(values[..].all((v) => v % 2 == 1)); // false var names = Vector.create(); // … push some names … names.sortBy((a, b) => a.view().length as i32 - b.view().length as i32); // move-type elements: fine names.retain((n) => n.view().length > 3); } ``` - **Methods.** Each takes a closure (§36.2); a plain function value converts to one, so `s.sortBy(compare)` works. The closure's parameters are references to the elements, so the element type may be any type, Copy or not: | Method | Receiver | Closure type | Result | |---|---|---|---| | `s.sortBy(cmp)` | `SliceMut`, or a vector (lending its elements mutably) | `borrow mut fn(borrow T, borrow T) -> i32` | `void` | | `s.forEach(visit)` | `Slice` (a `SliceMut` coerces), or a vector | `borrow mut fn(borrow T)` | `void` | | `s.any(test)`, `s.all(test)` | as `forEach` | `borrow mut fn(borrow T) -> bool` | `bool` | | `v.retain(keep)` | a `Vector` (a mutable place) | `borrow mut fn(borrow T) -> bool` | `void` | The closure types are `borrow mut fn`, so a closure that changes what it captures may be passed, and so may a shared one (§36.2). A slicing expression `a[lo..hi]` is a receiver as for `fill` (§16.5): `a[..].sortBy(cmp)`. - **`sortBy`** orders the elements so that `cmp(borrow s[i], borrow s[i + 1]) <= 0` for each `i`, when `cmp` is a consistent total order: negative for "less", zero for "equal", positive for "greater". - It is **not stable**: equal elements may end in any order. The order chosen is a deterministic function of the input and of `cmp`'s results. - It allocates nothing, calls `cmp` O(n log n) times in the worst case, and uses O(log n) stack: an introsort (ADR 0057): quicksort with a median-of-three pivot and a partition without a branch on `cmp`'s result; the elements equal to an earlier pivot set aside in one pass; insertion sort below 17 elements; heapsort past a depth of 2·log₂ n. A slice that is one ascending run from the start is left as it is, and one strictly descending run is reversed, after at most n − 1 calls of `cmp`. - If `cmp` is not a consistent order, or changes what it captures, the elements end in some order: they are still a permutation of the input, and nothing panics or misbehaves. - **`forEach`** calls `visit` on each element in index order. **`any`** calls `test` in index order until it returns `true` (then `true`; `false` for an empty slice), and **`all`** until it returns `false` (then `false`; `true` for an empty slice). - **`retain`** calls `keep` once on each element in index order. It keeps the elements for which it returned `true`, in their order, and destroys the others after the last call. The vector's capacity is unchanged. - **Loans.** The receiver is evaluated first and lent for the whole call (mutably for `sortBy`, `swap` and `retain`), so the closure cannot capture it: `v.retain((x) => x < v.length)` is N0241, as for any other conflicting loan. - **Effects.** The methods are `@effectsOf` their closure (§32.5): a call has the closure's effects, plus `retain`'s destruction of the elements it removes. So `s.sortBy((a, b) => a - b)` may be used in `@context(atomic)` and `@context(irq)` code, and `s.sortBy(compare)` may too if `compare` neither sleeps nor allocates. - **Implementation.** The methods are CeeSafe code in `lib/core/slices.cee`: static functions of the generic structs `SliceAlgorithms` and `VectorAlgorithms`, which the method calls name (so `SliceAlgorithms.sortBy(s, cmp)` is the same call). They use only safe CeeSafe and `swap` (§16.5). ### 36.5 Owning closures: `move` literals and `dyn fn` (M13 G3; ADR 0015 §3.6) ```ceesafe fn adder(n: u64) -> Box u64> { return Box.create(move (x: u64) => x + n)!; // n is moved (here, copied) into the closure } fn counter() -> Box u64> { var count: u64 = 0; return Box.create(move () => { count += 1; return count; })!; } fn main() { let add2 = adder(2); Console.writeLine(add2(3)); // 5 var next = counter(); next(); Console.writeLine(next()); // 2 var names = Vector.create(); names.push(String.create())!; let size: Box usize> = Box.create(move () => names.length)!; // a closure owning the vector: Box usize> Console.writeLine(size()); // 1 } ``` - **Types.** `dyn fn(T…) -> R` and `dyn mut fn(T…) -> R` (with an optional effect list, `dyn @effects() fn(…)`) are `dyn` types (§37): unsized, so they appear only behind a reference or in a box. `Box R, A>` is an **owning closure**: a move type that needs drop, which may be returned, stored in fields and collections, and outlive the function that made it. `borrow fn(…)` (§36.2) is unchanged and is not written `borrow dyn fn`. - **Move literals.** `move (x: T, …) => body` is a closure literal that captures **by move**: each local it names is moved into the closure when the literal is evaluated, or copied if its type is Copy. Afterwards the local is moved out (§14.3), as after any move. The parameter rules, `return`, `break` and `?` are those of §36.2. - **Where.** A move literal may only be the value argument of `Box.create(…)` or `Box.createIn(a, …)` whose expected result is a `Box` or a `Box`. Its parameter and result types come from that type. Anywhere else it is **N0333** (its own type is unnamed; OD-9a). - **Captures.** A move literal may not capture a scoped value (§19.1): a reference, a view, a slice, a closure, or a value with region parameters. That is **N0332**: the box would outlive the loan. - **Mutation.** A body that assigns to a capture, borrows it mutably or calls a mutating method on it changes the closure's own copy. It makes the literal a `dyn mut fn`: where a `dyn fn` is expected, that is N0299. A captured value cannot be moved out of a boxed body (N0251): it belongs to the closure, which may be called again. - **Run-once environments** (ADR 0074 §2.3). An `async move` child (§40.4, §40.5) runs its body at most once, so its body may move each capture out, once: `s.spawn(async move () => await serve(stream))`. A second move is N0250, and a capture whose type has a drop block cannot be partly moved (N0252). The body drops, as it returns, the captures it did not move, as a function drops its locals (§14.5): one that may or may not have been moved has a drop flag. A cancelled body returns along a normal path (§40.6), so it drops them there too; a child that never starts drops its whole environment at once. A field-precise capture (§36.2) is a reference and is never movable. - **Calls.** `f(args)` where `f` is a place of type `Box`, `borrow dyn fn…`, or a reference to such a box, calls the closure. A `dyn mut fn` needs mutable access: a `var` box, or a `borrow mut`, else N0248. A call has the effects of the type's effect list, else `sleep` and `alloc` (§36.3). - **Drop.** Dropping the box destroys the captured values, in capture order, then frees the box. - **Representation.** The closure's environment, a struct of the captured values after the code pointer, is the box's contents. The box is a `dyn` pair (§37.4) whose table has one entry, the body. Diagnostics: N0332 (a scoped capture), N0333 (a move literal outside `Box.create`). ## 37. Dynamic dispatch: `dyn` (M13 G2; ADR 0015) ```ceesafe interface Shape { fn area(borrow this) -> u64; fn grow(borrow mut this, by: u64); } struct Square { side: u64; } impl Shape for Square { fn area(borrow this) -> u64 => this.side * this.side; fn grow(borrow mut this, by: u64) { this.side += by; } } struct Rect { w: u64; h: u64; } impl Shape for Rect { fn area(borrow this) -> u64 => this.w * this.h; fn grow(borrow mut this, by: u64) { this.w += by; } } fn total(shapes: borrow Vector>) -> u64 { var t: u64 = 0; for (s in shapes) { t += s.area(); } // a call through the table return t; } fn main() { var shapes = Vector>.create(); shapes.push(Box.create(Square { side: 3 })!)!; // Box coerces to Box shapes.push(Box.create(Rect { w: 2, h: 5 })!)!; Console.writeLine(total(shapes)); // 19 var sq = Square { side: 1 }; let r: borrow mut dyn Shape = borrow mut sq; // a reference coerces too r.grow(2); Console.writeLine(sq.side); // 3 } ``` ### 37.1 The type `dyn I` - For an interface instantiation `I` (§21: `Shape`, `Comparable`), **`dyn I`** is the type of "a value of some type that implements `I`". Its size is not known, so, like `[T]` (§16), it is **unsized**: it appears only behind a pointer. - `borrow dyn I` and `borrow mut dyn I` are references (§13). They are scoped, follow every reference rule, and `borrow dyn I` is Copy. - `Box` (and `Box` for `SystemHeap`) owns its value (§18.2). It is a move type that needs drop. - Anywhere else, `dyn I` is **N0330**: a local, parameter or result of type `dyn I`, a field, an array or vector element, an optional, a type argument (`Vector`; `Vector>` is fine), or an element of a slice type. - **Dyn-compatible interfaces** (amended by ADR 0086 §2.3). `dyn I` is **N0331**, naming the member, if a method of `I` takes `this` by value, has no receiver (a static function), has type or const parameters of its own (§20.5), or names `Self` outside its receiver, or if `I` has an associated type or a constant, or requires `Copy`; and the same for each of its supertraits (§21.1). Defaults do not matter: a default is in the table like any method. The rule keeps `T = dyn I` sound (`dyn I` implements `I` for bounds, §37.2): generic code may call `T.create(…)`, which no table answers, and every member it lets through is a method with a reference receiver, which the table holds. A generic interface is checked once its type arguments are given. An async method does not make an interface incompatible (§40.1, ADR 0036). - `dyn` is a keyword in types only (grammar.md §2). `dyn I` binds tighter than `borrow`, so `borrow dyn I` is a reference to a `dyn I`. ### 37.2 Coercions - A value converts to a `dyn` type only where that type is **expected**: an annotated `let`, an argument, a field initialiser, a `return`, or an argument of a collection method such as `push`. Inference never invents a `dyn` type. - `borrow T` → `borrow dyn I`, and `borrow mut T` → `borrow mut dyn I` (or `borrow dyn I`). A `borrow mut T` variable or field converts through a reborrow, as every use of a `borrow mut` does (§13.3): it stays usable after the reborrow ends, and is frozen while it lives (B4); - `Box` → `Box`: a move of the box, with no new allocation. The allocator type must be the same. - `T` must implement `I` (§21.1), else **N0268**, as for an unsatisfied bound. - **Scoped `T` behind a reference** (ADR 0064 §3.2, OD-10a). A type with region parameters that implements `I` (§25.3), such as `View`, coerces to `borrow dyn I` and `borrow mut dyn I`. The `dyn` reference carries the loan of the reference it came from. As for any `borrow(a) View`, `r` outlives `a`, so the referent's loans outlive the `dyn` reference, and borrowing the value keeps its loans alive (B4). No region is written on the `dyn` type. Other scoped types (references, closures, views, slices) are not `dyn` (they implement no interface), and `Box` takes no scoped `T` (**N0268**): a box owns its value, and its type keeps no region. - `T` must be a sized type: a struct, an enum, or a built-in type such as `i32`. A `dyn I` does not coerce to another `dyn` type, and there is no conversion back to `T` (no downcasting; OD-10b). - **`dyn I` implements `I`** (ADR 0064 §3.1). For bounds, `dyn I` (and `dyn I + Send`) implements its own interface, with the same type arguments: each method calls through the value's table. So a generic `fn layout(m: borrow M)` takes a `borrow dyn Metrics` as well as a `borrow Fixed`, with `M = dyn Metrics`. `dyn I` is unsized, so it may be a type argument only where the generic holds it behind a reference or a box: - A function instance whose parameters, locals or result hold a `dyn` value by value is N0222 at the use, with the reason. - A user struct or enum instantiated with `dyn I` (`ByRef`, written or inferred) is N0330 if it holds the value by value (`struct Wrap { inner: T }`). ### 37.3 Calls - `x.m(args)`, where `x` is a `borrow dyn I`, a `borrow mut dyn I`, a `Box`, or a place of one of these types, calls the method `m` of `I` through the value's table. The method is that of `T`'s `impl I for T`. - The receiver rules are those of a reference to `T` (§13, §21.1): - a `borrow this` method may be called through any of the three; - a `borrow mut this` method needs mutable access: a `borrow mut dyn I`, or a mutable place of `Box`. Through a `borrow dyn I` it is N0248, as for any value behind a shared reference. - Only `I`'s methods can be called. `T`'s other methods and fields are not visible through `dyn I` (N0234). - **Async methods** (§40.1, ADR 0036). For an async method, `x.m(args)` is a task (N0342 unless awaited, run or boxed): `await x.m(args)` has the method's result type, as through `T: I`. The receiver and the arguments are lent to the callee's frame until it returns. - **Effects** (§32). An interface method may carry an effect list, which bounds every `impl` of it: ```ceesafe interface Visitor { @effects() fn visit(borrow mut this, value: u64); } ``` An `impl` method whose effects exceed its interface's list is N0295, reported at the `impl` method. A call through `dyn I` has the effects of `I`'s list, or `sleep` and `alloc` without one, as for a closure without a list (§36.3); an async method without one has `suspend` and `alloc`, since an async body cannot sleep (ADR 0036). A call on a generic `T: I` is unchanged: it has the effects of the instantiated method (§21.2). - **Drop.** Dropping a `Box` destroys its value with `T`'s drop glue, then frees it with `A`. ### 37.4 Representation (c-backend.md §2) - A `borrow dyn I`, a `borrow mut dyn I` and a `Box` with a zero-sized `A` are each two words: `{ data, vt }`. `data` points at the value, and `vt` points at the table for (`T`, `I`). - The table is a `static const` struct per pair (`T`, `I`) that a coercion names. It holds the drop glue, the value's size and alignment (for a user allocator's `deallocate`), then one function per method of `I`, in declaration order. Each function is a generated adapter taking `void *` as its receiver, so no function is called through a pointer of another type. An async method's entry starts `T`'s method in the frame of a call through the table, and the table also holds the size of that frame for `T` (§40.6, c-backend.md §3a). - `dyn I` values have no address identity beyond `data`. Two `borrow dyn I` values are not comparable (N0209). Diagnostics: N0330 (`dyn I` not behind a pointer), N0331 (an interface that is not dyn-compatible). ## 38. Threads (M13 G4; ADR 0016) ```ceesafe struct Counter { n: u64; } fn main() { let counter = Arc>.create(Mutex.create(Counter { n: 0 })!)!; var workers = Vector>.create(); for (i in 0..4u64) { let mine = counter.clone(); // another owner, moved into the thread workers.push(Thread.spawn(move () => { var guard = mine.get().lock(); let data = guard.data(); data.n += i + 1; return i * 10; })!)!; } var total: u64 = 0; loop { match (workers.pop()) { Some(t) => { total += t.join(); } null => { break; } } } Console.writeLine(total); // 60 var guard = counter.get().lock(); Console.writeLine(guard.data().n); // 10 } ``` - Static functions of the library's generic types take their type arguments, as for any generic struct (§20.2): `Arc>.create(…)`. ### 38.1 `Send` and `Share` - Two properties of types, **inferred** and never declared by user code (v1), which may also be used as bounds, like `Copy` (§21.2): ``, ``. - **`Send`**: a value may be moved to another thread. It holds for: - integers, floats and `bool`; - structs and enums whose fields and payloads are all `Send`, and arrays of a `Send` element; - `Box`, `Vector`, `String`, `SlotMap`, `StableVector` and `Handle`, with `T: Send` and the system heap (or `FailingAllocator`); - `File`, `TcpListener`, `TcpStream`; - `Box` and `Box R + Send>`, and with `+ Share` (§38.4); - the library's thread-safe types (§38.3): `Arc`, `Mutex`, the atomics. - It fails for: - references, slices, views and every other scoped type (§19.1); - closures (`borrow fn`), function values aside (`fn(…)` is `Send`: a code address); - raw pointers and imported C types; - a box or collection with a user allocator; - regions and pools; - `Thread` (§38.2); - `Box` and `Box` without `+ Send`. - **`Share`**: shared references to a value may be used from several threads at once. It holds for a `Send` type that does not own, through any path of fields, payloads, elements or box contents, a value that changes through a shared reference (ReachesSharedChange): - a `StableVector`, whose `push` takes `borrow this` (§24.2); - a `RawCell` (§41.1), so every cell (`Cell`, `RefCell`) and every type holding one (ADR 0084 §2.2); - a `dyn` box, `Box` or `Box`: what it holds is not known, and may be such a value (a `dyn … + Share` box is not, §38.4: the walk stops there); - a type parameter without a `Share` bound. The walk stops at the library's thread-safe types (§38.3), which are `Share` by design: `Mutex>` is `Share`. `Thread` is not `Send`, so not `Share`. (Until 2026-11-09, every `Send` type was `Share`, which let two threads push to one shared `StableVector`, or share a `dyn` box, in safe code: a data race. ADR 0084 added the cells.) A type that holds a cell is still `Send` when its contents are: a cell may move to another thread, not be used from two at once. - Inference is structural and recursive. A recursive type is `Send` unless a path through it reaches a type that is not. Generic code knows only its bounds: a `T` is `Send` only under a `T: Send` bound. ### 38.2 Spawning and joining - **`Thread.spawn(move () => body)`** starts a thread that runs the move literal's body (§36.5) and returns `Result, SpawnError>`, where `T` is the body's result type. `SpawnError` is the enum `SpawnError.Resources` (the thread or its closure could not be allocated). - The literal takes no parameters. Its captures must be `Send` (**N0334**), and so must `T` (**N0334**). - `Thread.spawn` also takes a `Box T + Send>`. - A move literal is allowed here as well as in `Box.create` (amending §36.5). - **`Thread`** is a move type that needs drop. `t.join()` consumes it, waits for the thread to finish, and returns its `T`. Dropping a `Thread` that was not joined waits for it and destroys its result. There are no detached threads: every thread a program starts has ended before `main` returns. - A panic in any thread aborts the process (OD-5). - **Effects** (§32): `spawn` has `sleep` and `alloc`. `join`, and dropping a `Thread`, have `sleep`. ### 38.3 Shared state (library types: `lib/std/sync.cee`, and the atomics in `lib/core/atomics.cee`) - **`Arc`**: shared ownership. - `Arc.create(v)` returns `Result, AllocError>`. `a.clone()` returns another owner of the same value, and `a.get()` returns a `borrow T` (shared only). - The count is atomic. The last owner's drop destroys the value and frees it. - **`Mutex`**: mutual exclusion. - `Mutex.create(v)` returns `Result, AllocError>`; the platform lock is allocated. - `m.lock()` waits for the lock and returns a `MutexGuard`, scoped to `m` like C7's guards. Its `data()` lends `borrow mut T`, and its drop unlocks. The data is held in a `RawCell` (§41.1), since the guard changes it while the mutex is shared. `T: Send` already refuses every loan-holding `T`, so N0351 cannot arise there. - `lock` has the `sleep` effect. - **Atomics**: `AtomicU32`, `AtomicU64`, `AtomicUSize` and `AtomicBool`, in every profile (ADR 0026). Each operation is a static inline function of the runtime header, so it compiles to the machine's atomic instructions. - `create(v)`, then `load()`, `store(v)`, `swap(v)` and `compareExchange(expected, new)` (returns `Result`, `Err` with the current value). - The integer types also have `fetchAdd(v)` and `fetchSub(v)`, which wrap, and (ADR 0041) `fetchOr(v)` and `fetchAnd(v)`; each returns the value before the change. - **Layout.** `AtomicU32` and `AtomicU64` are laid out exactly as `u32` and `u64` (size and alignment, checked by `_Static_assert` in every unit), and `AtomicUSize` as `usize`; so an atomic may be a field of a `@repr(C)` or imported struct, as its integer member (§28.5, §31.1; ADR 0041). `AtomicBool` is a `u32`. - Every operation takes `borrow this`, is sequentially consistent, and has no effect. - **With an ordering** (ADR 0021): `loadWith(o: LoadOrdering)`, `storeWith(v, o: StoreOrdering)`, and `swapWith`, `compareExchangeWith`, `fetchAddWith`, `fetchSubWith`, `fetchOrWith`, `fetchAndWith` taking `o: Ordering` (`AtomicBool` has the first four). The enums are `LoadOrdering { Relaxed, Acquire, SequentiallyConsistent }`, `StoreOrdering { Relaxed, Release, SequentiallyConsistent }` and `Ordering { Relaxed, Acquire, Release, AcquireRelease, SequentiallyConsistent }`, with C11's meanings (C11 §7.17.3). A load cannot be given release ordering, nor a store acquire, which C11 leaves undefined. A compare-exchange that fails orders as a load: `AcquireRelease` as `Acquire`, `Release` as `Relaxed`. - An ordering changes which values an operation may observe, never whether two accesses race: every access is still atomic, so T1–T3 are unchanged. A too-weak ordering is a logic error that neither the interpreter (which runs one thread at a time, a sequentially consistent execution) nor ThreadSanitizer can find. - These types are `Send` and `Share` by the library's declaration (`@threadSafe`, accepted only in the library's own files). Their `unsafe` internals are argued in `ownership.md` T1–T3. ### 38.4 `dyn … + Send` - `dyn I + Send` and `dyn fn(…) -> R + Send` (and with `mut`) are `dyn` types (§37, §36.5) whose boxes are `Send`, but not `Share` (§38.1): a box may move to another thread, not be read from two at once. A coercion to one needs `T: Send`, which for a move literal means `Send` captures (N0334). A `dyn … + Send` converts to the same `dyn` type without `+ Send` (the same value: they share their C representation and tables), never the other way. `Thread.spawn(f)` with `f: Box T + Send>` runs a closure made elsewhere. - **`dyn … + Share`** (ADR 0084 §2.5): `dyn I + Share` and `dyn fn(…) -> R + Share` (and with `mut`) are `dyn` types whose boxes are `Send` and `Share`; `+ Share` implies `+ Send`, as `Share` implies `Send`, and `+ Send + Share` is the same type. A coercion to one needs `T: Share` (N0334 otherwise: a value holding a cell, §41, or a closure capturing one, cannot become one). ReachesSharedChange stops at such a box, so `Arc>` is allowed and both `Thread.both` children may read one. It converts to `dyn I + Send` and to `dyn I` (the same value and tables), never the other way. ### 38.5 Profiles - Threads, `Arc` and `Mutex` are in the **hosted profile**: hosts with pthreads or Win32 threads, and CosmoOS userland over `cosmo_thread_*` and `cosmo_mutex_*` (`profiles.md`). In a freestanding build their names are N0293. The kernel uses lib/kernel's locks (ADR 0025). The atomics and their orderings are in `lib/core`, in every profile (ADR 0026). - `lib/std/` is the hosted library: compiled into hosted packages only. Like `lib/core` (§29.2), a package may shadow its names, and only what a program uses is emitted. ### 38.6 Fork-join: `Thread.both` (OD-25; ADR 0018) - **`Thread.both(a, b)`** runs `a` on a new thread and `b` on the calling thread, waits until both have finished, and returns `void`. It is in the hosted profile, with `Thread` (§38.5). - Each argument is a closure literal with no parameters and a `void` result (§36.2), or a function value of type `fn()` (§36.1). A closure that is not a literal (a variable or a parameter of type `borrow fn()`) is refused (**N0334**): its captures cannot be seen, and the rule below is about them. - Results travel through captures: `Thread.both(() => { low = sum(halves.left); }, () => { high = sum(halves.right); });` - **Captures.** `a` runs on another thread while `b` runs on this one, so each capture of either closure must be safe to use from another thread at the same time: - a capture used **shared** needs a **`Share`** type, where, for this rule only, `borrow T`, `Slice` and `StringView` are `Share` when `T` is; - a capture used **mutably** needs a **`Send`** type, where, for this rule only, `borrow mut T` and `SliceMut` are `Send` when `T` is; - a captured closure is refused; - otherwise **N0334**, naming the capture and its type. These extended rules apply here only: outside `Thread.both` references are neither `Send` nor `Share` (§38.1). - **Loans.** Both closures are arguments of one call, so both, and every loan they carry, are live for the whole call (§36.2): the borrow checker refuses captures of one value by both where either changes it (B-rules, N0241), and nothing either captures can be changed, moved or ended until `both` returns. - **No failure.** If the new thread cannot be started (its runtime record cannot be allocated, or the platform refuses), `both` runs `a` and then `b` on the calling thread. The result is the same either way, so `both` returns `void` and a program cannot tell whether `a` ran in parallel. - A panic in either closure aborts the process (OD-5). - **Effects** (§32): `sleep` and `alloc`. Diagnostics: N0334 (a value that is not `Send` where one must cross to another thread; for `Thread.both`, a capture that may not be used from two threads at once, or an argument that is not a literal or a function value). ## 39. Static items (M14 K1; ADR 0024) ```ceesafe static OPENED: AtomicU64 = AtomicU64.constant(0); static NAME: StringView = "ceesafe"; fn openOne() -> u64 => OPENED.fetchAdd(1) + 1; fn opened() -> borrow(static) AtomicU64 => borrow OPENED; static mut BOOT_FLAGS: u32 = 0; /// Safety: called once on the boot CPU, before anything else reads BOOT_FLAGS. unsafe fn setBootFlags(flags: u32) { BOOT_FLAGS = flags; } ``` ### 39.1 The item - `static NAME: T = e;` and `static mut NAME: T = e;` at package level, with an optional visibility, like `const` (§10). The type is required. A static is never generic, and a function cannot declare one (N0337). - A static is one value for the life of the program, at a fixed address. It is never dropped: a `T` with a `drop` is allowed, and its drop never runs. ### 39.2 `static` - `T` must be `Share` (§38.1), because every thread may use a static (N0337). Views of static string data are the one exception, since they cannot change and outlive everything: `T` may be `StringView`, and in a `static` (not a `static mut`) also `[StringView; N]`, whose initialiser is an array of string literals or constant views (ADR 0066). Such a table is a place, so unlike a constant table (§10) it is sliced and borrowed where it is named: `static LEVELS: [StringView; 3] = ["Low", "Medium", "High"];` then `f(LEVELS[..])`. A lib/core `CStr` (§22.3) initialised by a `c"…"` is static string data too (ADR 0087 §2.4), and so is a `Slice` in a `static` initialised by `embed` (§10.1), by a constant view's `.bytes()` (§10) or by a constant holding one, and, in a `static`, an array of them (`static FILES: [Slice; 2] = [embed("a.bin"), embed("b.bin")];`). - `NAME` in an expression denotes the place `*S`, where `S` is a reference to the static of type `borrow(static) T`. `static` is the built-in region of §25.1, which outlives everything. So a static is used exactly as a value behind a shared reference is: - it can be read when `T` is `Copy`, borrowed (`borrow NAME`, `borrow NAME.field`), and its methods taking `borrow this` called; - it cannot be assigned or moved, and nothing reached through it can be changed (§13.2: N0211, N0248; a move out of it is N0251, as for any place behind a reference); - a reference to it, or to anything in it, has the `static` region. It may be returned from a function whose return type names `static` (`borrow(static) T`, §13.5), stored, and sent to another thread. - Mutation goes through `T`'s own synchronisation: an atomic's operations, a lock's guard. ### 39.3 `static mut` - `T` need not be `Share`. - `NAME` denotes the place `*S` with `S: borrow(static) mut T`, so it can be read, assigned, borrowed mutably and moved out of as such a place can. - **Every use is an unsafe operation** (§27.3), N0280 outside an unsafe context. The unsafe code's `/// Safety:` comment must argue what the checker cannot. Two uses give two references that may alias, and two threads may race on it (`unsafe.md` U3). - **What may be written** (ADR 0067). `S` refers to memory that outlives every borrow, so a value written to it, or into it, may carry no loan: N0272 otherwise. Static data carries none: a string literal, a constant view, or a view read from another static, and a call's result whose type names only `static` (§25.6, ADR 0068). A view of a local or of a parameter is rejected. The unsafe block makes the use unsafe; it does not lift this rule. - **C objects** (ADR 0042). `T` may be an imported C type (§31), or hold one: a device, a protocol's ops table. C code reaches it through the addresses CeeSafe gives it and writes it, which is why it is a `static mut`: a plain `static` of an imported type is N0337, since CeeSafe would read it as unchanging. Its initialiser is C's (§39.4). ### 39.4 Static initialisers `e` is evaluated at compile time to a C constant initialiser. It must be a **static initialiser** (N0338 otherwise): - a constant expression of an integer, `bool` or float type (§10), or a constant's name; - `"…".ptr as *T`, a string literal's address, for a C string (`"name\0".ptr as *CChar`; §28); - a string literal, as a `StringView`, or a constant concatenation of constant views (`PREFIX + "name"`, §10; ADR 0087 §2.7), as a `StringView` or, with a `c"…"` right operand, a `CStr`; - `embed(path)` as a `Slice`, `embedText(path)` as a `StringView` (§10.1), and a constant view's `.bytes()` as a `Slice` (§10); - a struct literal (§12) whose fields are static initialisers. Private fields may be given only where §12 allows it: inside the struct's own functions, so inside its `@constant` constructors (below); - an array literal or repeat (`[e; N]`) of static initialisers. In a static's type and initialiser the elements may be of a non-Copy type with no `drop` block, such as atomics (ADR 0075 §2.15): a static is never moved out of or destroyed, and `[e; N]` there evaluates `e` N times at compile time, so `static SLOTS: [AtomicU64; 4] = [AtomicU64.constant(0); 4];` is a table of shared counters, `SLOTS[i].fetchAdd(1)`. Elsewhere array elements stay Copy (N0259). A library's static is emitted in C only where compiled code uses it; - a call of a `@constant` function whose arguments are static initialisers; - a call of a C constant macro whose arguments are static initialisers or `here` (below); - `layoutOf()` (§30.1). - **For C objects** (ADR 0042), where the expected type is an imported C type, an `extern "C" fn` or a raw pointer: - a struct literal of an imported type (§31.2's one exception), naming any of its declared fields: the members it does not name are zero, as C's designated initialisers leave them (C11 §6.7.9p21), whether CeeSafe declares them or not. A `[u8; N]` member (a C `char` array, ADR 0034) may be given a string literal of at most `N` bytes, which C zero-pads (longer: N0338); - the name of an `extern "C"`, `@export` or `@callback` function: its C address (§29.5); - `borrow S as *T`, `borrow mut S as *mut T`, and the same of a field of `S` or an element at a constant index, for a static `S` (any static, declared before or after): its address, an address constant (C11 §6.6p9). Taking a `static mut`'s address is not an unsafe use. `S`'s value, or a field's, is N0338; - `0 as extern "C" fn(…) -> R` and `0 as *T`: null. Not in v1: enum values (including optionals), references other than the static string data above, closures, and `dyn`. Each of these is N0338. **`@constant` functions.** `@constant` on a function or method (generic or not) makes it callable in a static initialiser. Its body must be a single expression (`=> e`, or `{ return e; }`) that is a static initialiser in which its parameters may also appear (N0338). It is an ordinary function too, callable at run time with the same result. A call in an initialiser substitutes the arguments for the parameters, and instantiates generic code as any call does. **C constant macros.** `@constant` on a function in an `extern "C"` block (§28.2) declares a C macro that expands to a constant initialiser of its result type: `@constant fn SPINLOCK_INIT(name: *CChar) -> spinlock_t;`. It may be called only in static initialisers and `@constant` bodies (N0338 elsewhere). CeeSafe trusts the declaration, as it trusts an extern's `@effects` (R-2). **`here`** (ADR 0027). As an argument of a C constant macro, `here` is the address of the place the macro's result initialises, with type `*mut T` for the macro's result type `T` (N0338 if the parameter's pointee is another type). Through nested `@constant` constructors the place is the whole path: in `static TABLE: Mutex = Mutex
.constant(name, data)`, the constructor's `MUTEX_INIT_AT(here, name)` is given the address of `TABLE`'s field `raw`. So a macro may build C state that points to itself, as CosmoOS's list heads, wait queues and mutexes do. `here` is a name, not a keyword: a variable or item called `here` hides it, and outside static construction it is an ordinary unknown name (N0200). Anywhere else in a static initialiser or a `@constant` body, including as an argument of a `@constant` function, it is N0338. A function that uses it builds C state, so it is compile-time only (below). **C state built in place** (§33.3). A type with a `@placed init` may be in a static only through a `@constant` constructor, whose initialiser must leave the value as `init` would: for CosmoOS's locks, the C macro does. `init` does not run for a static. **C state at compile time** (§33.1–§33.2). C state is otherwise only made in place, by `Box.create`, and never held as a value. A static initialiser is the other way to make it, at the static's fixed address, so three places may hold it: - a static's type, which may hold C state: a static never moves; - a C constant macro's result type, an imported C type (not FFI-checked: it is C's initialiser, not a value crossing a call; §28.3); - a `@constant` function's result type, and its struct literal, which may give a C-state field the result of a C constant macro. A `@constant` function whose result holds C state is **compile-time only**: it is evaluated in static initialisers and other `@constant` bodies, called anywhere else is N0338, and it is never compiled to C. ### 39.5 Lowering - A static is a C object at file scope named `ceeS_` (with the package's name for a dependency's, as in §2.1 of `c-backend.md`), `static` unless `--split` places it in `ceesafe_instances.c` and renames it (c-backend §2.4). It is not `const` in C, because its methods may write through the pointers they derive from it. - Its initialiser is the evaluated value: a designated initialiser for a struct (`{ .f_count = 0 }`), braces for an array, the macro call for a C constant macro, `(const char *)"…"` for a string literal's address, and a string view literal for a `StringView`. `here` is `(&ceeS_.…)`, an address constant (C11 §6.6p9), and `layoutOf()` of a C-laid-out `T` is `{ .f_size = sizeof(…), .f_align = _Alignof(…) }`. - `S` is `&ceeS_`. - **C objects** (ADR 0042): an imported type's literal is a designated initialiser over its C member names (`{ .name = "lo", .mtu = 65535, .ops = ((struct netif_ops *)&ceeS_LO_OPS) }`); a function's address is its C symbol (the extern's name, the export's, or a callback's `ceeW_` function, declared before the statics); a static's address is `((T)&ceeS_.…)`. When an initialiser holds a static's address, every static is first declared by a tentative definition (C11 §6.9.2), so the order of the items does not matter. - The interpreter evaluates every initialiser before `main` into its memory. A C constant macro has no value it can compute, so a program whose statics use one cannot be interpreted (a fault at start). Diagnostics: N0337 (a static whose type is not `Share`, or one that is generic or local), N0338 (an initialiser or `@constant` body that is not a static initialiser, or a C constant macro used elsewhere), N0280 (a use of a `static mut` outside an unsafe context). ### 39.6 C data (ADR 0080 §3.2) - **Exported statics.** `@export` or `@export("symbol")` on a `static` or `static mut` makes it C data with external linkage under `symbol` (default: its name), declared in `ceesafe_exports.h`. Its type is one C spells (§28.5's member types: integers, `bool`, `CChar`, atomics, raw pointers, `extern "C" fn` types, `@repr(C)` and imported C types; N0291 otherwise); an array is not supported yet (N0216). The symbol follows §28.4's rules (N0292, N0201). - A `static mut` is C's `T symbol = …;`: C may read and write it, and every CeeSafe use stays unsafe (§39.3). - A plain `static` holding no atomic is C's `const T symbol = …;`, matching a header's `extern const struct ops data_ops;`, and may be of an imported C type (which §39.3 otherwise refuses): neither side writes it. - **Imported statics.** In an `extern "C"` block, `static NAME: T;` and `static mut NAME: T;` declare data its headers define (`extern T NAME;`), named `NAME` in C or its `@cname("symbol")`, of a type as above; arrays included (`static names: [*CChar; 8];` for `extern const char *const names[8];`). A `static mut` is used as one (unsafe); a plain `static` is read-only, for `const` C data and for data C changes only atomically (an atomic type). - **Lowering.** The C object is named by its symbol instead of `ceeS_`. No definition is emitted for an imported static. A use takes its address through `uintptr_t` to CeeSafe's pointer type, as for a C array member (§31.3), which also drops a `const`. - The interpreter has none of C's data: a program with an imported static, or with a `const NAME: T;`, faults at start. ## 40. Async functions and structured concurrency (L1; ADR 0031) ```ceesafe async fn fetchBoth(net: borrow Net, a: StringView, b: StringView) -> Result { var left = Buffer.withCapacity(4096)?; var right = Buffer.withCapacity(4096)?; await Async.scope(NetError.OutOfMemory, (s) => { s.spawn(async () => { await net.get(a, mut left)?; return Ok(); }); // borrows `left` from this frame s.spawn(async () => { await net.get(b, mut right)?; return Ok(); }); // capturing `left` here too: N0241 })?; // both children have returned return Ok(Pair { left: left, right: right }); } fn main() { let result = Async.run(fetchBoth(Net.system(), "a.example", "b.example")); } ``` (`Net`, `Buffer` and `Pair` stand for library or user types.) ### 40.1 Async functions and tasks - **`async fn f(…) -> T`** (grammar.md §2), at package level, as a method, or as an interface method. An `extern` function, a `drop` block and a `@constant` function cannot be async (N0290). `async` comes after `unsafe`. - **Interface methods.** An impl's method is async exactly when the interface's is (N0267). Through a type parameter (`T: I`) each instance awaits the concrete method, whose frame is known. Through `dyn I` (§37.3, ADR 0036) the awaiter's frame has room for the largest frame of `I.m` among the types coerced to `dyn I` anywhere in the program: the program is compiled whole, so that set is known. `dyn I` and `dyn I + Send` have one such frame, with room for the types coerced to either, since `+ Send` changes no layout (bug B15). An await through the table that may reach its own frame is recursion through `await` (below). `Async.boxed` takes such a call too, with a frame of the receiver's own size. - **A call of an async fn is a task**, not a value: it runs nothing until it is awaited (§40.2), run (§40.4) or spawned (§40.5). A task call anywhere else (a `let`, an argument of any other call, a field, a return) is **N0342**. There is no type of tasks in v1, so a task cannot be stored, returned or passed on. - **Arguments** are evaluated at the call, in order, and moved or lent into the task's frame. Borrowed arguments stay borrowed until the task has returned (§40.6), by the rules for references (§13, `lifetimes.md`). - **The body** is checked like a function body with result type `T`, with three differences: it may `await` (§40.2); it is a context that forbids `sleep` (§32.3), so a blocking call is N0295 with its chain of calls; and its effects (§32.1) are those of its operations, `suspend` among them if it awaits. - **Recursion through `await`** (an async fn that awaits itself, directly, through other async fns, or through `dyn`) would make its frame contain itself. In the hosted profile each await on such a cycle is **stacked** (ADR 0056): its callee's frame comes from the task's **frame stack**, and the awaiter holds a pointer to it, so the program is written as sync recursion is. The stack grows in 16 KiB chunks (or a frame's size), which a returning call gives back to its executor's pool (up to 64) for the next tasks; if no chunk can be had, the program panics (`async frame stack: out of memory`), as a sync program whose call stack overflows stops. Awaits not on a cycle are embedded as before. `--explain-frames` names the stacked ones. **N0343** remains where there is no frame stack (the freestanding profile), for a cycle through a `race` or `all` child, and for an async instance calling itself with its closure (§32.5). A recursive *sync* call from an async fn is an ordinary call. - **`await Async.boxed(alloc, f(…))`** awaits a task whose frame comes from `alloc` (an allocator type, as for `Box.createIn`, §30) and is given back when it returns; the awaiting frame holds only a pointer, so recursion through it is allowed. Its value is `Result`: `Err(AllocError.OutOfMemory)` if the frame cannot be had, and then the task does not run. N0343's sure fix is `Async.boxed(SystemHeap {}, f(…))!`; in hosted code it keeps the failure as a value where a stacked await would panic. - A generic async fn is checked once and instantiated like any generic function (§20.3). ### 40.2 `await` - **`await e`**: `e` must be a task (a call of an async fn or an async closure), or one of the `Async` operations that produce one (§40.4); otherwise **N0342**. Its value is the task's result, of the task's result type. - `await` is allowed only in the body of an async fn or async closure: anywhere else, including a sync closure inside an async fn, a `drop` block and a deferred statement, it is **N0341**. - **What it means.** The current task runs `e` to completion; while `e` waits, other tasks of the same thread may run. Every `await` is therefore a point where other tasks run and where cancellation may be observed (§40.7). Nothing else suspends a task. - **Borrows across `await`.** A frame never moves (§40.6), so a reference to a local may be held across an `await` with no extra rule. The borrow checker treats `await e` as a call: what `e` borrows is borrowed until it returns. - **Effects:** `suspend` (§32.1), so an `await` while a `@context(atomic)` value may be live is N0295. ### 40.3 Async closures - **`async (x, …) => body`** (grammar.md §2.1) is a closure literal (§36.2) whose call is a task. Its captures, their modes and their loans are §36.2's. `return` and `?` in the body return from the closure's task. - It is accepted only as the argument of `spawn`, `race`, `all` and `shield` (§40.4, §40.5), or of an `@effectsOf` parameter (§32.5, ADR 0037), the way §38.6 accepts only literals: its captures must be visible where it is used (N0342 elsewhere). A named async fn becomes such a closure only for an `@effectsOf` parameter. It is never a function or closure value otherwise (N0298). - Its body is an async body: it may await and forbids `sleep`. ### 40.4 The `Async` operations `Async` is a built-in type, like `Thread` (§38), in the hosted profile until L1 A4 (§38.5). Every operation below except `run` and `blocking`'s closure produces a task, so it is written after `await`. | Operation | Result | Notes | |---|---|---| | `Async.run(f(…))` | `T`, the task's result | Runs the task on this thread until it returns. Sync code only: it has `sleep` and `alloc`, so in async code it is N0295. The operand is a call of an async fn: `Async.scope`, `all`, `race`, `timeout` and the others are awaited inside one, so as the operand they are N0342 (0.15.2) | | `Async.runWorkers(n, f(…))` | `T` | `Async.run` with `n` workers, this thread among them (`n: u32`; ADR 0038): `n − 1` more threads, started for the run and joined when it returns, may take its `spawnSend` children (§40.5). A worker that cannot start is skipped: the result is the same. Hosted only | | `Async.scope(oom, body)`, `Async.scopeIn(alloc, oom, body)` | `Result` | §40.5 | | `Async.race(a, b)` | `Result, E>` | Two async closures of result `Result`, run at once. The first to return decides: an `Ok` wins and the other is cancelled and awaited; `Raced` (lib/std) holds `winner: u32` (0 or 1), `value: T`, and `other: T?`, the loser's value if it also returned `Ok`. An `Err` first is the result. The children's type comes from an expected `Result, E>`, else from `a`'s expression body | | `Async.all(a, b)` | `Result` | Two async closures of result `Result`, run at once; the first `Err` cancels the other and is the result. Results travel through captures, as in §38.6. Without an expected type or an expression body for `a`, `E` is `AsyncError`. The two may not capture one local where either changes it (N0241) | | `Async.shield(ms, body)` | the body's result | Runs an async closure with cancellation ignored (§40.7) for at most `ms: u64` milliseconds. The body's result type is the expected one; without one, an expression body's own, and a block body's `Result` | | `Async.timeout(ms, timedOut, body)`, `Async.deadline(at, timedOut, body)` | `Result` | ADR 0046. Runs `body`, an async closure of result `Result`, beside a deadline `ms: u64` milliseconds from the call (`timeout`) or at `at: u64` on `Timer.now()`'s clock (`deadline`). If the body returns first, its result is the result. Once the deadline passes first, the body is cancelled and awaited: a late `Ok` is kept, and any `Err` becomes `timedOut: E`, which is evaluated before the body starts and dropped if unused. A cancellation from outside gives the body's own result, never `timedOut`; a tie is decided by which task runs first, as in `race`. The body's type comes from an expected `Result`, else from its expression body | | `Async.checkpoint(cancelled)` | `Result` | `Err(cancelled)` if the current task is cancelled and not shielded, else `Ok`: a cancellation point for long computations. A call of lib/std's `AsyncControl.checkpoint` | | `Async.isCancelled()` | `bool` | Whether the current task is cancelled and not shielded (`AsyncControl.isCancelled`) | | `Async.blocking(closure)` | `T` | Runs a sync closure literal with no parameters on a worker thread and waits for it without blocking other tasks. Its captures follow §38.6's rule and `T` must be `Send` (N0334). If no worker can be started, it runs on the calling thread: the result is the same. The body's effects are the worker's (the task has `suspend`), and the wait is not cancelled: a cancelled task still waits for its worker | | `Async.park()`, `Async.waker()` | `void`, `u64` | Library-only primitives (**N0345** elsewhere): suspend the current task until its waker is woken; the current task's waker id | | `AsyncSignal.create()`, `s.raise()`, `await s.wait()` | `AsyncSignal`, `void`, `Result` | lib/core (ADR 0039): `raise` is `@effects()`, so another thread or an `@context(irq)` handler may call it; `wait` parks the task until a raise, which a raise before the wait satisfies. One task waits at a time; a cancelled wait returns `Err(Cancelled)` | `race`, `all`, `shield`, `timeout` and `deadline` place their children's frames in the caller's frame, so they never allocate. A child of `race` or `all`, and the body of `timeout` or `deadline`, may be `async move () => …` (ADR 0074 §2.3): it owns what it captures, moved into its frame, and as it runs once its body may move a capture out (§36.5). `let r = await Async.timeout(ms, E.Timeout, async move () => await TlsStream.accept(stream, cfg));` gives `stream` to the body: after the deadline it is gone, dropped as the cancelled body returned. Two children of one `race` or `all` may not both capture a local where one changes it (N0241); a local moved into one is moved (N0250 in the other). The library (`lib/std/async.cee`, Decision 0008) builds everything that waits for the outside world on `park` and `waker`: timers, console (`AsyncConsole`), files (`AsyncFiles`: each operation on a worker thread, through `Async.blocking`, since a regular file is always ready), sockets (`AsyncTcpListener`, `AsyncTcpStream`, on non-blocking descriptors; and `AsyncSockets`, the same calls on raw descriptors for code with its own wait loop; their addresses below), `AsyncMutex` (a guard that may be held across `await`, its waiters served first come, first served), and channels (`Channel`, ADR 0032: `create(capacity)`, `clone`, `close`, blocking `send`/`recv` for threads, `await sendAsync`/`recvAsync` for tasks; `trySend` and `tryRecv`, which never wait and have no effect, and `isClosed()`, ADR 0060: `tryRecv` is the next buffered value or null, empty or closed, and a channel is drained when `isClosed()` and then `tryRecv()` is null). Its operations return `Result`, where `AsyncError` has `Cancelled`, `TimedOut` and `Io(IoError)`. **Socket addresses** (lib/std's `net.cee`, ADR 0044): - `SocketAddress` is an enum: `V4([u8; 4], u16)` and `V6([u8; 16], u16, u32)`, the address bytes in network order, the port, and an IPv6 scope id (0: none). Through the interface `SocketEndpoint` it has `port()`, `withPort(p)`, `isLoopback()`, `isUnspecified()` and `write(w: borrow mut TextWriter)`: `127.0.0.1:8080`, `[::1]:8080`, `[fe80::1%2]:8080`, IPv6 in RFC 5952's canonical form (lowercase, the first longest run of two or more zero groups as `::`, an IPv4-mapped address's last 32 bits dotted), the scope as its number. - An enum has no functions of its own (§15.1), so the constructors are `SocketAddresses`': `parse(text)` (`a.b.c.d:port`, `[v6]:port`, `[v6%zone]:port`; the zone a number or an interface's name, which the runtime resolves where it can, not on Windows), `parseIp(text, port)` (without the port), each `SocketAddress?`; and `v4Any(port)`, `v6Any(port)`, `v4Loopback(port)`, `v6Loopback(port)`. IPv4 parts are 1 to 3 digits without leading zeros, at most 255; IPv6 text is RFC 4291 §2.2's. - `ListenOptions` has `backlog: u32` (0: `SOMAXCONN`), `reuseAddress` (`SO_REUSEADDR`; ignored on Windows, whose option lets another socket take the port), `reusePort` (`SO_REUSEPORT`; `Unsupported` where the platform has none, Windows), and `v6Only` (`IPV6_V6ONLY`, always set explicitly on an IPv6 socket); `ListenOptions.defaults()` is `{ backlog: 0, reuseAddress: true, reusePort: false, v6Only: false }`. - `AsyncTcpListener.bindWith(address, options)` and `listener.localAddress()`, both `Result<_, IoError>`; `bind(text, port)` takes IPv4 or IPv6 text and the defaults. `await AsyncTcpStream.connectTo(address)`; on a stream, `peerAddress()`, `localAddress()`, `shutdownRead()` and the setters of §26.4, none async. A dual-stack listener's IPv4 peer is `V4`, not `::ffff:a.b.c.d`. **Name resolution** (lib/std, ADR 0048): `await AsyncDns.resolve(host, port)` is `Result, AsyncError>`: the addresses of both families, each with `port`, in the resolver's order, duplicates removed, never empty. - A literal (`SocketAddresses.parseIp`) is itself at once. - Otherwise `getaddrinfo` (`GetAddrInfoW` on Windows; `AF_UNSPEC`, `SOCK_STREAM`, no flags) runs on one of at most four resolver threads, the rest queued. The name is passed as given (an internationalised one in its ASCII form). - A cancelled task returns `Err(Cancelled)` at once; its lookup is unqueued, or left to its thread, which frees it. - Errors are `Io` kinds: `HostNotFound` (no such name, or no address; ADR 0060, before which it was `NotFound`), `TimedOut` (a temporary failure), `InvalidInput` (empty, over 253 bytes, a NUL), `OutOfMemory`, `Unsupported` (CosmoOS userland; the interpreter, which knows only literals, `localhost`, as 127.0.0.1 then ::1, and names under the reserved `.invalid`, RFC 6761 §6.4, which are `HostNotFound`). **Vectored sends** (lib/std, ADR 0045): - `IoParts` (`net.cee`) is a list of up to eight byte parts, a value built by copying, since a slice is not an array element (§25.4): `IoParts.of(a)`, then `parts.then(b)`, which is `IoParts?`, null when eight are there already. `count()`, `part(i)` (empty past the last), `totalLength()`, and `after(n)`: the bytes after the first `n`, as parts, whole parts dropped and the one where byte `n` falls cut; no parts from `totalLength()` on. Empty parts are allowed and send nothing. - `await stream.writeVectored(borrow parts)` is one step: the task waits until the socket takes at least one byte, and the result, `Result`, is how many it took, which may stop inside any part. Empty parts return `Ok(0)` without a system call. `await stream.writeAllVectored(borrow parts)` sends every byte, as `write` does for one slice. `await AsyncSockets.sendVectored(fd, borrow parts)` is the one step on a raw descriptor. Errors and cancellation are `write`'s (§40.7): a send the kernel completed before the cancellation reports its count. **Sending files** (lib/std, ADR 0051): - `await stream.sendFile(borrow file, offset, length)` (`file: borrow File`, `offset` and `length` `u64`) is one step: the task waits until the socket takes at least one byte of the file from `offset`, and the result, `Result`, is how many went; 0 if the file ends at `offset`. A `length` of 0 is `Ok(0)` without a system call. The file's own position is neither used nor moved, so one file may be sent on several streams at once. - `await stream.sendFileAll(borrow file, offset, length)` sends `length` bytes in as many steps as the socket needs; if the file ends first, the result is `Err(Io(InvalidInput))`, with the bytes before its end sent. - Where the system has no file send for the socket and the file (the runtime's `CEE_ASYNC_NO_SENDFILE`), a step reads at most 64 KiB at the offset on a worker thread (`Async.blocking`) and writes them: the same results, through a copy. Errors and cancellation are `write`'s (§40.7). - A send to a connection the peer has reset is an error (`BrokenPipe` or `ConnectionReset`), never a signal (ADR 0078 §2): on Linux, where `sendfile` has no `MSG_NOSIGNAL`, the runtime blocks SIGPIPE on the thread for each call and takes one the call raised, whatever the call returned. The process's disposition of SIGPIPE is unchanged. **UDP** (lib/std, ADR 0050): - `AsyncUdpSocket.bind(address, options)` (`UdpOptions`: `reuseAddress`, `reusePort`, `v6Only`, `sendBuffer`, `recvBuffer`, `receiveEcn`, `receiveLocal`; `UdpOptions.defaults()` sets none) is `Result`, not async; port 0 lets the system choose. `localAddress()` is the bound address. The socket owns its descriptor and closes it when destroyed. - `await s.sendTo(bytes, to)` sends one datagram, whole or not at all: `Result`. A datagram too long for the system is `Io(InvalidInput)`. An empty slice sends an empty datagram. - `await s.recvFrom(buffer)` waits for one datagram: `Datagram { length, from, truncated }`. A datagram longer than the buffer fills it, and `truncated` says the rest is gone. - `await s.sendMsg(borrow message)` sends a `UdpMessage { parts, to, from, ecn, segmentSize }`: the parts (an `IoParts`, §40.4) as one datagram, from the local address `from` if given (packet info), with the ECN bits `ecn` (0: not set). With `segmentSize > 0` the bytes are sent as datagrams of that size, the last shorter: in one call where the system can (Linux's `UDP_SEGMENT`, up to 64 segments), else one by one. The result is the bytes sent, whole segments only: if a segment fails after others went, the bytes before it; if the first fails, its error. - `await s.recvMsg(buffer)` is `UdpReceived { length, truncated, from, to, ecn }`: `to` is the local address the datagram arrived at, with the socket's port (with `receiveLocal`; else null), `ecn` its ECN bits (with `receiveEcn`; else 0). - An IPv4 peer of a dual-stack IPv6 socket is reported as `V4`, and a `V4` destination is sent IPv4-mapped. A port unreachable is not reported on a later receive (on Windows `SIO_UDP_CONNRESET` turns its report off). Windows refuses a sender's ECN value 3 (CE) as `Io(InvalidInput)`. CosmoOS userland has no UDP yet: `bind` is `Io(Unsupported)`. - Cancellation is `read`'s and `write`'s (§40.7). `AsyncSockets.udpBind`, `udpSend` and `udpRecv` are the raw calls on descriptors, one try each, for code that runs its own wait loop. ### 40.5 Scopes - **`await Async.scopeIn(alloc, oom, (s) => { … })`**, and `Async.scope(oom, body)` with the system heap (and the `alloc` effect). With `scopeIn`, each child's task and frame come from `alloc` and go back to it when the child returns; the spawn has the effects of its `allocate` and `deallocate`. `body` is a sync closure literal taking the scope `s` and returning `void`. `oom: E` is the error the scope reports if a child's frame cannot be allocated (ADR 0031 §3.3, as amended). - **`s.spawn(async () => …)`** starts a child whose result type must be `Result`, the same `E` for every child of the scope. It returns `void`. The child's frame comes from the scope's allocator. If it cannot be allocated, the child is not started, the scope's other children are cancelled, and the scope's result is `Err(oom)`. - **`s.spawnSend(async () => …)`** (ADR 0038) is `spawn` for a child that any worker of `Async.runWorkers` may run. It may move only before it starts: once started it runs, and everything it awaits or spawns starts, on that worker, so frames never cross threads. What it is given does: its captures follow `Thread.both`'s rule (§38.6), so one it reads needs `Share` and one it changes `Send`, references counting by their target, and a captured closure is refused. Its error type must be `Send`, and its frame comes from the system heap (not `Async.scopeIn`'s allocator). Each of these is **N0334**. A child's error cancels its siblings wherever they run. Under `Async.run`, which has one worker, `spawnSend` is `spawn`. - **The scope returns only after every child has returned.** Its result is `Ok()` if every child returned `Ok`; otherwise the first `Err` (in the order the children returned), after the first `Err` has cancelled the others. - **The body is a block** of the awaiting function, not a closure (ADR 0031 §6.6): `s` is not a local, and names the scope only in `s.spawn(…)`. `return`, `?`, `break` and `continue` cannot leave the body (**N0214**). - **The scope-loan rule**, on the children's captures (§36.2), local by local: - every child's captures are borrowed from its `spawn` until the scope returns, and the awaiting function is suspended from the body's end until then; - two children capturing one local where either changes it are **N0241**; - a statement of the body that uses a local a child has captured is **N0241**; - a child that captures one of the body's own locals, which end when the body does while the child may still run, is **N0344**: a child borrows from the function that awaits the scope, never from the body. - **Copies** (ADR 0033): a child holds its own copy of each local it reads and does not change whose type is `Copy`, whoever owns it, taken when it is spawned. A body local may be captured so, which is how a loop spawns a child per iteration (`for (k in 0..v.length) { let r = borrow v[k]; s.spawn(async () => await f(r)); }`). A child's frame holds its own copy of its closure's environment, made when it starts. - **What a child holds lasts until the scope returns** (§3.2): every loan a child's environment carries, from a copied borrow or a borrow moved in, is live from its spawn until the scope's wait, in every iteration of a loop that spawns. So the body cannot change what a child borrows (N0240, N0241), and a child cannot borrow a local of one iteration of the body (N0243 when it ends, N0240 when the next iteration makes it again). The borrow checker holds each spawn's environment until its scope's wait, and `Builtin(AsyncScopeStart, [site])` at the body's start ends what the scope's previous run held (intermediate-representation.md). - **`async move () => …`** (§3.3) moves every capture into the child's frame, as a `move` literal moves into its environment (§36.5): the body cannot use what it moved (N0250), and the child drops what it owns when it returns (a cancelled child returns early). A child that cannot be started (no frame) drops it at once. A borrow may be moved in, and is held until the scope returns as a copied one is; it makes the environment scoped (§25). The child's body runs once, so it may move its captures out (§36.5, ADR 0074 §2.3). - **No `Send` or `Share` check** applies to children: they run on the scope's own thread (ADR 0031 §3.5). - There is no detached task: every task a program starts has returned before `Async.run` returns. **Task sets** (ADR 0053): children started while the set runs, from any function the set is passed to. - **`await Async.tasks(limit, oom, async (set) => …)`** is `Result`, `E` being `oom`'s type, which must be `Copy` (N0268). It is a call of lib/core's `AsyncTasks.run` (ADR 0037): the body runs in the awaiting task, as the set's owner, with `set: borrow TaskSet`, and may pass the set to the functions it calls. When the body has returned, the set waits for every child. Its children's tasks and frames come from the system heap, so it is hosted only (N0293). **`await Async.tasksIn(alloc, limit, oom, body)`** takes them from `alloc`'s allocator type instead (an allocator, N0263), as `Async.scopeIn` does, in every profile: lib/core's `AsyncTasksIn.run` gives the set its allocator when it starts, so a spawn in any function uses it. - **`await set.spawn(async move () => …)`** starts a child of result `Result` and is `Result`: while `limit` children run, the task waits for one to return. **`await set.spawnSend(child)`** is the same, but the child may run on another worker of `Async.runWorkers` (ADR 0038): each capture and `E` must be `Send` (N0334); a set from `tasksIn` with a user allocator starts it on this worker, since its frame is freed where it returns. Once the set or its task is cancelled, or if the child's frame cannot be allocated, it starts nothing and is `Err(oom)`; the set goes on. **`set.trySpawn(child)`** is `bool`, false instead of waiting (or once cancelled, or with no frame); it is not awaited. `set` is a local or parameter of type `TaskSet` or a reference to one. Not awaiting `spawn` is N0342; a child that is not an `async move` literal with no parameters is N0209. - **A child owns what it captures** (N0349): each capture is moved or copied, and its type holds no loan (no reference, slice, `StringView`, or struct with region parameters), since a child may outlive the code that started it. So a child cannot capture the set: shared state goes through `Arc` or a `Channel`, or values are passed down. - **Errors.** A child's `Err` cancels no sibling; the set keeps the first. The body's `Err` cancels every child. The set's result, once the body and every child have returned, is the body's `Err`, else the first child's, else `oom` if a child's frame could not be allocated, else `Ok`. A cancellation of the set's owner reaches every child (§40.7), since the owner is the task that awaits the set. ### 40.6 Frames - Each async fn or async closure has a **frame**: its parameters, the locals it needs across a suspension point (or borrows), storage for what it waits on (the awaited task's frame, shared by every await site), its result and its resume state. Its size is known at compile time, and `ceesafec build --explain-frames` reports it (c-backend.md §3a). - **A frame is placed once and never moves.** The root frame of `Async.run` is in its caller's storage. An awaited task's frame is in its awaiter's frame. A child's frame is from its scope's allocator, and `race`, `all` and `shield` embed their children's. So every reference into a frame is valid until its task returns. - **A frame is destroyed only after its task returns.** Cancellation never destroys a frame (§40.7). Its locals are dropped as the task returns, in the order of §14. - Lowering: `intermediate-representation.md` (`StartTask`, `Await`, `Park`) and `c-backend.md` (frame structs and resume functions). ### 40.7 Cancellation - **Cancelling a task sets its flag.** A scope cancels its children when one fails or one cannot start (§40.5), `race` and `all` cancel the other child, `timeout` and `deadline` cancel their body at the deadline (ADR 0046), and the runtime may cancel a root task (the `CEESAFE_CANCEL_AT` sweep, `testing.md`). The flag is inherited: an awaited task shares its awaiter's, and a scope's children are cancelled with the task that owns the scope. A cancellation wakes the tasks it reaches that wait on a deadline, a descriptor or a cancellable wake, so they see it; a task it does not reach is left parked (bug B5: every waiting task used to be woken, to retry and park again, at every cancellation). - **Only the library's waiting operations observe it,** together with `Async.checkpoint` and `Async.isCancelled`: a cancelled task's next such operation returns `Err(AsyncError.Cancelled)` at once, or, for an operation already waiting, as soon as it can do so safely (a completion-based read first cancels and waits for the kernel's operation). Code between them runs normally, so cancellation is visible only at an `await`. - Cleanup is ordinary code: `?` returns the error, and the frame's locals are dropped as the task returns. - **`Async.shield(ms, body)`:** within `body`, the library's operations ignore the flag. Once `ms` milliseconds have passed since the shield began, they return `Err(AsyncError.TimedOut)`, never `Cancelled`. After the shield the flag is observed again. ### 40.8 The interpreter and determinism - The interpreter runs tasks on one thread with a virtual clock (timers fire in deadline order without waiting; the wall clock, §26.11, is 2000-01-01 plus it) and a run order fixed by a seed. A program whose output does not depend on timing prints the same in C and in the interpreter. It has one worker: `Async.runWorkers(n, …)` is `Async.run`, and `spawnSend` is `spawn` (ADR 0038). A program's output must not depend on which worker ran what. - **Profiles** (profiles.md §4, ADR 0039): in the freestanding profile the executor runs over its environment's hooks. Children need `Async.scopeIn`, and threads and I/O stay hosted. Diagnostics: N0341 (`await` outside an async fn or async closure), N0342 (a task that is not awaited, run or spawned, or an operand of `await` that is not a task), N0343 (recursion through `await`), N0344 (a spawned child that captures the scope body's own locals), N0345 (`Async.park` or `Async.waker` outside the library); N0295 (blocking in async code, `await` while a `@context(atomic)` value is live, `Async.run` in async code); N0241 (conflicting captures of a scope's children); N0334 (`Async.blocking`'s captures and result, and `spawnSend`'s captures, error type and allocator). ## 41. Interior mutability: `RawCell`, `Cell` and `RefCell` (ADR 0084) A shared loan freezes the value it borrows (`ownership.md` B2), with one exception: the contents of a `RawCell`. The three cells are lib/core types (`lib/core/cells.cee`), in every profile. A type that reaches a cell is `Send` when its contents are, never `Share` unless the path to the cell passes through a library `@threadSafe` type (§38.1): so a cell captured shared by `Thread.both` (N0334), `Arc>` (`Arc`'s `T: Share`, N0334) and `static C: Cell` (N0337, §39.2) are refused. ### 41.1 `RawCell`: the primitive - A struct declared `@interiorMutable` and `@noncopy` in lib/core. `@interiorMutable` is accepted only in lib/core's own files (N0290 elsewhere), and user code may not claim `@threadSafe` for a type holding one. A `RawCell` has `T`'s size and alignment (one field). - **Never Copy.** A cell, and every struct or enum holding one, is a move type (`@noncopy`, §14.1): a copy would read the contents behind a live guard's back. A copy of a `Cell` is `Cell.create(c.get())`. - `RawCell.create(v)`; `c.pointer()` (`borrow this`) returns `*mut T`, safe to take and `unsafe` to use; `c.getMut()` (`borrow mut this`) returns `borrow mut T`; `c.intoInner()` (`this`) returns `T`. - **The rule it changes** (amending `ownership.md` B2, `unsafe.md` §2's `p.write` and `Ptr.toBorrowMut` rows and its pointers made from references, and ADR 0010): the `T` inside a `RawCell` may be read and written through `c.pointer()` while shared loans of the cell, or of a value that holds it, are live. A `borrow mut` of the cell still excludes every other access (B1). Unsafe code still must not make two live `borrow mut T` from the pointer, nor write while a `borrow T` made from it is live. - `T` may not hold loans (a reference, view, closure or other scoped type, §19.1): **N0351**, "a cell of a loan-holding type", reported at the type that names the cell, also when the cell is inside a generic struct instantiated with such a type. The raw-pointer rule of §27.1 already forbids the pointer; refusing at the type avoids needing variance for regions. - **C.** A `borrow T` stays a `const T *`, which C allows to be written through when the object is not `const`; no value that holds a `RawCell` in its own storage is declared `const` (an exported immutable static, §39.5), and a cell is never a tracked local (`optimisations.md` §2.3). - In the interpreter a cell is ordinary memory. ### 41.2 `Cell` - `Cell.create(v)`; `c.get()` returns a copy of the contents; `c.set(v)` stores `v`; `c.replace(v)` stores `v` and returns the old contents; these take `borrow this`, so any number of shared loans of the cell coexist with them. `c.getMut()` (`borrow mut this`) lends `borrow mut T`, and `c.intoInner()` (`this`) returns the contents. - `T` is bounded `Copy` on the struct (an inherent impl cannot add a bound, §21.1): `Cell>` is N0268. A non-Copy value that is replaced or taken through a shared reference is a `RefCell` (§41.3). - No method lends a reference to the contents, so a change is never seen through a stale reference, and a call between two methods, such as a re-entrant callback that sets the cell through a second shared reference, is harmless. A Copy `T` has no `drop`, so `set` runs no user code. - **`==` and `!=`.** Two `Cell` compare their contents, `a.get() == b.get()`, when `T` has the operator (numbers, `bool`, fieldless enums, raw pointers, arrays and cells of them): N0207 otherwise. - `get` is one load and `set` one store through the cell's pointer, in functions small enough for the C compiler to inline. A cell's value is never a tracked fact (`optimisations.md` §2.3). ### 41.3 `RefCell`: checked shared and exclusive access - `RefCell.create(v)`. Its state is one `Cell` beside the contents: 0 free, `n > 0` readers, −1 writing. - `c.read()` returns a `CellRef` and `c.write()` a `CellRefMut` (`borrow` is a reserved word, so Rust's `borrow()` and `borrow_mut()` are `read()` and `write()`). Both take `borrow this`. Each guard is a scoped struct over a region of the cell, written like `MutexGuard` (§38.3): `CellRef`'s `data()` (`borrow this`) lends `borrow T`, `CellRefMut`'s `data()` (`borrow mut this`) lends `borrow mut T`. Dropping a guard gives the access back. - **On conflict it panics** with "cell already borrowed": `write` while any guard is live, `read` while a `CellRefMut` is (or when the reader count would pass 2^31 − 1, the same on every target). A program's call is checked at the call, so the panic names the program's line, in both back ends; in a freestanding program it is an explicit panic (`CEE_PANIC_EXPLICIT`). Panic is abort (OD-5). - `c.tryRead()` and `c.tryWrite()` return the guard, or `null` in the same cases. - `c.replace(v)` stores `v` and returns the old contents, and `c.take()`, on a `RefCell` only, returns them and leaves `null` (lib/core's `RefCellTaking.take`). Both panic as `write` does. The old value is moved out before they return, so its `drop` runs in the caller with no guard live, and may use the cell again. `c.getMut()` (`borrow mut this`) and `c.intoInner()` (`this`) need no check. `RefCell` has no `==`: it would take a read guard and could panic. - **Why it is sound.** A guard is scoped to the cell (§25.4), so it cannot outlive it (N0243), be stored past it, or cross a thread (scoped types are not `Send`). The reference `data()` lends is tied to the guard (§13.5), so the B-rules end it before the guard's drop resets the state. The cell cannot be copied behind a guard's back (§41.1). - **Guards made in a condition.** An `if` or `while` condition is its own statement (§14.5): a guard that is one of its temporaries ends once the condition is evaluated, before the body or the `else` block runs, and on every pass of a `while`. So `if (c.read().data().isEmpty()) { … } else { c.write()… }` does not panic, nor does an `if` expression whose branch writes. A `match` scrutinee's temporaries live until the end of the `match` (§18.4), as Rust's do. `e is P` (§15.4) follows the same rule: a guard made in its condition ends with the condition, or, when a binding borrows from it, with the then-branch or the pass of the `while` body, never in the `else` block. - **Across `await`.** A local guard live where an async fn or async closure may suspend is warning **N0402**: another task resumed meanwhile panics on the same cell. A local counts if it holds a guard in its own storage or in what it owns: a struct or enum field, an optional, an array, a `Box`, a `Vector`, a `StableVector`, a `SlotMap`, or a closure's environment. Tasks that cross threads already need `Send` captures, and a guard is not `Send`. - A `RefCell` in a task frame is fine (frames never move, `ownership.md` A1). # Diagnostics Status: M1 increment 1a. Lists every diagnostic code the compiler can emit. Codes are stable: a retired code is never reused. Source of truth in code: `DiagnosticCodes` (`src/diagnostics/catalog.cee`). ## Format ```text ::: error[N0100]: expected ';', found 'let' | 2 | let x = 1 | ^ ``` - `` is the path as given on the command line (never made absolute), so output is reproducible. A package's files are named by their path from the package's directory, and a dependency's from the current directory (`../alpha/src/util.cee`; ADR 0082 §2.1); the JSON form also names the file's `package`. - Line and column are 1-based. Columns count UTF-16 code units (grammar.md §1.1). - Diagnostics are emitted in the order they are found. The lexer finishes before the parser starts, so all lexical errors of a file come before its syntax errors. - `--diagnostics-format=json` gives the same diagnostics as one JSON document, with fixes, and `ceesafec explain ` prints a code's row (M12; `tooling.md` §5). ## Recovery policy The parser reports **one** diagnostic per broken statement or item, then re-synchronises (grammar.md §3). The parse-fail corpus enforces this: each test lists exactly the expected diagnostics, so any cascade fails the test. ## Codes ### Lexer (N0001–N0099) | Code | Meaning | Example | |---|---|---| | N0001 | Unexpected character (including non-ASCII outside literals and comments) | `let a = 1 # 2;` | | N0002 | *(unassigned)* | | | N0003 | Unterminated block comment | `/* ...` at EOF | | N0004 | Integer literal does not fit in 64 bits | `18446744073709551616` | | N0005 | Unterminated string literal (newline or EOF before `"`) | `"abc` | | N0006 | Invalid escape sequence, or `\u{...}` that is not a Unicode scalar value; a NUL byte in a `c"…"`, or in the left operand of a constant `+` whose right operand is one (ADR 0087 §2.4, §2.7) | `"\q"`, `"\u{D800}"` | | N0007 | Invalid char literal: empty, more than one character, or unterminated | `''`, `'ab'` | | N0008 | Invalid digit for the radix, or no digits after a radix prefix | `0b102`, `0x` | | N0009 | Unknown literal suffix | `12abc`, `1f32` (float suffix needs a float literal) | | N0010 | Misplaced digit separator `_` (must sit between two digits) | `1__0`, `0x_FF`, `1_.5` | | N0011 | A raw `"""` string literal laid out wrongly: text after the opening `"""` on its line, or a content line that is not blank and is indented less than the closing `"""` (`grammar.md` §1.6, ADR 0074 §2.6) | `""" x`, a line left of the closing `"""` | ### Parser (N0100–N0199) | Code | Meaning | Example | |---|---|---| | N0100 | Expected a specific token | missing `;`, `(` after `if`, `}` before `fn` | | N0101 | Expected an item (`fn` or `const`) at the top level | `let x = 1;`, `struct S {}` (not in M1) | | N0102 | Expected an expression | `let a = ;` | | N0103 | Comparison operators chained | `a < b < c` | | N0104 | Invalid assignment target | `1 = 2;`, `f() = 3;` | | N0105 | Expected a type, or `Self` where it names no type (outside an interface, an `impl` or a struct's or enum's body, ADR 0086 §2.2) | `fn f(x: ) {}` | | N0106 | Namespace declaration not first in the file | `fn a() {}` then `namespace X;` | | N0107 | Expected a field or `fn` inside a struct body; an imported union with no member, or a union inside an anonymous union member (ADR 0041 §3.2) | `struct A { 42; }` | | N0108 | Expected a `match` pattern (`_`, a literal, string or range, a name, `name @ P`, `(P)`, `[…]`, or `E.V(…)`) | `match (n) { + => { } }` | | N0109 | An `import` declaration after an item | `fn a() {}` then `import geometry;` | | N0110 | Nested more than 1,000 levels deep (grammar.md §2.2: blocks, statements, types, patterns, unary operands, and each link of an operator, call or member chain). The rest of the file is not parsed | 1,000 nested parentheses; a sum of 1,001 terms | ### Names, types and flow (N0200–N0299) **Suggestions** (ADR 0065). A member that does not exist (N0200, N0230, N0234 on a number) names up to three it might be: the type's members within a few edits of it, then those whose doc comment mentions it (`TextWriter.unsigned` gives `signed`, `decimal` or `hex`); for a built-in type (`Process`, `Console`, …) the members the built-in types page lists for it, the longest shared start first; and for a small type with no close member, all its members. A reserved word where a name goes (N0100) says so. Semantic checks run only when every file parsed without errors. | Code | Meaning | Example | |---|---|---| | N0200 | Name not found (variable, function, or built-in member) | `missing`, `Console.print(1)` | | N0201 | Item defined twice in the package (a library item's name is not a clash since ADR 0075 §5: the package's item shadows it), or a C name declared twice by C functions, exports or `@repr` types, even in different packages (`type-system.md` §28.2, §28.5, ADR 0081 §2.2) | two `fn twice()`; `fn strlen` in two `extern "C"` blocks | | N0202 | Parameter declared twice | `fn f(a: i32, a: i32)` | | N0203 | Local redeclares a local or parameter in scope (no shadowing of locals) | `let x = 1; let x = 2;` | | N0204 | Unknown type name | `fn f(a: int)` | | N0205 | Type mismatch against the expected type | `let a: bool = 1;`, `if (n) {}` | | N0206 | Integer literal out of range for its type | `256u8`, `let c: i8 = 128;` | | N0207 | Operator not applicable to the operand types (includes mixed types, signed bitwise ops, negating unsigned values; `==` on an enum with payloads, or on slices or arrays of floats or structs, ADR 0072; a field without `==` under `@derive(Equal)`, ADR 0087 §2.3; `+` on views outside a constant or static initialiser, ADR 0087 §2.7) | `a + b` with `u8`/`i32`, `s & s` with `i32` | | N0208 | `as` conversion that can lose information | `a as u32` with `a: i32` | | N0209 | Wrong number of arguments | `two(1)` | | N0210 | Not callable, or a function or built-in used as a value | `local()`, `let f = two;` | | N0211 | Assignment to an immutable binding (`let`, parameter, constant, function), or an assignment or `borrow mut` of a const parameter (ADR 0083 §2.3) | `param = 3;` | | N0212 | Local read before it is definitely assigned | `var x: i32; return x;` | | N0213 | End of a non-`void` function is reachable (for a `never` function too, type-system §15.3) | `fn f() -> i32 { }` | | N0214 | `break`/`continue` outside a loop | `break;` at function level | | N0215 | `return` with or without a value contradicts the return type, or `return` in a `never` function | `return 1;` in a `void` function | | N0216 | Construct not supported in M1 (string values, member access, qualified types, generic arguments other than `truncate`; floats and `char` until M11); also ceesafec's internal check that an expression with no value, which no diagnostic explains, never reaches code generation (a defect of ceesafec, ADR 0087 §9) | `truncate(x)` | | N0217 | Not a constant expression, or constant evaluation would panic, or a constant `+` of views longer than 16 MiB (ADR 0087 §2.7) | `const C: u8 = 200 + 100;` | | N0218 | Constant depends on itself | `const A: i32 = A + 1;` | | N0219 | Invalid `main` signature | `fn main(args: i32)` | | N0220 | `let` without initializer | `let a: i32;` | | N0221 | `var` without type annotation or initializer | `var c;` | | N0222 | A generic's substituted copy does not check for its type arguments, and no other error explains why (type-system §20.4) | `fn copyOf(m: borrow M) -> M => m;` used with `dyn Metrics`, which no value holds (ADR 0064 §3.1) | | N0223 | *(unassigned)* | | | N0224 | Value of type `void` used as a value, or a `let` of a `never` expression | `let d = nothing();` | | N0225 | Expression statement that is not a call | `1 + 2;` | | N0226 | `void` used other than as a return type, or `never` other than as a function's return type | `var e: void;`, `fn f(x: never)` | | N0227 | Struct contains itself by value (infinite size); reported at the field that closes the cycle. Also an interface that is its own supertrait (ADR 0086 §2.6) | `struct Node { next: Node; }` | | N0228 | Duplicate member (field or function) in a struct, or an associated constant named like a field, method, variant or another constant of its type (ADR 0074 §2.7) | `struct D { a: i32; a: i64; }` | | N0229 | Struct literal does not initialise every field | `Point { x: 1 }` | | N0230 | Unknown field (in a literal or an access), or a function named where a field was expected | `p.z`, `p.sum` | | N0231 | Field initialised twice in a literal | `Point { x: 1, x: 2, y: 3 }` | | N0232 | `this` outside a method, or a `this` parameter outside a struct | `fn f() -> i32 => this;` | | N0233 | Method called as a static function, or a static function called on a value | `Point.sum()`, `p.origin()` | | N0234 | Field access, method call or literal on something that is not a struct (an optional has only `isSome` and `isNull`, ADR 0072) | `n.x` with `n: i32` | | N0235 | A block branch of an `if` or `match` expression can complete, so it has no value (type-system.md §15.3) | `let a = if (c) { f(); } else 2;` | | N0236 | A closure whose type cannot be inferred: a parameter without a type, or a block body returning a value, where no function or closure type is expected (type-system.md §36.2) | `let f = (x) => x + 1;` | ### References and borrows (N0240–N0249; `lifetimes.md`) N0240–N0245 come from the NIR borrow checker. It runs only on programs that type-check, and its errors carry notes ("borrowed here", "the borrow is used later here"). | Code | Meaning | Example | |---|---|---| | N0240 | Assignment to a place while it is borrowed (shared or mutable) | `let r = borrow x; x = 2; use(r)` | | N0241 | Borrow conflicting with a live borrow | two live `borrow mut x`, or `borrow mut x` while `borrow x` is live | | N0242 | Read of a place while it is mutably borrowed | `let m = borrow mut x; print(x); m = 1;` | | N0243 | A place or temporary goes out of scope while borrowed | `let r = same(five()); use(r)` | | N0244 | Returning a reference to a local or temporary (E1) | `return borrow x;` with `x` local | | N0245 | Return-source errors (E2): missing or invalid `borrow(...)` list, a result derived from an unlisted parameter, or a deep result (one naming only regions of a parameter's referent, ADR 0074 §2.4) that borrows the referent's own storage | `fn f(a: borrow T, b: borrow T) -> borrow T`; `fn f(borrow this) -> Slice => this.buf[0..2]` | | N0246 | Reference in a disallowed position: struct field, constant, `var` local, reference to reference, source list outside a return type. Since M4a this covers every scoped type, scoped results included (`type-system.md` §19.1) | `struct S { r: borrow i64; }` | | N0247 | `borrow` of something that is not a place | `borrow 5`, `take(mut five())` | | N0248 | `borrow mut` (or a `borrow mut this` call) on an immutable place | `borrow mut param`, `shared.grow()` | | N0249 | `mut` argument marker missing or misplaced | `take(x)` for `borrow mut`, `look(mut x)` for `borrow` | ### Moves and destruction (N0250–N0259; `type-system.md` §14) N0212 and N0250–N0253 come from the NIR move checker (since M2c), which also runs only on programs that type-check. A move while borrowed is reported by the borrow checker as N0240. | Code | Meaning | Example | |---|---|---| | N0250 | Use of a value that was moved (on at least one path), including reading a partially moved struct as a whole; a `name @ P` pattern that moves its value while a binding under `P` moves part of it (ADR 0085 §2.5) | `consume(r); consume(r);` | | N0251 | Move out of a place reached through a reference; since ADR 0075 `*r` of a referent that is not Copy | `let a = r.a;` with `r: borrow Pair` and `Pair.a` a move type | | N0252 | Move of a field out of a value whose type has a `drop` block | `consume(o.res)` with `struct Owner { res: Res; drop {} }` | | N0253 | Move out of a `using` binding | `using u = make(); consume(u);` | | N0254 | `return`, `break`, `continue` or `defer` inside a deferred statement | `defer return 1;` | | N0256 | `match` that does not cover every value (add the missing variants or `_`); arms with a guard do not count (ADR 0085 §2.2) | `match (s) { Shape.Empty => { } }` | | N0257 | Unreachable `match` arm: a duplicate pattern, or any arm after `_`; a guarded arm too, when earlier unguarded arms match all it does | `1 => { } 1 => { }` | | N0258 | *Retired in M3c:* a non-Copy payload bound from a value scrutinee now moves out, and the match consumes the scrutinee (`type-system.md` §18.4) | — | | N0259 | Invalid array (`type-system.md` §16): a length that is not an integer literal, a `usize` constant or a `usize` const parameter (ADR 0083 §2.7), from 1 to 2^48 − 1 (also in an instance: `Grouped<0>`, §20.5), an element type that is not Copy, or an array larger than 2^31 − 1 bytes; since M9b also `copyFrom` or `fill` on a slice whose element type is not Copy (§16.5), and since ADR 0071 a vector's `extend` or `resize`; since ADR 0075 `[]` where no slice is expected (a static's array may hold non-Copy elements without a `drop` block) | `[Res; 2]`, `[i32; 0]`, `let e = [];` | Arrays and slices otherwise reuse codes: indexing or slicing a non-array, a non-`usize` index, or a bad `for` sequence or range is N0205; writing an element of a `let` array or through a `Slice` is N0211; a slice in a field, a `var` local or an array element is N0246; `borrow mut a[..]` of an immutable array is N0248; slicing a non-place is N0247. Since M9b, `copyFrom` or `fill` on a `Slice` is N0211, on a slice of an immutable array N0248, and a source that is not a slice of the element type N0205. Slices are references, so the borrow checker reports their conflicts as N0240–N0245. ### Region parameters (N0270–N0279; `type-system.md` §25, M6x) | Code | Meaning | Example | |---|---|---| | N0270 | A scoped field or payload in a struct or enum without region parameters | `struct S { v: StringView; }` | | N0272 | Storing a value through a `borrow mut` parameter whose loans the signature does not let flow into it, or storing a value with any loan into a `static mut` (ADR 0067) | `fn put(v: borrow mut Vector, s: StringView) { v.push(s)!; }` | | N0273 | An unknown region name, region arguments on a type without region parameters, the wrong number of them, or `static` in a parameter's type (ADR 0068) | `StringView`, `Plain` | | N0274 | A generic used with a scoped type argument where it needs an unscoped one (it boxes `T`, makes an array of `T`, …) | `boxed(s)` with `fn boxed(v: T) -> Box` | N0271 is unassigned: destroying a value that holds loans while their referents are gone reports N0243. ### Unsafe and raw pointers (N0280–N0289; `type-system.md` §27, `unsafe.md`, M8a) | Code | Meaning | Example | |---|---|---| | N0280 | An unsafe operation outside an unsafe context: a read, write or arithmetic through a raw pointer, `Ptr.toBorrow`/`toBorrowMut`, `Slice.fromRaw`/`SliceMut.fromRaw`, a call to an `unsafe fn`, or a use of a `static mut` (U1, type-system.md §39.3) | `let v = p.read();` in a safe function | | N0281 | An invalid raw-pointer pointee: a reference, slice, `StringView` or other scoped type, or `never` (a type parameter is allowed since M8d; a scoped argument for it is N0274) | `*borrow i32`, `*Slice` | | N0282 | A pointer operation that the pointer or its pointee does not allow: reading or writing a non-Copy type, writing through a `*T`, `toBorrowMut` or `SliceMut.fromRaw` from a `*T`, any access through `*void` | `p.write(1)` with `p: *i32`; `q.read()` with `q: *void` | | N0283 | An `unsafe fn`, or a function containing `unsafe`, without a doc comment with a `Safety:` line (U2) | `unsafe fn f() {}` with no `/// Safety: …` above it | ### C interoperability and profiles (N0290–N0299; `type-system.md` §28, M8b; `profiles.md` §4, M8c) | Code | Meaning | Example | |---|---|---| | N0290 | An unknown attribute, an attribute in the wrong place or with wrong arguments, a repeated attribute, an ABI other than `"C"`, or an extern declaration with a body, receiver or type parameters; `@export` on `main`; a `@derive` of anything but `Equal`, `HashKey` and `OrderKey` on a struct or enum, `HashKey` without `Equal`, or an `impl DerivedEqual` written by hand (ADR 0087 §2.3); an effect name other than `sleep`, `alloc` and `suspend`; `async` on an extern, interface or `@constant` function (`type-system.md` §40.1) | `@inline fn f() {}`, `@repr(C) enum E { A }`, `extern "Rust" { … }` | | N0291 | A type that cannot cross the C boundary in an extern declaration, an export, or a `@repr(C)` struct's field; a `drop` on a `@repr(C)` struct that holds C state (ADR 0029) | `fn f(s: Slice);` in `extern "C"`, `@export fn g() -> Box` | | N0292 | A C name that is not a valid C identifier, is a C keyword (or `bool`, `true`, `false`, `NULL`, `main`), is reserved by C, or starts with `ceesafe_`, `cee_`, `CEE_` or `cee` and an uppercase letter | `@export("int")`, `fn ceesafe_alloc();` in `extern "C"`, `@repr(C) struct S { default: i32; }` | | N0293 | A hosted-only name (`Console`, `Process`, `File`, the heap allocators and every collection or box that needs them, …) a call of a library function whose signature names one, or a `main` function in a package built with `--profile freestanding` (`profiles.md` §4.1) | `Console.writeLine(1);`, `let b = Box.create(5)!;` | | N0294 | An imported C type (type-system.md §31), or a C-laid-out type (§33), used where CeeSafe would need its size or would move it: as a local, parameter, result, field, element, optional or type argument, in a struct literal, or with `read`, `write`, `add`, `sub`, `offset` or `fromRaw` through a pointer; or a borrow of a pointer, `usize` or `isize` field of one | `fn f(i: cosmoboot_info) {}`, `p.add(1)` with `p: *cosmoboot_info` | | N0295 | An operation whose effect (`sleep`, `alloc`, `suspend`) the context forbids: in an `@context(atomic)` or `@context(irq)` function, in an async fn or async closure (`sleep`; type-system.md §40.1), while an `@context(atomic)` value (a spin-lock guard) is live (`sleep`, `suspend`), or beyond a CeeSafe function's `@effects` bound (type-system.md §32); a closure body beyond its type's effect list, or a named function with effects its expected function type's list lacks (§36.3) | `mutex_lock(m)` while a `SpinGuard` is live | | N0296 | A `private` field named outside the source file that declares its struct: read, assigned, borrowed or given in a literal (type-system.md §12) | `lock.data` outside `sync.cee` | | N0297 | A float type, literal or `fromBits` in a build with `--no-float` (type-system.md §35.9) | `let x = 1.5;` under `ceesafec build --no-float` | | N0298 | A function that cannot be a function value: an `extern` or `unsafe` function, one with region parameters, `main`, or an async fn other than as the argument of an `@effectsOf` parameter (type-system.md §36.1, §32.5) | `let f = puts;` with `puts` declared in an `extern "C"` block | | N0299 | A closure of the wrong kind: a `borrow mut fn` where a `borrow fn` is expected, or a capturing closure where a plain `fn` is expected (type-system.md §36.2); a function or closure value whose type's effects are not in the expected type's effect list (§36.3) | `let f: fn(i32) -> i32 = (x) => x + k;` | Calling an extern function outside an unsafe context is N0280; two extern declarations or exports with one C name are N0201, even in different packages. It is reported at each declaration but the one that owns the name (type-system.md §28.2 says which: never a dependant's when its dependency declares it too), with a note at the owner's, which names its package; for a direct dependency's extern, the message says to call it through the package (``use `sys.strlen`, which package 'sys' declares; remove this declaration``) or to make it `public` there, and an identical declaration has a fix that removes it (type-system.md §28.2; ADR 0081 §2.2). Two `@repr(C)` structs, or a `@repr` enum and another C declaration, with one C name (their declared names, type-system.md §28.5) are N0201 by the same rule, with the same note and no hint. (`CChar` as a value type was N0281 until ADR 0080 §3.5, type-system.md §28.3.) Raw pointers reuse codes: an invalid pointer cast is N0208, ordering comparisons on pointers are N0207, a constant of pointer type is N0216, and an `unsafe main` is N0219. (An `unsafe` interface method was N0216 until C5.) ### Results and the heap (N0260–N0269; `type-system.md` §17–§18) | Code | Meaning | Example | |---|---|---| | N0260 | `Ok(...)` / `Err(...)` (or, since M4c, `Pool.create(...)`; since M6c, `null`) without an expected type that fixes its type arguments, or where another type is expected | `let a = Ok(1);`, `let p = Pool.create(SystemHeap {}, 4u32);`, `let n = null;` | | N0261 | `?` in a function that does not return a `Result` with the same error type, or an enum with an `@from` variant for it (ADR 0075 §2.3; the message names the variant to add); `?` on an optional in a function that does not return an optional (M6c) | `?` on `Result` in a function returning `Result` | | N0262 | `?` or `!` on something that is not a `Result` or optional value (a reference to one included), or `??` with a left operand that is not optional; an `@from` variant without exactly one payload, or two `@from` variants of one payload type (ADR 0075 §2.3) | `n?` with `n: i32`, `@from A` | | N0263 | An allocator argument that is not an allocator type (`SystemHeap`, `FailingAllocator`, or for `Box` since C5 a struct without fields implementing `Allocator`) | `Box`, `Box.createIn(point, 1)` | | N0265 | Type arguments that cannot be inferred: a generic call, struct literal or enum value whose arguments, fields, expected type or unique bound instantiations (ADR 0086 §2.7) do not determine every type parameter. Not reported when an argument has an error of its own (ADR 0075 §2.16) | `let d = Maybe.Nothing;`, `let e = make();` | | N0266 | Generic instantiation nested more than 64 deep (polymorphic recursion) | `fn deep(x: Maybe) { deep(Maybe.Just(x)); }` | | N0267 | An `impl` that does not match its interface (receiver, parameter or return type, a missing method without a default, or an extra method), a duplicate `impl`, an `impl` for a type parameter or a scoped type other than `S` under `impl`, a generic impl whose target is not a generic struct or enum or does not name each of its type parameters, overlapping impls, an impl outside the package of its interface and of its type (the orphan rule, ADR 0054), an impl method whose receiver or number of type parameters differs from the interface's (ADR 0086 §2.3), a missing or undeclared constant or associated type, a constant of another type (ADR 0086 §2.4, §2.5), or a method call, constant or associated type `T.Name` that two bounds both provide | `impl Shape for Square { fn area(this) -> i64 … }` | | N0268 | A type argument that does not satisfy a bound of its type parameter (an interface it does not implement, or `Copy`), an impl's associated type that does not meet its declared bounds (ADR 0086 §2.5), an impl's target that does not implement a supertrait of its interface (ADR 0086 §2.6), or a type with region parameters coerced to `dyn I` (ADR 0054) | `total(plain)` with `fn total(…)` | | N0264 | A type a region cannot hold (R2): one that needs drop, a scoped type, or a non-Copy fill value for `allocArray` | `arena.alloc(Res { id: 1 })`, `arena.alloc(someReference)` | Generic code (M6a) reuses codes: - an operator on a type parameter is N0207, a field or method on one is N0234, and a literal for one is N0205; - a generic type without its arguments, or with the wrong number of them, is N0204; - a duplicate or shadowing type parameter is N0201; - an interface method's type parameters are compared with its impl's by number and position (N0267, ADR 0086 §2.3); - an interface's method, or a method of an `impl` of an interface, with region parameters of its own is N0216 (ADR 0074 §2.4); - a generic `main` is N0219; - naming something that is not an interface as a bound or in an `impl` is N0204; - a method on a type parameter that no bound declares is N0234; - an `impl` method clashing with a type's own method is N0228, and so is an inherited interface default whose name the target already has, reported at the impl (ADR 0086 §2.1); - an interface default is checked at the interface, with `Self` opaque: a field of `Self` is N0234, and an inherited instance that does not check for its target is N0222 at the impl (ADR 0086 §2.1); - `Self` outside an interface, an `impl` block or a struct's or enum's body is N0105 in a type and N0200 in an expression (ADR 0086 §2.2); - a projection `T.Name` whose bounds declare no associated type `Name` is N0200, and a generic impl's own constant N0216 (ADR 0086 §2.4, §2.5). Region values reuse codes: - assigning a `using` binding as a whole is N0211; - nesting a frame that is not a `using` binding is N0248; - a method call on a frame that is not a local is N0247; - a storage that is not `[u8; N]` or `SliceMut` is N0205; - returning a frame or fixed-buffer allocator without sources is N0245. `?` inside a deferred statement is N0254, and `Ok`/`Err` with the wrong number of values is N0209. The names `Result`, `Ok`, `Err`, `AllocError`, `Box`, `SystemHeap` and `FailingAllocator` cannot be declared (N0201). Moving out of a box's contents is N0251 until M3c. ### Build (N0300–N0399) | Code | Meaning | Example | |---|---|---| | N0300 | `ceesafec build --emit exe` without a `main` function | a library-only file | Names across packages (N0310–N0319; `type-system.md` §2, ADR 0013): | Code | Meaning | Example | |---|---|---| | N0310 | The package named in an import, or in a struct literal `p.T { … }`, is not a dependency of the package (elsewhere an unknown `p` in `p.x` is N0200) | `import geometri;` | | N0311 | A dependency has no item of that name | `geometry.Pointt` | | N0312 | The item exists but is not `public`, or a field or method of a public struct is not | `geometry.helper()` with `fn helper` internal | | N0313 | A simple name is provided by two imports | `import a; import b;` both with `Point`, then `Point` | | N0314 | A public item's signature names a type that other packages cannot see | `public fn make() -> Hidden` with `struct Hidden` internal | | N0315 | An item or a type parameter has the name of one of the package's dependencies (a local, parameter, loop variable or binding may, ADR 0074 §2.10) | `struct geometry { … }` | Packages (N0320–N0329; `tooling.md` §4, ADR 0013): | Code | Meaning | Example | |---|---|---| | N0320 | `ceesafe.toml` or `ceesafe.lock` is not valid in the TOML subset | `name = app` (no quotes) | | N0321 | A manifest field is missing, unknown or invalid, or a dependency is an executable | `kind = "binary"` | | N0322 | A path dependency's directory or manifest is missing, or its name differs from its key | `geometry = { path = "../geo" }` with no `../geo/ceesafe.toml` | | N0323 | A git dependency cannot be fetched or resolved, or is absent with `--offline` | an unknown tag | | N0324 | The lock file is missing or does not match the manifest | a dependency added without `ceesafec fetch` | | N0325 | A git checkout's manifest and sources do not match the lock's hash | a file edited in the cache | | N0326 | One package name reached through two different sources | two paths to two `geometry` packages | | N0327 | A dependency cycle | `a` depends on `b`, `b` on `a` | | N0328 | An invalid version, version requirement, or index entry (`tooling.md` §4.6) | `shapes = "1.x"` | | N0329 | No versions satisfy the requirements: the conflicting requirements and where each came from (§4.7) | `^2.1 (from geometry 1.4.0) and ^1.0 (from plot 0.3.0)` | | N0335 | `ceesafec publish`: the package cannot be published as it is (§4.8) | a path dependency, or uncommitted changes | Dynamic dispatch and owning closures (N0330–N0339; `type-system.md` §37, §36.5, ADR 0015): | Code | Meaning | Example | |---|---|---| | N0330 | `dyn I` where it is not behind a reference or a `Box`: a local, parameter, field, element, optional or type argument | `fn f(s: dyn Shape)`, `Vector` | | N0331 | `dyn I` for an interface that is not dyn-compatible (type-system.md §37.1, ADR 0086 §2.3): one of its methods takes `this` by value, has no receiver, has type or const parameters of its own or names `Self` outside its receiver, or it has an associated type or a constant, or requires `Copy`; or the same holds for a supertrait | `interface Sink { fn close(this); }` then `borrow dyn Sink` | | N0332 | A `move` literal captures a scoped value: a reference, view, slice, closure, or a value with region parameters (`type-system.md` §36.5) | `let v = s.view(); Box.create(move () => v.length)` | | N0333 | A `move` literal anywhere but as the value of `Box.create`/`createIn` with an expected `Box` | `let f = move (x: u64) => x;` | | N0334 | A value that is not `Send` where it must cross to another thread: a spawned closure's capture or result, a `dyn … + Send` coercion's source, a `Send` bound's argument, or, for `Thread.both`, a capture that may not be used from two threads at once or an argument that is not a closure literal or `fn()` value; for `s.spawnSend`, the same rule on its captures, an error type that is not `Send`, or a scope with a user allocator (`type-system.md` §38, §38.6, §40.5, ADR 0038) | `Thread.spawn(move () => view.length)` with `view: StringView` | | N0336 | `@effectsOf` that names no parameter, or a parameter that is not of a function or closure type, or has no names (`type-system.md` §32.5) | `@effectsOf(n)` with `n: i32`; `@effectsOf()` | | N0337 | A static that cannot be one: a `static` whose type is not `Share`, or a generic or local one (`type-system.md` §39) | `static C: Cell = …` with `Cell` not `Share` | | N0338 | Not a static initialiser: an initialiser or `@constant` body outside §39.4, or a C constant macro called elsewhere | `static N: u64 = compute();` | | N0339 | `offsetOf(field)` whose `T` is not a struct, or has no field of that name (`type-system.md` §30.1, ADR 0030) | `offsetOf(x)`; `offsetOf(c)` | | N0340 | `callerAddress()` outside the body of an `@export` function, including in a closure inside one (`type-system.md` §28.4, ADR 0030) | `fn f() -> usize => callerAddress();` | | N0341 | `await` outside the body of an async fn or async closure: in a sync function or closure, a `drop` block or a deferred statement (`type-system.md` §40.2, ADR 0031) | `fn f() { await g(); }` | | N0342 | A task that is not awaited, run or spawned: a call of an async fn, an async closure, or a call with an async closure for an `@effectsOf` parameter, anywhere else; an `await` whose operand is not a task; an `Async.run` or `Async.runWorkers` operand that is not a call of an async fn (`Async.run(Async.scope(…))`); or an `@effectsOf` function that uses such a closure other than by calling it or passing it on to an `@effectsOf` parameter (`type-system.md` §40.1, §40.3, §32.5) | `let t = fetch(url);` | | N0343 | Recursion through `await` where no frame stack takes it (ADR 0056): in the freestanding profile, an async fn whose frame would contain itself, directly or through a call through `dyn` that may reach it; in any profile, a cycle through a `race` or `all` child, or an `@effectsOf` function's async instance calling itself with its closure (`type-system.md` §40.1, §32.5, ADRs 0036, 0037) | `async fn walk(n: u32) { if (n > 0) { await walk(n - 1); } }` | | N0344 | A scope's child that captures one of the scope body's own locals, which end before the child does, other than a `Copy` value it only reads, which it copies, or what an `async move` child moves (`type-system.md` §40.5, ADR 0033) | `Async.scope(E.Oom, (s) => { var n = 0; s.spawn(async () => { n += 1; return Ok(); }); })` | | N0345 | `Async.park()` or `Async.waker()` outside the library (`type-system.md` §40.4) | `await Async.park();` in a program | | N0346 | A member of an imported union whose type has bit patterns that are not values: anything but an integer, a raw pointer, an array of these, or an imported type whose fields are such (`type-system.md` §31.1, ADR 0041 §3.2) | `union u { on: bool; n: u32; }` in `extern "C"` | | N0348 | A `@test` function of the wrong shape: a method, with parameters or arguments, generic, exported, or returning anything but `void` or `Result`; or a call of a test from anything but the generated harness (`build` leaves tests out) (`tooling.md` §10, ADR 0049 §3.1) | `@test fn t(x: u32) {}` | | N0349 | A task set's child that captures a value holding a loan (a reference, a slice, a `StringView`, a struct with region parameters, the set itself): it may outlive the code that started it (`type-system.md` §40.5, ADR 0053 §3.2) | `await set.spawn(async move () => { let k = n + 1; return Ok(); })` with `n: borrow u32` | | N0347 | A further argument of a C variadic function whose type C's default argument promotions would change or that cannot pass through `...`: an integer narrower than 32 bits, `bool`, `f32`, or anything but `i32`, `u32`, `i64`, `u64`, `isize`, `usize`, `f64` and raw pointers (`type-system.md` §28.2, ADR 0041 §3.3) | `printf(f, b)` with `b: u8` | | N0351 | A cell of a loan-holding type: `RawCell`, `Cell` or `RefCell`, directly or inside a generic struct, with a `T` that holds loans (a reference, view, closure or other scoped type), whose contents could then outlive what they borrow (`type-system.md` §41.1, ADR 0084 §2.1) | `RefCell`, `Cell` | | N0352 | A const parameter of a type that cannot be one: anything but `bool`, an integer type or a fieldless enum the program or a dependency declares (a float, `StringView`, a struct, an enum with payloads, an imported C enum, a type parameter) (`type-system.md` §20.5, ADR 0083 §2.2) | `fn f()` | | N0353 | A const argument that is not a constant: a run-time value (a local, a parameter, a call), an interface constant read through a type parameter (`H.NAME`), or an expression over a const parameter, as a generic argument or an array length (`type-system.md` §20.5, ADR 0083 §2.3, §2.4) | `run()` with `ts: bool`; `[u8; N + 1]` | | N0354 | A generic argument of the wrong kind: a type for a const parameter, or a value (a literal, a constant, an enum variant, a const parameter) for a type parameter or where a type is expected (`type-system.md` §20.5, ADR 0083 §2.4) | `Grouped` for `struct Grouped`; `Vector` | | N0355 | A `match` guard that moves, assigns or `mut`-lends the scrutinee, a place inside it or one of the arm's bindings, or that awaits: the guard only reads them (`type-system.md` §15.2, ADR 0085 §2.2) | `Slot.Full(r) if (consume(r)) =>` with `r` not Copy | | N0356 | The alternatives of an or-pattern bind different names, or one name with different types or `var`-ness (`type-system.md` §15.2, ADR 0085 §2.3) | `Some(x) \| null =>` | | N0357 | An empty range pattern: `lo..=hi` with `lo > hi`, or `lo..hi` with `lo >= hi` (`type-system.md` §15.2, ADR 0085 §2.4) | `'z'..='a' =>` | | N0358 | A constant pattern of a type that cannot be a pattern (anything but an integer or `bool`), or `T.NAME` read through a type parameter, or `Self.NAME` in an interface's default method, or a bare name that names a const parameter (`type-system.md` §15.2, ADR 0085 §2.6, ADR 0083 §2.3) | `HALF =>` with `const HALF: f64` | | N0359 | An `e is P` whose pattern binds where the test is not known true: under a negation or a logical or, as a whole `let` initialiser or argument, or as the last operand of an `&&` chain outside an `if` or `while` condition; bindings are seen only by the chain's later operands and the then-branch or body (`type-system.md` §15.4, ADR 0085 §2.8) | `if (!(x is Some(n))) { … }` | | N0360 | A `debugAssert` whose condition or message may change what exists before the call, which release builds would not do: an assignment, `mut` loan, move or drop of a place declared outside the arguments, a write through a raw pointer or to a `static mut`, I/O or another `sleep` or `suspend` operation, a call of a C function (even one declared `@effects()`) or through a closure, function value or `dyn` method, an early exit (`?`), a `break` or `continue` out of the arguments, a process exit, or a call or drop that does one of these at any depth; the message names the chain of calls (`type-system.md` §8, ADR 0087 §2.5) | `debugAssert(events.next() != null)` with `events` declared before the call | | N0361 | An `embedText` file whose bytes are not UTF-8 (`type-system.md` §10.1, ADR 0087 §2.8) | `embedText("logo.png")` | | N0362 | An `embed` or `embedText` path that cannot be embedded: not a string literal or constant view; empty, absolute, or holding `\`, `:`, `$`, `%` or a leading `~`; no file there, or a real path (every symbolic link resolved, of the file opened) outside the package's directory and its `[embed] roots`, which one message names without telling the two apart; a directory or not a regular file; larger than 16 MiB; or a source with no file on disk (`type-system.md` §10.1, ADR 0087 §2.8) | `embed("../../.ssh/id_ed25519")` | | N0363 | Two CeeSafe files of a `build --split` whose C units' paths, `c//.c`, differ only in letter case: on macOS's and Windows's file systems they would be one file (`c-backend.md` §2.4, ADR 0082 §2.1). Reported at the later file | `src/Util.cee` and `src/util.cee` in one package | | N0364 | A package of the closure that does not check, reported by `check` in a package at the line of its manifest that leads to it (the first such dependency), once for the whole package instead of its own diagnostics, with a note at its first error and `ceesafec check --package ` to see them all (`tooling.md` §4.4, ADR 0082 §2.7, §8.5). Whether the cache held its record or not, the report is the same, and the check stops where the whole program's would. `check --all` and `--whole-program` report the package's own diagnostics instead | `mid = { path = "../mid" }` when `mid`, or a package it depends on, has a type error | ### Warnings (N0400–N0499) A warning is reported like an error, `warning[N0400]` in the text format and `"severity": "warning"` in the JSON document (counted in `"warnings"`), but it never stops `check`, `build`, `run`, `test` or `fix`, and never changes the exit status. `check` reports it with the errors; `build`, `run` and `test` write it to standard error before their work, or as the JSON document after it. Warnings are reported for the root package's files only, never for a dependency's, lib/core's or lib/std's. `--deny-warnings` (`check`, `build`, `test`) reports every warning as an error, and the command fails (ADR 0073 §2.4). | Code | Meaning | Example | |---|---|---| | N0350 | A configured item used where its condition may not hold: a C function, an imported field or an export declared `@cfg("X")` used outside `if (cfg("X")) { … }` and outside an export configured so (type-system.md §28.4, ADR 0080 §3.3) | `fields.debugOnly` outside `if (cfg("CONFIG_DEBUG"))` | | N0400 | A string literal's text has `{name}`, `name` bound where the literal stands (a local, a parameter, `this`, a constant or a static): CeeSafe has no string interpolation, so it is printed as written. `\{` means the brace and does not warn (`grammar.md` §1.6) | `let x = 3; Console.writeLine("x = {x}");` | | N0401 | `name.x` where `name` is a local, parameter, loop variable or binding with the name of a dependency that has a public item `x`: the local hides the dependency there, so this is the local's member and not `name.x`, the item (`type-system.md` §2, ADR 0074 §2.10) | `fn f(tls: Conn) { tls.connect(); }` with a dependency `tls` that has `public fn connect` | | N0402 | A `RefCell` guard (`CellRef`, `CellRefMut`, or a value holding one) live at an `await` or another point where an async fn or async closure may suspend: another task resumed meanwhile panics on the same cell (`type-system.md` §41.3, ADR 0084 §2.6) | `let g = cell.read(); await pause();` | # Unsafe Code — Contract and Checks Status: **normative** from M8a (2026-10-24). Rules U1–U3 are in `ownership.md`; the typing rules are in `type-system.md` §27; the decisions are in ADR 0007. Safe CeeSafe code cannot cause undefined behaviour (master prompt §6, `ownership.md` §1). Unsafe code can. This document says what unsafe code must guarantee, so that safe code keeps that property, and what the toolchain checks. ## 1. What unsafe permits Exactly the operations of `type-system.md` §27.3, and calls to `extern "C"` functions (§28.2; M8b). Later increments add MMIO accesses: - reading and writing through a raw pointer; - pointer arithmetic (`add`, `sub`, `offset`); - making references and slices from raw parts (`Ptr.toBorrow`, `Ptr.toBorrowMut`, `Slice.fromRaw`, `SliceMut.fromRaw`); - calling an `unsafe fn`. Making, copying, comparing and casting pointers is safe (§27.2): a pointer that is never used cannot do harm. An unsafe context relaxes nothing else (`type-system.md` §27.4). ## 2. The contract (U2) Every unsafe operation must be **valid** when it runs: | Operation | Requirement | |---|---| | `p.read()` | `p` is non-null, aligned for `T`, and points to a live, initialised `T` inside one object. For a `T` that is not Copy, the value is moved out (ADR 0027): until the next `p.write`, nothing may read, drop or borrow the `T` at `p`, or the value would have two owners (O1) | | `p.write(v)` | `p` is non-null, aligned for `T`, and points to live storage for a `T` inside one object, which nothing currently borrows (no live loan of it, shared or mutable; a shared loan of a `RawCell`, or of a value holding it, does not count for the cell's contents, type-system.md §41.1). The old contents are not dropped: if they held a `T` that needs drop, it leaks unless it was read out first | | `p.add/sub/offset(n)` | always defined (the address wraps); the *result* must meet the requirements above before it is used | | `Ptr.toBorrow(p)` | as `read`, for as long as the reference is used; nothing writes the `T` meanwhile (except through the reference itself, for `toBorrowMut`), and for `toBorrowMut` nothing else reads it either | | `Slice.fromRaw(p, n)` | as `Ptr.toBorrow`, for each of the `n` elements; `n × size(T)` ≤ 2^63 − 1 | | calling an `unsafe fn` | the function's own `Safety:` contract | | calling an `extern "C"` function | the C function's documented contract, and the declaration's types match its definition; pointers it is given are valid for what it does with them, for as long as it keeps them; values it returns meet CeeSafe's type invariants (`@repr` enums are checked) | **Pointers made from references** (M9c, ADR 0010): a pointer made from a reference (`r as *T`, `r as *mut T`, or `s.ptr` of a slice) may be used to **write** its target, directly or through a reference made from it by `Ptr.toBorrowMut` or `SliceMut.fromRaw`, only while the reference's loan could still be live. That is, only until the target is next used by anything other than that pointer: read, written, borrowed, moved, or its storage ended. After that, such a write is not valid even though the storage is live and nothing borrows it. The one exception is a `RawCell`'s contents, reached through `c.pointer()` (type-system.md §41.1; ADR 0084): they may be written while shared loans of the cell are live, and a reference made from that pointer by `Ptr.toBorrowMut` is valid while no other reference made from it is used. Optimisations rely on this (`optimisations.md` §2): a vector's length, read directly, stays what it was until the next `borrow mut` of the vector. "Live" means the object's storage has not ended: the local is in scope (between its `StorageLive` and `StorageDead`), the box or vector buffer has not been freed, the region has not been reset or destroyed. "Inside one object" means within one local, one heap block or one region allocation, as laid out by `TargetInfo`. In addition, unsafe code must leave every **safe-code invariant** intact *as observed by safe code* (U2): ownership (O1–O5: a value has one owner, is destroyed once), borrowing (B1–B5: no write to a place while a shared loan of it is live, no second access while a mutable loan is live), regions (R1–R4), the heap (H1–H3), and the type invariants (a `bool` is 0 or 1, an enum's tag is one of its variants, a `StringView` is UTF-8). For example, writing `2` through a `*mut u8` that points at a `bool` breaks safe code later, even though the write itself is valid. **Unconstrained references** (U3): a reference made by `Ptr.toBorrow`, `Ptr.toBorrowMut` or `fromRaw` holds no loans, so the borrow checker cannot see what it refers to. The unsafe code must bound it. The usual way is a safe function whose signature ties the result to a parameter, so that safe callers are checked against that parameter: ```ceesafe struct Buffer { data: *mut u8; length: usize; /// Safety: `data` points to `length` live bytes owned by this buffer (the constructor's contract). public fn bytes(borrow this) -> Slice => unsafe { Slice.fromRaw(this.data, this.length) }; } ``` A caller's slice from `b.bytes()` borrows `b`, so `b` cannot be changed or destroyed while it is used. Without such a bound, the reference could outlive its target, and nothing would report it. ## 3. The `Safety:` rule Each `unsafe fn` and each function containing `unsafe` carries a doc comment with a `Safety:` line (N0283; `type-system.md` §27.4): - for an `unsafe fn`, it states what the **caller** must guarantee; - for a safe function, it explains why the unsafe operations inside meet §2 **for every safe caller**. If that argument depends on something the caller could break, the function should be `unsafe` instead. The compiler checks only that the comment exists. Reviews check that it is true. ## 4. Lowering to C (c-backend §2) | CeeSafe | C | |---|---| | `*T`, `*mut T` | `T *` (C's type of `T`); `*void` is `void *`. No `const`: read-only is a CeeSafe rule, and CosmoOS's `-Wcast-qual` would reject casting it away | | `p.read()` / `p.write(v)` | `(*p)` / `(*p) = v` | | `p.add(n)` | `(T *)(uintptr_t)((uintptr_t)p + (uintptr_t)n * sizeof(T))`; `sub` subtracts, and `offset` converts the signed `n` to `uintptr_t` first (modulo 2^64) | | `r as *T` | `(T *)(uintptr_t)r` | | `p as *U` | `(U *)(uintptr_t)p` (no pointer-to-pointer cast, so no `-Wcast-align` or `-Wcast-qual`) | | `p as usize` / `n as *T` | `(uint64_t)(uintptr_t)p` / `(T *)(uintptr_t)n` | | `0 as *T`, `p.isNull()` | `(T *)(uintptr_t)0`, `p == NULL` | | `Ptr.toBorrow(p)` | `p` (a C pointer, as references are) | | `Slice.fromRaw(p, n)` | the slice struct `{ .ptr = p, .length = n }` | C itself makes pointer arithmetic outside an object, and comparisons of unrelated pointers with `<`, undefined. CeeSafe's lowering avoids both: arithmetic goes through `uintptr_t`, and pointers have no ordering (only `==`/`!=`, which C defines for any two pointers). A conversion between a pointer and `uintptr_t` is implementation-defined in C11 (6.3.2.3); clang and GCC define it as the address on every target CeeSafe supports (inferred from their documentation; checked on the corpus by the sanitizers and the interpreter). ## 5. What is checked - **Statically**: the capability rule (U1, N0280), pointee and operation rules (N0281, N0282), the `Safety:` comment (N0283). Safe code is checked as before (`type-system.md` §27.4). - **The interpreter** (`intermediate-representation.md` §5) runs unsafe code with its model of memory, and reports as an interpreter fault (exit status 70): a null pointer; an address in no live object, or in an object whose storage has ended; an access that does not fit inside the object; a misaligned access; reading uninitialised bytes. It does not track loans (no Stacked Borrows), so it does not detect a write through a pointer while safe code holds a loan of the target. - **Natively**, the run corpus also runs with ASan and UBSan. - **Not checked**: that the `Safety:` comment is true; the type invariants of §2 (a bad `bool` written through a pointer is caught only when it misbehaves); the rule for pointers made from references (§2), since the interpreter does not track loans. # Ownership, Borrowing and Lifetimes — Formal Safety Design (v0) Status: Milestone 0 proposal. This document is normative for Milestone 2, unless an item is marked *future*. **B1–B5 and E1–E3 are implemented as specified in `lifetimes.md` (M2b),** which records the v0 simplifications: immutable, once-initialised reference bindings; and B5 implemented as "the receiver's loan starts after the arguments". Related: `memory-model.md`, `language-spec.md`, `intermediate-representation.md`, `unsafe.md` (M8a). ## 0. Scope of the guarantee **Memory safety** for CeeSafe means: no execution of a well-typed program that contains no `unsafe` code (and links only correctly-specified `unsafe` libraries) performs any of: 1. use-after-free; 2. double free; 3. dereference of a dangling reference; 4. out-of-bounds access; 5. read of uninitialised memory; 6. type-confused access; 7. invalid ownership transfer; 8. unsynchronised concurrent access where at least one access is a write (data race; only once threads exist, M-later). **Not** included in memory safety, but each with defined behaviour: | Condition | Defined behaviour | |---|---| | Integer overflow (`+ - *`, negation, `abs`) | **Panic** in all profiles (OD-6). `wrapping*`, `saturating*`, `checked*` methods available | | Division / remainder by zero, `MIN / -1` | Panic | | Shift by ≥ bit width | Panic (`wrappingShl` masks) | | Out-of-bounds index | Panic (`get(i)` returns `T?`) | | Narrowing numeric cast | `as` is **only** allowed where lossless; `truncate(x)`, `checkedCast(x)` explicit | | `T?` unwrap of `null` | Panic (`!` postfix operator) | | Allocation failure | **Value** (`Result<_, AllocError>`); panicking convenience APIs exist only in the userland profile | | Stack overflow | Profile-defined: userland relies on guard page → process termination; kernel profile uses static stack-depth checking (see `profiles.md`) | | Resource exhaustion, deadlock, logic errors, leaks via `forget` | Not prevented | | Panic | **Abort**, no unwinding (OD-5). Destructors do not run on panic. Userland: print + `abort()`; kernel: CosmoOS `panic()` | Leaks are memory-safe (as in Rust), but the safe standard library must not leak in ordinary use, and no CeeSafe feature may use leaking as a substitute for lifetime soundness (master prompt §5.6). ## 1. Vocabulary - **Place**: a storage location expression: a local, a field of a place (`p.f`), an element of a place (`p[i]`), or the target of a reference (`*r`, written implicitly in surface syntax). - **Copy type**: a type whose values may be duplicated bitwise: primitives, `borrow T` (shared), raw pointers, and `struct`/`enum` types all of whose fields are Copy and which declare no `drop` (and are not marked `@noncopy`). Everything else is a **move type** (affine). - **Loan**: the creation of a reference to a place. Kinds: `shared` (`borrow T`) and `mut` (`borrow mut T`). - **Region**: a set of points in a function's control-flow graph (NIR) during which a loan must remain valid. - **Scoped type**: any type that contains a reference (`borrow T`, `borrow mut T`, `Slice`, `StringView`, arena handles, guard-protected handles), transitively, including through generic arguments. A non-scoped type is **free-standing**: it may outlive any particular stack frame. - **Owner**: the place that holds a value of a move type. Destroying the owner destroys the value. ## 2. Invariants The checker establishes these properties on the NIR (control-flow IR) of every function. "At point p" means on entry to the NIR statement at p. ### Ownership - **O1 — Unique owner.** Every live value of a move type is held by exactly one initialised place. - **O2 — Move de-initialises.** Moving from a place makes it *uninitialised*. A move out of a place is permitted only if the place is a local, or a field path rooted at a local, whose type has no `drop` (partial move). It is never permitted through a reference, an index, or out of a `drop`-implementing struct. - **O3 — Definite initialisation.** A place may be read, borrowed, or have a field assigned only if it is *definitely initialised* on every path reaching that point. (Forward must-dataflow over NIR. The same analysis implements use-after-move detection.) - **O4 — Exactly-once destruction.** At every point where a place goes out of scope (`StorageDead`), and before every assignment that overwrites an initialised place, the compiler inserts `drop(place)` if the place is definitely initialised, or a guarded drop if it is *maybe* initialised (a drop flag). A definitely uninitialised place gets no drop. Drop order: locals in reverse declaration order; struct fields in declaration order; vector elements in index order. - **O5 — Partial initialisation.** Struct literals initialise all fields atomically from the program's point of view. Their C lowering constructs in place, and no partially built struct is observable. A partially *moved* struct (O2) drops only its remaining initialised fields. ### Borrowing - **B1 — XOR aliasing.** At any point, for any place P, the live loans that overlap P are either all shared, or exactly one is `mut`. Overlap is path-prefix based: `a` overlaps `a.f`; `a.f` does not overlap `a.g`; `a[i]` overlaps `a[j]` (no index disjointness in v0). - **B2 — Loan validity.** While a loan L of P is live (inside its region), P must not be moved, dropped, go out of scope, or be assigned. If L is shared, P must also not be mutated or mutably borrowed, except for the contents of a `RawCell` in P, which unsafe code may change through the cell's pointer (type-system.md §41.1; ADR 0084). If L is `mut`, P must not be accessed at all except through L. - **B3 — Liveness regions (NLL).** A loan's region is the smallest set of CFG points containing every point at which some reference derived from the loan (transitively, through reborrows and copies of shared refs) may still be used. It is computed by backward liveness over NIR, then by solving outlives constraints. A shared reference copied into another variable propagates the loan. - **B4 — Reborrowing.** `borrow mut T` passed to a `borrow mut T` parameter is implicitly reborrowed. The original is unusable until the reborrow dies and becomes usable again afterwards (no move). - **B5 — Two-phase borrows.** For method calls `recv.m(args)` with a `borrow mut this` receiver, the mutable loan of `recv` is *reserved* while `args` are evaluated (permitting shared reads) and *activated* at the call. Required for `v.push(v.length)`. ADR 0073 §2.2 extends it to every `mut p` argument, whose later arguments may only read `p` (`lifetimes.md` §5). ### Escape (lifetime) rules - **E1 — No escape beyond the owner.** A value of scoped type may flow only to: (a) locals of the same function; (b) callee arguments; (c) the function's return value, if the elision rule E2 links it to a parameter; (d) fields of scoped *local* aggregates (tuples/anonymous temporaries), which then become scoped. - **E2 — Return elision.** A function returning a scoped type must have exactly one `borrow`/`borrow mut` parameter (including `this`), or must annotate the source: `fn pick(a: borrow T, b: borrow T) -> borrow(a) T`. The returned value's region is constrained to be within the named parameter's loan at the call site. A function with no borrow parameters cannot return a scoped type, except `static` data (string literals, `static let`), which have the `static` region. - **E3 — Scoped fields only with region parameters.** In v0, user `struct`/`enum` declarations could not contain scoped types. Since M6x (OD-3, ADR 0006) a struct or enum may hold scoped fields if it declares region parameters (`struct Parser { input: StringView }`), and generic instances with scoped type arguments are scoped themselves (`type-system.md` §25.3–§25.4). Region values and pool boxes still stay in locals. - **E4 — No scoped globals or heap.** Scoped values may not be stored in `static`s, constants, `Box`, pools, or arrays. Since M6x, `Vector`, `StableVector`, `SlotMap` and region memory may hold scoped values; the container is then scoped and carries their loans (`type-system.md` §25.4, §25.7). E1–E4 deliberately trade expressiveness for an implementation of tractable size (see "Why not full Rust lifetimes" below). ### Regions (arenas) and guards - **R1 — Allocation borrows the region.** `arena.alloc(...)` takes `borrow this` and returns a scoped value carrying a *shared loan of the arena*. Therefore: - the arena cannot be dropped, moved, reset, or mutably borrowed while any allocation from it is live (B2); - arena allocations cannot escape the arena's scope (E1–E4). - **R2 — Trivially-destructible contents (v0).** Arena allocation is permitted only for types with no `drop` (transitively). Bulk release is then semantically equivalent to individual destruction. Arena-with-drop-list is *future* (OD-8). - **R3 — Pinned scoped bindings.** `using x = e;` declares a binding that cannot be moved, reassigned, or returned; it is dropped exactly at the end of the enclosing block. Arenas, lock guards and quiescence guards are normally bound with `using`. (Types may require `using` by declaration: `@scopedOnly`.) - **R4 — Guard-protected borrows.** An API may return values scoped to a guard (`table.lookup(borrow guard, key) -> Obj?` where `Obj` is scoped to `guard`). By B2/E1 these cannot outlive the guard. This is the mechanism for epoch/quiescence-protected kernel lookups (`cosmoos/kernel-memory-and-lifetimes.md`). ### Heap - **H1 — Box ownership.** `Box` is a move type that owns exactly one allocation of `T` obtained from allocator `A`. Its drop runs `T`'s drop, then `A.free`. `A` must be a free-standing *allocator type* (§3 of `memory-model.md`). Allocations from non-free-standing allocators are scoped values (R1). - **H2 — Collections.** `Vector` owns its buffer. Element access returns loans of the vector (`borrow this`/`borrow mut this`), so growth (which takes `borrow mut this`) is rejected while element borrows are live (B1/B2). Stable references into collections use `SlotMap`/`Handle` with generation validation (library type, M6). - **H3 — No owning cycles.** Only `Box` and owning collections own, and their ownership graph is a forest by construction (O1). Back-references and graphs use indices, handles, or arenas. ### Threads (M13 G4; ADR 0016, type-system.md §38) - **T1 — Only `Send` values cross.** A value reaches another thread only by being moved into a spawned closure, or returned from one through `join`. Each is checked `Send` (N0334). A `Send` value holds no loan and no raw pointer, so after the move the spawner reaches nothing the new thread owns. - **T2 — Sharing is through `Arc` of `Share` types.** The only safe way for two threads to reach the same memory is an `Arc` with `T: Share`, which lends only `borrow T`. A v1 type can be changed through a shared reference only if it is one of the library's `@threadSafe` types: `Mutex` (whose guard lends `borrow mut T` to one thread at a time) and the atomics (whose operations are atomic), or holds a `RawCell` or a `StableVector`, and such a type is not `Share` (type-system.md §38.1, §41). So two threads can write the same memory only through a mutex or an atomic: there is no data race in safe code. - **T3 — The library's `@threadSafe` types** keep T2 in their `unsafe` code. `Mutex.lock` takes the platform lock before lending. The guard's drop releases it, after the guard's loan has ended (B-rules). `Arc`'s count changes only by atomic operations, and only the owner that drops it to zero destroys the value. A spawned thread is joined before `main` returns, so no thread outlives the values `main` dropped (O2). `@threadSafe` is accepted only in `lib/std`, so user code cannot claim it (N0290). - **T4 — Fork-join borrows end with the call** (`Thread.both`, §38.6; ADR 0018). A closure passed to `Thread.both` may reach the caller's values through references, because it runs only during the call and the call returns only after the thread running it has ended. Both closures' loans are live for the whole call, so the B-rules keep them from reaching one value in conflicting ways. A value reached from both threads is reached shared by both and is `Share`; one reached mutably is reached by one thread only and is `Send`. So T2's conclusion holds: there is no data race in safe code. ### Async tasks (L1; ADR 0031, type-system.md §40) - **A1 — Frames never move.** A task's frame is placed once (§40.6) and freed only after the task returns, never while it is suspended. A reference into a frame, held across an `await`, therefore points to live storage, and a buffer lent to the kernel by a suspended task stays valid until the kernel is done with it. - **A2 — No task outlives what it borrows.** A task is awaited where it is called, or spawned into a scope that returns only after its children (§40.5). Its loans are those of a call (an awaited task) or of an element of a scoped collection (a child), so the B-rules end them only after the task returns. - **A3 — Tasks stay on their thread.** A task never moves to another thread, so its frame needs no `Send`. A value crosses threads only through T1–T4 (`Async.blocking` checks its captures as `Thread.both` does). - **A4 — Cancellation destroys nothing.** Cancelling sets a flag that the library's waiting operations turn into `Err(AsyncError.Cancelled)`. The task then returns by ordinary code, and its locals are dropped in order. There is no path on which a suspended frame is dropped. ### Unsafe - **U1 — Capabilities.** Raw pointer dereference, pointer arithmetic, calls to `extern` or `unsafe fn`, `transmute`, access to `static mut`, MMIO and inline assembly are allowed only inside `unsafe { }` or an `unsafe fn`. - **U2 — Contract.** Every `unsafe` block must preserve O1–O5, B1–B5, E1–E4, R1–R4, H1–H3 and T1–T3 *as observed by safe code*. Every `unsafe fn` and every safe function containing `unsafe` must state its contract in a `/// Safety:` doc comment. A lint enforces that the comment exists. - **U3 — Raw pointers are inert.** Raw pointers (`*T`, `*mut T`) are Copy, carry no loan, and are ignored by the borrow checker. Converting a raw pointer to a reference (`unsafe { Ptr.toBorrow(p) }`; `type-system.md` §27.3) creates a reference with an *unconstrained* region, which must be immediately bounded by a safe wrapper's signature (as with Rust's unbounded lifetimes; this is the main hazard in unsafe code). ## 3. How the compiler rejects invalid programs Pass order (see `architecture.md`): 1. **Type check (HIR)**: kinds (Copy / move / scoped), E2 annotations, E3/E4 structural checks, `unsafe` context (U1), `using` restrictions (R3), arena element constraint (R2). 2. **NIR construction**: explicit `StorageLive/StorageDead`, `Move`, `Copy`, `Borrow(kind, place)`, two-phase reservation markers, and the `drop` sites still unelaborated. 3. **Definite-initialisation / move analysis** (O2–O3): forward dataflow, with a per-place bitset of {init, uninit, maybe}. 4. **Borrow check** (B1–B5, E1–E2, R1, R4): a. compute liveness of every reference-typed local; b. generate loans at `Borrow` statements and region variables for every reference type occurrence; c. generate outlives constraints from assignments, calls (signature instantiation), and returns (E2); d. solve regions as sets of points (NLL-style fixpoint); e. for every access, report a conflict if any live loan in the solved region at that point conflicts (B1/B2). 5. **Drop elaboration** (O4): inserts static drops and drop flags. Runs after the borrow check, so drops are checked as accesses in step 4 using the *unelaborated* drop sites. Diagnostics name the loan's creation point, the conflicting access, and the later use that keeps the loan alive, as three labelled spans. This is the single most important usability feature of the checker (see `diagnostics.md`). Example compile-fail cases (each becomes a test in `tests/corpus/compile-fail/`): ```ceesafe fn dangling() -> borrow i32 { // error E2: no borrow parameter to derive from let x = 5; return borrow x; } fn useAfterMove(b: Box) -> void { consume(b); print(b.value); // error O3: `b` used after move at line 2 } fn invalidate(v: borrow mut Vector) -> void { let first = v[0]; // Copy of i32: fine let r = borrow v[0]; // shared loan of *v v.push(1); // error B1: mutable loan while `r` live print(r); // ... `r` used here } fn arenaEscape() -> Slice { // error E2: no borrow parameter using arena = Arena.create(SystemHeap)!; return arena.allocArray(16)!; } ``` ## 4. Why not full Rust lifetimes in v0 Named lifetime parameters on aggregates, higher-ranked bounds and variance are where most of Rust's borrow-checker complexity and annotation burden live. E1–E4 keep the analysis entirely **intraprocedural** (signatures summarise callees completely), with no lifetime parameters on types. That is enough for: borrowing parameters, slices, string views, iterators over a borrowed collection (as a local), arena-scoped computations, and guard-scoped kernel lookups. It is not enough for: long-lived parsers holding `StringView`s, structs of references, or zero-copy data structures. Those are OD-3, designed in M6f (`od3-design.md`) so that v0 programs remain valid. ## 5. Behaviours requiring runtime checks | Check | Elidable when | Pass | |---|---|---| | Array/slice bounds | Index proven `< len` by range analysis (M9) | Lowering emits `ceesafe_check_index` | | Integer overflow | Value ranges proven (M9) | `ceesafe_add_i32` etc. | | Division by zero / `MIN/-1` | Divisor is a non-zero, non-minus-one constant | — | | Shift width | Constant shift amount | — | | Optional unwrap `!` | Flow-typing after `!= null` | — | | Drop flags | Static init state known (most cases) | Drop elaboration | | Allocation failure | Never elided (it is a value) | — | | Handle generation check (`SlotMap`) | Never in v0 | Built-in (M6e; library in M8) | | Kernel: stack depth | Static call-graph bound (kernel profile) | Link-time report | ## 6. Testing obligations (M2 acceptance) - Every invariant above has at least two compile-fail tests and one compile-pass test that is "close" to failing (for example NLL cases where a loan ends before the conflicting access). - Mutation suite: for each compile-pass test, automated mutants (swap statement order, remove a scope, duplicate a move) whose expected verdict is computed independently, by hand-labelling or a reference interpreter, not by the checker itself. - Generated C for compile-pass tests is run under ASan + UBSan in CI (supporting evidence only). # References and the Borrow Checker (M2b) Status: normative for increment 2b. This document turns `ownership.md` invariants B1–B5 and E1–E3 into an algorithm on NIR. The surface rules for references are in `type-system.md` §13. Where this document is stricter than `ownership.md`, it records a v0 simplification that later milestones may relax. ## 1. What makes the v0 problem small The algorithm can be simple and **exact for v0** because of four language rules (type-system §13): 1. **References are values of type `borrow T` / `borrow mut T` held only in locals and temporaries.** They never appear in struct fields (E3) or constants. 2. **A reference binding is immutable and initialised at its declaration** (`let r = …;`; no `var r: borrow T`). Assigning to `r` writes *through* it (`*r = v`) and never re-points it. So each reference local is assigned exactly once, which makes the points-to sets below flow-insensitive without losing precision. 3. **No nested references** (`borrow borrow T` is rejected). 4. **A use of a `borrow mut T` value is an implicit reborrow** of its referent (§4). Two usable mutable references to one place can therefore never coexist. Together with lexical scoping, (1) and (2) mean a reference cannot outlive its referent *inside* a function, except through a **temporary** borrowed at a call site, or through **`return`**. Both are checked explicitly (§6, §7). Conflicting accesses while a loan is live (B1/B2) are the remaining work, and they need liveness (§5). ## 2. NIR additions ```text projection = Field(f) | Deref | Downcast(V) | Index(operand) place = local projection* -- (*r).f is r Deref Field(f) rvalue += Ref(Shared | Mut, place) -- creates a loan | Slice(Shared | Mut, place, lo, hi) -- M2d: also a loan, of the whole array or slice viewed stmt += StorageDead(local) -- the local's storage ends (scope exit, statement end for temps) ``` Lowering facts the checker relies on: - Every `Ref` is assigned directly to a local or temporary: `t = Ref(k, p)`. - A temporary lives until the end of the source statement that created it. At that point the builder emits `StorageDead(t)`. The condition of an `if` or `while` counts as its own statement (M15 S2): its temporaries end once it is evaluated, before the branch, and each evaluation of a `while` condition ends its own. A borrow taken in a condition therefore does not reach the body. - User locals get `StorageDead` at the end of their block, and on `break`/`continue` for the blocks being left. (`return` ends every local; references escaping through `return` are §7's job.) - A use of a `borrow mut T` operand at a call argument, an initialiser or a return is lowered as `Ref(Mut, (*r))`, a reborrow. A coercion of `borrow mut T` to `borrow T` is `Ref(Shared, (*r))`. ## 3. Loans and points-to sets - Each `t = Ref(k, p)` at program point P creates a **loan** `L = (k, p, P)`. - Each reference local `r` has a set `loans(r)` of the loans it may refer to (directly or through reborrows). It is the least solution of: | NIR | Constraint | |---|---| | `t = Ref(k, p)` | `L ∈ loans(t)`; and if `p` contains `Deref` of a reference local `q`, then `loans(q) ⊆ loans(t)` (a reborrow keeps the original loan alive) | | `t = Slice(k, p, lo, hi)` | as `Ref(k, p)`: a slice is a reference to `[T]`, and its loan covers all of `p`, whatever the range | | `t = Use(Place(r))` with `r` of reference type | `loans(r) ⊆ loans(t)` | | `t = Call(f, args)` where `f` returns a reference | `loans(arg_i) ⊆ loans(t)` for every parameter `i` that `f`'s signature says the result is derived from (§7) | | reference **parameter** `p_i` | `Param_i ∈ loans(p_i)`, a placeholder loan for "whatever the caller lent" | Because every reference local is assigned once (§1), solving these subset constraints by fixpoint iteration gives the same result as a flow-sensitive analysis. **Loan-holding locals (M4a).** Since M4a, every local of a *scoped* type holds loans (`type-system.md` §19.1), not only reference locals. The constraints generalise: - **Copies and moves.** When an assignment's rvalue reads a place rooted at a loan-holding local `q` (`Use`, aggregates, enum values, built-in calls), then `loans(q) ⊆ loans(t)`. This covers a payload read such as `t = (q as Ok).0`. - **`Unpack`.** `Unpack(q, …)` gives each target `loans(q)`. - **Borrowing a loan holder.** `Ref(k, p)` where `p` is rooted at a loan-holding local `q` and does not go through a deref: `loans(q) ⊆ loans(t)`. A reference to a scoped value keeps that value's loans alive. - Liveness (§5) and the return check (§7) consider all loan-holding locals. ## 4. Accesses Each NIR statement and terminator performs **accesses** to places, in evaluation order: | Construct | Access | |---|---| | operand `Place(p)` | **Read** p | | `Ref(Shared, p)` | **BorrowShared** p | | `Ref(Mut, p)` | **BorrowMut** p | | `Assign(p, …)` | the rvalue's accesses, then **Write** p | | `StorageDead(l)` | **Dead** l | Two places **overlap** if they have the same root local and one projection list is a prefix of the other. `a` overlaps `a.f`, `a.f` does not overlap `a.g`, and `(*r)` overlaps `(*r).f`. Two `Index` projections always compare equal (M2d): `a[i]` overlaps `a[j]`, because v0 does no index reasoning. Two mutable slices of one array therefore cannot be live at once, even if their ranges are disjoint. Places with different roots never overlap. That is correct because aliasing between different roots only arises through loans, and every loan is checked when it is created. **Conflicts** (B1, B2): | Live loan \ access | Read | Write | BorrowShared | BorrowMut | Dead | |---|---|---|---|---|---| | Shared | ok | **conflict** | ok | **conflict** | **conflict** | | Mut | **conflict** | **conflict** | **conflict** | **conflict** | **conflict** | ## 5. Liveness (NLL) - A reference local is **live** at a point if some path from that point reads it (as an operand or as the root of a place) before the function ends. Reference locals are never redefined (§1), so there is no kill except at the definition. This is standard backward dataflow over the NIR CFG. - The **live loans after point P** are the union of `loans(r)` over every reference local `r` live after P. - **Check:** for every access `a` at point P to place `q`, and every loan `L` live after P with `L` not created at P, it is an error if `L.place` overlaps `q` and the table in §4 says conflict. The errors are N0240 (write), N0241 (borrow), N0242 (read) and N0243 (Dead: the place stops existing while borrowed). This accepts `let r = borrow x; print(r); x = 5;`, because `r` is dead after `print`. It rejects `let r = borrow x; x = 5; print(r);`. **Overwriting a reference is not a use of its referent (M11, G3).** A **Write** or **Dead** of a local `r` whose type is a reference does not conflict with a loan whose place goes through `*r`, such as `(*r).next`. The write replaces which object `r` points to; it does not change, move or end the object it pointed to. That object stays protected by the loans the new reference inherited from `r` when it was made (§3: a reborrow `Ref(k, (*r)…)` gets `loans(r)`). A box is not a reference: overwriting a box destroys its contents, so a loan through `*b` still conflicts. This accepts a cursor walking a list, `node = cursor!; … cursor = next_of(node);`, and a temporary that is reassigned in each iteration of a loop while a reference made through the previous value lives on. **Writes happen after the value (M2d).** In `Assign(p, rvalue)` the rvalue is evaluated first and `p` is written last. The **Write** access is therefore checked against the loans live *after* the statement; the rvalue's own accesses use the loans live before it. This accepts `a[0] = sum(a[..])`: the slice is dead once `sum` has returned, before `a[0]` is written. Rust's MIR gets the same result by writing a call's result in a separate step. **B5, two-phase borrows (as implemented; ADR 0073 §2.2).** For a method call `e.m(args)` whose receiver is `borrow mut this` and `e` is a place, the receiver's loan is created **after** `args` are evaluated, right before the call. Arguments may therefore read `e` (`v.push(v.len())`), but they cannot hold a borrow of `e` across the call. The place itself, with its index values and their bounds checks, is found where the receiver stands, left to right: `ps[i].set(bump(mut i))` borrows the element `i` named before `bump` ran (before ADR 0073, after). The loan exists only for the call, which is exactly the interval in which it must be exclusive. Every other argument `mut p` (`p` a place) and every `borrow mut` local passed on to a `borrow mut` parameter (B4) is two-phase too, in calls of functions, methods, closures and function values: its place is found where it stands, and its loan made at the call. Unlike a receiver it is **reserved** in between: when a later argument does more than read (a call, a borrow, a move), a shared loan of the place is held from the argument to the call, so a later argument may read the place (`fail(mut s, s.count)`) but a write, a move or a mutable borrow of it conflicts (N0240, N0241), and so does a shared borrow still live at the call (`f(mut s, s.name.view())`). Reads in later arguments of what a reserved loan may cover are taken where they stand, before the loans are made. ## 6. Temporaries `f(makeValue())` with a `borrow T` parameter borrows a temporary that dies at the end of the statement (`StorageDead`). If the result of `f` is derived from that argument and is stored in a reference local used after the statement, the `Dead` access conflicts with the live loan: N0243 ("temporary dropped while still borrowed"). The fix is to bind the value to a local first. The same holds for a temporary made for a receiver or a slice (ADR 0073 §2.1): `make().view()` is valid within its statement, and N0243 once the view is used after it. ## 7. Returning references (E1, E2) - **Signature.** A function whose return type is a reference must name the parameters the result may be derived from. With exactly one reference parameter (a `borrow`/`borrow mut` receiver counts), that parameter is implied (elision). Otherwise the return type must be written `borrow(a, b) T` or `borrow mut(a) T`, naming reference parameters (N0245 if missing or wrong). `borrow mut` results may be derived from `borrow mut` parameters only. - **Body check at each `Return(op)`.** For every loan `L ∈ loans(op)`: - `L = Param_i`: `i` must be among the named parameters (N0245); - `L` is a real loan whose place starts with `Deref` of reference parameter `p_i`: allowed if `i` is named (N0245 otherwise). Examples: `borrow this.items` with `this: borrow Self`, or `borrow (*a).f`; - any other real loan borrows a local or temporary of this function: N0244 ("returns a reference to local data"). - **Deep results** (type-system.md §25.6, ADR 0074 §2.4). A reference parameter that is not a view has two placeholder loans: `Outer_i`, the loan of the reference itself, and `Param_i`, what its referent holds. A value read through `*p_i` carries `Param_i` and not `Outer_i`; so does a borrow of a place beyond another reference stored in the referent. If the result is a deep result of `i`, the body check also rejects (N0245) `Outer_i`, and a real loan whose place starts with `Deref` of `p_i` and passes no other reference: the referent's own storage. - **Callers** use the signature as the `Call` constraint in §3. For a deep result of parameter `i` whose argument is `t = &p` (`&mut p`), the result takes `p`'s local's loans, less `Outer` placeholders of a parameter `p` is reached through, instead of `t`'s; for an argument that is a reference parameter, its loans less its `Outer`; for an argument `t = f(a)`, the only assignment of `t`, where `f`'s result passes through its parameter `a` (type-system.md §25.6, B18), what `a` contributes as that argument, followed again if `a` is defined the same way. ## 8. What the v0 checker deliberately does not do - No references in aggregates, and no reference re-pointing — **relaxed in M6x** (OD-3, `type-system.md` §25): aggregates with region parameters, scoped collections and re-pointable reference fields. The checker keeps one loan set per value; writes into fields, elements and through references add the written loans to the holder and to what its mutable loans point to (iterated to a fixpoint); calls apply their signature flows; and a loan conflicts only at points reachable from where it was created (holders such as a vector receive loans after their own definition). Destroying a value whose `drop` may read its loans is a use of them. - Index-sensitive disjointness (`a[i]` versus `a[j]`) came in 2d with arrays, conservatively: all elements overlap. Since M13 G5, two different **constant** indices are different places, so `borrow mut a[0]` and `borrow mut a[2]` may be live together. That is sound because a constant index names one element whatever the array's length. Any index that is not a constant still overlaps every element. - Moves and drops are 2c. The `StorageDead` points emitted now are where drops will go. - **`await` adds no rule** (L1, type-system.md §40.2). An awaited call is a call in NIR (`intermediate-representation.md` §3), so its arguments' loans last until the task returns, as for any call. A frame never moves, so a loan of a local across an `await` needs nothing more. A scope's child is an element of a scoped collection (§40.5), so its loans last until the scope returns. ## 9. Diagnostics Every borrow error names the conflicting access (primary span) and carries notes for the loan's creation point and for a later use of the reference that keeps it live, when one exists. # CeeSafe Memory Model and Allocation Architecture (v0) Status: Milestone 0 proposal. Safety rules referenced as O*/B*/E*/R*/H*/U* are defined in `ownership.md`. Goose findings are in `research/goose.md` (commit `f605b5a`). Other prior art is in `research/comparative.md`. The CosmoOS kernel mapping is in `cosmoos/kernel-memory-and-lifetimes.md`. ## 1. Candidate architectures ### A. Ownership-based stack + heap (Rust-like) Values live on the native stack by default. Long-lived and dynamically sized data live in owned heap objects (`Box`, `Vector`, `String`). Safety comes entirely from ownership and borrowing. Arenas are ordinary library types built with `unsafe` plus lifetime-carrying APIs. ### B. Region-based + owned heap (Cyclone/Tofte–Talpin-like) Every allocation is placed in a *region*. Regions are lexically scoped (stack frames, dynamic regions) or the heap region. Pointer types carry a region, and an outlives/effect system prevents dangling. Owned heap objects exist as unique pointers in the heap region. Region inference reduces annotations. ### C. Hybrid: ownership + explicit regions + conservative escape analysis Architecture A's ownership/borrow core is *the only* safety mechanism. Regions (arenas, scratch stacks, fixed buffers) are **explicit library values whose allocations are loans of the region** (R1), so the borrow checker covers them with no separate region type system. A conservative escape/placement analysis (M5) may move allocations to cheaper storage only when the move is **unobservable** (§5). Goose-derived techniques (data stacks for scoped temporaries, construction in place, grow-only containers, typed pools, BCE) are adopted as **implementation strategies and library types**, not as the safety argument. ## 2. Evaluation | Criterion | A: Ownership | B: Regions | C: Hybrid | |---|---|---|---| | Safety argument | One mechanism (affine + XOR borrows). Proven for the λRust core | Region type system + uniqueness. Proven for core calculi, but **two** interacting mechanisms in practice | Same as A (one mechanism). Regions reduce to loans. Escape analysis is outside the trusted base | | Kernel fit (no hidden allocation, failure values) | Good | Good if regions are explicit. Poor with inference (hidden placement) | Good: placement changes require the profile to permit them (§5), and the kernel profile forbids implicit ones | | Ergonomics for C#/TS users | Annotation burden for structs of references. v0 limits mitigate (E3) | Many annotations, or inference with unpredictable lifetimes (MLKit space leaks) | As A, plus `using arena = ...` for batch lifetimes. Diagnostics explain placement | | Implementation complexity | High (NLL borrow checker) | High (region inference + effects) **plus** uniqueness | A's complexity + an *optional* M5 pass. Region support costs nearly nothing extra | | Runtime cost | `malloc`/`free` per heap object unless the user picks an arena | Cheap bump allocation everywhere; risk of space leaks | Same as A by default. Arena and scratch where the user or proven-safe analysis selects them | | Predictability | High | Low with inference | High: placement is either explicit or reported by `--explain-allocations` | | Goose techniques usable | Some (in-place construction, BCE) | Data stacks map naturally to lexical regions | All the "cheap, local" ones (goose.md §9 key cautions) | | Failure mode if the analysis is wrong | Unsound borrow check → safety bug | Unsound region inference → safety bug | Unsound borrow check → safety bug. Unsound placement → **caught**, because placement output is re-validated by the checker (`architecture.md` §1) | ### Recommendation: **C — Hybrid** 1. It keeps **one** safety mechanism to specify, test and eventually verify. Goose's own history shows the cost of a second, fragile mechanism: its lifetime checker is graded "C/D, structurally fragile" by its own inventory, and roughly half of its recent commits fixed holes (goose.md §9.4). Option B would add a region type system *alongside* uniqueness. Option C reduces regions to borrows. 2. Arenas, scratch regions and fixed buffers are first-class from M4 with no new type-system concept: "an allocation borrows its region" is checked by the same B1/B2/E1 rules as `borrow x`. 3. Escape analysis stays outside the safety argument (master prompt rules 10–11), and its effects are visible and reportable. 4. It fits CosmoOS: every allocation in the kernel profile names its allocator type or region value, so nothing allocates implicitly. Cost: the v0 restriction E3 (no references in user structs) is felt most for arena-heavy code (e.g. AST nodes pointing to other arena nodes). The v0 answer is indices or `Span` offsets (Ex16), and the long-term answer is OD-3. ## 3. Storage categories ### 3.1 Native stack (C automatic storage) - Scalars, fixed-size structs/enums/arrays, references, `Box` pointers, temporaries. - Size limit per value: `maxStackValue` per profile (userland default 64 KiB, kernel default 512 B). Larger fixed-size locals are a compile error suggesting `Box` or a region. This avoids unbounded native-stack consumption. - No variable-length arrays (C VLAs and `alloca` are never emitted). - Kernel profile: per-function frame-size estimates are reported, and the deepest call chain is checked against the kernel stack budget (C4/C7; see `profiles.md`). ### 3.2 Heap (owned) Implemented in C5 (`type-system.md` §30, `lib/core/alloc.cee`): `Layout` and `Allocator` with `allocate` and an `unsafe deallocate` (no `reallocate` yet), user allocators as field-less structs, for `Box` only. ```ceesafe // core library (sketch) public struct Layout { size: usize; align: usize; } // align: power of two public interface Allocator { // `this` is a free-standing allocator value: ZST token or a 'static handle. unsafe fn allocate(borrow this, layout: Layout) -> Result<*mut u8, AllocError>; unsafe fn deallocate(borrow this, ptr: *mut u8, layout: Layout) -> void; unsafe fn reallocate(borrow this, ptr: *mut u8, old: Layout, newSize: usize) -> Result<*mut u8, AllocError>; // default: alloc+copy+free } ``` - *Free-standing allocator types* (`SystemHeap`, `KernelHeap`, `SlabCache`-backed allocator singletons) are zero-sized or `'static`. Only these may parameterise `Box`, `Vector`, `String` (H1). This keeps `Box` one pointer wide and makes "Box outlives allocator" trivially true. - *Instance allocators* (arenas, pools, fixed buffers) are values. Their allocations are scoped values carrying a loan of the instance (R1). A `Pool` hands out `PoolBox` (scoped, individually freeable). An `Arena` hands out `borrow mut T` / `SliceMut` (freed in bulk). - **Allocator lifetime**: free-standing allocators live for the whole program (or kernel lifetime). Instance allocators cannot be dropped while allocations are live (B2). Outstanding allocations therefore can never outlive their allocator (master prompt §5.5). - **Ownership of allocations**: always exactly one owner (O1). Freeing with the wrong allocator is impossible because the allocator is part of the type (`A`) or of the loan. - **Thread safety**: an allocator type declares `Sync` if it may be used concurrently (`SystemHeap`, `KernelHeap`: yes; `Arena`, `FixedBuffer`: no). Enforced once threads exist (M-later). - **Failure**: every allocating API returns `Result<_, AllocError>`. Convenience `...OrPanic` / `!` usage is allowed in userland and **rejected by a lint (error) in the kernel profile**. - **Testability**: `FailingAllocator(failAfter: n)` and a fault-injection hook in `SystemHeap` (env var in hosted test builds) make every failure path testable (M3 acceptance). - *As implemented in M3b (ADR 0003):* `FailingAllocator` is a built-in, zero-sized allocator type over the platform heap, steered by `FailingAllocator.failAfter(n)` and `.reset()` (a process-wide countdown). - The runtime counts every allocation attempt when asked to (ADR 0059). `CEESAFE_FAIL_ALLOC_AT=k` fails attempt *k*, and `CEESAFE_ALLOC_REPORT=1` prints `ceesafe alloc: attempts= live=` when `main` returns. Either one, or `ceesafec test`, turns counting on; without them the runtime counts nothing. - **In a host program** (ADR 0088 §2.2), whose `main` is not CeeSafe's, the same settings come from `ceesafe_init`'s config (`ceesafe/embed.h`): `fail_alloc_at` and `alloc_report`, or with `CEESAFE_INIT_ENVIRONMENT` the two variables (the environment's `CEESAFE_FAIL_ALLOC_AT` wins over the config's when set). The report is printed by `ceesafe_shutdown`, which the host calls (in its exit path), and which returns `live`; the generated `main` calls `ceesafe_init` with the environment flag before anything else and `ceesafe_shutdown` when `main` returns. Counting cannot start under live blocks: a config asking for counting after the runtime has allocated is `CEESAFE_INIT_LATE`, and changes nothing (without counting, it succeeds; when only the environment asks, it succeeds without counting and says so on stderr). The generated `main` counts regardless, as before `ceesafe_init`, when a C constructor has allocated through CeeSafe. A host that never calls it gets no counting, no failure injection and no report, whatever the environment says. `ceesafe_init`'s copy of `argv` is not counted. - **With threads.** The counters are atomic, so every attempt, whatever its thread, gets its own number in one order, and attempt *k* is the only one that fails. That order is the order in which the threads' attempts reach the counter. A sweep (`k` = 1, 2, …) therefore fails every attempt of a run that allocates from one thread at a time in a fixed order, as before. Where threads allocate concurrently, the attempt numbered *k* may be a different allocation from one run to the next: the sweep still fails one allocation per run and never races, but it may not reproduce, and it may miss an allocation that is never numbered *k* in its run. `FailingAllocator`'s countdown is atomic in the same way (`type-system.md` §18.1). The same holds for a host's threads calling exports at once. - C code's allocations through `CEE_ALLOC_FOREIGN` (ADR 0060, type-system.md §28.6) count in `attempts` and can be failed, but not in `live`: when there were any, a second line, `ceesafe alloc: foreign held=`, gives the blocks C still holds, and no tool reads it as a leak. `CEE_ALLOC_FOREIGN_INIT` (ADR 0061), for a C library's one-time initialisation, is neither counted in `attempts` nor failed; its blocks are in `foreign held`. - The test runner's sweep uses both (`testing.md`). ### 3.3 Arenas (regions) - `Arena.create(parent: A, chunkSize)`, an instance allocator over a free-standing parent. Bump allocation in chunks, with all chunks released at drop. - v0 contents must have no `drop` (R2), so bulk release equals destruction. Arena-with-drop-list is OD-8. - `reset(borrow mut this)` requires no live loans (B2), so it is safe by construction. - An arena is **not** the native stack: it is heap-backed (master prompt rule 9). ### 3.4 Scratch regions (Goose-style data stacks) From goose.md §9.1/§9.2: a per-thread, compiler-known **scratch stack** used for dynamically sized temporaries with nested lifetimes: ```ceesafe fn render(n: usize) -> Result { using frame = Scratch.enter(); // records watermark let buf = frame.allocArray(n)?; // bump; scoped to `frame` ... } // watermark restored: one store ``` - Semantically an arena with LIFO discipline. Safety follows from R1 + R3 (`using` frames cannot be moved, and their allocations cannot escape). - Storage: hosted/userland uses a reserved virtual region with guard pages, plus an explicit capacity check. Exhaustion returns `AllocError` **unless** the frame was created with `Scratch.enterOrHeap()`, in which case it falls back to the parent heap. Kernel: a fixed per-thread buffer of profile-defined size with explicit checks. No reliance on page faults. - Nested frames must be strictly LIFO. *Corrected by ADR 0004:* `using` alone does not ensure this. Nested frames come from `parent.enter()`, which mutably borrows the parent, and only one root frame per thread may be live (a second `Scratch.enter()` panics). Goose's "one growable on top" rule is **not** adopted in v0. The scratch region hands out fixed-size allocations only. Growable scratch arrays are a future extension (OD-12). ### 3.5 Fixed-buffer and pool allocators - `FixedBufferAllocator.over(mut storage)` borrows its backing storage mutably for its whole lifetime, so the backing array cannot be touched directly while it is used. - *As implemented in M4c (ADR 0004):* `Pool` is a fixed-capacity pool whose `take(v)` returns a scoped `PoolBox`: a box that holds a loan of the pool and returns its slot when destroyed (`type-system.md` §19.6). `SlotMap` with generation-checked handles is still M6. - `Pool` / `SlotMap`: typed slots with generation counters (goose.md §9.8 plus Vale). Handles are free-standing `Handle` values (index + generation) validated on access (`get(h) -> borrow T?`). ### 3.6 Static data String literals and `static let` constants (immutable). `static var` is `unsafe` to access in v0 (data races); safe statics with synchronisation come with concurrency. ## 4. Collections and invalidation | Type | Ownership | Growth | Element references | Notes | |---|---|---|---|---| | `[T; N]` | value | — | loans of the array | Copy iff T Copy | | `Slice` / `SliceMut` | scoped borrow | — | reborrows | bounds-checked | | `Vector` (M6d, type-system §23.1) | owns buffer | reallocates (amortised ×2) | loans of the vector. Growth requires `borrow mut` → **rejected while element loans are live** | contiguous; **no address stability promise** | | `StableVector` (M6e, type-system §24.2) | owns chunks | appends a chunk, never moves | loans with *shared* access permitted during `push` (grow-only: goose.md §9.3) | push takes `borrow this` with interior bump. Not `Sync` | | `SlotMap` (M6e, type-system §24.1) | owns slots | may reallocate | `Handle` (not a loan) validated by generation | stable *identity*, not stable addresses | | `String` / `StringView` | owns / scoped | as Vector | as Vector | UTF-8 invariant | | `Box` | owns one object | — | loans of the box | `T?` niche | Iterator invalidation is impossible in safe code: iterators are scoped values holding a loan of the collection. ## 5. Placement analysis (M5; on by default since M9e) — rules The analysis may change *where* a value lives only under these conditions: 1. **Box → native stack / scratch**: allowed iff (a) the Box is created and dropped in the same function on all paths, (b) it is never moved into a callee or returned, (c) its address is not exposed to `unsafe`/FFI, (d) the profile marks `A` as *placement-transparent* (`SystemHeap` in userland: yes; **`KernelHeap`: no**, because kernel allocation is observable through accounting, and failure injection must remain meaningful), and (e) the size is below the profile's stack limit. Failure behaviour: the original program could fail with `AllocError`, and the optimised one cannot. This is permitted only because the placement-transparent contract declares allocation failure of that allocator "may spuriously succeed". 2. **Never** the reverse: a scoped/borrowed value is never promoted to the heap to make a program type-check (master prompt §7 and rule 11). There is no such code path, because placement runs after the borrow check on accepted programs only. 3. Destruction order and timing are preserved (master prompt rule 12): drops of demoted Boxes run at exactly the same NIR points. 4. Every decision is reported by `--explain-allocations`: `src/a.cee:12:9: Box placed on stack (does not escape; SystemHeap is placement-transparent)` `src/a.cee:20:9: Box stays on heap: moved into call to 'insert' at 21:5` ### 5.1 The M5 implementation (`PlacementAnalysis`) The analysis works per function on NIR after drop elaboration. It is deliberately simple and conservative: a box it cannot follow stays on the heap. Rule 1's conditions become the following checks. - **Sites.** A site is `R = Box.create(v)` or `Box.createIn(a, v)`, with `R` a whole local. The site is **eligible** when: - the allocator is `SystemHeap`, the only placement-transparent allocator so far (rule 1d). `FailingAllocator` is not transparent: its failures are its purpose; - `T` is at most 1 KiB, and the function's stack-placed boxes total at most 4 KiB (rule 1e); - each site has at most one box alive at a time, so one storage slot per site suffices. A site in no loop of the function's graph has this by construction. A site in a loop has it when every local of its flow set `F` (below) ends, by its `StorageDead`, on every path from the site back to itself (M9e): no local can then hold one execution's box when the site runs again, and since the box does not escape, it has been dropped by then. A local that is not ended inside the loop keeps the box on the heap ("may outlive its loop iteration"). The interpreter checks this rule: a site whose previous box is still alive when it runs again is an interpreter fault. - **The flow set** `F` of a site is the set of locals that may hold its box: `R` itself, and every whole local assigned (moved or copied) from a place rooted in `F` whose type is the box type or `R`'s result type. That includes the unpacked `Ok` payload and copies of the result. The analysis iterates until `F` stops growing. - **Escapes.** The box stays on the heap, with the first reason found, if: 1. a local in `F` has more than one definition. Its drops could then see a box from elsewhere; 2. a value of the box type or of `R`'s type, read from a place rooted in `F`, flows anywhere but into a local of `F`: a call or built-in argument, an aggregate, a return, or a non-local place (rule 1b); 3. a place rooted in `F` of the box type is borrowed **mutably** (`borrow mut Box`). The borrower could replace the box, and drop ours with the heap's drop glue. Borrowing the box shared, or borrowing its contents either way, is fine: the borrow checker ends those borrows before the box's drop; 4. a `Drop` destroys a place rooted in `F` that is neither a whole local of the box type nor inside its contents. Destroying contents in place is fine: drop elaboration does that to the fields left after a move out. A result holding the box, though, would be destroyed by the heap's drop glue. A result *may* be destroyed where it cannot hold the box: in a block dominated by a non-`Ok` target of a switch on that result's own discriminant, where the target is entered only from the switch. That is the error path of `?` and `!`, whose drop glue sees an `Err`. Reads through the box (`b.f`, `b.f = x`, moving a field out) and `discriminant` of the result are not escapes. Rule 1c (unsafe and FFI), from M8a: a function that makes a raw pointer from a reference, or a reference or slice from a raw pointer, keeps all its boxes on the heap (reason "address exposed"; `type-system.md` §27.5). Rule 1a (created and dropped in one function) follows from rules 2 and 4. - **Demotion.** A stack-placed site writes `v` into a per-site slot of the function's frame and makes `Ok(&slot)` without allocating; that allocation cannot fail. - A `Drop` of a box local in `F` destroys the contents only, and does not free. - A `BoxFree` of one does nothing. - Drops therefore run at the same NIR points with the same contents (rule 3). ## 6. No-GC baseline and cycles No tracing GC, no implicit RC. Ownership is a forest (H3). Graph-shaped data uses one of: `SlotMap` handles, arena + indices, or (future, OD-13) an explicit `Rc`/`Weak` library type with documented cycle-leak behaviour. In the kernel, explicit refcounted *kernel objects* wrap CosmoOS's own refcount protocol (`cosmoos/kernel-memory-and-lifetimes.md`). That is a different type (`KRef`) with type-specific release, not a generic `free`. ## 7. Goose technique adoption summary | Goose technique (goose.md §9) | CeeSafe adoption | Milestone | |---|---|---| | Data stacks for scoped temporaries | `Scratch` regions (§3.4), with heap fallback and a no-MMU kernel mode | M4/M5 | | One-growable-on-top non-moving growth | Not adopted; `StableVector` instead | M6 | | Grow-only vs grow-shrink split | `StableVector`, monotone lengths feed BCE | M6/M9 | | Root inference by monomorphisation | **Rejected** (whole-program, fragile). NLL + E1–E4 instead | — | | Construction into destination / guaranteed NRVO | NIR destination places + guaranteed RVO for single-named-result functions | M2 (RVO), M7 | | Inline variable-size layouts | Future opt-in `@flat` types (OD-14) | post-M9 | | Relative references + verifier | Future library feature for zero-copy formats | post-M9 | | Type-safe pools | `Pool`/`SlotMap` with generations | M6 | | BCE with index types, 2^48 size cap, `checkfree` | Adopt the size cap in the spec now (all `len ≤ 2^48`), BCE + `@checkfree` in M9 | M0 (spec) / M9 | | `return … from` | Not adopted; `Result` + `?` | — | | Share-nothing workers | Possible library model later | post-M9 | **Spec rule adopted now (from Goose BCE):** every object, slice and collection length is `≤ 2^48 − 1` elements. Allocators reject larger requests with `AllocError`. This makes `len * sizeof(T)` overflow-free for `sizeof(T) < 2^15` and lets index arithmetic on `usize` be proven non-overflowing. # Formatting (M10 T1) `ceesafec fmt` rewrites CeeSafe source into one canonical style. It has no options: two programmers who run it get the same spacing, indentation, blank lines, brace placement and comment alignment. Where the style leaves a choice to the author (§3), it is a choice of line breaks, which the formatter keeps. The formatter changes only whitespace, and the trailing commas of §3.3. A formatted file parses to the same tree as the original (the same AST, apart from spans) and keeps every comment, in order. Formatting a formatted file changes nothing. ## 1. Command ```sh ceesafec fmt file.cee... # rewrite each file in place (only if it changes) ceesafec fmt --check file.cee... # change nothing; list the files that would change, exit 1 if any ceesafec fmt --stdout file.cee # print the formatted text of one file ``` A file with a syntax error is not formatted: its diagnostics are printed, the file is left as it is, and the exit status is 1. A directory argument means every `.cee` file under it. ## 2. Lines and indentation - The text is UTF-8 with LF line endings; it ends with exactly one newline. No line has trailing whitespace. - Indentation is 4 spaces per level; there are no tabs. - A level is opened by a `{` that ends its line (a block, a struct, enum, interface or impl body, a match, a multi-line struct or array literal) and by a broken list (§3.3). Its contents are indented one level, and its closing delimiter is on its own line at the opening line's indentation. - **Blank lines:** at most one in a row. None directly after an opening `{` or before a closing `}`, and none at the start or end of the file. Elsewhere, between two items, two members or two statements, a blank line is kept if the source has one or more, and not added if it has none. - **Line width:** 120 columns, counting the code and not a trailing comment. A line longer than that is broken by §3.4 where it can be. A line that cannot be broken (a long string literal, a long name) stays long. - **Raw literals** (`grammar.md` §1.6, ADR 0074 §2.6): the content lines and the closing line of a `"""` literal move with the line its opening `"""` ends, by the change in that line's indentation, and are never reflowed or trimmed: their trailing spaces are part of the value. Fewer columns are removed than the change when the closing line's indentation is narrower, since removing more would change the value. Their width does not count. ## 3. Line breaks ### 3.1 What the author decides The formatter keeps these choices and only lays out the result: - whether a block, struct, enum, interface or impl body, or match is written on one line (`{ return x; }`) or across lines (anywhere between its braces); - whether a list (arguments, parameters, struct fields, array elements, attribute arguments) is broken, and, in a broken list, which elements start a new line (so a literal can keep several fields per line); - whether an expression body (`=> expr`) starts on the line of the `=>` or on the next one; - where a long binary expression or a chain of member calls (a `.` after a call, an index, `!` or `?`) is broken, and which `else`s of an `if` expression (`if (c) a else b`) start a new line; - whether an attribute is followed by a line break. Any other line break in the source is removed: two tokens on different lines that no rule here separates are joined with their §4 spacing. In particular `else` always follows the `}` before it on the same line; only an `if` expression whose value is not a block may start an `else` on a new line (§3.4). A one-line form is kept only if it fits in 120 columns and holds no line comment; otherwise it is broken, as if the author had written it across lines. ### 3.2 One-line forms - A one-line block has one space inside each brace: `{ x += 1; }`, `{ break; }`. An empty block is `{}`. - A one-line struct body is `struct Point { x: i32; y: i32; }`; a one-line enum is `enum E { A, B(i32) }`. - A one-line match is `match (v) { 0 => "zero", _ => "many" }`, and as a statement `match (o) { Some(v) => { use(v); } null => {} }`. - A one-line struct literal is `Point { x: 1, y: 2 }`; an array literal is `[1, 2, 3]`; argument and parameter lists are `f(a, b)` and `fn f(a: i32, b: i32)`. One-line lists have no trailing comma. - **An if/else chain is laid out as a whole:** if one of its blocks is broken, or the author started an `else` on a new line, every block of the chain is broken. ```ceesafe if (c) { a(); } else { b(); } // fits: kept on one line if (c) { // otherwise: every block broken a(); } else { b(); } ``` ### 3.3 Broken lists Statements, members, enum variants and match arms of a broken body are one per line. A list that is broken (by its author, or by §3.4) is laid out as a block: - the opening delimiter ends its line; - the elements are indented one level; each element that the author started on a new line starts a new line, and the others follow the element before them after `, `; a list broken by §3.4 has one element per line; - the closing delimiter starts its own line at the opening line's indentation; - the last element is followed by a comma, where the grammar allows one (grammar.md §2: parameters, arguments, struct field initialisers, array elements, attribute arguments, enum variants, and the arms of a match expression, except an arm whose value is a block). The commas between the arms of a match statement are left as written. Type parameters, generic arguments, closure parameters, function-type parameters and interface method parameters are never broken. ```ceesafe let bodies = [ Body { x: 0.0, y: 0.0, z: 0.0, vx: 0.0, vy: 0.0, vz: 0.0, mass: SOLAR_MASS }, Body { x: 4.84143144246472090e+00, y: -1.16032004402742839e+00, z: -1.03622044471123109e-01, vx: 1.66007664274403694e-03 * DAYS_PER_YEAR, vy: 7.69901118419740425e-03 * DAYS_PER_YEAR, }, ]; ``` ### 3.4 Breaking a line that is too long When a line does not fit, the formatter breaks one construct that starts on it and lays the file out again, until the line fits or nothing on it is left to break. It takes the first kind in this order, and of that kind the outermost: 1. a function's expression body (`=> e`): the expression moves to the next line, indented one level; 2. a list or a one-line block or body (§3.3); 3. the value of a match arm or a closure (`=> e`), moved to the next line; 4. an initialiser or an assigned value (`= e`), moved to the next line; 5. an `if` expression whose then-value is not a block: before each `else` of its chain at once, every `else` one level in (an `else if` chain is indented once, not once per `if`); 6. a binary expression: after its operator, with the right operand indented one level; 7. a chain of member calls: before a `.`, indented one level. ```ceesafe @header( "kernel/blk.h", "kernel/errno.h", "kernel/gpt.h", "kernel/kmalloc.h", "kernel/page.h", "kernel/part.h", "kernel/ramblk.h", ) let which = if (kind == VNODE_DIR) OPS_DIR else if (kind == VNODE_LNK) OPS_LNK else OPS_FILE; ``` The author's breaks of the same kinds (§3.1) are laid out by the same rules. ## 4. Spaces - One space around binary operators (`a + b`, `x == y`, `a && b`), around `=` and the compound assignments, around `->` and `=>`, after `,`, `;` (inside a one-line form) and `:` (`x: i32`, `x: 1`). In patterns (ADR 0085), one space around the `|` of an or-pattern (`' ' | '\t'`) and the `@` of `name @ P` or `rest @ ..`, none around `..=` (`'a'..='z'`), and a guard is written `P if (g) =>`. A slice pattern is written like an array literal, its `..` an item: `[first, .., last]`, `['"', inner @ .., '"']`. - No space around `..` (`0..n`, `a[i..j]`, `a[..]`), `.`, inside parentheses, brackets and generic arguments (`f(a)`, `[1, 2]`, `Vector`), before `(` in a call or declaration, after a unary operator (`-x`, `!b`, `*p`), before `?` and `!` (`f()?`, `r!`), or before `,` and `;`. - Keywords: one space after `if`, `while`, `for`, `match`, `return` (with a value), `fn` (in a declaration), `let`, `var`, `borrow`, `mut`, `move`, `as` and the visibility keywords; `if (c) {`, `} else {`, `} else if (d) {`. - One space before an opening `{` that follows code on its line. - Attributes are followed by one space or a line break, as written: `@context(atomic) fn f()`. ## 5. Comments - Comments keep their text and order. A comment on a line of its own is indented like the code that follows it (or, at the end of a block, like the code before the `}`). - A comment after code on the same line (a trailing comment) follows it after one space. Trailing comments on consecutive lines are aligned: each starts one space after the longest code among those lines. A line without a trailing comment, or a blank line, ends the group. - Doc comments (`///`) stay directly before their item; a blank line between a doc comment and its item is removed, since it would detach the comment (grammar.md §1.2). - A block comment (`/* … */`) inside a line keeps one space on each side. ## 6. Guarantees and their tests - **Same tree:** for every file of the repository that parses (the corpus included), the formatted text parses without error to an AST equal to the original's, spans aside. - **Comments:** the comments of the formatted text are those of the original, in order. - **Same tokens:** apart from commas, the tokens are the original's; a raw literal is compared by its value, which its re-indentation keeps. - **Fixed point:** formatting the formatted text changes nothing. `tools/fmt_snapshots.py` checks the reviewed snapshots in `tests/formatting/` (`NAME.cee` formats to `NAME.formatted.cee`, which formats to itself). - **The repository's own code** (`lib/`, `programs/`, `benchmarks/`, `integrations/`, the README's examples) is kept formatted: `ceesafec fmt --check` runs over it in the test suite. The test corpus is not reformatted, since its annotations name lines and some files test unusual layout; it is the formatter's test input. # Tooling (M10) The commands of `ceesafec` beside `build`, `check` and `run`. The formatter has its own document (`formatting.md`). ## 1. Documentation generator (T2) ```sh ceesafec doc file.cee... # Markdown for the files' public items, on standard output ceesafec doc lib/core -o spec/api/core.md --title "lib/core" ceesafec doc --missing file.cee... # list the public items without a doc comment; exit 1 if there are any ``` The files are one package, as for `check` (a directory means every `.cee` file under it, in ordinal path order). `lib/core` is documented only when it is named. A file with a syntax error stops the command: its diagnostics are printed and nothing is written. The package does not have to type-check: documentation is made from the syntax, so it can be written for code that does not build yet. ### 1.1 What is documented - Every item declared `public`: a function, a constant, a struct, an enum, an interface, and a function in an `extern "C"` block. - Inside a public struct: its `public` fields, its `public` methods, and whether it has a `drop` block. Inside a public enum: every variant. Inside a public interface: every associated type, constant and method (ADR 0086 §2.4, §2.5), in source order. - An `impl I for T` block, listed under `T` as "Implements `I`" when `T` is a public struct or enum of the package. Nothing else is listed. Items in a `namespace` are listed under their simple names, as the language names them today (type-system.md §1). ### 1.2 The page The output is deterministic: the same files give the same bytes. - A title (`--title`, or the first file's name without its extension) as a level-1 heading, then a list of the documented items, each a link to its section. - One level-2 section per item, in the order of the files as given and of the items in each file. Its heading is the item's kind and name (`struct Volatile`, `fn sortBy`). - The item's **signature** in a `ceesafe` code block: the item's attributes and its declaration up to its body (the `{ … }` or `=> …` of a function, the members of a struct), as `ceesafec fmt` prints them (formatting.md). A struct's block lists its public fields; an enum's, its variants; an interface's, its associated types, constants and methods. - The item's **doc comment** (grammar.md §1.2): its lines as written, which are Markdown. An item without one has only its signature. - A struct's public methods follow as level-3 sections (`StructName.method`), each with its signature and doc comment; then "Implements `I`" for each `impl`. ### 1.3 Missing documentation `--missing` writes nothing but a line per public item without a doc comment, `path:line: kind name`, and exits 1 if there is any. It is a lint for a package's public surface; nothing requires it to pass. ### 1.4 In the repository `spec/api/core.md` is generated from `lib/core` with `ceesafec doc lib/core -o spec/api/core.md --title "lib/core"`, run on the embedded `lib/core` (its sources under CeeSafe's names). `tools/spec_check.py` fails if it is out of date. ## 2. Language server (T3) ```sh ceesafec lsp # a Language Server Protocol server on standard input and output ``` The server speaks LSP 3.17 over JSON-RPC 2.0, with `Content-Length` framing, on standard input and output. It is part of `ceesafec` and has no dependency beyond CeeSafe's own libraries. The editor starts it; `editors/vscode` is a VS Code extension that does (§2.4). ### 2.1 What a document is checked with A document that is a source of a package (a `ceesafe.toml` above it whose sources include it) is checked with its package, as `ceesafec check` in the package would check it (§4.4): the editor's text for the document, the package's other files from disk, and the dependencies its `ceesafe.lock` names, never fetching; the manifest's profile and `float` apply. Any other document, or one whose package does not resolve, is checked as a package of its own, with `lib/core`, as `ceesafec check file.cee` would check it, using the text the editor holds. Its profile is hosted, unless its first line is `// profile: freestanding` (the corpus convention, `testing.md` §3). Checking reads the files the document's `embed` and `embedText` calls name (`type-system.md` §10.1, ADR 0087 §2.8), under the same rules as the command line: inside the package's directory and its `[embed] roots`, or, for a document checked alone, below the document's own directory. A document whose URI is not a file on disk embeds nothing. An embedded file is read again at each check, so a change to it shows at the document's next change or save. ### 2.2 Capabilities | Request or notification | What the server does | |---|---| | `initialize` | Answers with the capabilities below: full-text synchronisation, hover, definition, document symbols, formatting, quick fixes | | `textDocument/didOpen`, `didChange`, `didClose` | Keeps the text; after an open or change, checks the document (§2.1) and publishes its diagnostics. A close clears them | | `textDocument/publishDiagnostics` (sent) | Every diagnostic of the check: its range, severity, code (`N0205`) and message, with each note as related information | | `textDocument/hover` | On a name or an expression: its type, as CeeSafe writes it. On a local or parameter, `let x: T` or `var x: T`; on a function or method, its signature; on a field, `name: T` | | `textDocument/definition` | On a local, parameter, function, method, field, struct, enum or interface name: the location of its declaration, if it is in the document | | `textDocument/documentSymbol` | The document's items and their members, as a tree: functions, structs (fields and methods), enums (variants), interfaces, constants | | `textDocument/formatting` | The document formatted by `ceesafec fmt` (`formatting.md`), as one edit; nothing if it has a syntax error | | `textDocument/codeAction` | The fixes (§5.2) of the diagnostics in the requested range, as `quickfix` actions (§5.5) | | `shutdown`, `exit` | Stops | Anything else is answered with the JSON-RPC error `MethodNotFound` (requests) or ignored (notifications). Positions are LSP positions: zero-based lines, and columns in UTF-16 code units, which the server converts to and from the documents' UTF-8 bytes. Hover and definition use the typed tree of the last check; if the document does not parse, they answer null and only the diagnostics are shown. ### 2.3 Errors The server never stops on a bad document. A request that fails inside the compiler is answered with the JSON-RPC error `InternalError` and the exception's message, and the server goes on. A message that is not valid JSON-RPC is answered with `ParseError` when it has an id. ### 2.4 The VS Code extension `editors/vscode` holds: - a TextMate grammar for `.cee` files (keywords, literals, comments, attributes), whose keyword list is the compiler's (`editors/vscode/test/keywords.test.mjs` compares them); - a language configuration (comments, brackets, auto-closing pairs); - the client, which starts `ceesafec lsp` (the `ceesafe.server.path` setting, default `ceesafec` on the `PATH`) through the `vscode-languageclient` library, bundled with it into `dist/extension.cjs`. It is built with [Vite+](https://viteplus.dev/) (`vp`), not with `ceesafec`: ```sh cd editors/vscode vp install # the dependencies, pinned by pnpm-lock.yaml vp test # the grammar and the server tests below vp run check # lint and formatting vp run package # bundle the client (vp pack) and make ceesafe-language-0.1.0.vsix (vsce) ``` Its tests: `test/grammar.test.mjs` tokenizes CeeSafe lines with `vscode-textmate`, as VS Code does, and checks the scope each token gets; `test/server.test.mjs` drives a built `ceesafec lsp` through `vscode-jsonrpc`, the library under VS Code's client (diagnostics, hover, definition, an unknown method, shutdown and exit). ## 3. Debugging (T4) A CeeSafe program is debugged as the C it compiles to, with the C mapped back to CeeSafe lines. No debugger needs to know about CeeSafe. ### 3.1 A debug build `ceesafec build -g file.cee -o program` writes `#line` directives into the C (`c-backend.md`, source maps) and compiles it with `-g -O0`. A debugger then shows CeeSafe files and lines: breakpoints set by CeeSafe line stop there, and stepping goes from one CeeSafe statement to the next. A line can take more than one step, since one CeeSafe statement can be several C statements (a checked addition, a temporary). A debug build also turns debug assertions on (ADR 0087 §2.5): `debugAssert(…)` is evaluated, and a false one panics. `--no-debug-assertions` turns them off in a `-g` build, `--debug-assertions` on in any `build` or native `run`. `ceesafec test` and the interpreter (`run --interp`) evaluate them unless given `--no-debug-assertions`. The C names the choice once, `#define CEE_DEBUG_ASSERTIONS 1` (or `0`), in a program that has a `debugAssert`; each assertion is in an `#if CEE_DEBUG_ASSERTIONS` block. | | lldb | gdb | |---|---|---| | Stop at a CeeSafe line | `breakpoint set -f demo.cee -l 12` (`b demo.cee:12`) | `break demo.cee:12` | | Stop when a check fails, before the program aborts | `b ceesafe_panic` | `break ceesafe_panic` | | Where am I | `bt` | `bt` | ### 3.2 Names The C names follow `c-backend.md` §2.1, so a CeeSafe name can be found in the debugger: | CeeSafe | C | |---|---| | function `length` | `ceeF_length` | | method `Counter.bump` | `ceeM_7Counter_bump` (the type's name, prefixed by its length) | | local or parameter `sum` | `v_sum_1` (a number makes each local's name unique in its function) | | a temporary the compiler made | `t_3` | | field `x` | `f_x` | | struct `Point` | `ceeT_Point` | ### 3.3 CeeSafe names in the debugger `editors/debug/ceesafe_lldb.py` and `editors/debug/ceesafe_gdb.py` add three commands, which print with CeeSafe names: - `ceesafe-locals`: the current frame's locals and parameters, `name = value`, without the compiler's temporaries; struct values show their fields by their CeeSafe names; - `ceesafe-bt`: the backtrace, one frame a line, `CeeSafe name at file:line` (`Counter.bump at demo.cee:3`); an async fn's resume function shows as the fn (`async closure` for a closure); - `ceesafe-tasks`: the stopped thread's async tasks (type-system.md §40), one a line, from the runtime's `ceesafe_async_tasks()` (c-backend.md §6): `gather at demo.cee:18 (waiting for its scope)`, a scope's children indented under it. Load them with `command script import editors/debug/ceesafe_lldb.py` (lldb) or `source editors/debug/ceesafe_gdb.py` (gdb). Split C (`--split`, c-backend.md §2.4) renames the program's functions to `ceeF__…`: set `CEESAFE_PROGRAM` to the program's name before starting the debugger, and the helpers read them as before. ### 3.4 What is checked `tools/debugger_check.py` builds a program with `-g` and runs it under lldb (macOS) or gdb (Linux), in batch mode. A breakpoint at a CeeSafe line must stop at that line, `ceesafe-locals` must show the locals by their CeeSafe names, and a breakpoint on `ceesafe_panic` must give a backtrace whose frames are CeeSafe functions at CeeSafe lines. In an async program stopped inside a child task, `ceesafe-tasks` must list the awaiting task, the running child and its waiting sibling at the lines they wait at. On a machine with neither debugger, the test does nothing, and the run log says which debugger ran. ## 4. Packages (T6, ADR 0013) ### 4.1 The manifest A package is a directory with `ceesafe.toml`. Its sources are the `.cee` files under the directories `sources` names (default `src`), in ordinal path order. The manifest is a subset of TOML: `[table]` headers, `key = value` lines whose value is a string, a boolean, an array of strings or an inline table of strings, and `#` comments. ```toml [package] name = "app" # required: an identifier, not a keyword and not "core" version = "0.1.0" # optional; required to publish (§4.8), MAJOR.MINOR.PATCH kind = "executable" # "executable" (default), "library", or "object" (profiles.md §4.3) profile = "hosted" # or "freestanding" float = true # false is --no-float (type-system.md §35.9) kernel = "cosmoos" # with profile = "freestanding": include lib/kernel, as --kernel cosmoos (ADR 0025) trusted = true # its types may be declared @threadSafe (ADR 0025 §3.2); listed for reviewers sources = ["src"] c = ["src/shim.c"] # C files compiled and linked with an executable (type-system.md §28.6) tests = ["tests"] # test directories (§10): default "tests"; their files are sources for check and test only ceesafec = ">=0.20.0" # the compiler this package needs (ADR 0076): a requirement, as a dependency's (§4.7) links = ["ssl", "crypto"] # system libraries to link (ADR 0047): -lssl -lcrypto; ssl.lib crypto.lib with cl lib-dirs = ["$OPENSSL_DIR/lib"] # where they are; absolute, or relative to the package include-dirs = ["$OPENSSL_DIR/include"] # headers for this package's own C files only [target.windows] # also [target.macos] and [target.linux]: added after [package]'s on that system links = ["libssl", "libcrypto"] [embed] # where `embed` may read besides the package's directory (ADR 0087 §2.8) roots = ["../build/codegen"] [dependencies] geometry = { path = "../geometry" } lz4 = { git = "https://example.org/lz4-cee.git", tag = "v1.2.0" } # or rev = "", or branch = "main" shapes = "2.1" # from the registry (§4.6): a version requirement json = { version = "0.9", index = "https://example.org/my-index.git" } # from another index ``` **The compiler requirement** (ADR 0076). When a manifest is read (the root's, and each dependency's, by every command that reads it), a ceesafec whose version does not satisfy `ceesafec` reports N0321 at the value, before anything is checked: `this package needs ceesafec >=0.20.0; this is ceesafec 0.19.1`. A development version (`0.20.0-dev`) counts as its MAJOR.MINOR.PATCH. A compiler older than the field reports it as an unknown field at the same line. A dependency's key is its package's `name`: a dependency is known by the name its own manifest gives it. A dependency of kind `executable` is N0321: only a library's items are used. Errors in the manifest are N0320 (its syntax) and N0321 (a missing, unknown or invalid field), with the line. **System libraries** (ADR 0047): - `links` names libraries: letters, digits, `_`, `.`, `+` and `-`, not starting with `-`. `lib-dirs` and `include-dirs` are directories, absolute or relative to the package, not starting with `-`. - A directory may start with `$NAME` or `${NAME}`: the environment variable's value, read when the manifest is. An unset one is N0321. Nothing runs (ADR 0013), and the hash (§4.3) is of the manifest's text, not the value. - A `[target.OS]` table (`linux`, `macos` or `windows`) holds the same three fields. The host's is added after `[package]`'s; every one is checked. - The libraries and library directories of every package in the build reach the executable's link, a library's included. Packages are taken dependents first (by their longest path from the root). A library named twice keeps its last place, so it follows what uses it; a directory keeps its first. - A package with `include-dirs` has its own C files compiled apart, with `-c` and those directories, and the objects join the build. No other C file sees its directories. - `--emit c` writes the link line as `ceesafe_link.txt` beside the C, and `--emit obj` and `--emit lib` as `.link.txt`, when the manifests name any: one argument per line, each separately compiled C file after its include directories, then the library directories and the libraries (`cl`'s spelling when `--cc` is `cl`). **Embed roots** (ADR 0087 §2.8, `type-system.md` §10.1). A package's files may embed files inside the package's directory (the one holding `ceesafe.toml`), and inside each directory `[embed] roots` lists: - A root is relative to the package's directory, with `/` separators, and nothing in it is expanded: an absolute path, a drive, a `\`, `:`, `$` or `%`, or a leading `~` is N0321 at the array, and so is any other field in `[embed]`. It may use `..`. Roots are resolved when a file is embedded, not when the manifest is read: one that does not exist (yet: a build's generated directory) reaches nothing, and an `embed` below it is N0362 until it does. - A root is its real path: every symbolic link in it is followed, so a root that is a symbolic link stands for the directory it leads to. - **Only the root package and the path dependencies it reaches through path dependencies alone may have roots outside their directory.** They are code the user works on, whose manifests the user wrote. A git or registry dependency, and every package reached through one (its path dependencies, at any depth), embeds only inside that checkout: a root counts only when its real path is inside it, so a checkout reaches nothing outside itself, through `..`, through a symbolic link it holds, or through a path dependency it names. So does a package whose directory lies in the package cache (`$CEESAFE_HOME`, §4.3), even as the root package: the language server going to a definition in a checkout, or `check` run there, gives it no more than the build that fetched it, and the path dependencies such a root reaches are kept inside its directory, wherever they lie. From the package being built, every dependency is resolved, and then its dependencies, recursively: - `path` is a directory relative to the manifest that names it, holding the dependency's `ceesafe.toml`; - `git` is a repository; `rev` names a commit, `tag` and `branch` are resolved to one (§4.3). The commit is checked out into the cache, `$CEESAFE_HOME/git/checkouts//` (default `~/.ceesafe`), and the dependency is the package at the checkout's root. A package name reached by two different sources (two paths, two commits, a path and a commit) is N0326, naming both dependents. A package that depends on itself, directly or not, is N0327. A missing directory or manifest is N0322. Nothing runs during resolution except `git` (ADR 0013 D5). ### 4.3 The lock file `ceesafe.lock`, beside `ceesafe.toml`, records the resolution so that every build uses the same code: ```toml # Written by ceesafec fetch. Commit it with ceesafe.toml. [[package]] name = "lz4" source = "git+https://example.org/lz4-cee.git" commit = "3f2a9c0d…" # the full commit id hash = "sha256:…" # of the checkout's manifest and sources [[package]] name = "geometry" source = "path+../geometry" ``` - `ceesafec fetch` resolves every dependency, fetching git repositories and resolving tags and branches (`git ls-remote`, `git fetch`), and writes the lock. A `rev` is used as written; a `tag` or `branch` is the commit it names at that moment, until the next `ceesafec fetch`. - Every other command reads the lock. It fails with N0324 if the manifest has a dependency the lock lacks or records differently (run `ceesafec fetch`), and with N0325 if a git checkout's manifest and sources no longer have the lock's hash. A missing checkout of a locked commit is fetched, unless `--offline` is given (N0323 if it cannot be). - A path dependency has no hash: it is code being worked on beside the package. - The hash is SHA-256 over, for the manifest and each source file in ordinal order, the path relative to the package root, a zero byte, the file's bytes and a zero byte. ### 4.4 Commands - `ceesafec new ` makes `/ceesafe.toml` and `/src/main.cee` (a `main` that prints a line); `--lib` makes a library with `src/lib.cee` instead. It also writes `.gitignore` (`out/`) and the agent instructions of §7: `AGENTS.md` by default, others with `--agent`, none with `--no-agent`. - `ceesafec agents [--agent …]` writes or refreshes the agent instructions of the package in the current directory or above it (§7.3). - `ceesafec fetch [--offline] [--update [name]]` resolves and writes `ceesafe.lock` (§4.7). - `ceesafec publish` checks that the package can be published and prints its index entry (§4.8). - `ceesafec test` runs the package's `@test` functions (§10). - `ceesafec build`, `check`, `run`, `doc` and `fmt` without file arguments use the package of the nearest `ceesafe.toml` in the current directory or above it. `build` writes `out/` (an executable, or `out/.o` for `object`), the C sources with `--emit c`, or `out/.a` with `--emit lib` (§8.3); the manifest's profile, `float` and C files apply, and command-line options add to them. `fmt` formats the package's own sources, not its dependencies'. - `ceesafec check` in a package (ADR 0082 §2.7, §8.5) is one check of that package, its tests included, and of every package it depends on. A dependency whose **interface record** (§4.4.1) is valid is parsed and its declarations resolved, but its bodies are checked again only where a dependent compiles or reads them (generic code, async functions, functions with a closure or function parameter or `@effectsOf`, `@constant` functions); every other body's effects come from the record. Any other dependency is checked in full in the same check: one without a record, one whose record is opaque, and one with errors, which is never taken from a record. - **Never slower, never weaker than `--whole-program`.** `check --whole-program` checks every package in full as one program (ADR 0013 D4) and reads no record. Plain `check` does no more work than it: where there is no usable cache (no owners and modes to check, Windows, a `$CEESAFE_HOME` that is not writable) or no record is valid (a cold cache), it is that same check. It rejects whatever `--whole-program` rejects: the oracle `tools/package_tests.py` §23 compares the two for every package of its fixtures, cold, warm and without a cache. - **The same report whatever the cache holds.** The diagnostics are the package's own, as `--whole-program` gives them, and, for each package of its closure with errors, at any depth, one N0364 at the line of the package's manifest that leads to it (the first such dependency), with a note at its first error and the command that shows them all, `ceesafec check --package `. A dependency's own diagnostics, its warnings too, are not repeated. As in any check, errors before the effect checks stop what follows: a dependency's type error hides another's ownership error, as it does in `--whole-program`. - **The records are made by the checks.** Once the report is out, each dependency the check checked in full whose closure has no error gets its record, from what the check found: the effect checker records every such package's functions, so the next check finds them. Before the check, a few missing records low in the graph are made first (each such dependency checked alone, without its tests or entry function), but only while that costs at most a twentieth of the whole check and lets the cache's other records be found: after an edit in a package that leaves its interface (§4.4.1), such as a comment or a body, the records of the packages above it hold again. - `--package ` checks another package of the lock as from its directory; `--record` makes the package's record in that way and prints it. `check` runs in one process and takes no `--jobs`. - `ceesafec check --all` checks every package of the lock together, each one's tests included, and prints one report (§5.1): its diagnostics sorted by package, file and position, in both forms, then, as text, a line per package with errors or warnings, or as JSON a `packages` summary of the diagnostics by their package. Where errors stop the check before the effect checks, it checks again the packages none of whose closure has errors, so that each package's own errors are found as its own check would find them; a package's diagnostics are those of the last round that checked it. No N0364 is added: the failing package's own diagnostics are in the report. It exits with 1 if any package has an error. Its cost is about one `--whole-program` check (ADR 0082 §8.5). - `build`, `run`, `test` and `fix` check every package as one program and read no record: a record is a cache of a result `check` proved, and only `check` uses it (ADR 0082 §2.7, §8). - The language server (§2.1) checks a document that belongs to a package (a `ceesafe.toml` above it, whose sources include it) with that package: the open document's text, the package's other files from disk, and its locked dependencies. Without a lock, or outside a package, the document is checked alone as before. ### 4.4.1 Embedded files and what is cached `embed` and `embedText` (`type-system.md` §10.1) make a program depend on files that are not sources. Every command that checks a package reads them afresh, so a file changed after `check` is what `build` compiles, and the language server sees a change at its next check. Within one compilation each file is read once, by its real path. Neither the lock's hash (§4.3), which covers a package's manifest and sources, nor `fetch` looks at embedded files. **Interface records** (ADR 0082 §2.7, §8.5) are what `check` keeps of a package between runs, in `$CEESAFE_HOME/cache/records/-.rec`: - **The key** is a SHA-256 of the record format, `ceesafec --version`'s banner, the identity of the `ceesafec` that runs (the SHA-256 of its executable, read once per process: builds of one version share a banner), the profile (hosted or freestanding, `--kernel`, `--no-float`), the package's name, manifest and sources (each with its path in the package), as read for the check that uses or makes the record, and its direct dependencies' interface hashes. Any change to one of them is another key: a miss, and a check in full. - **The record** says whether the package checked and the stage of its first error; and for each function whose body dependents skip, where it is declared, its inferred effects (`type-system.md` §32.1) and the explanation of each (the steps an N0295 or N0360 names through it, and its rank). - **The interface hash** covers the package's tokens, with those skipped bodies left out, what the record says, and its dependencies' interface hashes. It holds no position: a comment, a blank line, or a declaration moved leaves it, and so every dependent's key, as it was, while the diagnostics, made from the sources as they are, keep their positions. Nothing a dependent's check can say of the package holds one either: an explanation names a closure by its place in its function (`{closure environment 2 in Lexer.scan}`), not by its line. A change inside a skipped body that changes neither the effects nor whether the package checks leaves the hash as it was too. - **Opaque records.** A package that embeds a file, or whose skipped bodies make a `dyn` table of an interface with an async method (whose frames, and recursion through `await`, belong to the whole program), has an opaque record: its dependents check its bodies. Whether it fails is said the same way as for any other package: one N0364. - **Trust.** A record is a cache of a result this compiler proved, never a promise from outside, and any doubt is a miss. `$CEESAFE_HOME/cache` and `cache/records` are made with mode 0700, then opened once, each never through a symbolic link (`O_NOFOLLOW`), and used only if both belong to the user running `ceesafec` and no one else may write to them; `$CEESAFE_HOME` itself and the directories above it are trusted as they are named. A record is opened from the descriptor of `records`, never through a symbolic link, and read only if it belongs to that user and no one else may write to it, from the descriptor whose owner and mode were checked; one that cannot be read, is cut short (its last field is the SHA-256 of the rest), or names another key, banner, profile or package is a miss. A record is written to a new name created for it alone (mode 0600) in that directory and renamed there, so agents that check at once share the cache and none reads half a record. The compiler is identified by its own executable as the system knows it (`/proc/self/exe` on Linux, the kernel's path of the process on macOS), never by the name it was started with. Owners and modes are read where `ceesafec` knows `fstat`'s layout, macOS and Linux on arm64 and x86-64; elsewhere, Windows included, no record is read or written, and `check` is the whole program's check. ### 4.5 Errors | Code | Meaning | |---|---| | N0320 | `ceesafe.toml` or `ceesafe.lock` is not valid in the TOML subset of §4.1 | | N0321 | A manifest field is missing, unknown, or has an invalid value | | N0322 | A path dependency's directory or manifest is missing, or a dependency is named differently from its key | | N0323 | A git dependency cannot be fetched or resolved, or is needed with `--offline` and absent | | N0324 | The lock file is missing or does not match the manifest: run `ceesafec fetch` | | N0325 | A git checkout's manifest and sources do not match the lock's hash | | N0326 | One package name reached through two different sources | | N0327 | A dependency cycle | | N0328 | An invalid version, version requirement, or index entry | | N0329 | No versions satisfy the requirements: the chain of requirements that conflict | | N0335 | `ceesafec publish`: the package cannot be published as it is | ### 4.6 The registry (M13 G6, ADR 0017) A **registry dependency** names a package and a **version requirement**. The package is found in an **index**: a git repository, by default `https://github.com/CeeSafe/CeeSafe-index`, or `$CEESAFE_INDEX`, or the dependency's `index`. - **Versions** are `MAJOR.MINOR.PATCH`, three decimal numbers with no leading zeros, compared numerically. There are no pre-release or build suffixes in v1. - **Requirements** are one or more comparators separated by commas, all of which must hold: - `"1.4"`, `"^1.4"` and `"^1.4.2"` are caret requirements: at least that version, below the next major version, or below the next minor for `0.x` (`"0.3"` is `>=0.3.0, <0.4.0`) and the next patch for `0.0.x`. - `"=1.4.2"` means exactly that version. - `">=1.4"`, `">1.4"`, `"<2"` and `"<=1.9.3"` compare. A missing minor or patch is 0. - `"*"` means any version. - **The index** holds `packages/.toml`, with one `[[version]]` table per published version, in publishing order: ```toml [[version]] version = "1.4.0" git = "https://github.com/someone/geometry.git" commit = "3f2a9c0d…" # a full commit id hash = "sha256:…" # of the commit's manifest and sources (§4.3) dependencies = { shapes = "2.1" } # its registry requirements, as its ceesafe.toml writes them yanked = false # optional; a yanked version is never chosen for a new resolution ``` Entries are only appended. A published package's dependencies are all registry dependencies, and those without an `index` come from the index that published the package. ### 4.7 Version solving `ceesafec fetch` resolves path and git dependencies as before (§4.2), gathering the registry requirements of the root and of every path and git package. It then chooses one version of each registry package, transitively, from the index's entries: - The **newest** version that satisfies every requirement on it is chosen, and so on through its own dependencies. On a conflict, the solver backtracks. - A version the lock already records is preferred while it satisfies the requirements, so `ceesafec fetch` changes nothing that needs no change. `--update` prefers the newest for every package, and `--update ` for that one. - **One version per package name** (OD-26). A name reached through a path or git dependency and through the registry is N0326. - If no choice satisfies every requirement, N0329 shows the requirements on the package that could not be chosen, and where each came from: `no version of 'shapes' satisfies ^2.1 (from geometry 1.4.0) and ^1.0 (from plot 0.3.0)`. - Each chosen version's commit is checked out as for a git dependency (§4.2). Its hash must be the index's (N0325), and its manifest's name and version must be the entry's (N0328). - The lock records each as `source = "registry+"` with `version`, `repository` (the git URL), `commit` and `hash`. Other commands use the lock: an entry whose version no longer satisfies the manifest's requirement is N0324. ### 4.8 Publishing `ceesafec publish` (no network but `git`, and nothing written) checks the package in the current directory: - it is a library with a valid `version`; - every dependency is a registry dependency; - it type-checks with its dependencies; - its git working tree is clean, and its commit is on a remote branch. It then prints the index entry for the version: the remote's URL, the commit, the hash and the requirements. To publish, add that entry to `packages/.toml` in the index by pull request. The index's continuous integration runs `ceesafec index-check`, which checks every new entry: - the commit exists; - its manifest has the entry's name and version; - its hash and dependencies are the entry's; - the version is newer than every earlier entry. A new package name is reviewed by the index's maintainers. Later versions from the same repository merge after the checks pass. ## 5. Diagnostics for programs (M12 A1) People read diagnostics as text (`diagnostics.md`, "Format"). Programs, AI agents and editors among them, read the same diagnostics as JSON, and can apply the fixes the compiler is sure of. ### 5.1 `--diagnostics-format=json` `check`, `build`, `run` and `parse` accept `--diagnostics-format=json` (or `=text`, the default). With `json`, diagnostics are not written to standard error; instead one JSON document is written to standard output, as one line, after the command's own work. `build` still writes its outputs, and `run` still runs the program, if there are no errors; the program's own output then precedes the document. The exit status is unchanged. `"severity"` is `"error"` or `"warning"`, and `"warnings"` counts the warnings (`diagnostics.md`, "Warnings"): a successful `build` or `run` writes the document only when there are warnings, and a warning never changes the exit status unless `--deny-warnings` is given. ```json { "version": 1, "errors": 1, "warnings": 0, "diagnostics": [ { "code": "N0249", "severity": "error", "file": "src/main.cee", "package": "app", "start": { "line": 7, "column": 10 }, "end": { "line": 7, "column": 11 }, "message": "this parameter is 'borrow mut': write 'mut' before the argument to let the call modify it", "summary": "`mut` argument marker missing or misplaced", "notes": [ { "file": "src/main.cee", "start": { … }, "end": { … }, "message": "…" } ], "fixes": [ { "title": "write 'mut' before the argument", "edits": [ { "file": "src/main.cee", "start": { "line": 7, "column": 10 }, "end": { "line": 7, "column": 10 }, "newText": "mut " } ] } ] } ] } ``` - `version` is the format's version: fields may be added within a version, never removed or changed. - Positions are 1-based lines and columns, columns in UTF-16 code units, as in the text form. `end` is exclusive. - `file` is the path as the text form prints it: a path given on the command line, or a file of the root package, as given (relative to the current directory when it is under it); a dependency's file as its path from the current directory, `../alpha/src/util.cee`, so that it can be opened (ADR 0082 §2.1). - `package` (added in version 1 by ADR 0082 §2.1) is the name of the package the file belongs to, in package mode (`build`, `check` or `run` without file arguments): the root package's or a dependency's. It is left out for a build of files, and for lib/core's and lib/std's files. - `summary` is the code's "Meaning" in `diagnostics.md`, which `ceesafec` embeds. - Diagnostics are in the order the text form prints them. Keys are written in the order shown. - A package of the closure with errors (N0364, §4.4) is reported in the manifest of the package checked: `file` is `ceesafe.toml` from the current directory, and `package` that package's name; its note is at the failing package's first error. - `check --all` (§4.4; added in version 1 by ADR 0082 §2.7) writes one document for every package of the lock. Its diagnostics are sorted by `package`, then `file`, then position (a diagnostic's order of discovery breaks ties), the order the text form prints them in too, and `errors` and `warnings` count them all. Each diagnostic is there once, and none is an N0364. After `diagnostics` comes `packages`: one entry per package, by name, with its `name` and the number of `errors` and `warnings` among the diagnostics whose `package` it is: ```json "packages": [ { "name": "app", "errors": 1, "warnings": 0 }, { "name": "mid", "errors": 0, "warnings": 0 } ] ``` ### 5.2 Fixes A fix is a list of edits that the compiler is sure resolves the diagnostic. It is offered only where the intent is unambiguous; a diagnostic with several plausible repairs offers none. Applying a fix removes that diagnostic and introduces none at the edited place. The kinds: | Code | When | Edit | |---|---|---| | N0249 | an argument for a `borrow mut` parameter lacks `mut`, and is a place that can be borrowed mutably | insert `mut ` before the argument; if the place is a `let` local, also `let` → `var` at its declaration (one fix) | | N0249 | an argument for a shared parameter has `mut` | delete the `mut ` | | N0211 | assignment to a `let` local declared in the function | `let` → `var` at its declaration | | N0248 | `borrow mut` (or a `borrow mut this` call) of a `let` local declared in the function | `let` → `var` at its declaration | | N0208 | `e as T` between integer types that can lose information | `truncate(e)` (wrap-around, as the message says) | | N0201 | an extern declaration whose C name a dependency's `public` extern declaration has, identical to it in CeeSafe (type-system.md §28.2; ADR 0081 §2.2) | remove the declaration, with its doc comment and attributes; its call sites are left as they are | | N0100 | `expected ';'`, with no earlier error in the file, after a declaration, assignment, `return`, or a call statement (not after an expression that cannot be a statement, N0225) | insert `;` after the previous token | ### 5.3 `ceesafec fix` `ceesafec fix [FILES]` (or the package, without files) applies every fix, writes the files, checks again, and repeats until no fix applies or after 8 rounds. It then prints the remaining diagnostics, as text or with `--diagnostics-format=json`, and exits 1 if errors remain. With `--dry-run` it changes no file and prints the edits of the first round. Edits to one file never overlap: when two fixes would, the later one waits for the next round. ### 5.4 `ceesafec explain` `ceesafec explain N0249` prints the code's row of `diagnostics.md` (its meaning and example) and the paragraphs of `diagnostics.md` that mention it. An unknown code is exit status 2. ### 5.5 In the language server `textDocument/codeAction` returns the fixes of the diagnostics that touch the requested range as `quickfix` actions, each with its diagnostic and a `WorkspaceEdit` for the document (§2.2). A fix that would edit another file is not offered. ## 6. The MCP server (M12 A2) `ceesafec mcp` serves the [Model Context Protocol](https://modelcontextprotocol.io) on standard input and output, so an AI agent's host (Claude Code, an IDE, any MCP client) can use CeeSafe's compiler, interpreter and documentation as tools. It is part of `ceesafec`, with no other dependency, and serves the documents embedded in that `ceesafec`, so the answers always match the compiler that gives them. ```json { "mcpServers": { "ceesafe": { "command": "ceesafec", "args": ["mcp"] } } } ``` ### 6.1 Protocol - JSON-RPC 2.0, one message per line of UTF-8 on standard input and output; nothing else is written to standard output. The server stops at the end of its input. - `initialize` negotiates the protocol version: the client's, if it is one the server supports (`2025-06-18`, `2025-03-26`, `2024-11-05`), otherwise the newest of those. The server declares `tools`, `resources` and `prompts`, none of which change while it runs, and gives brief usage `instructions`. - `ping`, `tools/list`, `tools/call`, `resources/list`, `resources/read`, `prompts/list` and `prompts/get` are answered; other requests are `MethodNotFound` (-32601); notifications are ignored. - With version `2025-06-18`, each tool declares an `outputSchema` and its result carries `structuredContent`, with the same data as JSON text in `content`. With earlier versions, only `content`. - A tool call that the tool cannot carry out (a file it cannot read, a source that does not parse when it needs to) is a result with `isError: true` and the reason. Unknown tools and invalid arguments are `InvalidParams` (-32602). The server never stops on a bad request. ### 6.2 Sources Every tool that compiles takes its program as either - `files`: an array of `{ "path": "main.cee", "text": "…" }`, compiled as one package, with nothing read from disk; or - `paths`: an array of `.cee` file paths, read from disk relative to the server's working directory; plus `profile` (`hosted`, the default, or `freestanding`) and `noFloat` (default false). Paths in results are the paths given. `embed` and `embedText` (`type-system.md` §10.1) read files relative to the source's file on disk: a `paths` file embeds below its own directory, and a `files` source, which is no file, embeds nothing (N0362). The server reads nothing else for them. ### 6.3 Tools | Tool | Input (besides §6.2) | Result | |---|---|---| | `ceesafe_check` | | the diagnostics document of §5.1, with fixes | | `ceesafe_fix` | | `files`: each file's text with every fix applied, over rounds as `ceesafec fix` does (§5.3); `applied`: the number of fixes; `diagnostics`: what remains. Nothing is written to disk | | `ceesafe_format` | `text`, `path` (optional) | `text`: formatted (`formatting.md`); or an error with the syntax diagnostics | | `ceesafe_run` | `stdin` (text), `args`, `maxSteps` (default 10,000,000), `maxOutputBytes` (default 65,536) | `outcome` (`exited`, `panicked`, `fault`, `not-compiled`), `exitCode`, `stdout`, `stderr`, `truncated`, and the diagnostics if it did not compile | | `ceesafe_emit_c` | `explain`: any of `allocations`, `bounds-checks`, `overflow-checks` | `files`: the generated C (`main.c`, and `ceesafe_exports.h` when there is one; not the runtime); `explanations`: the `--explain-*` text | | `ceesafe_explain` | `code` | `code`, `meaning`, `example`, `paragraphs` of `diagnostics.md` (§5.4) | | `ceesafe_spec_search` | `query`, `limit` (default 10) | `results`: the sections whose heading or text contain every word of the query, best first: `document`, `heading`, `snippet`, `uri` | | `ceesafe_spec_section` | `document` (`type-system`, `grammar`, …), `section` (`31.2`, or heading text) | `text` of that section, with its subsections | | `ceesafe_symbol` | `path`, `line`, `column` (1-based) | the type or signature under the position, and its declaration's location, as hover and definition give them (§2.2) | | `ceesafe_api` | `name`: a type (`TextWriter`), a member (`TextWriter.decimal`) or a bare member name (`decimal`) | `results`: the entries of `spec/api/core.md`, `spec/api/std.md` and `guide/builtins.md` that document the name (`document`, `heading`, `text`): a type's whole section, a member's entry, or the built-in table rows naming it. With no entry, `suggestions`: up to eight names, the closest by edit distance, the type's real members for `Type.member`, and the entries whose text mentions the member's name | `ceesafe_run` runs the program in the interpreter (`run --interp`) **sandboxed**: - files, sockets and processes are refused (a fault that names the refusal); - `maxSteps` statements at most (a fault), and `maxOutputBytes` of each output kept (`truncated` says if more was written; the program goes on); - 256 MiB of interpreter memory at most: the heap then reports out of memory, which the program sees as `AllocError.OutOfMemory`; a local or static allocation beyond it is a fault; - standard input is `stdin`, the arguments are `args`, and the run has no access to the server's environment. A fault's exit code is 70 and a panic's 134, as for `run --interp`. ### 6.4 Resources Each embedded document is a resource `ceesafe://docs/` (for example `ceesafe://docs/docs/type-system.md`, `ceesafe://docs/site/guide/basics.md`), `text/markdown`, named by its first heading. `resources/read` returns its text. An unknown URI is the error -32002 (resource not found). ### 6.5 Prompts | Prompt | Arguments | What it asks | |---|---|---| | `write-ceesafe` | `task` | To write CeeSafe for the task: the agent guide (`guide/for-agents.md`), then the task, with the edit-check loop of `ceesafe_check` and `ceesafe_fix` | | `port-c-to-ceesafe` | `file`, `api` | To port a C file to CeeSafe as C6 and C9 did: imported C structs from the header, safe code over slices, `unsafe` only at the exported boundary, and the C's own tests, a differential test and a benchmark (`cosmoos/c9-report.md`) | ## 7. Agent instructions in packages (M12 A4) AI coding agents read a file of instructions at the root of a project. `ceesafec new` writes one, so that an agent working in a new CeeSafe package knows from the start what CeeSafe is, which commands check its work, and the mistakes to avoid. The convention follows Vite+'s (`vp create`). ### 7.1 The CeeSafe section `AGENTS.md` holds the **CeeSafe section**, between the lines `` and ``: what CeeSafe is and where its guide for agents is; the package's layout; the commands (`check`, `fix`, `run --interp`, `build`, `fmt`, `explain`, `--diagnostics-format=json`); how to use the MCP server; a checklist before finishing; and the common mistakes of the agent guide. The text is embedded in `ceesafec` (`templates/AGENTS.md`) and is the same for every package. Everything outside the markers belongs to the package's authors: ceesafec never changes it. ### 7.2 Agents `--agent` takes one or more of these names, separated by commas or by repeating the option; the default is `agents`. Each name writes the files below; `AGENTS.md` is written whenever any agent is chosen, since the others refer to it. | Name | Agent | Instructions | MCP configuration (`ceesafec mcp`, §6) | |---|---|---|---| | `agents` | Codex, Amp, OpenCode and others that read `AGENTS.md` | `AGENTS.md` | — | | `claude` | Claude Code | `CLAUDE.md`: `@AGENTS.md` (Claude Code's import) | `.mcp.json` | | `gemini` | Gemini CLI | `GEMINI.md`: `@AGENTS.md` (Gemini CLI's import) | `.gemini/settings.json` | | `copilot` | GitHub Copilot | `.github/copilot-instructions.md`: a copy of the CeeSafe section | `.vscode/mcp.json` | | `cursor` | Cursor | `.cursor/rules/ceesafe.mdc`: a copy of the CeeSafe section, applied always | `.cursor/mcp.json` | An unknown name is a usage error (exit status 2), and so is `--agent` with `--no-agent`. The MCP configurations name the server `ceesafe`, with command `ceesafec` and argument `mcp`, in each tool's format: `{"mcpServers": {"ceesafe": …}}`, or `{"servers": {"ceesafe": {"type": "stdio", …}}}` for VS Code. ### 7.3 `ceesafec agents` In a package, `ceesafec agents [--agent …]` writes the chosen agents' files (default `agents`): - a missing file is written as `ceesafec new` writes it; - in an instructions file that has the markers, the text between them is replaced with the current CeeSafe section, and the rest is kept; a file without them gets the CeeSafe section appended, after a blank line; a file that only imports `AGENTS.md` (`@AGENTS.md`) is left as it is; - an MCP configuration that exists is left as it is, and the command says so: it may hold the authors' other servers. It prints what it wrote and what it left. Run it after updating `ceesafec`, so the instructions match the compiler. ## 8. The C compiler (`ceesafec build`) `ceesafec build` writes C and gives it to a C compiler. It needs no configuration when a usual compiler is installed. ### 8.1 Which compiler `--cc ` names it; otherwise `$CC`; otherwise: - on Linux and macOS, `cc`; - on Windows, the first of these that exists: `clang` on `PATH`; `%ProgramFiles%\LLVM\bin\clang.exe` (where the LLVM installer, `winget install LLVM.LLVM`, puts it); `gcc` on `PATH` (MinGW-w64); `cl` on `PATH`; Visual Studio's `cl` (§8.2). clang and gcc are fully supported; MSVC is best effort (`c-backend.md` §1.1). With none, the error names what was looked for and how to install one. ### 8.2 Visual Studio's environment (Windows) clang on Windows and `cl` compile against Visual Studio's C library and the Windows SDK, which they find through the environment that Visual Studio's developer prompt sets (`INCLUDE`, `LIB`, `PATH`). When that environment is missing (`INCLUDE` unset) and the compiler is clang or `cl`, `ceesafec build` sets it up for the compiler, as the developer prompt would: 1. find Visual Studio, or its Build Tools, with the C++ tools for this machine: `vswhere -latest -products * -requires Microsoft.VisualStudio.Component.VC.Tools.ARM64` on an ARM64 Windows, `…VC.Tools.x86.x64` otherwise (other products that `vswhere` knows, such as SQL Server Management Studio, have no C++ tools and are not taken); 2. run its `VC\Auxiliary\Build\vcvarsall.bat` for the machine's architecture (`arm64` or `x64`) and take the environment it leaves; 3. run the compiler in that environment. The user's own environment is not changed. Without Visual Studio, clang still works where the environment is already set; otherwise the error says to install Visual Studio's "Desktop development with C++" workload (or the Build Tools), or MinGW-w64. ### 8.3 Flags | Compiler | Flags | |---|---| | clang, gcc, `cc` | `c-backend.md` §1's: `-std=c11`, the strict warnings, `-ffp-contract=off -fno-math-errno`, `-O2` (or `-g -O0`), `--cflag`s, the C files and the separately compiled packages' objects, the manifests' `-L` and `-l` (§4.1), `-o `; on Windows also the 8 MiB stack, `-Wl,/STACK:8388608` for clang and `-Wl,--stack,8388608` for gcc. With `--split`, each C file is compiled alone with the same flags and `-c`, at most `--jobs` at once, and the objects are linked with `-g` (for `-g`), the `--cflag`s, the libraries and the stack (`c-backend.md` §2.4) | | `cl` (MSVC) | `/nologo /std:c11 /W3 /O2` (or `/Zi /Od` with `-g`), `--cflag`s, `/Fo:` the build directory (so no `.obj` lands in the project), `/Fe:`, `/link /STACK:8388608` (`c-backend.md` §1.1), then the manifests' `/LIBPATH:` and `.lib` (§4.1) | A compiler is MSVC when its file name is `cl` or `cl.exe`. `--emit obj` (one relocatable object) needs clang or gcc. **`--emit lib`** (ADR 0088 §2.1) writes a static library for a host program whose `main` is not CeeSafe's (type-system.md §28.6): each C file of the build compiled with `-c` and `--emit obj`'s flags (`--cflag`s included), then archived with ` rcs` into `.partial.` (ceesafec's process id, so two builds never share it), made afresh, and renamed over the output, so a failing archiver (`ceesafec: the archiver '' failed`) leaves an earlier archive as it was. The members are one per C file: the runtime's keep their names (`ceesafe_rt_core.o`, …); every other one is `_` and the C file's path without `.c`, each `_` written `__` and each directory separator `_s` (`main.c` gives `_main.o`; with `--split`, `c/embed/src/lib.c` gives `_c_sembed_ssrc_slib.o`), so two paths never give one name; a package's separately compiled C files are `_ceesafe_package_.o`. Two C files whose member names differ only in case would be one file on macOS and Windows, so such a build fails (status 1) naming the member. The default output is `.a` (`out/.a` in a package). Unlike `-r`, an archive keeps the host's LTO: with `--cflag -flto=thin` (or `-flto`) every member is bitcode, which the host's link optimises with its own code (a ThinLTO host that builds its own C with `-fno-split-lto-unit`, as Bun does, needs `--cflag -fno-split-lto-unit` too). One archive per host binary: two archives built separately each carry the runtime and clash as two objects do (ADR 0088 §2.7). - **The C `main`.** A package with a CeeSafe `main` gets no C `main` in its archive, since the host has its own (two would not link); `--lib-main` keeps it, for an archive linked into an executable alone. - **The host's headers**, with `--emit lib`, are written beside the output: `ceesafe/embed.h` (its freestanding form in a freestanding build), and, when the program has exports, the exports header, named by `--exports-header ` (default `ceesafe_exports.h`). `-o build/libtext.a` gives `build/ceesafe/embed.h` and `build/ceesafe_exports.h`, so a host compiles with `-I build`. Each starts with `/* Generated by ceesafec`, and a file already at either path is replaced only if it starts so too: otherwise the build fails (status 1) before compiling, naming the file, and leaves it as it was. `--emit obj` writes no header, as in 0.27.0. - `--ar ` names the archiver, a path or a name looked up on `PATH`; by default it is `llvm-ar` beside an LLVM clang (`clang-18` beside `llvm-ar-18`; the C compiler found as for `--emit obj`, on `PATH` when it has no directory), else `ar` on `PATH` (Apple's `ar` indexes bitcode members). - Usage errors (status 2): `--ar` or `--lib-main` without `--emit lib`; `--ld` with `--emit lib`, which archives instead of linking; `--emit lib` when no archiver is found, or `--ar`'s is not, naming `--ar`. `--jobs N` (`build --split`, ADR 0082 §2.5) compiles at most `N` C files at once; the default is the number of CPUs (`getconf _NPROCESSORS_ONLN`, or `NUMBER_OF_PROCESSORS` on Windows), and `--jobs 1` compiles them one at a time in the bytewise order of their paths. The objects are linked in that order, and the C compiler's output is printed in it, whatever finishes first, so neither the C nor the output depends on `N`; an output identical to an earlier file's (a bad `--cflag`'s error, printed by every compile) is printed once, then `ceesafec: the C compiler printed the same for of the C files`. `--jobs` without `--split` is a usage error (status 2), since the C is then one command. With `cl`, each file is compiled with `/c`, `/Fo:` its own object and, for `-g`, `/FS`, and the objects are linked by `cl /Fe:`. On macOS a `-g` build's debug information is gathered into `.dSYM` by `dsymutil` after the link, as the compiler driver does when it compiles and links in one command, since the objects are removed with the build directory. A build without `--split` is one `main.c` and the runtime's files in one compiler command, as before. ### 8.4 The output The executable is `out/` in a package, or `-o `. On Windows, a path without an extension gets `.exe`, so `ceesafec build` makes `out\hello.exe`. The output's directory is created if it does not exist. ## 9. Porting C to CeeSafe: `c2cee` CosmoOS's roadmap 2.0 moves kernel C into CeeSafe one file at a time (SA2, SA3), and every port repeats the same mechanical steps: map the C types, declare each function the file calls, choose `@header`s that do not include the port's own header back, find what must stay in a C floor, rename members that are keywords, wire the build, and run the same checks. `integrations/cosmoos/c2cee.py` does the analysis and the checks, and drafts the `.cee` file, the floor and the build lines (§9.4). Since 2026-10-31 CosmoOS's kernel language is CeeSafe: a finished port lands in its tree as a `.cee` file, and its build generates the C with the ceesafec it pins (its `scripts/cee-gen.py` and `build/cee.mk`; the C is not committed). It reads C through clang's own front end (`-Xclang -ast-dump=json`), so it needs nothing but clang and Python 3, and never edits a file. It parses with the kernel's include path (CosmoOS's `KERNEL_CFLAGS`), the headers the build generates from CeeSafe (`out/cee/kernel/include`, written by `make cee`) first. ### 9.1 `c2cee.py analyse [--header ] [--cosmoos ] [--arch x86_64|aarch64] [--json]` Parses `` twice, as a debug build and as a release one (`CONFIG_DEBUG=1`, `0`), with CosmoOS's include paths for `--arch` (default x86_64), and reports: - **Functions** the file defines: their C and CeeSafe signatures, and whether a module export (`EXPORT_SYMBOL`) names them. - **Calls**, each callee with the header that declares it and what that means for the port: - a function: an `extern "C"` declaration; - `static inline` in a header: callable through that header (type-system §28.2); - an ordinary function in one build and `static inline` in the other (lockdep's hooks): it **must** be declared through its header, or the build where it is inline fails to link (CosmoOS #499); - variadic (`panic`, `klog`): a C floor; - through a function pointer: a C floor when the pointer is a member of an imported struct (§31.1). - **Builtins**: `__builtin_return_address` (CeeSafe's `callerAddress()` in an export), `__atomic_*` (an atomic view of the field; a `bool` field needs a `u32`, since lib/core's `AtomicBool` is four bytes), `__atomic_thread_fence` (a floor: CeeSafe has no fence). - **Macros** expanded in the file's functions, from their expansion locations: `wait_event*` (wait_floor.c's helpers), `list_for_each_entry*` (a loop over lib/kernel's `List` or `offsetOf`), `might_sleep` (`wait_floor_check_sleep`), `KASSERT`, `WARN`, `EXPORT_SYMBOL` (a floor). - **Locals of struct type** that hold C state (a wait entry, a timer): a CeeSafe local cannot (§33.1), so a floor helper. - With `--header `, the **structs it defines**, field by field: the CeeSafe type, members whose names are CeeSafe keywords (renamed, or `@cname` on an imported struct), `const` pointer members (a `@repr(C)` struct's carry no `const`), fields used atomically, and every other file in the tree that names a member. - **`@header` candidates**: the headers that declare what the file uses, each marked unusable when it includes ``, directly or not, since the generated types' header is included from `` (ADR 0030; `thread.h` → `completion.h` → `wait.h`). The report is text, or JSON with `--json`. It exits 0 unless clang fails. ### 9.2 `c2cee.py verify [--cosmoos ] [--arch x86_64|aarch64|both] [--no-boot] [--max-load ] [--ceesafec ]` Runs a port's checks in a CosmoOS tree, in order, with `CEESAFEC` set to `--ceesafec` (default `$CEESAFEC`, else `ceesafec` on `PATH`, else `~/.ceesafe/bin/ceesafec`: the release CosmoOS pins, which its Make runs on the tree's CeeSafe sources), and prints one line per step and a summary. A tree without `scripts/cee-gen.py` (from before CosmoOS's move to CeeSafe) is refused. 1. `make cee-check` (every CeeSafe source compiles with the pinned ceesafec, and the negative sources are refused as annotated); 2. `make host-test`, with every recorded trace digest it prints; 3. `make check-docs`; 4. per architecture: the debug and the release build, `make analyze` (and, for the first architecture, `make fuzz-build`, whose shims define some of a port's functions over its types), and unless `--no-boot` a debug and a release boot (`make test`). Before each boot it waits until the one-minute load average is under `--max-load` (default 8), since the boot tests' time bounds fail on a loaded host. A boot that fails after the load average reached the bound at any point during it (another job started meanwhile; it is sampled every five seconds) is run once more on a quiet host: both lines are printed, and the second decides. It exits 1 if any step failed, naming the first. ### 9.3 Checks of the tool `c2cee.py selftest` analyses `integrations/cosmoos/c2cee/fixture.c` with `fixture.h` and compares the report with `fixture.expected.json`, and drafts its port (§9.4) and compares that with `fixture.draft.cee` and `fixture_floor.draft.c`. The fixture holds one of each case of §9.1. ### 9.4 `c2cee.py port --header --out [--cosmoos ] [--arch …]` Writes a **draft** of the port into ``, never into the tree: `.cee`, `_floor.c` and `BUILD.md` (the `ceesafec check` of the finished port, CosmoOS's `cee-gen.py`, `kernel.mk` and `host.mk` lines, where the `.cee` file goes and the C it replaces, and what to change in ``). The draft is unsafe CeeSafe that mirrors the C, statement by statement, for a person to finish and review; every place it could not translate, or translated with a decision someone must check, carries `TODO(c2cee)` with the reason, and it never drops a construct silently. - **Names** stay C's (functions, parameters, locals, statics), so the draft reads beside the original; a name that is a CeeSafe keyword gets `_` appended. Each function's body is one `unsafe` block, and its doc comment has a `Safety:` line marked `TODO(c2cee)`. A function `EXPORT_SYMBOL` names, or that the header declares, is `@export("name")`; one the file passes as a function pointer is `@callback`. - **Types**: ``'s structs become `@repr(C)` structs (a keyword member renamed, `fn` to `callback`, `class` to `lock_class`, otherwise with `_` appended, and listed in `BUILD.md`); every other struct the file uses is imported, with the fields it reads; typedefs of integers are their CeeSafe integer, of structs `@ctype` imports. - **Declarations**: an `extern "C"` block per kind: callees whose headers may be `@header`s (§9.1), and the rest without a header; the floor's functions; `@constant` macros used in static initialisers. - **Statics**: integer, `bool`, pointer and array statics, and those initialised by a C constant macro (`SPINLOCK_INIT`), are CeeSafe `static mut`s; a static of a C struct type, or one whose initialiser names the static itself (`WAITQUEUE_INIT(name)`), stays in the floor, and the draft reaches it through a floor function that returns its address. - **Statements and expressions**: blocks, declarations, `if`, `while`, `for` (as a `while`, unless its body has a `continue`), `do` (as a `loop`), `return`, `break`, `continue`, assignments and compound assignments, `++`/`--` as statements, calls, `?:` (an `if` expression), casts (`as` when it cannot lose information, else `truncate`), pointer comparisons with null (`isNull()`), `*p` (`read`/`write`), `p->f` (`Ptr.toBorrow`, and for a write a `let` of `Ptr.toBorrowMut` first), `&x` (`borrow mut … as *mut T`), `sizeof` (`layoutOf`), string literals, the `__atomic` builtins on integers and pointers (an atomic view), `__builtin_return_address` (`callerAddress()`). Anything else, `switch`, `goto` and `continue` in a `for` among them, is a `TODO(c2cee)` with the C text. - **Macros**: `KASSERT(c)` becomes `if (!(c))` and a floor call that panics with the condition's text; `might_sleep()` `wait_floor_check_sleep`; `wait_event(q, c)` and `wait_event_timeout(q, c, t)` `wait_floor_until(_timeout)` with the condition as a `@callback` over a context struct of the locals it reads; other macros are translated from their expansion. - **The floor**: one C function per call of a variadic function (`panic`, `klog`, `kwarn`), with the format fixed and the other arguments as parameters; the statics that stay in C, with their accessors; file-scope `STATIC_ASSERT`s; the `EXPORT_SYMBOL` lines. - **Builds**: a function that differs between the debug and the release build (an `#if CONFIG_DEBUG` inside it, or present in one build only) is drafted from the debug build with a `TODO(c2cee)` naming the difference: a draft from one build alone would change the other. ## 10. Tests: `ceesafec test` (ADR 0049) ### 10.1 Declaring tests A test is a top-level function marked `@test` (type-system.md §28.1): no parameters, not generic, not exported, `async` or not, returning `void` or `Result`, where an `Err` is its failure. Any other shape, and a call of a test from anything but the generated harness, is N0348. - **Where tests are.** Beside the code in the package's sources, or in its test directories (`tests = ["tests"]` in `[package]`, by default `tests`). They are part of the same package and see its private items. - **Build and check.** `check` checks the test directories too. `build` leaves them out, and emits no `@test` function. Only the root package's tests are run; a dependency's are not. - **Checks** (lib/std's `testing.cee`): `Check.isTrue`, `isFalse`, `equalU64`, `equalI64`, `equalText`, `equalBytes` and `fail`, and `CheckResult.isOk` and `isErr`. Each returns `Result`, used with `?`; a failure holds its message. Generic equality waits for an equality interface. ### 10.2 The command `ceesafec test [--filter TEXT] [--list] [--interp] [--jobs N] [--timeout SECONDS] [--sweep alloc|cancel] [--format text|json]`, in a package: 1. It checks the package with its tests and writes the harness and the list of tests to `out/ceesafe-test/`. 2. It builds the harness once, as `out/-tests`. 3. It runs each test whose name contains `--filter` (all by default), in the order of their names (bytewise), `--jobs` at a time (default 1), each as its own process. `--list` prints the names instead. A `--filter` that no name contains is an error (exit status 1), so a mistyped filter cannot pass. `ceesafec test --help` lists the options. Each test's outcome: | Outcome | When | |---|---| | passed | it returned `Ok`, or `void` | | failed | it returned `Err`; its message is shown (`IoError`'s and `AsyncError`'s message, a `TestFailure`'s, or for the test's own enum the variant: `returned Err(Mine.Broke)`), and its captured output | | panicked | the runtime's panic report, with its location | | crashed | a signal or exception: `abort`, a fault (a status of 129 to 192 on POSIX, as `Process.capture` reports a signal; an exception's code on Windows) | | exited | the test ended the process itself with another status (`Process.exit(3)`) | | timed out | it ran past `--timeout` (default 60 s) | | leaked | it passed, but allocations were live at the end (`CEESAFE_ALLOC_REPORT`) | | skipped | with `--interp`, it calls C, which the interpreter cannot run | - **Exit status.** 1 if any test is not passed or skipped, else 0. - **JSON.** `--format json` prints one document: each test's name, outcome, milliseconds, message, standard output and error; the sweeps' failures and runs; and the counts. - **`--interp`** runs each test with `ceesafec run --interp` on the harness: the virtual clock and the seeded run order of type-system.md §40.8. The timeout holds there too, and so does the leak check: the interpreter counts allocations as the runtime does. The sweeps are native only: `--interp --sweep` is a usage error (exit status 2). - **`--sweep alloc`** runs each passing test again once per allocation attempt, the k-th failing. **`--sweep cancel`** runs it once per suspension point, the k-th cancelling its root task, sampled past 200 as `tools/cancel_sweep.py` is. A run may fail; it may not panic, crash, time out or leak. ### 10.3 The harness protocol (version 1) The generated function `ceesafeTestMain` is the harness's entry. An executable package's own `main` is checked and compiled but not run. - **Listing.** `--ceesafe-list` prints `ceesafe-test 1` and then one name a line. - **Running.** `--ceesafe-test ` runs one test (0: none for the last three): - lib/std's `TestRun.begin` turns on the runtime's allocation and cancellation reports and the two points (`ceesafe_test_begin`), and arms a watchdog thread (`ceesafe_thread_watchdog`). The watchdog prints `ceesafe-test 1 timeout` and ends the process with status 124. - The harness then writes `ceesafe-test 1 passed`, or `ceesafe-test 1 failed`, the message and `ceesafe-test 1 end`, to standard error. - The runner reads these lines, the panic report, the allocation report (`ceesafe alloc: attempts=… live=…`), the cancellation report (`points=…`) and the exit status. # C Backend Design (v0) Status: Milestone 0 proposal. Applies from M1. CosmoOS-specific flags are covered in `cosmoos/abi.md` and `cosmoos/build-integration.md`. ## 1. Target dialect - **C11** (ISO/IEC 9899:2011), compiled with `-std=c11` (or `gnu11` where the host project requires it). Generated code must also compile under C17. - Headers used by generated code: **only** freestanding headers (``, ``, ``, ``, ``, ``) plus `ceesafe/rt.h`. Everything hosted lives behind runtime functions. - Compiler-specific features are allowed only through macros in `ceesafe/rt.h` with a portable fallback: `__builtin_*_overflow`, `__builtin_expect`, `__attribute__((noreturn, cold))`, `_Static_assert`. - Supported compilers: clang ≥ 15 (CosmoOS's floor; its CI uses Debian clang 19.1.7) and gcc ≥ 11 for hosted builds. **CosmoOS profiles are clang/lld only** (`research/cosmoos-platform-audit.md` §3). MSVC is best-effort for the hosted profile (C11 mode, VS 2022), without the kernel profile. - The generated C must compile warning-free under `-Wall -Wextra -Wpedantic -Wshadow -Wconversion -Wsign-conversion -Werror` **and** under CosmoOS's own warning set (`-Wcast-qual -Wmissing-prototypes -Wstrict-prototypes -Wvla -Wundef -Wimplicit-fallthrough`, `-Wframe-larger-than=2048` for the kernel; see `cosmoos/abi.md`). All non-exported functions are `static`, and every exported one has a prototype in the generated header. - Correct both with and without `-fno-strict-aliasing`. CosmoOS compiles with it, but CeeSafe does not rely on it. - **No floating-point contraction or fast-math** (M11 G7, type-system.md §35.6). `rt.h` turns contraction off where a pragma can (clang, MSVC) and requires `FLT_EVAL_METHOD == 0`, in programs that use floats (`CEE_FLOATS`; a program without floats compiles under `-mgeneral-regs-only`); builds pass `-ffp-contract=off` to clang and gcc and never `-ffast-math` or its parts, so each float operation rounds once, as in the interpreter. - **No compiler-rt / libgcc.** CosmoOS links no builtins library, so generated code must never need one. That rules out `__int128` division, `long double` arithmetic, 16-byte atomics and, in AArch64 userland, outline-atomic helpers (to be verified in C1; pass `-mno-outline-atomics` if needed). The runtime header supplies portable fallbacks instead of builtins when a builtin would lower to a libcall. - `-nostdinc` builds: freestanding headers come from `-isystem $(clang -print-resource-dir)/include` (CosmoOS invariant B2). Generated code must not include libc headers directly; only `ceesafe/rt*.h` does, and only in hosted and userland profiles. - **Runtime per profile (M8c, `profiles.md` §4.2).** Hosted programs get `ceesafe/rt.h`, the core runtime `ceesafe_rt_core.c`, and the platform layer `ceesafe_rt_hosted.c` / `ceesafe_rt_io.c` (C library calls). A hosted program that prints a float also gets `ceesafe_rt_float.c`, the float formatter and its table (type-system.md §35.7; M11 G7b). Freestanding programs get `ceesafe/rt.h` and `ceesafe_rt_core.c` compiled with `CEE_FREESTANDING` (a `#define` at its top), which leaves out everything that needs a platform service; the environment defines `ceesafe_panic`. Freestanding builds add `-ffreestanding -fno-stack-protector -fno-asynchronous-unwind-tables -fno-unwind-tables` (the stack protector's `__stack_chk_*` symbols come from the C library; unwind tables are useless with panic = abort and have no unwinder to read them in a kernel). - **What `--emit lib`'s archive holds** (ADR 0088 §2.1; tooling.md §8.3): one object per C file the build compiles, each with the flags above: the generated code (`_main.o`, or with `--split` one per unit and `_ceesafe_instances.o`), every runtime file the program needs, chosen as for an executable (`ceesafe_rt_core.o`, `ceesafe_rt_hosted.o`, `ceesafe_rt_io.o`, and `ceesafe_rt_float.o`, `ceesafe_rt_thread.o`, `ceesafe_rt_async.o`, `ceesafe_rt_window.o` when its calls appear), and a package's separately compiled C files (`_ceesafe_package_.o`); tooling.md §8.3 gives the names. Headers are not members: they are written beside the archive. The generated `main` is left out, since the host has its own; with `--lib-main` it is kept, in the generated code's member, so the archive of a program with a CeeSafe `main` links into an executable alone. - Plain C `char` is **never** used for CeeSafe data (`char` is signed on x86-64 and unsigned on AArch64; CosmoOS's `limits.h` is also wrong about it on AArch64). Bytes are `uint8_t`. ## 1.1 MSVC, best effort (M7e) `ceesafec build --cc cl` uses it, and so does `ceesafec build` on Windows when no clang or gcc is found (`tooling.md` §8). The hosted profile also builds with Microsoft's C compiler: `cl /std:c11 /W3 /WX`, checked on x64 (CI) and ARM64 (a Windows 11 VM). Programs are linked with an 8 MiB stack (`/link /STACK:8388608`), as Linux and macOS give the main thread. Windows' default is 1 MiB, which `run-pass/deep-recursion.cee` overflows. This is **best effort**: no CosmoOS profile uses MSVC, and the strict warning set (§1) is checked with clang and GCC only. What differs: - **`rt.h`** uses compiler builtins only behind a test for GCC or clang, with portable code otherwise: - checked arithmetic falls back to the portable overflow checks, as with `CEE_NO_BUILTIN_OVERFLOW`; - counting leading zeros (stable vectors) uses `cee_clz64`, a shift loop off GCC and clang; - `CEE_UNUSED` and `CEE_COLD` are empty; - `cee_max_align_t` stands for C11's `max_align_t`, which MSVC's `` lacks in C mode. There it is `double`. The generated alignment assertions use it. - **Process behaviour** (`ceesafe_rt_hosted.c`, under `_WIN32`): - standard output and standard error are switched to binary mode at the first write, so `"\n"` stays one byte; - a panic ends with exit status 134 (`_exit(134)` after flushing), the status `abort()` gives on Linux and macOS. Windows' own `abort()` would give 3. - **I/O** (`ceesafe_rt_io.c`, under `_WIN32`): - program arguments work as elsewhere; - files use the C runtime's `_open`, `_read`, `_write`, `_close` and `_unlink` in binary mode; - `Process.run` uses `_spawnvp(_P_WAIT, …)`; - **TCP** (L1 A4a) is Winsock 2, started once (`InitOnceExecuteOnce`). A socket's descriptor is an id from `CEE_SOCKET_BASE` (`0x40000000`) in a table of `SOCKET`s under a slim reader-writer lock, so lib/std's `i32` descriptors never hold a truncated handle; `ceesafe_fd_read`, `_write` and `_drop` send an id to `recv`, `send` and `closesocket`, anything lower to the C runtime. Winsock errors become the C runtime's `errno` values (`WSAECONNREFUSED` is `ECONNREFUSED`), so `IoError`'s kinds are as elsewhere. Async sockets are nonblocking (`FIONBIO`); the executor's wait is described in §6. - **Test.** The Windows checks (`tools/host_c_check.py`, and the run corpus under `--cc cl`) cover three things: - every run-pass and run-fail program builds with MSVC and gives the corpus expectations; - `nwc` reads files and standard input; - a program runs `cmd /c exit 7` with `Process.run`, and sees a missing program fail. **A workaround for MSVC's optimiser:** ceesafec builds with `/O2 /Oi-`. At plain `/O2`, MSVC 19.44 to 19.51 (for ARM64 and x64) miscompile a loop that stores through the pointer of a slice copy, followed by a `memmove` into the same array: `run-pass/slice-bulk.cee` printed 3 for 70003, and the standalone `tests/c-compilers/msvc-o2-store-through-copy.c` (valid C11) fails the same way. clang, GCC, `/Od`, `/O1`, inlining off (`/Ob0`) and intrinsics off (`/Oi-`) are right. `/Oi-` makes `memcpy` and `memmove` calls the optimiser does not see through, and keeps every other optimisation (2026-11-01). With it every run-pass and run-fail program passes. The `test-windows` CI job runs the tests on x64. Locally, a Windows 11 ARM64 VM (MSVC 19.51) runs them. On other hosts they return early. **`/bigobj`:** every `cl` compile ceesafec runs passes `/bigobj`, as does the seed's `cl` line (`bootstrap/seed/README.md`, `.github/workflows/release.yml`). A large C file, such as the compiler's own 45 MB `main.c`, has more sections than a plain COFF object holds, and MSVC stops with `C1128` (2026-11-10, ARM64, MSVC 19.51). ## 1.2 The runtime ABI version The runtime and the C that `ceesafec` generates are versioned together, and nothing between them is stable across versions. `ceesafe/rt.h` defines the version right after its include guard: ```c #define CEESAFE_ABI_VERSION 1 ``` and every generated unit that includes it (`main.c`, and each unit of the split form, §2.4) checks it on the three lines after the `#include`: ```c #if CEESAFE_ABI_VERSION != 1 #error "this C was generated for CeeSafe's runtime ABI version 1" #endif ``` so C generated by one `ceesafec` and compiled against another's runtime fails to compile instead of linking wrong code. A change to the runtime's interface (a helper's name, signature or meaning, a layout the generated code relies on) raises the version. Version 1 is the first. ## 2. Lowering of types | CeeSafe | C | |---|---| | `i8..i64`, `u8..u64` | `int8_t..int64_t`, `uint8_t..uint64_t` | | `never` as a function's result (ADR 0074 §2.2, type-system.md §15.3) | `void`, with `_Noreturn` before the function's prototype and definition. A call of one is a statement followed by `ceesafe_panic(CEE_PANIC_UNREACHABLE, loc)`, NIR's `Unreachable` | | `isize`/`usize` | the fixed-width type of the target's pointer width (`int64_t`/`uint64_t` on LP64), plus `_Static_assert(sizeof(void *) == 8)`. `ptrdiff_t`/`size_t` are used at FFI boundaries (M8) | | `bool` | `bool` (`_Bool`) | | `char` | `uint32_t` | | `borrow fn(T…) -> R` (M11 G8b) | `const ceeCK_ *`, a pointer to the first member, the code pointer `R (*)(const void *, T…)`, of an environment `ceeCE_` that holds a pointer per capture; the body is `ceeCB_`, which converts `env` back through `uintptr_t`; a call is `(*f)(f, args…)` (type-system.md §36.2) | | `borrow dyn I`, `borrow mut dyn I`, `Box` (M13 G2) | `ceeDR_` = `struct { void *data; const ceeDT_ *vt; }` (16 bytes; the dyn type mangles as `D`, `dyn Shape` → `D5Shape0`), typedef'd before every struct, so any struct may hold one. Not a niche optional: `Box?` has a tag. The table type `ceeDT_…` is defined after the structs: `drop` (NULL if `T` needs none), `size`, `align`, then `m0…` in the interface's method order, each taking `void *` for the receiver. Per (`T`, `I`) pair that a coercion names: `static const ceeDT_… ceeVT__`, with adapters `ceeVT_…_` that convert the address back through `uintptr_t` and call `T`'s method, and `ceeVT_…_drop` over `T`'s drop glue. A coercion is `((ceeDR_…){ .data = (void *)(uintptr_t)p, .vt = &ceeVT_… })`; a call through `x` is `x.vt->m(x.data, args…)`; borrowing `*x` is `x` itself. An async method's entry is `void (*m)(void *frame, void *self, args…)` with the size `s` beside it (§3a, ADR 0036). `Box`'s drop glue calls `vt->drop` and frees `data` with `A` (a user allocator's `deallocate` gets the table's size and alignment) (type-system.md §37.4). **`dyn fn`** (M13 G3, §36.5) mangles as `DF` (`DG` for `dyn mut fn`), its effect list and its signature; its table's one entry `m0` is a move literal's body, whose `self` is the boxed environment `ceeCE_` (the code pointer, then the captured values; drop glue `ceeD_CE_`) | | `fn(T…) -> R` (M11 G8a) | a typedef `ceeN_` of a pointer to the functions' C signature; a value is a function's address, a call an indirect call (type-system.md §36.1) | | `f32`/`f64` | `float`/`double` (`rt.h` checks the IEEE formats; literals by their bits through `cee_f32_from_bits`/`cee_f64_from_bits`; float→integer conversions through range-checked helpers; type-system.md §35). The CosmoOS kernel forbids floats (`profiles.md`; `--no-float` in G7c) | | `struct S` | `typedef struct ceeT_S ceeT_S;` then `struct ceeT_S { … }` with fields `f_` in declaration order, structs emitted after the structs they contain. Each struct is followed by `_Static_assert`s on `sizeof`, `_Alignof` and every field's `offsetof`, against `TypeTable.LayoutOf`. An empty struct gets a `uint8_t cee_empty` member (C has no empty structs). `@repr(C)` is the default in v0; field reordering is an M9 opt-in (`@repr(auto)`) | | `Box` with a user allocator (C5, type-system §30) | `T *` as below; `ceeB_` calls `A`'s `allocate` method with `{ sizeof(T), _Alignof(T) }` (a `{ 0 }` value of the field-less allocator struct as `this`), and the drop glue and `BoxFree` call `ceeBF_`, which calls `deallocate` with the same layout | | `Box` (M3b) | `T *` (a shared reference to a box is `T *const *`). Drop glue `ceeD_X` destroys `*p`, then calls `ceesafe_free(p, CEE_ALLOC_SYSTEM \| CEE_ALLOC_FAILING)`. `Box.create`/`createIn` call a per-type helper `ceeB_X(T v)`: `ceesafe_alloc(sizeof(T), id)`; on NULL it destroys `v` and returns `Err(OutOfMemory)`, otherwise it stores `v` and returns `Ok(p)`. It asserts `_Alignof(T) <= _Alignof(max_align_t)`. **Built in place** (M9e′): when the argument is a temporary assigned by the statement just before, by a construction (an array repeat, an array, a struct or enum aggregate), and used nowhere else, and `T` needs no drop and holds no C state, the site calls `ceeBA_X()` (the allocation alone: `p` or NULL) and then writes the construction into `*p`, with no temporary and no by-value copy. When the construction is an array repeat of a constant whose bytes are all zero (an integer or `bool` 0, a float +0.0) and `A` is a heap allocator rather than a user-defined one (§30), the site calls `ceeBZ_X()` instead, which returns `ceesafe_alloc_zeroed(sizeof(T), id)` (counted and fault-injected like `ceesafe_alloc`; the hosted platform's `calloc`), and writes nothing: a C library can skip zeroing memory that already is. The construction's operands are already evaluated, so it cannot panic or allocate, and the order of allocation and construction is unobservable; `T` has no drop, so a failed allocation leaves nothing to destroy. A stack-placed box (§2.2) is likewise built in its slot | | Deep boxes (M3c) | If destroying `T` ends, as its last step, in a `Box` (followed through struct fields and enum variants' last payloads), the glue of `Box` is a loop: destroy the node except that tail (`next = tail pointer`), `ceesafe_free` the node, continue with `next` (type-system.md §18.5). `Unpack` becomes one assignment per target; `BoxFree` is `ceesafe_free` | | `Arena` (M4a) | `cee_arena` from `ceesafe/rt.h` (40 bytes, asserted there), created by `ceesafe_arena_create(allocator, chunk)`, dropped by `ceesafe_arena_drop`. A shared reference to a region is a plain `cee_arena *`: allocation mutates the arena through a shared borrow. `alloc`/`allocArray` call per-type helpers `ceeAA_`/`ceeAR_`, which bump-allocate with `ceesafe_arena_alloc` (chunks come from `ceesafe_alloc`, so they are counted and fault-injectable) and return the scoped result. Element names: `P`/`Q` + T for shared/mutable references, `U` + T for `[T]`, `G` for regions | | `FixedBufferAllocator`, `ScratchFrame` (M4b) | `cee_fba` (24 bytes) and `cee_scratch` (32 bytes) from `ceesafe/rt.h`. Allocation helpers take `(void *region, uint32_t kind, …)` and call `ceesafe_region_alloc_fast`, an inline function of `ceesafe/rt.h` that bumps an arena's pointer within its current chunk itself and otherwise calls `ceesafe_region_alloc`, which dispatches to the arena, the fixed buffer, or the runtime's scratch stack (heap chunks via `ceesafe_alloc`; a frame's drop frees the chunks newer than its entry state). `Scratch.enter()` is `ceesafe_scratch_enter_root(loc)`, which panics if a frame is live; `frame.enter()` is `ceesafe_scratch_enter_child`. A frame's borrow-checker parts (`memory`, `allocation`) are places of the frame itself in C | | `Pool`, `PoolBox` (M4c) | `cee_pool` (48 bytes; `ceesafe_pool_create`/`ceesafe_pool_take`/`ceesafe_pool_release`/`ceesafe_pool_drop`; slots of one size, allocated at the first take, with a free list through them). `PoolBox` is `ceePB_` = `struct { T *ptr; cee_pool *pool; }`, and its contents are `(*b.ptr)`. Its drop glue `ceeD_Y` destroys `*ptr`, then releases the slot; `BoxFree` of a partially moved pool box releases only. `pool.take` calls `ceePT_(pool, v)`, which destroys `v` when no slot is free | | Interfaces (M6b) | No runtime representation: dispatch is static. An `impl` block's methods are methods of their type, named like other members (`impl Comparable for i32` → `ceeM_3i32_compare`). Calls through a bound in generic code exist only in generic versions, which are not emitted; each instantiation calls the concrete method | | Generic code (M6a) | Generic versions are type-, move- and borrow-checked but never emitted. Each instantiation is a separate C type or function. Struct and enum instances are `ceeT_`/`ceeE_` + ``, using the element codes (`Pair` → `ceeT_4Pair1i32`; `S4Pair1i32` inside other names). Generic function instances are `ceeG_` (`id>` → `ceeG_2id1S4Pair1i32`). Members of an instance are `ceeM__`. A const argument (ADR 0083 §2.6) is `L`, its type's code, the value in decimal (`n` before a negative one; `0`/`1` for a `bool`; the variant's index for an enum), then `_`: `run` → `ceeG_3run1Lbool1_`, `Grouped<5>` → `ceeT_7Grouped1Lusize5_`, `Neg<-3>` → `ceeT_3Neg1Li32n3_`, `hoist` → `ceeG_5hoist1LE6Method0_`. `L` is no other type's code and `_` ends the number, so the codes stay prefix-free. Every non-generic form starts with a letter or a length, so the forms cannot collide | | `Result` (M3a) | like an enum, named `ceeR_` with the element codes below (`Result` → `ceeR_S3ResE4Code`, `void` → `v`). Its drop glue is `ceeD_R`. `e!` on `Err` calls `ceesafe_panic(CEE_PANIC_UNWRAP, loc)` | | `enum E { A, B(T) }` | `struct { uint32_t tag; union { T B; } u; }`. The tag width is the smallest unsigned that fits, unless `@repr(u8)` etc. A value without a payload is `((E){ .tag = n })`. A value with a payload of scalars and field-less enums is a compound literal too. A variant whose payload may hold a pointer (anything else) has a constructor `static inline ceeMk__v_(payload…)`, defined after the type, which zero-initialises the value and stores the tag and each payload field; its values are built by it, in the helpers too. Clang's static analyzer loses a pointer read back from a union built by a compound literal, so a `Box`'s allocation looked leaked (M14, ADR 0025 §7). The constructors are kept off scalar payloads because gcc inlined less around them (`string-building` ran 1.8× slower with one on its cold growth path, inferred from inlining). Static initialisers (§39) keep compound literals, which C requires there | | `T?` (M6c) | like an enum with variants `null` (tag 0) and `Some` (tag 1, field `f__0`), named `ceeO_` with the element codes (`i32?` → `ceeO_i32`; `O` inside other names). A niche optional (type-system §22.1, M9e′) is `struct ceeO_ { T f__0; }`: its tag is `(o.f__0 != NULL)`, `Some`'s payload is `o.f__0`, and `null` is `{ .f__0 = NULL }`; drop glue `ceeD_O`. `o!` on `null` calls `ceesafe_panic(CEE_PANIC_UNWRAP_NULL, loc)`; `o == null` compares the tag with 0. The niche layout for `Box` and references is future work (type-system.md §22.1) | | `Vector`, `String` (M6d) | `ceeV_<[T] or Z>` = `struct { T *ptr; uint64_t len; uint64_t cap; }` (24 bytes, asserted), element code `V or Z>` (`Vector` → `ceeV_Ui32S10SystemHeap`). An empty vector points to the runtime's `ceesafe_vec_empty` (a `max_align_t` object, never dereferenced) with `cap == 0`, so slices of it never do arithmetic on NULL. Growth is `ceesafe_vec_grow` (doubling, at least 4; a first buffer is allocated, a later one resized with `ceesafe_realloc`, which keeps the old bytes; NULL with nothing changed on overflow or allocation failure). Push and append call it only when the buffer is full, and pass the capacity through a local, so the vector's address does not escape the helper and the C compiler can keep its length in a register. Per type: `ceeVP_` push (destroys the value on failure), `ceeVO_` pop, `ceeVC_` clear, `ceeVA_` append (strings; it copies a text of up to 16 bytes with a byte loop and a longer one with `ceesafe_mem_copy`, since a `memcpy` of a length unknown to the C compiler is a library call for gcc, also for one byte), `ceeVW_` withCapacity; drop glue `ceeD_V…` destroys the elements in index order, then frees the buffer if `cap != 0`. The elements place `*v` is the vector itself, so elements are `v.ptr[i]` and views are rebuilt from `ptr` and `len` | | `SlotMap`, `Handle` (M6e) | `ceeK_` = `struct { ceeKS_… *ptr; uint64_t len, cap, count; uint32_t free; }` (40 bytes) with slots `ceeKS_…` = `{ uint32_t gen; uint32_t next; T value; }`; element code `K`. `Handle` is `cee_handle` = `{ uint32_t index; uint32_t gen; }` from `ceesafe/rt.h` (code `H`), compared with `ceesafe_handle_eq`. Generations are odd while a slot is live; a removal whose generation reaches `UINT32_MAX - 1` retires the slot (never put back on the free list), so generations never wrap. Helpers `ceeKI_` insert (grows the slots with `ceesafe_vec_grow`, at most `UINT32_MAX - 1` slots), `ceeKV_` validity, `ceeKG_`/`ceeKM_` get (an optional pointer), `ceeKR_` remove; drop glue destroys values in odd-generation slots | | `StableVector` (M6e) | `ceeSV_` = `struct { void *chunks[CEE_SVEC_CHUNKS]; uint64_t len; }` (264 bytes); element code `W`. Chunk `k` holds `8 << k` elements and is allocated by `ceesafe_svec_slot` when its first element is pushed; chunks never move or shrink. Element `i` is `(*ceeSVE_…(&sv, i))` (`ceesafe_svec_at`: chunk `60 - clz(i + 8)`). A shared reference to a stable vector is a non-const pointer, since `push` mutates through it. Drop glue destroys the elements, then `ceesafe_svec_free` | | `File`, `TcpListener`, `TcpStream` (C3) | `int32_t` descriptors; drop glue is the runtime's `ceesafe_fd_drop` (close). I/O built-ins call `ceesafe_*` functions declared in `ceesafe/rt.h` and implemented in `runtime/hosted/ceesafe_rt_io.c`; each returns `cee_io_result { value, error }`, and a per-type helper `ceeIOR_` turns it into `Result` (written only for an `X` an I/O built-in returns: `void`, an integer or a descriptor) (the runtime maps errno to the variant; `Other` keeps the number). The generated `main` is `int main(int argc, char **argv)` — the one place plain `char` appears — and passes argv to `ceesafe_init` (§6) | | `StringView` (M6c) | `cee_str` = `struct { const uint8_t *ptr; uint64_t len; }` from `ceesafe/rt.h` (16 bytes); element code `PZ` (`Z` is the unsized `str`). A literal is `((cee_str){ .ptr = (const uint8_t *)"…", .len = n })`, pointing to static storage. A literal longer than 4095 bytes, the most C99 and C11 require a compiler to accept (§5.2.4.1; clang and GCC warn beyond it under `-Wpedantic`, and MSVC rejects much longer ones), is instead a file-scope `static const uint8_t ceeLS_[] = { 0x…, … };`, emitted once per distinct text, and `.ptr = ceeLS_` (0.15.2). The same goes for a static's initialiser and `Console.write` of a literal. A file's bytes from `embed` or `embedText` (type-system.md §10.1, ADR 0087 §2.8) are such an array at any length but zero, once per distinct contents however many constants, statics and uses name them; an `embed` value views that array as a `Slice`, and an empty file is an empty literal. In a split build (§2.4) each array is declared `extern const uint8_t ceeLS_[n];` in `ceesafe_program.h` and defined once, in `ceesafe_instances.c`, its name carrying the program's as other split data does: a file is in the image once, and only one unit parses its bytes. The interpreter holds the same bytes in the program it runs, from its start. `s[lo..hi]` calls `cee_check_str_range` (range, then character boundaries; `CEE_PANIC_STR_BOUNDARY`); `==`/`!=` call `ceesafe_str_eq`; `s.bytes()` rebuilds the struct as `ceeSl_u8`; printing calls `ceesafe_write_view` | | `*T`, `*mut T` (M8a) | `T *` for both, `void *` for `*void`: read-only is a CeeSafe rule, and casting `const` away would trip `-Wcast-qual`. Element codes `N` / `M`. Every conversion to or from a pointer goes through `uintptr_t` (`(U *)(uintptr_t)p`), and `add`/`sub`/`offset` compute the address in `uintptr_t`, so no C pointer arithmetic or pointer-to-pointer cast is ever emitted; `p.read()` / `p.write(v)` are the place `(*p)`; `s.ptr` is the slice's `ptr` field; `Slice.fromRaw` builds the slice struct (`unsafe.md` §4) | | `borrow T` | `const T *`. `borrow mut T` → `T *` (`restrict` only where the borrow checker proves exclusivity *and* M9 enables it). `Ref(k, p)` is `&p`, a deref place is `(*r)`, and reference locals are zero-initialised to `NULL` (never read before assignment) | | `Slice` / `SliceMut` | `ceeSl_` = `struct { const T *ptr; uint64_t len; }` and `ceeSm_` = `struct { T *ptr; uint64_t len; }` (M2d; size 16, align 8, asserted). Slices are only made from arrays (N ≥ 1), so `ptr` is never NULL; `a[n..]` is the one-past-the-end pointer with length 0. An element is `s.ptr[i]`; a reborrow or `SliceMut`→`Slice` coercion rebuilds the struct from `ptr` and `len` | | `Box` | `T*` when `A` is zero-sized; `struct { T* ptr; A a; }` otherwise | | Arrays `[T; N]` | `ceeA_` = `struct { T e[N]; }`, so assignment, parameters and return by value work (M2d). An element is `a.e[i]`; `[v; N]` is a `for` loop over `e`; zero-initialised with `{0}`. Element names: integers and `bool` by name, `S` for structs, `E` for enums, `A_` for arrays (`[[i32; 3]; 2]` → `ceeA2_A3_i32`) | | Function pointers | C function pointer types via typedef | Names (as implemented in M2a): - functions: `cee_`; - struct members: `ceeM__`, where the length prefix makes the boundary unambiguous; - struct types: `ceeT_`; - fields: `f_`; - locals: `v__` and `t_`. The prefixes keep the namespaces apart and avoid C keywords. Planned: package/namespace qualification, plus a stable hash of the type arguments for monomorphised instances (not .NET `GetHashCode`). `@export("sym")` overrides the name. Arrays and slices are defined with the structs, each after the types it contains by value; slices come after their element types. Every array and slice type the program uses is emitted, in the order the checker created it. Bounds checks (M2d) call `cee_check_index(i, len, loc)` and `cee_check_range(lo, hi, len, loc)` from `ceesafe/rt.h`; they panic with "index out of bounds" and "slice range out of bounds". A constant index below a constant length, and a constant range within a constant length, are not checked. Since M9c, an index check that bounds-check elimination proved cannot fail is not emitted either (`optimisations.md` §2; `--no-bce` keeps them all). Likewise `+`, `-`, `*` and negation proved not to overflow are written without their helper, as `(T)(a + b)` (`optimisations.md` §3; `--no-oce` keeps them all). `d.swap(i, j)` (M11 G8c), after its two index checks, exchanges the elements through a temporary of the element's C type, which moves them whatever the type. `v.truncate(n)` is an inline block that lowers the length and calls the element type's drop glue on each removed element, lowest index first. Bulk slice operations (M9b, type-system §16.5): `d.copyFrom(s)` is `cee_check_length(s.len, d.len, loc)`, which panics with "slice lengths differ" (`CEE_PANIC_LENGTH`), then `cee_slice_copy(d.ptr, s.ptr, d.len, sizeof *d.ptr)`, which calls `ceesafe_mem_move` unless the length is 0 (C11 leaves `memmove` on a null pointer undefined even for 0 bytes, and a slice from raw parts may have one). `d.fill(v)` is `cee_slice_fill_byte`, a `ceesafe_mem_set`, for a one-byte integer or `bool` element, and otherwise an element loop in the generated C, which the C compiler may itself turn into `memset`. `ceesafe_mem_move` and `ceesafe_mem_set` are `__builtin_memmove` and `__builtin_memset` under clang and gcc, which may lower to calls to `memmove` and `memset`: both are in the freestanding allowlist (`profiles.md` §4.4). Under MSVC they are ``'s functions. Layout verification: for every emitted struct the backend emits `_Static_assert(sizeof(S) == N && _Alignof(S) == A, "...")` and `offsetof` assertions, where `N`/`A` come from `TargetInfo`. If the C compiler for the actual target disagrees with CeeSafe's model, the build fails, and nothing miscompiles silently. The message names the CeeSafe type. It is an unevaluated string, which may not contain numeric escapes (C23; clang rejects them in every mode), so `?` is written `\?` (otherwise `Box??` would contain the trigraph `??)`), and any byte outside printable ASCII becomes `_` (M9a). ## 2.1 C names (M7d) Generated code and the runtime share one C namespace, so their names are kept disjoint by prefix: | Owner | Names | |---|---| | **Runtime** (`ceesafe/rt.h`, `ceesafe_rt_*.c`) | `ceesafe_*`; `cee_` followed by a lowercase letter (`cee_add_i32`, `cee_check_index`, `cee_str`, `cee_loc`); `CEE_*` | | **Generated** | `cee` followed by a capital letter: `ceeF_name` (top-level functions), `ceeM_` (members), `ceeG_` (generic instances), `ceeXS_` (variant `n` of the function named `` made by closure-call specialisation, optimisations.md §5), `ceeT_`/`ceeE_` (types), and the helper families (`ceeD_`, `ceeB_`, `ceeVP_`, …; for async fns `ceeAF_` frames, `ceeAI_` initialisers and `ceeAR_` resume functions, §3a); locals `v__` and `t_`; fields `f_`; variants `v_`; labels `bb`; files `cee_file_`; source locations `ceeL_` (§6); string literals over 4095 bytes `ceeLS_` | **Packages** (M10 T5, ADR 0013 D4): an item of a dependency, rather than of the package being built, has its package's name in its C name: `ceeF_8geometry_area`, `ceeT_8geometry_Point`, `ceeM_08geometry5Point_norm`. The package name is part of the item's name as the backend sees it (`geometry.Point`), so every rule above applies unchanged, and two packages may declare the same name. In C the package's name comes after its length, where the item's name would start: `8geometry_Point`, and inside a name written with its length (a method's owner, a mangled type argument), `0` then the package's name and the item's, each after its length: `08geometry5Point`, since no length starts with `0`. A digit starts no CeeSafe identifier, so a package's own `geometry_Point` (`ceeT_geometry_Point`) never meets a dependency's `geometry.Point`, and the lengths keep a package `geo_x`'s `P` apart from a package `geo`'s `x_P`. Up to 0.27.0 the `.` was written `_` (`ceeT_geometry_Point` for both), so a package's own `geometry_Point` or `geometry_area` beside its dependency `geometry`'s `Point` or `area` was a C redefinition. A `@repr` type's C interface name is the exception (type-system.md §28.5): `geo.Point` with `@repr(C)` is `struct Point`, its tag in the generated C and in `ceesafe_exports.h`, with `typedef struct Point ceeT_3geo_Point;`, and a `@repr` enum `geo.Shade` is `Shade` in the header and in exports' prototypes, while its internal struct stays `ceeE_3geo_Shade`. Those names have one declaration in a program (N0201). Before M7d, top-level functions were `cee_`. A function named `add_i32` then collided with the runtime's `cee_add_i32`, and the C failed to compile. The random program generator found this, and `run-pass/runtime-names.cee` keeps it fixed. ## 2.2 Stack-placed boxes (M5; on by default since M9e, `--no-placement`) When `CEmitOptions.Placement` is on (the default since M9e, `optimisations.md` §4), `PlacementAnalysis` decides each `Box.create` site (`memory-model.md` §5.1). - **Slot.** A stack-placed site has a slot `CEE_UNUSED T cee_box_ = {0};` among the function's locals. A site in a loop reuses its slot in every iteration; the analysis places it only when the previous box is gone by then. - **Creation.** `R = ceeB_…(v)` becomes `cee_box_ = v;` (or the construction written into `cee_box_`, as for a box built in place, §2) followed by `R = ceeMk__v_Ok(&cee_box_);`. Nothing is allocated, so nothing can fail. - **Destruction.** - A `Drop` of a box local of the site calls the contents' drop glue `ceeD_T(b)` if `T` needs drop, and nothing else. A drop flag still guards it. - A `BoxFree` of one emits nothing. Everything else is unchanged. Heap boxes, and every other site, use the drop glue of §2. ## 2.3 C interoperability (M8b; type-system.md §28) - **Extern functions.** Each `extern "C"` function gets a prototype with the C types of §28.3 (`size_t strlen(const char *);`) and a `static` wrapper `ceeC_` with CeeSafe's own C types, which converts the arguments (`(size_t)n`; a `T *` converts to `const T *` implicitly), calls the function and converts the result back (a `const T *` through `uintptr_t`; a `@repr` enum through a check that panics with `CEE_PANIC_FFI_ENUM`). CeeSafe code calls the wrapper like any function. The prototypes use C's own types so that they are compatible with the C library's declarations: the generated code includes no C library header (unless a `@header` names one), but clang and GCC know the library functions' types (`-Wincompatible-library-redeclaration`, `-Wbuiltin-declaration-mismatch`), and C11 6.5.2.2 makes a call through an incompatible type undefined. - **Exports.** Each `@export` function gets a non-`static` wrapper under its symbol, with the C types, that converts the arguments (checking `@repr` enums), calls `ceeF_`, and converts the result. Nothing else in the generated code has external linkage except `main`. In a hosted build the wrapper, and each `@callback`'s C function (`ceeW_`), first calls `ceesafe_entry_check()` (ADR 0088 §2.4): a relaxed load of `ceesafe_any_panicked`, a global set at any thread's first panic, and only when it is set, a call that reads the thread's own panicked flag and on a thread that has panicked prints that panic's line and aborts. A freestanding build has no flag and no call. - **`@repr(C)` structs** are `struct S` with members named as in CeeSafe (other structs are `struct ceeT_S` with members `f_`), and `typedef struct S ceeT_S;`, so generated code uses them like any struct, and C code that includes `ceesafe_exports.h` sees the same type (C11 6.2.7 makes struct types in two translation units compatible only if their tags and members match). **One definition** (ADR 0030): the structs are defined only in the exports header (with the CeeSafe structs they hold, such as lib/core's `Volatile`), which `main.c` (or, split, `ceesafe_program.h`) includes after the `@header`s; its layout assertions follow the include. So a C header may include the exports header and a CeeSafe program may include that C header. `--exports-header ` sets the name the header is included by and written under, and its guard (`kernel/wait_types.h` → `KERNEL_WAIT_TYPES_H`); without it, `ceesafe_exports.h`. An array field is a C array member (`CE f[N]`) in both, used in CeeSafe's C as its array struct, `(*(ceeA_ *)(uintptr_t)&p->f)`, after an assertion that the two have one size and alignment; a struct literal assigns it after the compound literal, and a static's initialiser writes it as `{ … }` (ADR 0029). An imported member is its C spelling (`spinlock_t lock;`); a `@repr(C)` struct that holds one is C-laid-out (§33.1), so neither file asserts its layout. - **`@repr(uN)` enums** keep the internal form `struct { uintN_t tag; }` (the tag has the repr's width and signedness); wrappers convert to and from the integer. - **`ceesafe_exports.h`**: an include guard; ``, ``, ``; each `@repr(C)` struct (after the structs it contains); each `@repr` enum as `typedef E;` and constants `E_Variant`; each export's prototype. It is generated only when there is something to declare. - **Imported C types (C6b, type-system.md §31)**: `main.c` and `ceesafe_exports.h` `#include "…"` each `@header` after `ceesafe/rt.h`, once, in declaration order; they are the only headers generated code includes besides the runtime's. An imported type has no definition, typedef or layout assertion in generated code: it is its C spelling (`struct name` or the `@ctype` name), and a field place is a member access. Pointer, `usize` and `isize` fields convert through `uintptr_t` or a cast when read and written, as wrappers do. For each declared field, two `_Static_assert`s (on `_Generic` of the member, with the field's C type, and on its `sizeof`) make a header that disagrees a C compile error that names the struct, the field and the CeeSafe declaration's file and line. A member of an imported enum spelled `enum name` is matched through its address (`enum name *`), since Clang targeting Windows reads `enum name:` as an enum with a fixed underlying type (type-system.md §31.3). - **C arrays in imported types (C9, ADR 0014)**: a field `f: [E; N]` is the member seen as CeeSafe's array struct, `(*(ceeA_ *)(uintptr_t)&p->f)`: no copy, and every array operation works on it. Its checks are `_Generic(&member, CE (*)[N]: 1, default: 0)` (the member itself would decay to a pointer) and equal `sizeof` and `_Alignof` with the array struct. A `@cname` field uses its C name in both. An array of an imported type is emitted as an array struct of the C type, with no layout assertion (C lays it out), and `add`/`sub`/`offset` on a pointer to an imported type scale by C's `sizeof`. - **Exported constants (C9, type-system.md §28.4)** are read-only C data, `const CE symbol[N] = { … };` or `const CT symbol = v;`, each after an `extern` declaration of itself (so `-Wmissing-variable-declarations` is quiet), and declared `extern` in `ceesafe_exports.h`. CeeSafe code uses the constant's value, not the object. - **C sources** given to `ceesafec build` are copied unchanged to `c/` beside `main.c` and compiled with it (`-I` names the directory, so `#include "ceesafe_exports.h"` works). - **Function pointers (M8d, §29.5)** are `typedef R (*ceeP_)(A, …);` with the C types of §28.3 (element code `F`), declared after the struct names and before the structs. A value is the extern function's name or the export's symbol (exports are declared before every function); a call converts its arguments and result like an extern wrapper. `ceesafe_exports.h` declares the struct names first, then the function pointer types. - **`lib/core` (M8d, §29.2)** is compiled with every package, but only the functions that emitted code calls, the types it mentions, and their files' names are emitted, so a program that uses none of it gets the same C as before. - **Volatile accesses** (§29.1) are `(*(volatile T *)p)`; `@align(n)` (§29.4) is `_Alignas(n)` on a struct's first member; `Cpu.spinHint()` and the target queries are `ceesafe/rt.h` inlines. ## 2.4 Split output (M14 K2a, ADR 0022) `ceesafec build --split` writes the program's C in several translation units, so a C compiler can build it in parallel and rebuild only what changed. Without it the output is one `main.c`, as above, byte for byte. - **`ceesafe_program.h`**, included by every unit, holds what one `main.c` has before its first function body (the source-location tables, the types, the prototypes, the C extern wrappers, `dyn` tables, thread and fork-join helpers), and the per-type helpers that `main.c` defines after its functions: drop glue, the `Box`, vector, container and I/O helpers. Each of them is `static`, the per-type helpers `static inline`, so every unit has its own inlinable copy. It starts with an include guard named after the program. - **One unit per CeeSafe source file**, at `c//.c`, its extension replaced (ADR 0082 §2.1): `c/alpha/src/util.c` and `c/beta/src/util.c`, `c/alpha/src/a/mod.c` and `c/alpha/src/b/mod.c`. Package names are unique in a build and paths unique in a package, so two units of packages never share a path. Files given without a manifest are the package named by the program's name (`--program`, below): `src/demo.cee` of a program `demo` is `c/demo/src/demo.c`. A `..` in such a file's path becomes `__`, so a unit stays under `c/`, and a file outside the current directory is named by its file name (§7). Two such files can then give one path (`/p/util.cee` and `/q/util.cee`; `../x.cee` and `__/x.cee`): the later, in the order of the files, gets `-2` before `.c` (`c/demo/util-2.c`), or the first of `-3`, `-4`, … that no earlier unit has ignoring letter case, so units never merge (ADR 0082 §7). A unit holds the functions declared in its file and the closures written in them; lib/core's and lib/std's go to `ceesafe_instances.c`. Two units whose paths differ only in letter case would be one file on macOS's and Windows's file systems: **N0363**. - **`ceesafe_instances.c`** holds what is defined once and belongs to no user file: lib/core's functions, generic instances, `@export` constants, callbacks, and C's `main`. - **Linkage.** Every function defined in a unit or in `ceesafe_instances.c` has external linkage, and its prototype in the header has no `static`. The header renames each of them with the program's name, `#define ceeF_main ceeF__main` (the name after the family prefix; `ceeARG` becomes `ceeARG_`), so two CeeSafe programs linked into one image never define the same symbol. The program's name is `--program`, the package's, or the first file's. `@export` names are not renamed. - **Determinism.** The units, their names and each function's placement are a function of the packages' file lists and the program, and two builds are byte-identical. - **Compiling** (ADR 0082 §2.5). `ceesafec build --split` compiles each C file of the build (the units, `ceesafe_instances.c`, the runtime's, the packages' own) to its own object with `-c`, at most `--jobs N` at once (default: the number of CPUs), then links the objects in the bytewise order of the files' paths. The C compiler's output is printed in that order too, whatever finishes first; `--jobs 1` compiles in that order. The C and the executable's inputs do not depend on `--jobs`. MSVC's `cl` is run the same way, `/c` and `/Fo:` per file (one `cl` command for every file would give `c/alpha/src/util.c` and `c/beta/src/util.c` one `util.obj`). - **Inlining.** A call from one unit to a CeeSafe function in another (lib/core's included) is not inlined without link-time optimisation. The helpers in the header are in every unit, so calls to them still inline: with the per-type helpers in `ceesafe_instances.c` instead, `vector-growth` and `string-building` ran 1.7–1.9× slower, and with them in the header as fast as one file (Mac, 2026-10-29). - **Checked by** the run corpus's `clang -O2 split` configuration (`CToolchains`): every run-pass and run-fail program is built split, with the strict warnings, and must behave as in every other configuration. ## 3. Lowering of control flow and cleanup **Flat form** (M1; `--flat-c`): each NIR block becomes a C label. Terminators become `goto`, `switch` (SwitchInt) or `return`. It is the reference form: simple enough to check by reading. **Structured form** (M7b; the default). It is a readability pass and adds nothing that correctness depends on. One invariant makes it safe: **every block's code ends in an explicit transfer**: `goto`, `break`, `continue`, `return`, or a `_Noreturn` call. Code therefore never falls into the next block by accident. Where a block is placed changes only how the code reads, and a transfer may be dropped only where falling through provably reaches the same label. 1. **Analysis.** - Reverse postorder, dominators (Cooper–Harvey–Kennedy), and *forward predecessors*: in-edges that are not back edges, counted with multiplicity, so a switch with two cases to one block counts twice. - A *back edge* `u → h` is one where `h` dominates `u`. A retreating edge that is not a back edge makes the graph irreducible. The function is then emitted flat. CeeSafe source has no `goto`, so this is not expected; it is a fallback. - The natural loop of a header `h` is `h` plus every block that reaches a back-edge source without passing through `h`. 2. **Placement.** Each block's code is emitted exactly once, inside the code of its immediate dominator `d`, in one of three ways: - **Inline**, at the branch to it: when it has one forward predecessor and leaves no loop that contains `d`. - **Exit follow**: when it leaves loops. Let `h` be the outermost loop header whose loop contains `d` but not the block. The block is emitted after `h`'s loop statement. One exception keeps early returns in place: a block that leaves a loop from a block other than the header itself is inlined if it has one forward predecessor and its dominator subtree is *closed* and small. Closed means no edge leaves the subtree; small means at most 2 blocks and 8 statements. - **Follow**: otherwise (a merge point). It is emitted after `d`'s own code, inside the same loops. Follows and exit follows are emitted in reverse postorder. 3. **Shapes.** - A loop header's code (with its inline and in-loop placements) becomes `for (;;) { … }`. - `NirBranch` becomes `if (c) { … } else { … }`, and `NirSwitch` becomes `switch` with one `case` body per target. - A branch to an inline block emits that block's code there. Every other branch is first emitted as `goto bbN;`. 4. **Transfer elimination**, in tree order and repeated to a fixpoint, since removing a `goto` can lengthen what falls through before it: - A `goto L` is removed when falling through reaches `L`. Falling through means passing over only labels, the end of an `if` arm, or the end of a loop body back to its own header's label. - Otherwise it becomes `continue` if `L` is the innermost loop's header. - Otherwise it becomes `break` if `L` is what falls through after the innermost loop, unless a `switch` lies in between. - Otherwise it becomes `break` if `L` falls through after the innermost `switch`. - The end of a `case` body never falls through: its transfer is kept, so no `case` falls into the next (`-Wimplicit-fallthrough`). 5. **Labels** that no remaining `goto` names are removed (`-Wunused-label`). A label at the end of a block is written `bbN: ;`. 6. **Guard clauses.** - An `if` arm that cannot complete normally is hoisted: `if (c) { A } else { B }` with `A` ending in a transfer becomes `if (c) { A } B`. Symmetrically, when only `B` ends in a transfer it becomes `if (!c) { B } A`. When both arms end in a transfer, the shorter arm is the guarded one. - An empty `then` arm is negated away. - An `else` holding a single `if` is printed as `else if`. The run corpus is built in both forms under every C configuration: structured by default, flat as the oracle in a `clang -O2 flat` configuration. Both must agree. Shape of `gcd` (structured): ```c v_x_2 = v_a_0; v_y_3 = v_b_1; for (;;) { t_5 = (v_y_3 != ((uint64_t)0)); if (!t_5) { break; } v_t_4 = cee_rem_u64(v_x_2, v_y_3, &ceeL_0); v_x_2 = v_y_3; v_y_3 = v_t_4; } t_6 = v_x_2; return t_6; ``` Flat form of the same function (M1), shown in an earlier, illustrative notation: ```c int32_t cee_demo_gcd(uint64_t a, uint64_t b) { uint64_t x; uint64_t y; uint64_t t; bb0: x = a; y = b; goto bb1; bb1: if (!(y != 0u)) goto bb3; goto bb2; bb2: if (y == 0u) ceesafe_panic_div0(CEE_SRC(3, 17)); /* inserted check (statically elidable later) */ t = x % y; x = y; y = t; goto bb1; bb3: return x; } ``` Cleanup: drop elaboration has already put `Drop` calls on every exit edge, so early `return`, `?` and `break` need no special backend support. Drop flags are `bool` locals initialised to `false`. No `setjmp/longjmp` and no C++. Panics are `noreturn` calls into the runtime (`ceesafe_panic(kind, src)`), so destructors are not run (panic = abort, OD-5). ## 3a. Async functions (L1; ADR 0031 §3.6, type-system.md §40) An async fn `F` whose C function name would be `` (§2.1) lowers to four C items instead of one function (`src/backend/c_async.cee`): ```c struct ceeAF_ { uint32_t cee_state; /* 0 at entry; k after suspension point k (intermediate-representation.md §3) */ /* the parameters, then every local, as fields named like the locals (v__, t_) */ /* one field per awaited call holding the awaited task's frame: struct ceeAF_ cee_a; */ /* the result, if F's result type is not void: R cee_result; */ }; static void ceeAI_(struct ceeAF_ *cee_frame, /* the parameters */); /* zeroes the frame, stores the arguments */ static bool ceeAR_(struct ceeAF_ *cee_frame); /* runs F until it returns (true) or suspends (false) */ static bool ceeAT_(void *frame); /* ceeAR_ as the runtime calls it */ ``` - **Frames are emitted child-first,** so a frame that embeds another's follows it. A cycle cannot occur: an await on a cycle of the await graph is **stacked** (ADR 0056; its NIR role `taskStacked`), and its awaiter's union member is a pointer `struct ceeAF_ *cee_a`, which needs only the struct's name. The await sets it to `ceesafe_async_stack_push(sizeof(struct ceeAF_), _Alignof(struct ceeAF_))`, starts and resumes the callee through it, reads its result, and calls `ceesafe_async_stack_pop` on it. The runtime keeps the task's top chunk in `cee_task.stack` (the task record is 192 bytes since ADR 0062, `ceesafe_task_slot` with it) and each executor's free chunks, which it frees as it ends. Freestanding code has no stack: there a cycle is N0343. - **Which locals are frame fields** (L1 A3): the parameters, every local that is ever borrowed, every local live just after a suspension point (backward liveness over the NIR, where a `StorageDead` reads nothing), and the locals a scope's end mentions (it reads its oom operand after resuming). Inside the resume function such a local's C name is defined as its field (`#define v_x_3 (cee_frame->v_x_3)`, undefined after the function), so statements are emitted as in a sync function, and a pointer to it stays valid while the frame does (type-system.md §40.6). Every other local is a C local of the resume function, zero-initialised as a sync function's. - **Waiting storage.** A task waits at one suspension point at a time, so what each needs only while it waits (an awaited child's frame `cee_a`, a join `cee_j`, a worker `cee_b`) shares one union `cee_u`. A scope's struct is its own field: it must be zero when its first child is spawned. - **Layout.** ceesafec computes each frame's size and alignment (`frameReport`, from the fields' C layouts and the runtime structs') and asserts them after the struct (`_Static_assert(sizeof(struct ceeAF_) == n, …)`), so the C compiler checks the arithmetic; `ceesafec build --explain-frames` prints the same numbers, the locals kept and the largest ones (the `frames` corpus family's `.frames` snapshots). - **The resume function is always in flat form** (§3), whatever `--flat-c` says. It opens with `switch (cee_frame->cee_state) { case k: goto cee_resume_k; … default: break; }`. Jumping into the middle of loops makes the graph irreducible, so structuring it would fall back to flat form anyway. - **An awaited call** `dest = G(args…)`, suspension point k, is ```c ceeAI_(&cee_frame->cee_a, args…); cee_frame->cee_state = k; cee_resume_: if (!ceeAR_(&cee_frame->cee_a)) return false; dest = cee_frame->cee_a.cee_result; ``` - **`Async.park()`** is `cee_frame->cee_state = k; return false; cee_resume_: ;`. - **`return e`** stores `cee_frame->cee_result = e` and returns `true`. Drop elaboration has already dropped the locals. - **`Async.run(G(args))`** declares the root frame `struct ceeAF_ cee_root` in a block of the caller's C function, then calls `ceeAI_(&cee_root, args…)`, `ceesafe_async_run(&cee_root, ceeAT_, &ceeAN_)`, and reads `cee_root.cee_result`. The thunk adapts `ceeAR_` to the runtime's `bool (*)(void *)`, so no function is called through another type. - **Scopes.** Scope `site` is a field `struct ceeAS_ cee_s` of the awaiting frame: the runtime's `ceesafe_scope`, then the first error, a `Result`. `s.spawn(child)` calls `ceesafe_async_spawn(&cee_frame->cee_s.rt, sizeof(struct ceeAF_), ceeAC_, NULL, NULL, &ceeAN_)` for the child's async fn `C` and initialises the frame it returns with the closure's environment (the closure value, the environment's first member, converted back). `ceeAC_` resumes the child and, once it returns, makes an `Err` the scope's first error, dropping a later one. The scope's end is a suspension point: `ceesafe_async_scope_done` parks the task until the last child returns, then the result is the first error, else `Err(oom)` if a child could not start, else `Ok`, and the scope is zeroed for its next run. An async closure's environment keeps a null code pointer: it runs only through its frame. - **Task sets** (ADR 0053). The record is lib/core's `TaskSetRecord` in the frame of `AsyncTasks.run`'s task: the runtime's `ceesafe_task_set` (a `ceesafe_scope` with `set` true, the limit, the oom flag, the error slot, the task waiting for room, the allocator), then the first child error. `AsyncSetSpawn` emits, in any function (sync too, for `trySpawn`): if the admission code is 0, `ceesafe_async_set_spawn(record, sizeof(struct ceeAF_), ceeAC_, &ceeAN_)` and the frame's initialisation, or code 3 if it is NULL; a child not started drops its environment. `spawnSend` calls `ceesafe_async_set_spawn_send`, then `ceesafe_async_publish`. `Async.setAllocator(record, alloc)` (lib/core's, for `tasksIn`; `Async.tasks` is `tasksIn` with `SystemHeap`) is `Builtin(AsyncSetAllocator, [A, record])`: `ceesafe_async_set_allocator(record, ceeAA_, ceeAD_)`, whose adapters are emitted as a `scopeIn` scope's are. A shared set (spawnSend children) admits and waits for room under the run's lock. The child's thunk copies an `Err` to `ceesafe_async_set_error(scope)` if `ceesafe_async_claim_failure` says it is the first. In the runtime a set's child error cancels no sibling, a failed allocation stops nothing, and a returning child wakes the task waiting for room (`ceesafe_async_set_admit`), as the last one wakes the task waiting for the set (`ceesafe_async_set_done`). - **`s.spawnSend(child)`** (ADR 0038) is a spawn whose Builtin has the role `taskSend`: it calls `ceesafe_async_spawn_send` (same arguments as `ceesafe_async_spawn`), initialises the frame, then `ceesafe_async_publish(frame)`, which queues the child for any worker. A child records its scope's first error only if `ceesafe_async_claim_failure(scope)` says it is the first, which takes the run's lock for a shared scope. `Async.runWorkers(n, task)` is `ceesafe_async_workers(n)` before the run's code. - **Async instances** (ADR 0037, type-system.md §32.5) are async fns like any other, named `ceeAS_` after the function they copy (`` the instance's HIR number). The caller awaits the instance; inside it, a call through a bound closure is an awaited call of the async closure's body, its frame a member of the instance's union. - **Calls through `dyn`** (ADR 0036). An async method `m` in slot `k` of a dyn type `D…` has its own frame, `struct ceeAF_dyn__m`: `cee_state` (always 0, for the debugger), `cee_resume`, the resume adapter of the receiver's method, `cee_size`, the result, then the union `cee_impl` with one member `i_` for the method of every type coerced to the dyn type (and `cee_none`, so it is never empty). Its `ceeAI_` calls the table's entry, `x.vt->m(frame, x.data, args…)`. The entry `ceeVT___` stores `ceeVT_…__r` and the size, then starts `T.m` in its member. `ceeAR_` calls `cee_resume`, which resumes `T.m` and, once it returns, copies its result into the frame. So an await, `Async.run` and `Async.boxed` lower as for a direct call. `Async.boxed` allocates the table's `s`, `offsetof(…, cee_impl)` plus `T.m`'s frame, and gives back `cee_size`. The frame is emitted after every impl's, and its size and the union's offset are asserted; `--explain-frames` reports it at each await. - **Joins.** `race` or `all` at `site` is a field `struct ceeAJ_ cee_j` of the awaiting frame: the runtime's `ceesafe_scope`, `first` (the child that returned first, 2 for neither), both results, and both children's task slots (`ceesafe_task_slot`) and frames, defined before the frame that holds it. The join zeroes it, initialises both frames, starts both with `ceesafe_async_start` (the caller's storage: nothing is allocated, nothing freed), and waits like a scope's end. `ceeAJ_` resumes child k and, once it returns, keeps its result, notes it if it was first, and calls `ceesafe_async_cancel` on the join: always for race and for a timeout, on an `Err` for all. A timeout's second child (ADR 0046) is a function, not a closure: its initialiser is given the deadline, and its result field has its own type. - **Blocking.** `Async.blocking(body)` at `site` is a field `struct ceeAB_ cee_b`: the runtime's `ceesafe_blocking`, the closure value and its result. `ceesafe_async_blocking_start` runs `ceeABT_` on a worker (`ceesafe_thread_start`; inline if none can start), which calls the closure through its code pointer, stores the result and calls `ceesafe_async_blocking_finish`. The task waits at a suspension point on `ceesafe_async_blocking_ready`, which joins the worker once it is done. - **Frames from an allocator** (L1 A3). For each allocator type `A` async code takes frames from, `ceeAA_(size, align)` and `ceeAD_(p, size, align)` call its `allocate` and `deallocate` with a `Layout` (§30.3), or the runtime's heap for `SystemHeap` and `FailingAllocator`. `Async.boxed(alloc, g(…))` is a suspension point whose waiting storage is a pointer `cee_x`: the frame from `ceeAA_` (on `NULL` the value is `Err` and `g` does not run), then `ceeAI_` and `ceeAR_` through the pointer, then the result in `Ok` and the frame back through `ceeAD_`. A `scopeIn` scope's spawns pass its adapters to `ceesafe_async_spawn`, which takes the child's block from them and gives it back when the child returns. - **Placement** (§2.2) and closure specialisation skip async fns: a box's C-stack slot would not survive a suspension. - **Frame information** (L1 A3, for debuggers). After its resume function, each async fn `F` has `static const uint32_t ceeAL_[]`, the source line of each resume state (`[0]` the line of `F`'s name, `[k]` the line of suspension point k's statement), and `const ceesafe_frame_info ceeAN_ = {name, file, ceeAL_, count}`: `F`'s name as `ceesafe-bt` shows it (`Counter.next`, or `async closure`), and its path as `#line` names it. Every task carries its fn's record: `ceesafe_async_run`, `_spawn` and `_start` take it. The runtime reads `cee_state`, the frame's first field, to find the line (§6). With `--split` the record has external linkage and an `extern` declaration in the header; the thunks there are `CEE_UNUSED`. The task header and executor are in `runtime/hosted/ceesafe_rt_async.c`, linked when the generated C calls `ceesafe_async_*` (§6). ## 4. Evaluation order and temporaries NIR has already sequenced every side effect into separate statements, so the C backend never emits an expression whose meaning depends on unspecified C evaluation order (function-argument order, `a[i] = i++`-style). Arguments with side effects are always assigned to temporaries first. ## 5. UB avoidance checklist (enforced by construction + tests) | Hazard | Strategy | |---|---| | Signed overflow | `ceesafe_add_i32_chk(a,b,src)` via `__builtin_add_overflow`, with a portable fallback that computes in unsigned and compares. Wrapping ops compute in unsigned and convert back through a well-defined helper (implementation-defined conversion avoided by `memcpy`-free bit tricks verified per compiler) | | Division | Check for zero and `MIN/-1` before `/` and `%` | | Shifts | Check `0 <= n < width`. Left-shifting a negative signed value is done in unsigned | | Unsigned promotions | `uint8_t`/`uint16_t` arithmetic is promoted to `int`. All narrow arithmetic is performed as `uint32_t` and truncated explicitly | | Pointer arithmetic | Never form out-of-range pointers. Bounds checks compare indices before computing `ptr + i`. Raw-pointer arithmetic (M8a) is done on `uintptr_t`, and raw pointers have no ordering comparisons (`unsafe.md` §4) | | Strict aliasing | No type punning. Reinterpretation (`transmute`, unsafe only) goes through `memcpy` (a builtin; supplied by the runtime in freestanding builds) | | Uninitialised reads | Definite-init analysis forbids them. A local of aggregate type (struct, enum, array, slice, vector, …) is zero-initialised (`{0}`) in C when any part of it can be written other than by assigning it whole: a write to a field, element or payload, a borrow of it or of a part (`&x`, `x.e + i`, shared or mutable), or being an `Unpack` target. So is one whose variant payload is read (a `match` arm, an `Unpack` source): the arm for a variant the local never holds reads bytes its assigned variants do not cover, and gcc's `-Wmaybe-uninitialized` (with `-Werror`) rejects that at `-O0`, although the tag test means it never runs (found by the M9d fuzz campaign); so indeterminate members or padding are never copied. Every other aggregate local is assigned whole before each read (definite assignment, which the interpreter checks on every run), and is declared without an initializer (M9d: gcc 12 does not remove the zeroing, which was most of `fbvalid`'s gcc gap, `milestone-9.md`). Scalars and references are always zero-initialised; the C compilers drop those stores | | Null | References are never null (type system). `T?` niche checks are explicit | | Alignment | All accesses are through properly typed lvalues. `@packed` fields are accessed via `memcpy` helpers | | Lifetime of compound literals / temporaries | Not used across statements. Temporaries are named locals | | `memcpy` overlap | Moves use `memmove` when the source and destination may alias; NIR knows when they cannot | | Float→int conversion out of range (UB in C) | Only via `toIntSaturating`/`checkedCast` helpers that range-check first | | Warnings on valid programs (M7d) | CeeSafe accepts code that C compilers warn about, and generated C must still be warning-free. `x = x;` emits nothing (`-Wself-assign`). A comparison of a place with itself is the constant `true`/`false` (`-Wtautological-compare`). So is an ordering comparison against the type's own minimum or maximum, such as `u < 0` or `x <= MAX` (GCC's `-Wtype-limits`, found in Linux CI by `FuzzTests`). A constant index past a fixed array's end, after the check that always panics, goes through `cee_opaque_index` (`-Warray-bounds`). So does a constant slice or vector index too large for any object (index × element size > 2⁶³ − 1), found by the M7e campaign (seeds 1009815, 1014252, 1016777). In the structured form, statements that print nothing are dropped, so no label ends a block (`-Wc23-extensions`). Found by the random program generator; each case is in `run-pass/warning-free.cee` or `run-fail/constant-index-past-end.cee` | | Infinite side-effect-free loops (C11 6.8.5p6 permits assuming termination) | Loops whose controlling expression is not a constant expression and that have no side effects get a `ceesafe_loop_barrier()` (an empty `asm volatile` / volatile access) | ## 6. Runtime surface (`ceesafe/rt.h`) Heap (M3b): - `ceesafe_alloc(size, allocator)`, `ceesafe_realloc(p, size, allocator)` and `ceesafe_free(p, allocator)` go through the platform's `ceesafe_platform_alloc`/`ceesafe_platform_realloc`/`ceesafe_platform_free` (hosted: `malloc`/`realloc`/`free`, which also serve CosmoOS userland). They count attempts and live allocations; a reallocation is one attempt, and on failure its block is unchanged and still live. `ceesafe_fail_after`/`ceesafe_fail_reset` implement `FailingAllocator`. - **Threads** (M13 G4, type-system.md §38): `runtime/hosted/ceesafe_rt_thread.c`, linked only when the generated C calls `ceesafe_thread_*`, `ceesafe_mutex_*` or `ceesafe_atomic_*`. - `ceesafe_thread_start`/`join` run over POSIX threads, or Win32 threads on Windows. - Mutexes are allocated: `pthread_mutex_t`, or `SRWLOCK` on Windows. - The atomics are sequentially consistent: `__atomic` builtins, or `Interlocked*` under MSVC. - Thread records, environments and mutexes come from `ceesafe_alloc`, so fault injection reaches them. - The allocation counters change with relaxed atomic adds (`CEE_COUNT`), since any thread may allocate. - A `Thread` is `ceeTB_ *`, a record of `{ cee_thread *thread; void *data; const void *vt; T result; }`. - `ceeTS_` boxes a spawned move literal's environment and starts `ceeTR_`, which calls the body through the `dyn mut fn() -> T` table and stores the result. - `ceeTJ_` joins, takes the result, then drops and frees the environment and the record. A `Thread`'s drop glue joins, then drops the result. - `Thread.both(a, b)` (§38.6) is a block, not a call: it starts `ceeTF_` on `a` with `ceesafe_thread_start`, calls `b`, then joins; if the start returned `NULL` it calls `a` first, through `ceesafe_scratch_aside(ceeTF_, a)` (`a` gets an empty scratch stack, as on a thread of its own, §19.5), then `b`. `ceeTF_` takes the closure as `void *`, converts it back to its own type and calls it, so no function pointer is called through another type. A `fn()` argument is passed as the address of a local holding it, which lives until the join. - **Async** (L1, type-system.md §40): `runtime/hosted/ceesafe_rt_async.c`, linked only when the generated C calls `ceesafe_async_*`. - `ceesafe_async_run(frame, resume, info)` resumes the root frame until it returns. Between resumes it waits, in `poll()`, for what the parked task registered: a deadline on the monotonic clock (`ceesafe_async_wait_until`, from `ceesafe_async_now`) or a descriptor becoming writable (`ceesafe_async_wait_writable`). A task that parks with nothing registered aborts with a message. - `ceesafe_async_write(fd, bytes, length)` writes at most `PIPE_BUF` bytes if `fd` is writable now, after flushing standard output or error: a count, 0 if it is not writable, or a negated errno (ADR 0031 §3.5). - One executor per thread: the root task and the scopes' children run in turn from a queue; a parked task waits on what it registered or on its scope's last child (`ceesafe_async_spawn`, `ceesafe_async_scope_done`). A child's task and frame are one `ceesafe_alloc` block, so fault injection reaches it; `NULL` sets the scope's `oom`. - Freestanding (ADR 0039): the same file, compiled with `CEE_FREESTANDING`, keeps the queue, scopes, joins, shields, cancellation and `AsyncSignal`, and leaves out descriptors, reactors, operations, channels, workers and `Async.blocking`. Time, idling and waking are the environment's `ceesafe_env_*` hooks (profiles.md §4.2). The executor is reached through `ceesafe_env_slot()`, the wake order and the sweep's root are its fields, and there is no global state. Deadlines are found by walking the parked tasks. - `AsyncSignal` (ADR 0039): `ceesafe_async_signal_raise` sets `raised`, takes the waiting task and wakes it, `busy` meanwhile. A task that loses that race waits until the raise is done before it goes on, so no raise touches a task that has returned. - Work stealing (ADR 0038): with `ceesafe_async_workers(n)`, n > 1, the run starts `n − 1` threads, each with its own executor (reactor, timers, wake pipe), and joins them when the root returns. The run shares a queue of published `spawnSend` children, which a worker with nothing ready takes, oldest first. A started task stays on its worker. A scope with such children is shared: its count, first error and waiter change under the run's mutex. Its cancellation is atomic and is announced to every worker (an epoch each worker compares when it wakes). Its last child wakes the waiting parent through the parent's executor (`cee_wake_task`). Each worker counts the run as a reason to wait for a wake, so its non-blocking wake pipe is watched. The `tasks` command lists the stopped thread's executor. - Cancellation (L1 A2b): a task is cancelled if its own flag is set, or its scope's (a child failed or could not start), or the scope's owner is, transitively. `ceesafe_async_status()` is 0, 1 (cancelled) or 2 (inside `ceesafe_async_shield_enter(ms)` … `_leave()`, past its deadline; while shielded, cancellation reads as 0). A cancellation unparks the tasks it reaches that wait on a deadline, a descriptor or a cancellable wake; a shield's deadline wakes its task. It finds them without walking the executor's parked tasks (ADR 0062). Each scope lists its children that run on its worker, linked through `cee_task`'s siblings, and each task lists the scopes it owns from their first child until they are done (`ceesafe_scope`'s `outer`). A cancellation visits the scope's children and their scopes, depth first. A shared scope's `spawnSend` children are not listed: a visit that meets one walks every parked task, as the cancellation sweep and another worker's epoch do. `ceesafe_scope` is 48 bytes. - `CEESAFE_CANCEL_AT=k` cancels the running `Async.run`'s root task at the program's k-th suspension, and `CEESAFE_CANCEL_REPORT=1` prints `ceesafe cancel: points=N woken=W visited=V` after each `Async.run` (`testing.md`): the suspensions, the tasks cancellations woke, and the tasks they looked at. - `ceesafe_async_start(scope, slot, frame, resume, info)` starts a child in the caller's storage (`race`, `all`), and `ceesafe_async_cancel(scope)` cancels a scope's children. - For debuggers (L1 A3): `ceesafe_async_tasks()` describes the calling thread's tasks in a static buffer, one line each, `name at file:line (how)`: the running task, the ready ones, then the parked ones with what they wait on (their scope, a wake, a descriptor or a deadline). The line is where the task's own async fn last suspended (`ceeAL_[cee_state]`), and a child is indented two spaces per scope under its owner. `ceesafe_async_dump()` writes the same text to standard error. - Waking from another thread (A2c): a task parked on a wake has `woken` set atomically by the waking thread, which then writes a byte to its executor's wake pipe (opened on first need); `poll` watches the pipe while a task waits to be woken, and the executor readies every woken task. A task woken between its check and its park is readied at once. Programs with async link the thread runtime. - Channels (ADR 0032 §5): `ceesafe_chan_*`, element-blind, under a spin lock; a task's wait node lives in its frame, and every change wakes the queued tasks through their executors. - Readiness (A2d): a task waits for a descriptor to be readable or writable (`ceesafe_async_wait_readable`, `_wait_writable`); `poll()` is the reactor on every POSIX host, with any number of descriptors. Sockets are `ceesafe_async_tcp_*`, `ceesafe_async_read`, `_send`, `_shutdown_write` and `_close` in `ceesafe_rt_io.c`: a value, `CEE_ASYNC_WOULD_BLOCK`, or a negated errno. ADR 0044's addresses cross as rt.h's `ceesafe_endpoint` (family 4 or 6, port, scope, 16 bytes; lib/std passes its fields): `ceesafe_async_tcp_listen_at` (with a backlog and the `CEE_LISTEN_*` flags), `_connect_at`, `ceesafe_async_op_connect_at`, `ceesafe_async_socket_name` (the result `(family << 16) | port`), `ceesafe_async_socket_option` (`CEE_SOCKET_OPTION_*`), `ceesafe_async_shutdown_read` and `ceesafe_interface_index`; the blocking `TcpStream`'s setters are `ceesafe_tcp_option` and `ceesafe_tcp_shutdown_read`. The text calls parse IPv4 or IPv6 (`ceesafe_endpoint_parse`). IOCP's AcceptEx address buffer, sized for IPv6, shares the operation record with a read's WSABUF, so the record stays 192 bytes. `ceesafe_async_poll` serves code with its own wait loop (the interpreter). Woken tasks run in the order they were woken. - Portability (L1 A4a): the runtime's atomics are rt.h's `ceesafe_atomic_*` (§38.3: the `__atomic` builtins, or `_Interlocked*` under MSVC), and a block's alignment is `cee_max_align_t`. On Linux the file defines `_POSIX_C_SOURCE 200809L` before its includes: under `-std=c11` glibc hides `clock_gettime` otherwise. On Windows the wake pipe is an auto-reset event: a wake calls `SetEvent`, and an executor whose tasks wait to be woken waits in `WaitForSingleObject` until the next deadline. - Windows sockets (A4a): a task waiting on a socket has it signal the same event (`WSAEventSelect`, `FD_READ`, `FD_ACCEPT` and `FD_CLOSE`, or `FD_WRITE`, `FD_CONNECT` and `FD_CLOSE`); the executor then asks `WSAPoll` with no wait which are ready, and waits on the event only if none is. A socket that becomes ready between the poll and the wait has already signalled, so no readiness is lost. An unparked task's socket is detached (`WSAEventSelect(s, NULL, 0)`). A console or file descriptor has no readiness there, so its task retries at once. `ceesafe_async_poll` uses `WSAPoll` for sockets, as the interpreter needs. - The executor's structures (L1 A4b). Nothing a wait does walks the parked list, which only cancellation and `ceesafe_async_tasks` walk; it is doubly linked, so a task leaves it in constant time: - deadlines (a shield's end counting as one) are a binary min-heap, so due tasks run earliest first; - descriptor waiters are a table by descriptor, readers and writers each in the order they parked; - the tasks waiting to be woken are a list of their own. - The reactor (A4b): kqueue on macOS and the BSDs, epoll on Linux, created at the first wait. A parking task arms its descriptor and direction one-shot (kqueue `EV_ONESHOT`, queued and sent with the next `kevent` wait; epoll `EPOLLONESHOT` through `EPOLL_CTL_MOD`, or `ADD` for a descriptor it does not have). A readiness event readies every task waiting in that direction; each retries. A task that leaves early (cancelled, or due) disarms what no task waits for any more. One-shot arming is one call per park, and no state outlives the waits, so a descriptor closed and reopened under the same number needs nothing. epoll refuses regular files (`EPERM`): their tasks count as ready. A failed arming readies its tasks, whose retry sees the error. The wake pipe stays registered. `CEESAFE_REACTOR_POLL=1`, or a host with neither, uses `poll()` over the waited descriptors. - Windows (A4b): a waiting socket is selected for the executor's event for every network event once, when its task parks (`ceesafe_socket_select`; a call only when that changes), since two tasks may wait on one socket in opposite directions; the end of `Async.run` deselects them (`ceesafe_socket_unselect`) before closing the event. - Measured by `benchmarks/async-reactor/run.py` (idle connections whose both ends wait, and one-byte round trips on another; ROADMAP §10): the time per round trip stays flat with kqueue and epoll as the idle connections grow, and grows with them with `poll()` and on Windows. - **Operations** (ADR 0035): lib/std's socket operations (accept, connect, read, send) are steps over an `AsyncOp` record in the awaiting frame (`ceesafe_async_op`, 192 bytes): `ceesafe_async_op_read`, `_send`, `_accept` and `_connect` return the result once the operation is complete, or `CEE_ASYNC_PENDING` (park and step again). On a readiness backend a step is an attempt that registers the wait if it would block. On a completion backend the first step submits, and the task waits on the operation (`on_op`, counted apart from wakes, so no wait scans for it) until its completion readies it. `ceesafe_async_op_cancel` cancels an operation in flight (`CancelIoEx`, `IORING_OP_ASYNC_CANCEL`) and returns true: the task then waits, not cancellable, for the completion, so a lent buffer outlives the kernel's use (type-system.md §40.7). A value completed meanwhile is kept: the operation returns it, and the next one sees the cancellation. Otherwise it releases what the record holds (a half-made connection) and returns false. - **DNS** (ADR 0048): `ceesafe_dns_start` copies the name into a record of platform memory and queues it; at most `CEE_DNS_THREADS` (4) detached resolver threads take lookups first come first served, and a thread ends when the queue is empty. One lock guards the queue, each record's state and its waiter. - `ceesafe_dns_poll` registers the current task to be woken, cancellably, as a channel's wait is, and the thread wakes it under the lock when the lookup is done. - The resolver is in `ceesafe_rt_async.c`, which only programs that use async link; `ceesafe_rt_io.c`, which every hosted program links, gives it `ceesafe_winsock_ready` on Windows. - `ceesafe_dns_end` unregisters: a done or queued lookup is freed, a running one marked abandoned, which its thread frees without waking anyone. - The test hook `CEESAFE_DNS_DELAY_MS` delays each lookup (tools/dns_check.py). - **Message operations** (ADR 0045): a vectored send's record is lib/std's `AsyncMsgOp` (`ceesafe_async_msg_op`, 512 bytes), which starts with an operation's header, so `ceesafe_async_op_cancel` and the completion paths take it too. Before the first step `ceesafe_async_msg_part(op, index, bytes, length)` puts each non-empty part in it (at most `CEE_MSG_PARTS`, 8; each capped at 2^30 bytes, as a send's chunk); then `ceesafe_async_op_sendv(op, fd, count)` steps: `sendmsg` with `MSG_NOSIGNAL` (Linux) or `writev` (macOS and the BSDs, whose sockets have `SO_NOSIGPIPE`), an overlapped or plain `WSASend` with the record's `WSABUF`s (IOCP, `WSAPoll`), or `IORING_OP_SENDMSG` with the record's `msghdr`. CosmoOS userland sends the first non-empty part. The record also holds a peer address and a control buffer (128 bytes; 96 on Windows) for datagram messages (U6). - **Sending a file** (ADR 0051): `ceesafe_async_op_sendfile(op, socket, file, offset, length)`, with the plain operation record and `file` pointing at a `File` (its `int32_t` descriptor), steps: `sendfile(socket, fd, &offset, count)` on Linux (epoll, and io_uring through the same readiness step), each call at most 0x7FFFF000 bytes; `sendfile(fd, socket, offset, &len, NULL, 0)` on macOS, where `EAGAIN` with `len > 0` is a partial count; an overlapped `TransmitFile` (found with `WSAID_TRANSMITFILE`) on IOCP, the offset in the `OVERLAPPED` and at most 2^31 − 2 bytes a call, the handle `_get_osfhandle(fd)`. It returns `CEE_ASYNC_NO_SENDFILE` (`INT64_MIN + 2`) on `WSAPoll`, on CosmoOS, and where `sendfile` refuses the file (`EINVAL`, `ENOSYS`; macOS `ENOTSUP`, `ENOTSOCK`, `EOPNOTSUPP`): lib/std then copies through `ceesafe_file_read_at(file, buffer, length, offset)`, `pread` (at most 2^30 bytes) or a `ReadFile` with the offset in an `OVERLAPPED` on Windows, which every hosted program links (`ceesafe_rt_io.c`). The interpreter has no file send: it copies, reading at the offset by opening the file's path again (its stage 0 `File` cannot seek). - **Datagrams** (ADR 0050): `ceesafe_async_udp_bind(family, bytes, port, scope, flags, send_buffer, receive_buffer)` makes a nonblocking `SOCK_DGRAM` socket with the `CEE_UDP_*` options (`IP_PKTINFO` or `IP_RECVPKTINFO`, `IPV6_RECVPKTINFO`; `IP_RECVTOS`, `IPV6_RECVTCLASS`; Windows `IP_RECVECN`, `IPV6_RECVECN`, and `SIO_UDP_CONNRESET` off). macOS needs `__APPLE_USE_RFC_3542` for the IPv6 packet info. A send fills the message record's parts, then `ceesafe_async_msg_send_to` writes the destination (IPv4-mapped for an IPv6 socket) into the record's name and builds the control messages: packet info for a source address, `IP_TOS`/`IPV6_TCLASS` for ECN on Linux (`IP_ECN`/`IPV6_ECN` on Windows; elsewhere the socket's option is set for the send and cleared after it), and `UDP_SEGMENT` on Linux. `ceesafe_async_op_sendmsg` steps it: `sendmsg`, `IORING_OP_SENDMSG`, or `WSASendMsg` (overlapped on IOCP); `CEE_ASYNC_NO_SEGMENTS` (`INT64_MIN + 3`) where segments cannot go in one call, and lib/std sends them one by one. An empty datagram is one empty part. `ceesafe_async_op_recvmsg` receives into one part with the record's name and control buffer: `recvmsg`, `IORING_OP_RECVMSG`, or `WSARecvMsg` (found with `WSAID_WSARECVMSG`), where `WSAEMSGSIZE` is a truncated datagram, not an error. Then `ceesafe_async_msg_address` reads the peer from the name and the local address from the packet info, and `ceesafe_async_msg_flags` the truncation (`MSG_TRUNC`) and the ECN bits. `EMSGSIZE` is `InvalidInput`. Windows' control buffer is 72 bytes, so the record still fits 512. `ceesafe_async_udp_send` and `ceesafe_async_udp_recv` are one nonblocking `sendto`/`recvmsg` each, for the interpreter. - **IOCP** (Windows' default): each thread has one completion port for its life, because a socket can be tied to only one port. A socket is tied to its thread's port at its first operation (`ceesafe_socket_associate`). Operations are `WSARecv`, `WSASend`, `AcceptEx` (a new overlapped socket, given an id when it completes) and `ConnectEx` (bound, tied, then connected). The executor waits in `GetQueuedCompletionStatusEx`, and another thread's wake posts to the port. A socket tied to another thread's port is used by readiness steps, polled between slices of 1 ms. `CEESAFE_REACTOR_POLL=1` keeps `WSAPoll` and the event. - **io_uring** (Linux, opt-in with `CEESAFE_REACTOR_URING=1`): a ring set up through the raw system calls (no liburing), with a completion queue of 16384, signalling an eventfd in the epoll set, so the executor waits in epoll as always and drains the ring when the eventfd fires. `IORING_OP_RECV`, `_SEND` (`MSG_NOSIGNAL`), `_ACCEPT` (nonblocking) and `_CONNECT` (a new nonblocking socket, the address in the record). A submission refused with `EBUSY` (completions overflowed) is retried after a drain, and a drain that leaves `IORING_SQ_CQ_OVERFLOW` set flushes with `IORING_ENTER_GETEVENTS`. Deadlines, consoles and the wake pipe stay on epoll. A kernel that refuses the ring leaves epoll alone. - The interpreter's steps are readiness ones over its real sockets. - **Start and end: `ceesafe/embed.h`** (ADR 0088 §2.2), written beside `ceesafe/rt.h` (which includes it) by every build that writes headers; in a freestanding build its form has only `size` and `flags` (a `#define CEESAFE_EMBED_FREESTANDING 1` at its top), and `ceesafe_init` checks the size and keeps no state, since the freestanding core has no mutable global (profiles.md §4.4); its `ceesafe_init` ignores `flags`, none being defined. The generated `main` fills a `ceesafe_config` with its size, `CEESAFE_INIT_ENVIRONMENT` and `argc`/`argv`, calls `ceesafe_rt_init_main` (`rt.h`'s internal twin of `ceesafe_init`, which starts counting even after a C constructor allocated through CeeSafe, as before `ceesafe_init`; a failure other than `AGAIN` is a runtime panic), then the entry, then `ceesafe_shutdown()`, which prints the allocation report if asked, and returns the entry's code. `Process.exit` and panics skip the report. A host program calls the same two functions (type-system.md §28.6); nothing else starts the runtime. - `ceesafe_init` moves an atomic state word from unstarted to starting with a compare-and-swap. If another call is starting, it waits (spinning) for that call's end: `CEESAFE_INIT_AGAIN` once the state is started, so a caller that goes on to call CeeSafe after `AGAIN` sees the config, or a new compare-and-swap after an error. It checks the config's `size` (`CEESAFE_INIT_SIZE`: NULL, or a size that is not a version's), its `allocator` (`CEESAFE_INIT_ALLOCATOR`: none is taken until ADR 0088's stage B) and, when the config asks for counting (a report or failure injection), whether the platform's allocation functions have handed out a block (`CEESAFE_INIT_LATE`: a relaxed flag they set; without counting an earlier block changes nothing), copies `argv` into one uncounted block from the C library (`CEESAFE_INIT_MEMORY` if it cannot), sets the counting configuration and the panic hook, and publishes them with the store of started. On an error it stores unstarted back and changes nothing. `ceesafe_shutdown` returns 0 and prints nothing unless it loads started. - **Panics** go through one path (`ceesafe_panic`, `ceesafe_panic_text`, and `ceesafe_panic_runtime` for the runtime's own aborts, which have no CeeSafe location): on the thread's first panic it sets `ceesafe_any_panicked` and the thread's panicked flag (`_Thread_local`), keeps a copy of the panic (the message's first 256 bytes in a thread-local buffer, since a message built at run time lives in a frame the re-entry line outlives), and calls the host's hook with the original, if `ceesafe_init` installed one; several threads may be in the hook at once; if the hook returns, or on a later panic of that thread, it prints the line and calls `ceesafe_platform_abort`, which nothing else calls. The executor's three aborts (its bookkeeping out of memory, a task waiting on a descriptor and a wake, every task parked with nothing to wake it) are `ceesafe_panic_runtime(CEE_PANIC_ASYNC, message)`, printed as `ceesafe panic: `. - The allocation counters are kept only when something reads them (ADR 0059): `CEESAFE_ALLOC_REPORT`, `CEESAFE_FAIL_ALLOC_AT`, or `ceesafec test`'s `ceesafe_test_begin`. That is decided before the program's first allocation and never changes afterwards. Without any of them, an allocation, reallocation or free touches no counter, and so makes no atomic operation. - `ceesafe_panic(kind, src)`: `noreturn`. Provided by the profile: hosted (the host's panic hook if one was given to `ceesafe_init`, else write to stderr + `abort()`), CosmoOS userland (same, through CosmoOS libc), kernel (calls the kernel panic routine; C5). - Checked-arithmetic helpers (`static inline`). - `ceesafe_mem_copy/move/set` (M9b): `__builtin_memcpy`, `__builtin_memmove` and `__builtin_memset` under clang and gcc, which may become calls to the C library's or the kernel's `memcpy`, `memmove` and `memset`; ``'s functions under MSVC. Used by the bulk slice operations (§2) and by vector growth. - Allocator vtable ABI for dynamic allocators (`memory-model.md` §3.2). - Source locations (M9d). A location is an `cee_loc`: the CeeSafe path as written to the compiler (a file table `cee_file_`, so no absolute host path is embedded unless one was given), and the 1-based line and column. Each location a checked helper (`cee_add_*`, `cee_check_index`, …) can panic with is a file-scope constant `static const cee_loc ceeL_`, one per distinct location, in the order of first use; the helper takes `const cee_loc *` and reads it only when the check fails. A direct `ceesafe_panic` passes the location by value (`CEE_LOC(file, line, column)`), and `ceesafe_panic` itself still takes `cee_loc` by value (`profiles.md` §4.2). Why a pointer: with the location by value, gcc 12 at `-O2` loads each check's location into registers on the path where the check passes, about half of what the checks cost in `data-oriented` (`milestone-9.md`, 9d). The table holds pointers to the file names, so it is in `.rodata` (`.data.rel.ro` in position-independent code), with absolute 64-bit relocations, which the CosmoOS module loader applies (`cosmoos/c4-report.md`). ## 7. Debugging and source maps (M7a) - **`#line` directives** map generated C back to CeeSafe source. They are off by default and turned on by `--line-directives`; `ceesafec build -g` turns them on and compiles the C with `-g -O0` instead of `-O2`. - With directives on, each function body maps to CeeSafe lines: - the function's opening line maps to the line of its name; - each statement and terminator maps to the first line of the source construct it came from (its span). A directive is written only when the line or file changes. Code without a span of its own, such as compiler temporaries, keeps the line before it. - A block's label shares the line of the block's first statement, so labels need no directive of their own. The local declarations share one line, attributed to the function's name. - After each function, `#line "main.c"` hands the following lines back to the C file. Everything outside function bodies (types, drop glue, helpers, `main`) is attributed to `main.c`. - **Paths.** Directives and panic locations name a source by its *source path*. This is the path given to the compiler, with `/` separators. The CLI rewrites an absolute path under the current directory to a relative one. Any other absolute path is reduced to its file name. A dependency's file is named by its package and its path in it, `alpha/src/util.cee` (ADR 0082 §2.1), wherever the package is. Generated C therefore never contains a host path (`build-integration.md` §6). - `ceesafec build` runs the C compiler with the current directory as its working directory. The debug info's compilation directory then makes the source paths resolve, so a debugger finds the `.cee` files of a build of files and of the root package (run from its directory). It does not find a dependency's: `alpha/src/util.cee` is relative to the root package, not to the current directory, so the debugger needs a source map (lldb's `target.source-map`, gdb's `directory`) until a later stage of ADR 0082 fixes it (ADR 0082 §7). - Locals keep stable names: `v__` for a named local, `t_` for a temporary. A C debugger therefore shows recognisable variables (`v_count_3`). - **Temporaries and debug info.** Without directives, a temporary is declared `CEE_TEMP` (`rt.h`): under clang `__attribute__((unused, nodebug))`, under GCC `unused` (it has no `nodebug` for variables), elsewhere nothing. An optimised build otherwise gives each of a function's temporaries a debug variable with a location list over the whole function, repeated at every inlined call; in CosmoOS's kernel, built `-O2 -g`, the location lists were 4.8 bytes per byte of the generated C's code, against 0.6 for its hand-written C, and most of the kernel's 12 MB (2026-11-05). `nodebug` changes no code. With directives (`ceesafec build -g`), temporaries stay `CEE_UNUSED` and visible to a debugger. Named locals are visible either way. - Directives never change the code itself. A program with directives must behave exactly like one without them. The run corpus is built both ways (§8). ## 7a. Implementation notes (M1, increment 1c) - Every CeeSafe function becomes `static CEE_UNUSED` with a prototype. CeeSafe allows unused functions, parameters and locals, and `-Wunused-*` must stay clean under `-Werror`. - Locals are declared at the top of the function, zero-initialised as in §5 (scalars only in M1); a temporary is `CEE_TEMP` rather than `CEE_UNUSED` without directives (§7). Only blocks that are jump targets get a label. - Runtime files: - `ceesafe/rt.h`: freestanding checked helpers `cee_add_i32`, `cee_div_u64`, `cee_shl_u8`, `cee_shift_amount_s/u`, `cee_as_iN`, …; - `ceesafe_rt_core.c`: output formatting and the panic message, with no libc; - `ceesafe_rt_hosted.c`: the platform layer over stdio. `ceesafec build --emit c` writes all of them next to `main.c`. Build with `cc -I /*.c`. - Panic output: `ceesafe panic: at ::`. Stdout is flushed first, then `abort()` (exit status 134). - `main() -> i32` becomes `int main(void) { return (int)ceeF_main(); }` (M7d renamed `cee_` to `ceeF_`, §2.1). `Process.exit(c)` calls `exit(c)`, which flushes stdio. ## 8. Validation - Golden-file tests for representative C output (mainly to review diffs). - Every `run-pass` test is compiled with clang and gcc (host) at `-O0` and `-O2`, with and without `-fsanitize=address,undefined`. Outputs must agree (differential testing). - M7a: every run-corpus program is also built with `#line` directives (clang `-O0 -g`) and must produce the same results. `tools/debugger_check.py` builds a program with `-g` and checks that a debugger stops at its `.cee` lines (`tooling.md` §3.4). - M7: a Csmith-style random program generator for CeeSafe; three-way differential testing (clang/gcc × opt levels), plus a reference interpreter on NIR (`ceesafec run --interp`) as an oracle (`milestone-7.md`). # lib/core - [struct PhysAddr](#struct-physaddr) - [struct VirtAddr](#struct-virtaddr) - [struct Layout](#struct-layout) - [interface Allocator](#interface-allocator) - [enum AsyncError](#enum-asyncerror) - [struct Timer](#struct-timer) - [struct AsyncControl](#struct-asynccontrol) - [struct AsyncLeaf](#struct-asyncleaf) - [struct Raced](#struct-raced) - [struct TaskSet](#struct-taskset) - [struct AsyncTasks](#struct-asynctasks) - [struct AsyncTasksIn](#struct-asynctasksin) - [struct AsyncSignal](#struct-asyncsignal) - [enum LoadOrdering](#enum-loadordering) - [enum StoreOrdering](#enum-storeordering) - [enum Ordering](#enum-ordering) - [struct AtomicU64](#struct-atomicu64) - [struct AtomicU32](#struct-atomicu32) - [struct AtomicU8](#struct-atomicu8) - [struct AtomicUSize](#struct-atomicusize) - [struct AtomicBool](#struct-atomicbool) - [struct ByteOrder](#struct-byteorder) - [struct VectorBytes](#struct-vectorbytes) - [struct RawCell](#struct-rawcell) - [struct Cell](#struct-cell) - [struct RefCell](#struct-refcell) - [struct RefCellTaking](#struct-refcelltaking) - [struct CellRef](#struct-cellref) - [struct CellRefMut](#struct-cellrefmut) - [struct CStr](#struct-cstr) - [interface DerivedEqual](#interface-derivedequal) - [struct DerivedEqualElements](#struct-derivedequalelements) - [struct TextWriter](#struct-textwriter) - [enum TextAlign](#enum-textalign) - [struct Formatted](#struct-formatted) - [interface AppendFormatted](#interface-appendformatted) - [interface Iterator](#interface-iterator) - [struct Volatile](#struct-volatile) - [struct MmioPtr](#struct-mmioptr) - [struct NumberParsing](#struct-numberparsing) - [struct ResultMethods](#struct-resultmethods) - [struct OptionalResults](#struct-optionalresults) - [struct SliceHalves](#struct-slicehalves) - [struct SliceHalvesMut](#struct-slicehalvesmut) - [struct SliceSplitting](#struct-slicesplitting) - [struct SliceAlgorithms](#struct-slicealgorithms) - [struct VectorEditing](#struct-vectorediting) - [struct TextHalves](#struct-texthalves) - [struct ByteHalves](#struct-bytehalves) - [struct TextSplit](#struct-textsplit) - [struct ByteSplit](#struct-bytesplit) - [struct TextSearch](#struct-textsearch) - [struct ByteSearch](#struct-bytesearch) ## struct PhysAddr ```ceesafe public struct PhysAddr ``` A physical address. ### PhysAddr.of ```ceesafe public fn of(bits: u64) -> PhysAddr ``` The address whose value is `bits`. ### PhysAddr.bits ```ceesafe public fn bits(this) -> u64 ``` The address's value. ### PhysAddr.offset ```ceesafe public fn offset(this, n: u64) -> PhysAddr ``` The address `n` bytes further on; panics if that overflows a `u64`. ## struct VirtAddr ```ceesafe public struct VirtAddr ``` A virtual address. ### VirtAddr.of ```ceesafe public fn of(bits: u64) -> VirtAddr ``` The address whose value is `bits`. ### VirtAddr.bits ```ceesafe public fn bits(this) -> u64 ``` The address's value. ### VirtAddr.offset ```ceesafe public fn offset(this, n: u64) -> VirtAddr ``` The address `n` bytes further on; panics if that overflows a `u64`. ## struct Layout ```ceesafe public struct Layout ``` The size in bytes and the alignment (a power of two) of a block of memory. ## interface Allocator ```ceesafe public interface Allocator { fn allocate(borrow this, layout: Layout) -> Result<*mut u8, AllocError>; unsafe fn deallocate(borrow this, p: *mut u8, layout: Layout); } ``` An allocator a `Box` can use: a struct without fields that implements this (type-system.md §30.2). ## enum AsyncError ```ceesafe public enum AsyncError { Cancelled, TimedOut, Io(IoError), } ``` Why an async operation failed (type-system.md §40.4, §40.7). ## struct Timer ```ceesafe public struct Timer ``` Time for tasks: the monotonic clock in milliseconds. ### Timer.now ```ceesafe public fn now() -> u64 ``` Milliseconds on the monotonic clock, from an arbitrary start. Safety: ceesafe_async_now only reads the clock. ### Timer.sleep ```ceesafe public async fn sleep(ms: u64) -> Result ``` Waits at least `ms` milliseconds without blocking the thread: other tasks run meanwhile. Safety: ceesafe_async_wait_until only records the deadline for the executor. A cancelled task's sleep returns `Err(AsyncError.Cancelled)` at once (type-system.md §40.7). ## struct AsyncControl ```ceesafe public struct AsyncControl ``` What `Async.isCancelled()` and `Async.checkpoint(cancelled)` call (type-system.md §40.4). ### AsyncControl.isCancelled ```ceesafe public fn isCancelled() -> bool ``` Whether the current task is cancelled and not shielded. Safety: ceesafe_async_status only reads the executor's state. ### AsyncControl.enterShield ```ceesafe public fn enterShield(ms: u64) ``` `Async.shield`'s start (type-system.md §40.7). Safety: it only counts the current task's shields. ### AsyncControl.leaveShield ```ceesafe public fn leaveShield() ``` `Async.shield`'s end. Safety: it only counts the current task's shields, after enterShield. ### AsyncControl.checkpoint ```ceesafe public fn checkpoint(cancelled: E) -> Result ``` `Err(cancelled)` if the current task is cancelled and not shielded, else `Ok`. ## struct AsyncLeaf ```ceesafe public struct AsyncLeaf ``` The library's own steps for its waiting operations: not for programs (type-system.md §40.4). ### AsyncLeaf.status ```ceesafe public fn status() -> Result ``` Err(Cancelled) if the current task is cancelled, Err(TimedOut) if its shield's time is up (type-system.md §40.7). Safety: ceesafe_async_status only reads the executor's state. ## struct Raced ```ceesafe public struct Raced { public winner: u32; public value: T; public other: T?; } ``` `Async.race`'s result (type-system.md §40.4): the child that returned `Ok` first (0 or 1), its value, and the other's value if it also returned `Ok` before it saw its cancellation. ## struct TaskSet ```ceesafe @noncopy // the one handle of its record public struct TaskSet ``` The set `Async.tasks`' body is given (ADR 0053 §3.1). The body, and the functions it passes the set to, start children with `await set.spawn(async move () => …)`, which waits while the set's limit of children run, and `set.trySpawn(async move () => …)`, which is false instead. A child owns what it captures: no loans, so it cannot capture the set (N0349). The calls are the checker's (src/check/async.cee): these are the steps it joins. ## struct AsyncTasks ```ceesafe public struct AsyncTasks ``` `await Async.tasks(limit, oom, async (set) => …)` (ADR 0053): `Async.tasksIn` with the system heap. ### AsyncTasks.run ```ceesafe @effectsOf(body) public async fn run( limit: u32, oom: E, body: borrow mut fn(borrow TaskSet) -> Result, ) -> Result ``` ## struct AsyncTasksIn ```ceesafe public struct AsyncTasksIn ``` `await Async.tasksIn(alloc, limit, oom, async (set) => …)` (ADR 0053): the body runs in the awaiting task, as the set's owner, with the set; then every child it started is waited for. The children's tasks and frames come from `alloc`'s allocator type. The result is the body's error, else the first child's, else `oom` if a child's frame could not be allocated, else Ok. The body's error cancels the children; a child's error cancels no sibling. ### AsyncTasksIn.run ```ceesafe @effectsOf(body) public async fn run( alloc: A, limit: u32, oom: E, body: borrow mut fn(borrow TaskSet) -> Result, ) -> Result ``` Safety: the record is this frame's, which outlives every child: the set waits for them before it returns. ## struct AsyncSignal ```ceesafe @noncopy // one signal, shared: a copy would never be raised public struct AsyncSignal ``` A one-shot wake-up from anywhere, to a task (ADR 0039): `await signal.wait()` parks the task until `signal.raise()`. `raise` has no effects, so an interrupt handler (`@context(irq)`) or another thread may call it, the way a driver tells the task waiting for its device. A raise before the wait is kept: the wait then returns at once. One task waits on a signal at a time. ### AsyncSignal.create ```ceesafe public fn create() -> AsyncSignal ``` A signal not raised. ### AsyncSignal.constant ```ceesafe @constant public fn constant() -> AsyncSignal ``` The same as create, in a static's initialiser (type-system.md §39.4): `static READY: AsyncSignal = AsyncSignal.constant();`, which an interrupt handler can raise. ### AsyncSignal.raise ```ceesafe @effects() public fn raise(borrow this) ``` Raises the signal: the task waiting on it, if any, runs again. Safety: the runtime changes the record atomically, and only wakes the task it names. ### AsyncSignal.wait ```ceesafe public async fn wait(borrow this) -> Result ``` Waits until the signal is raised, then lowers it. A cancelled task returns `Err(AsyncError.Cancelled)` and no longer waits (type-system.md §40.7). Safety: the record names this task only while it waits here; ceesafe_async_signal_forget takes it out. ## enum LoadOrdering ```ceesafe public enum LoadOrdering { Relaxed, Acquire, SequentiallyConsistent, } ``` The ordering of an atomic load (ADR 0021): C11's memory_order_relaxed, _acquire and _seq_cst. ## enum StoreOrdering ```ceesafe public enum StoreOrdering { Relaxed, Release, SequentiallyConsistent, } ``` The ordering of an atomic store: C11's memory_order_relaxed, _release and _seq_cst. ## enum Ordering ```ceesafe public enum Ordering { Relaxed, Acquire, Release, AcquireRelease, SequentiallyConsistent, } ``` The ordering of an atomic read-modify-write (swap, compare-exchange, add, subtract): C11's memory_order_relaxed, _acquire, _release, _acq_rel and _seq_cst. A compare-exchange that fails orders as a load: AcquireRelease as Acquire, Release as Relaxed, the others as themselves. ## struct AtomicU64 ```ceesafe @threadSafe @noncopy public struct AtomicU64 ``` A u64 that threads may read and change at once: every operation is atomic and sequentially consistent. ### AtomicU64.create ```ceesafe public fn create(value: u64) -> AtomicU64 ``` A new atomic holding `value`. ### AtomicU64.constant ```ceesafe @constant public fn constant(value: u64) -> AtomicU64 ``` The same as create, in a static's initialiser (type-system.md §39.4): `static N: AtomicU64 = AtomicU64.constant(0);`. ### AtomicU64.load ```ceesafe public fn load(borrow this) -> u64 ``` Safety: the runtime accesses `value` only atomically, and safe code reaches it only through these methods. ### AtomicU64.store ```ceesafe public fn store(borrow this, value: u64) ``` Safety: as load. ### AtomicU64.swap ```ceesafe public fn swap(borrow this, value: u64) -> u64 ``` Safety: as load. ### AtomicU64.compareExchange ```ceesafe public fn compareExchange(borrow this, expected: u64, desired: u64) -> Result ``` Ok(the old value) if it was `expected` and is now `desired`; else Err(the value it has). Safety: as load. ### AtomicU64.fetchAdd ```ceesafe public fn fetchAdd(borrow this, value: u64) -> u64 ``` Adds, wrapping; returns the old value. Safety: as load. ### AtomicU64.fetchSub ```ceesafe public fn fetchSub(borrow this, value: u64) -> u64 ``` Subtracts, wrapping; returns the old value. Safety: as load. ### AtomicU64.fetchOr ```ceesafe public fn fetchOr(borrow this, value: u64) -> u64 ``` Sets the bits of `value`; the value before (ADR 0041). Safety: as load. ### AtomicU64.fetchAnd ```ceesafe public fn fetchAnd(borrow this, value: u64) -> u64 ``` Keeps only the bits of `value`; the value before (ADR 0041). Safety: as load. ### AtomicU64.loadWith ```ceesafe public fn loadWith(borrow this, order: LoadOrdering) -> u64 ``` load, with an ordering (ADR 0021). Safety: as load. ### AtomicU64.storeWith ```ceesafe public fn storeWith(borrow this, value: u64, order: StoreOrdering) ``` store, with an ordering. Safety: as load. ### AtomicU64.swapWith ```ceesafe public fn swapWith(borrow this, value: u64, order: Ordering) -> u64 ``` swap, with an ordering. Safety: as load. ### AtomicU64.compareExchangeWith ```ceesafe public fn compareExchangeWith(borrow this, expected: u64, desired: u64, order: Ordering) -> Result ``` compareExchange, with an ordering; a failure orders as Ordering says. Safety: as load. ### AtomicU64.fetchAddWith ```ceesafe public fn fetchAddWith(borrow this, value: u64, order: Ordering) -> u64 ``` fetchAdd, with an ordering. Safety: as load. ### AtomicU64.fetchSubWith ```ceesafe public fn fetchSubWith(borrow this, value: u64, order: Ordering) -> u64 ``` fetchSub, with an ordering. Safety: as load. ### AtomicU64.fetchOrWith ```ceesafe public fn fetchOrWith(borrow this, value: u64, order: Ordering) -> u64 ``` fetchOr, with an ordering. Safety: as load. ### AtomicU64.fetchAndWith ```ceesafe public fn fetchAndWith(borrow this, value: u64, order: Ordering) -> u64 ``` fetchAnd, with an ordering. Safety: as load. ## struct AtomicU32 ```ceesafe @threadSafe @noncopy public struct AtomicU32 ``` A u32 that threads may read and change at once, as AtomicU64. ### AtomicU32.create ```ceesafe public fn create(value: u32) -> AtomicU32 ``` A new atomic holding `value`. ### AtomicU32.constant ```ceesafe @constant public fn constant(value: u32) -> AtomicU32 ``` The same as create, in a static's initialiser (type-system.md §39.4): `static N: AtomicU32 = AtomicU32.constant(0);`. ### AtomicU32.load ```ceesafe public fn load(borrow this) -> u32 ``` Safety: as AtomicU64.load. ### AtomicU32.store ```ceesafe public fn store(borrow this, value: u32) ``` Safety: as AtomicU64.load. ### AtomicU32.swap ```ceesafe public fn swap(borrow this, value: u32) -> u32 ``` Safety: as AtomicU64.load. ### AtomicU32.compareExchange ```ceesafe public fn compareExchange(borrow this, expected: u32, desired: u32) -> Result ``` Safety: as AtomicU64.load. ### AtomicU32.fetchAdd ```ceesafe public fn fetchAdd(borrow this, value: u32) -> u32 ``` Safety: as AtomicU64.load. ### AtomicU32.fetchSub ```ceesafe public fn fetchSub(borrow this, value: u32) -> u32 ``` Safety: as AtomicU64.load. ### AtomicU32.fetchOr ```ceesafe public fn fetchOr(borrow this, value: u32) -> u32 ``` Sets the bits of `value`; the value before (ADR 0041). Safety: as load. ### AtomicU32.fetchAnd ```ceesafe public fn fetchAnd(borrow this, value: u32) -> u32 ``` Keeps only the bits of `value`; the value before (ADR 0041). Safety: as load. ### AtomicU32.loadWith ```ceesafe public fn loadWith(borrow this, order: LoadOrdering) -> u32 ``` load, with an ordering (ADR 0021). Safety: as load. ### AtomicU32.storeWith ```ceesafe public fn storeWith(borrow this, value: u32, order: StoreOrdering) ``` store, with an ordering. Safety: as load. ### AtomicU32.swapWith ```ceesafe public fn swapWith(borrow this, value: u32, order: Ordering) -> u32 ``` swap, with an ordering. Safety: as load. ### AtomicU32.compareExchangeWith ```ceesafe public fn compareExchangeWith(borrow this, expected: u32, desired: u32, order: Ordering) -> Result ``` compareExchange, with an ordering; a failure orders as Ordering says. Safety: as load. ### AtomicU32.fetchAddWith ```ceesafe public fn fetchAddWith(borrow this, value: u32, order: Ordering) -> u32 ``` fetchAdd, with an ordering. Safety: as load. ### AtomicU32.fetchSubWith ```ceesafe public fn fetchSubWith(borrow this, value: u32, order: Ordering) -> u32 ``` fetchSub, with an ordering. Safety: as load. ### AtomicU32.fetchOrWith ```ceesafe public fn fetchOrWith(borrow this, value: u32, order: Ordering) -> u32 ``` fetchOr, with an ordering. Safety: as load. ### AtomicU32.fetchAndWith ```ceesafe public fn fetchAndWith(borrow this, value: u32, order: Ordering) -> u32 ``` fetchAnd, with an ordering. Safety: as load. ## struct AtomicU8 ```ceesafe @threadSafe @noncopy public struct AtomicU8 ``` A u8 that threads may read and change at once, as AtomicU32: for a byte member that C reads and writes atomically (ADR 0080 §3.5), such as a process's flag. ### AtomicU8.create ```ceesafe public fn create(value: u8) -> AtomicU8 ``` A new atomic holding `value`. ### AtomicU8.constant ```ceesafe @constant public fn constant(value: u8) -> AtomicU8 ``` The same as create, in a static's initialiser (type-system.md §39.4). ### AtomicU8.load ```ceesafe public fn load(borrow this) -> u8 ``` Safety: as AtomicU64.load. ### AtomicU8.store ```ceesafe public fn store(borrow this, value: u8) ``` Safety: as AtomicU64.load. ### AtomicU8.swap ```ceesafe public fn swap(borrow this, value: u8) -> u8 ``` Safety: as AtomicU64.load. ### AtomicU8.compareExchange ```ceesafe public fn compareExchange(borrow this, expected: u8, desired: u8) -> Result ``` Safety: as AtomicU64.load. ### AtomicU8.loadWith ```ceesafe public fn loadWith(borrow this, order: LoadOrdering) -> u8 ``` load, with an ordering (ADR 0021). Safety: as load. ### AtomicU8.storeWith ```ceesafe public fn storeWith(borrow this, value: u8, order: StoreOrdering) ``` store, with an ordering. Safety: as load. ## struct AtomicUSize ```ceesafe @threadSafe @noncopy public struct AtomicUSize ``` A usize that threads may read and change at once, as AtomicU64 (usize is 64 bits on every target, §29.3). ### AtomicUSize.create ```ceesafe public fn create(value: usize) -> AtomicUSize ``` A new atomic holding `value`. ### AtomicUSize.constant ```ceesafe @constant public fn constant(value: usize) -> AtomicUSize ``` The same as create, in a static's initialiser (type-system.md §39.4). ### AtomicUSize.load ```ceesafe public fn load(borrow this) -> usize ``` The value, sequentially consistent. ### AtomicUSize.store ```ceesafe public fn store(borrow this, value: usize) ``` Stores `value`, sequentially consistent. ### AtomicUSize.swap ```ceesafe public fn swap(borrow this, value: usize) -> usize ``` Stores `value` and returns the old value, sequentially consistent. ### AtomicUSize.compareExchange ```ceesafe public fn compareExchange(borrow this, expected: usize, desired: usize) -> Result ``` Ok(the old value) if it was `expected` and is now `desired`; else Err(the value it has). ### AtomicUSize.fetchAdd ```ceesafe public fn fetchAdd(borrow this, value: usize) -> usize ``` Adds, wrapping; returns the old value. ### AtomicUSize.fetchSub ```ceesafe public fn fetchSub(borrow this, value: usize) -> usize ``` Subtracts, wrapping; returns the old value. ### AtomicUSize.fetchOr ```ceesafe public fn fetchOr(borrow this, value: usize) -> usize ``` As fetchOr. ### AtomicUSize.fetchAnd ```ceesafe public fn fetchAnd(borrow this, value: usize) -> usize ``` As fetchAnd. ### AtomicUSize.loadWith ```ceesafe public fn loadWith(borrow this, order: LoadOrdering) -> usize ``` As load, with `order`. ### AtomicUSize.storeWith ```ceesafe public fn storeWith(borrow this, value: usize, order: StoreOrdering) ``` As store, with `order`. ### AtomicUSize.swapWith ```ceesafe public fn swapWith(borrow this, value: usize, order: Ordering) -> usize ``` As swap, with `order`. ### AtomicUSize.compareExchangeWith ```ceesafe public fn compareExchangeWith( borrow this, expected: usize, desired: usize, order: Ordering, ) -> Result ``` As compareExchange, with `order` (a failure orders as a load, §38.3). ### AtomicUSize.fetchAddWith ```ceesafe public fn fetchAddWith(borrow this, value: usize, order: Ordering) -> usize ``` As fetchAdd, with `order`. ### AtomicUSize.fetchSubWith ```ceesafe public fn fetchSubWith(borrow this, value: usize, order: Ordering) -> usize ``` As fetchSub, with `order`. ### AtomicUSize.fetchOrWith ```ceesafe public fn fetchOrWith(borrow this, value: usize, order: Ordering) -> usize ``` As fetchOrWith. ### AtomicUSize.fetchAndWith ```ceesafe public fn fetchAndWith(borrow this, value: usize, order: Ordering) -> usize ``` As fetchAndWith. ## struct AtomicBool ```ceesafe @threadSafe @noncopy public struct AtomicBool ``` A bool that threads may read and change at once. ### AtomicBool.create ```ceesafe public fn create(value: bool) -> AtomicBool ``` A new atomic holding `value`. ### AtomicBool.constant ```ceesafe @constant public fn constant(value: bool) -> AtomicBool ``` The same as create, in a static's initialiser (type-system.md §39.4). ### AtomicBool.load ```ceesafe public fn load(borrow this) -> bool ``` The value, sequentially consistent. ### AtomicBool.store ```ceesafe public fn store(borrow this, value: bool) ``` Stores `value`, sequentially consistent. ### AtomicBool.swap ```ceesafe public fn swap(borrow this, value: bool) -> bool ``` Stores `value` and returns the old value, sequentially consistent. ### AtomicBool.compareExchange ```ceesafe public fn compareExchange(borrow this, expected: bool, desired: bool) -> Result ``` Ok(the old value) if it was `expected` and is now `desired`; else Err(the value it has). ### AtomicBool.loadWith ```ceesafe public fn loadWith(borrow this, order: LoadOrdering) -> bool ``` As load, with `order`. ### AtomicBool.storeWith ```ceesafe public fn storeWith(borrow this, value: bool, order: StoreOrdering) ``` As store, with `order`. ### AtomicBool.swapWith ```ceesafe public fn swapWith(borrow this, value: bool, order: Ordering) -> bool ``` As swap, with `order`. ### AtomicBool.compareExchangeWith ```ceesafe public fn compareExchangeWith(borrow this, expected: bool, desired: bool, order: Ordering) -> Result ``` As compareExchange, with `order` (a failure orders as a load, §38.3). ## struct ByteOrder ```ceesafe public struct ByteOrder ``` Reading and writing integers in a byte order: static functions; the struct has no fields and is never made. ### ByteOrder.readU8BE ```ceesafe public fn readU8BE(s: Slice, at: usize) -> u8 ``` The `u8` in the 1 byte at `at`, big-endian. ### ByteOrder.readI8BE ```ceesafe public fn readI8BE(s: Slice, at: usize) -> i8 ``` The `i8` in the 1 byte at `at`, big-endian, in two's complement. ### ByteOrder.writeU8BE ```ceesafe public fn writeU8BE(s: SliceMut, at: usize, x: u8) ``` Writes `x` to the 1 byte at `at`, big-endian. ### ByteOrder.writeI8BE ```ceesafe public fn writeI8BE(s: SliceMut, at: usize, x: i8) ``` Writes `x` to the 1 byte at `at`, big-endian, in two's complement. ### ByteOrder.readU8LE ```ceesafe public fn readU8LE(s: Slice, at: usize) -> u8 ``` The `u8` in the 1 byte at `at`, little-endian. ### ByteOrder.readI8LE ```ceesafe public fn readI8LE(s: Slice, at: usize) -> i8 ``` The `i8` in the 1 byte at `at`, little-endian, in two's complement. ### ByteOrder.writeU8LE ```ceesafe public fn writeU8LE(s: SliceMut, at: usize, x: u8) ``` Writes `x` to the 1 byte at `at`, little-endian. ### ByteOrder.writeI8LE ```ceesafe public fn writeI8LE(s: SliceMut, at: usize, x: i8) ``` Writes `x` to the 1 byte at `at`, little-endian, in two's complement. ### ByteOrder.readU16BE ```ceesafe public fn readU16BE(s: Slice, at: usize) -> u16 ``` The `u16` in the 2 bytes at `at`, big-endian. ### ByteOrder.readI16BE ```ceesafe public fn readI16BE(s: Slice, at: usize) -> i16 ``` The `i16` in the 2 bytes at `at`, big-endian, in two's complement. ### ByteOrder.writeU16BE ```ceesafe public fn writeU16BE(s: SliceMut, at: usize, x: u16) ``` Writes `x` to the 2 bytes at `at`, big-endian. ### ByteOrder.writeI16BE ```ceesafe public fn writeI16BE(s: SliceMut, at: usize, x: i16) ``` Writes `x` to the 2 bytes at `at`, big-endian, in two's complement. ### ByteOrder.readU16LE ```ceesafe public fn readU16LE(s: Slice, at: usize) -> u16 ``` The `u16` in the 2 bytes at `at`, little-endian. ### ByteOrder.readI16LE ```ceesafe public fn readI16LE(s: Slice, at: usize) -> i16 ``` The `i16` in the 2 bytes at `at`, little-endian, in two's complement. ### ByteOrder.writeU16LE ```ceesafe public fn writeU16LE(s: SliceMut, at: usize, x: u16) ``` Writes `x` to the 2 bytes at `at`, little-endian. ### ByteOrder.writeI16LE ```ceesafe public fn writeI16LE(s: SliceMut, at: usize, x: i16) ``` Writes `x` to the 2 bytes at `at`, little-endian, in two's complement. ### ByteOrder.readU32BE ```ceesafe public fn readU32BE(s: Slice, at: usize) -> u32 ``` The `u32` in the 4 bytes at `at`, big-endian. ### ByteOrder.readI32BE ```ceesafe public fn readI32BE(s: Slice, at: usize) -> i32 ``` The `i32` in the 4 bytes at `at`, big-endian, in two's complement. ### ByteOrder.writeU32BE ```ceesafe public fn writeU32BE(s: SliceMut, at: usize, x: u32) ``` Writes `x` to the 4 bytes at `at`, big-endian. ### ByteOrder.writeI32BE ```ceesafe public fn writeI32BE(s: SliceMut, at: usize, x: i32) ``` Writes `x` to the 4 bytes at `at`, big-endian, in two's complement. ### ByteOrder.readU32LE ```ceesafe public fn readU32LE(s: Slice, at: usize) -> u32 ``` The `u32` in the 4 bytes at `at`, little-endian. ### ByteOrder.readI32LE ```ceesafe public fn readI32LE(s: Slice, at: usize) -> i32 ``` The `i32` in the 4 bytes at `at`, little-endian, in two's complement. ### ByteOrder.writeU32LE ```ceesafe public fn writeU32LE(s: SliceMut, at: usize, x: u32) ``` Writes `x` to the 4 bytes at `at`, little-endian. ### ByteOrder.writeI32LE ```ceesafe public fn writeI32LE(s: SliceMut, at: usize, x: i32) ``` Writes `x` to the 4 bytes at `at`, little-endian, in two's complement. ### ByteOrder.readU64BE ```ceesafe public fn readU64BE(s: Slice, at: usize) -> u64 ``` The `u64` in the 8 bytes at `at`, big-endian. ### ByteOrder.readI64BE ```ceesafe public fn readI64BE(s: Slice, at: usize) -> i64 ``` The `i64` in the 8 bytes at `at`, big-endian, in two's complement. ### ByteOrder.writeU64BE ```ceesafe public fn writeU64BE(s: SliceMut, at: usize, x: u64) ``` Writes `x` to the 8 bytes at `at`, big-endian. ### ByteOrder.writeI64BE ```ceesafe public fn writeI64BE(s: SliceMut, at: usize, x: i64) ``` Writes `x` to the 8 bytes at `at`, big-endian, in two's complement. ### ByteOrder.readU64LE ```ceesafe public fn readU64LE(s: Slice, at: usize) -> u64 ``` The `u64` in the 8 bytes at `at`, little-endian. ### ByteOrder.readI64LE ```ceesafe public fn readI64LE(s: Slice, at: usize) -> i64 ``` The `i64` in the 8 bytes at `at`, little-endian, in two's complement. ### ByteOrder.writeU64LE ```ceesafe public fn writeU64LE(s: SliceMut, at: usize, x: u64) ``` Writes `x` to the 8 bytes at `at`, little-endian. ### ByteOrder.writeI64LE ```ceesafe public fn writeI64LE(s: SliceMut, at: usize, x: i64) ``` Writes `x` to the 8 bytes at `at`, little-endian, in two's complement. ## struct VectorBytes ```ceesafe public struct VectorBytes ``` Appending integers to a byte vector in a byte order: `v.pushBE(x)` and `v.pushLE(x)` (ADR 0071 §2.2). Each is one `extend` of the bytes, so it grows the vector at most once and leaves it unchanged if that fails. A vector's allocator is `SystemHeap` or `FailingAllocator` (type-system.md §23.1); the `…Failing` functions are the latter's. ### VectorBytes.pushU8BE ```ceesafe public fn pushU8BE(v: borrow mut Vector, x: u8) -> Result ``` Appends `x` as 1 byte, big-endian. ### VectorBytes.pushI8BE ```ceesafe public fn pushI8BE(v: borrow mut Vector, x: i8) -> Result ``` Appends `x` as 1 byte, big-endian, in two's complement. ### VectorBytes.pushU8LE ```ceesafe public fn pushU8LE(v: borrow mut Vector, x: u8) -> Result ``` Appends `x` as 1 byte, little-endian. ### VectorBytes.pushI8LE ```ceesafe public fn pushI8LE(v: borrow mut Vector, x: i8) -> Result ``` Appends `x` as 1 byte, little-endian, in two's complement. ### VectorBytes.pushU16BE ```ceesafe public fn pushU16BE(v: borrow mut Vector, x: u16) -> Result ``` Appends `x` as 2 bytes, big-endian. ### VectorBytes.pushI16BE ```ceesafe public fn pushI16BE(v: borrow mut Vector, x: i16) -> Result ``` Appends `x` as 2 bytes, big-endian, in two's complement. ### VectorBytes.pushU16LE ```ceesafe public fn pushU16LE(v: borrow mut Vector, x: u16) -> Result ``` Appends `x` as 2 bytes, little-endian. ### VectorBytes.pushI16LE ```ceesafe public fn pushI16LE(v: borrow mut Vector, x: i16) -> Result ``` Appends `x` as 2 bytes, little-endian, in two's complement. ### VectorBytes.pushU32BE ```ceesafe public fn pushU32BE(v: borrow mut Vector, x: u32) -> Result ``` Appends `x` as 4 bytes, big-endian. ### VectorBytes.pushI32BE ```ceesafe public fn pushI32BE(v: borrow mut Vector, x: i32) -> Result ``` Appends `x` as 4 bytes, big-endian, in two's complement. ### VectorBytes.pushU32LE ```ceesafe public fn pushU32LE(v: borrow mut Vector, x: u32) -> Result ``` Appends `x` as 4 bytes, little-endian. ### VectorBytes.pushI32LE ```ceesafe public fn pushI32LE(v: borrow mut Vector, x: i32) -> Result ``` Appends `x` as 4 bytes, little-endian, in two's complement. ### VectorBytes.pushU64BE ```ceesafe public fn pushU64BE(v: borrow mut Vector, x: u64) -> Result ``` Appends `x` as 8 bytes, big-endian. ### VectorBytes.pushI64BE ```ceesafe public fn pushI64BE(v: borrow mut Vector, x: i64) -> Result ``` Appends `x` as 8 bytes, big-endian, in two's complement. ### VectorBytes.pushU64LE ```ceesafe public fn pushU64LE(v: borrow mut Vector, x: u64) -> Result ``` Appends `x` as 8 bytes, little-endian. ### VectorBytes.pushI64LE ```ceesafe public fn pushI64LE(v: borrow mut Vector, x: i64) -> Result ``` Appends `x` as 8 bytes, little-endian, in two's complement. ### VectorBytes.pushU8BEFailing ```ceesafe public fn pushU8BEFailing(v: borrow mut Vector, x: u8) -> Result ``` Appends `x` as 1 byte, big-endian. ### VectorBytes.pushI8BEFailing ```ceesafe public fn pushI8BEFailing(v: borrow mut Vector, x: i8) -> Result ``` Appends `x` as 1 byte, big-endian, in two's complement. ### VectorBytes.pushU8LEFailing ```ceesafe public fn pushU8LEFailing(v: borrow mut Vector, x: u8) -> Result ``` Appends `x` as 1 byte, little-endian. ### VectorBytes.pushI8LEFailing ```ceesafe public fn pushI8LEFailing(v: borrow mut Vector, x: i8) -> Result ``` Appends `x` as 1 byte, little-endian, in two's complement. ### VectorBytes.pushU16BEFailing ```ceesafe public fn pushU16BEFailing(v: borrow mut Vector, x: u16) -> Result ``` Appends `x` as 2 bytes, big-endian. ### VectorBytes.pushI16BEFailing ```ceesafe public fn pushI16BEFailing(v: borrow mut Vector, x: i16) -> Result ``` Appends `x` as 2 bytes, big-endian, in two's complement. ### VectorBytes.pushU16LEFailing ```ceesafe public fn pushU16LEFailing(v: borrow mut Vector, x: u16) -> Result ``` Appends `x` as 2 bytes, little-endian. ### VectorBytes.pushI16LEFailing ```ceesafe public fn pushI16LEFailing(v: borrow mut Vector, x: i16) -> Result ``` Appends `x` as 2 bytes, little-endian, in two's complement. ### VectorBytes.pushU32BEFailing ```ceesafe public fn pushU32BEFailing(v: borrow mut Vector, x: u32) -> Result ``` Appends `x` as 4 bytes, big-endian. ### VectorBytes.pushI32BEFailing ```ceesafe public fn pushI32BEFailing(v: borrow mut Vector, x: i32) -> Result ``` Appends `x` as 4 bytes, big-endian, in two's complement. ### VectorBytes.pushU32LEFailing ```ceesafe public fn pushU32LEFailing(v: borrow mut Vector, x: u32) -> Result ``` Appends `x` as 4 bytes, little-endian. ### VectorBytes.pushI32LEFailing ```ceesafe public fn pushI32LEFailing(v: borrow mut Vector, x: i32) -> Result ``` Appends `x` as 4 bytes, little-endian, in two's complement. ### VectorBytes.pushU64BEFailing ```ceesafe public fn pushU64BEFailing(v: borrow mut Vector, x: u64) -> Result ``` Appends `x` as 8 bytes, big-endian. ### VectorBytes.pushI64BEFailing ```ceesafe public fn pushI64BEFailing(v: borrow mut Vector, x: i64) -> Result ``` Appends `x` as 8 bytes, big-endian, in two's complement. ### VectorBytes.pushU64LEFailing ```ceesafe public fn pushU64LEFailing(v: borrow mut Vector, x: u64) -> Result ``` Appends `x` as 8 bytes, little-endian. ### VectorBytes.pushI64LEFailing ```ceesafe public fn pushI64LEFailing(v: borrow mut Vector, x: i64) -> Result ``` Appends `x` as 8 bytes, little-endian, in two's complement. ### VectorBytes.appendHex ```ceesafe public fn appendHex(v: borrow mut Vector, value: u64, minWidth: u32, upper: bool) -> Result ``` Appends `value` in hexadecimal, at least `minWidth` digits, uppercase when `upper` (ADR 0087 §2.9). ### VectorBytes.appendOctal ```ceesafe public fn appendOctal(v: borrow mut Vector, value: u64) -> Result ``` Appends `value` in octal (ADR 0087 §2.9). ### VectorBytes.appendFixed ```ceesafe public fn appendFixed(v: borrow mut Vector, value: f64, digits: u32) -> Result ``` Appends `value` with `digits` digits after the point, rounded half to even (ADR 0087 §2.9). ### VectorBytes.appendPadded ```ceesafe public fn appendPadded( v: borrow mut Vector, text: StringView, width: usize, align: TextAlign, fill: u32, ) -> Result ``` Appends `text` in a field `width` characters wide, filled with `fill` where `align` puts it (ADR 0087 §2.9). ## struct RawCell ```ceesafe @interiorMutable @noncopy public struct RawCell ``` The primitive (§41.1): a `T` that may be read and written through `pointer()` while shared loans of the cell, or of a value holding it, are live. It has `T`'s size and alignment. `T` may not hold loans (N0351). ### RawCell.create ```ceesafe public fn create(value: T) -> RawCell ``` A cell holding `value`. ### RawCell.pointer ```ceesafe public fn pointer(borrow this) -> *mut T ``` The contents' address: safe to take, `unsafe` to use. Unsafe code must not make two live `borrow mut T` from it, nor write while a `borrow T` it made is live (unsafe.md §2). ### RawCell.getMut ```ceesafe public fn getMut(borrow mut this) -> borrow mut T ``` The contents, for as long as the cell is borrowed exclusively: no other loan of it can be live. ### RawCell.intoInner ```ceesafe public fn intoInner(this) -> T ``` The contents, the cell consumed. ## struct Cell ```ceesafe public struct Cell ``` A `Copy` value that may be read and replaced through a shared reference (§41.2): `get` copies it out, `set` stores a new one. No method lends a reference to the contents, so no change can be seen through a stale one, and a call between two methods (a re-entrant callback) is harmless. `==` and `!=` compare the contents when `T` has them. ### Cell.create ```ceesafe public fn create(value: T) -> Cell ``` A cell holding `value`. ### Cell.get ```ceesafe public fn get(borrow this) -> T ``` A copy of the contents. Safety: no reference to the contents is ever lent, so nothing writes them during the read; `T` is Copy. ### Cell.set ```ceesafe public fn set(borrow this, value: T) ``` Stores `value`; the old contents need no drop (`T` is Copy), so no user code runs. Safety: as get; nothing reads the contents during the write. ### Cell.replace ```ceesafe public fn replace(borrow this, value: T) -> T ``` Stores `value` and returns the old contents. Safety: as set. ### Cell.getMut ```ceesafe public fn getMut(borrow mut this) -> borrow mut T ``` The contents, for as long as the cell is borrowed exclusively. ### Cell.intoInner ```ceesafe public fn intoInner(this) -> T ``` The contents, the cell consumed. ## struct RefCell ```ceesafe public struct RefCell ``` A value lent through a shared reference with checked access (§41.3): any number of `read` guards, or one `write` guard, at a time. A conflict panics ("cell already borrowed"); `tryRead` and `tryWrite` return null instead. The state is one word: 0 free, n > 0 readers, -1 writing. ### RefCell.create ```ceesafe public fn create(value: T) -> RefCell ``` A cell holding `value`, free. ### RefCell.read ```ceesafe public fn read(borrow this) -> CellRef ``` Shared access until the guard is dropped; panics while a `write` guard is live. ### RefCell.tryRead ```ceesafe public fn tryRead(borrow this) -> CellRef? ``` As read, or null while a `write` guard is live. ### RefCell.write ```ceesafe public fn write(borrow this) -> CellRefMut ``` Exclusive access until the guard is dropped; panics while any guard is live. ### RefCell.tryWrite ```ceesafe public fn tryWrite(borrow this) -> CellRefMut? ``` As write, or null while any guard is live. ### RefCell.replace ```ceesafe public fn replace(borrow this, value: T) -> T ``` Stores `value` and returns the old contents, moved out before the return: their drop runs in the caller, with no guard live. Panics while any guard is live. Safety: no guard is live, so no reference to the contents is; the old value is read out before the write. ### RefCell.getMut ```ceesafe public fn getMut(borrow mut this) -> borrow mut T ``` The contents, for as long as the cell is borrowed exclusively: no guard can be live. ### RefCell.intoInner ```ceesafe public fn intoInner(this) -> T ``` The contents, the cell consumed. ## struct RefCellTaking ```ceesafe public struct RefCellTaking ``` `RefCell.take` on a `RefCell` (§41.3): the contents, leaving null. ### RefCellTaking.take ```ceesafe public fn take(cell: borrow RefCell) -> T? ``` Panics while any guard is live, as replace. ## struct CellRef ```ceesafe public struct CellRef { drop { … } } ``` A `read` guard of a RefCell (§41.3): `data()` lends the contents shared; dropping it gives the access back. ### CellRef.data ```ceesafe public fn data(borrow this) -> borrow T ``` The contents, for as long as the guard is borrowed. ## struct CellRefMut ```ceesafe public struct CellRefMut { drop { … } } ``` A `write` guard of a RefCell (§41.3): `data()` lends the contents exclusively; dropping it gives the access back. ### CellRefMut.data ```ceesafe public fn data(borrow mut this) -> borrow mut T ``` The contents, for as long as the guard is borrowed exclusively. Safety: the guard set the state to -1, so no other guard is live and none can be made while it is; the loan ends before the guard's drop gives the access back (a RawCell's contents may change while the cell is shared, type-system.md §41.1). ## struct CStr ```ceesafe public struct CStr ``` A view of UTF-8 text with a NUL byte after its end, valid for region `r` as a `StringView` is: what a C function taking a `const char *` needs. Copy; a field of this type needs a region parameter, as a view's does. ### CStr.fromBytesWithNul ```ceesafe public fn fromBytesWithNul(b: Slice) -> CStr? ``` Bytes ending in a NUL that is their only one, as valid UTF-8 text before it; else null. ### CStr.ptr ```ceesafe public fn ptr(borrow this) -> *CChar ``` The address of the first byte, for C: the text is followed by its NUL. ### CStr.view ```ceesafe public fn view(borrow this) -> StringView ``` The text, without the NUL. ### CStr.length ```ceesafe public fn length(borrow this) -> usize ``` The text's length in bytes, without the NUL. ### CStr.bytesWithNul ```ceesafe public fn bytesWithNul(borrow this) -> Slice ``` The text and its NUL: `length + 1` bytes. ## interface DerivedEqual ```ceesafe public interface DerivedEqual { fn derivedEqual(borrow this, other: borrow T) -> bool; } ``` The equality `@derive(Equal)` gives a struct or enum: `a == b` on the type calls `a.derivedEqual(b)`, and `a != b` negates it. Only the compiler implements it: an `impl` written by hand is N0290, so `==` keeps one meaning (language-spec.md §6). ## struct DerivedEqualElements ```ceesafe public struct DerivedEqualElements> ``` `==` on slices, arrays and vectors of a type `@derive(Equal)` gave equality (ADR 0087 §2.3): the same length, and each element equal to the other's, in order. The compiler calls it for `a == b`; a program has no need to. ### DerivedEqualElements.equal ```ceesafe public fn equal(a: Slice, b: Slice) -> bool ``` Whether `a` and `b` have the same length and equal elements. ## struct TextWriter ```ceesafe public struct TextWriter ``` A writer of text into a caller's byte buffer, as C's `snprintf` writes: what does not fit is cut off, the text is always zero-terminated in a buffer of at least one byte (so `cText` can be handed to C), and `wanted` counts every byte asked for, as `snprintf`'s result does, so a caller can tell that the text was cut. ### TextWriter.create ```ceesafe public fn create(buffer: SliceMut) -> TextWriter ``` A writer at the start of `buffer`, which it empties (a zero first byte, if it has one). ### TextWriter.byte ```ceesafe public fn byte(borrow mut this, b: u8) ``` Appends one byte. ### TextWriter.bytes ```ceesafe public fn bytes(borrow mut this, data: Slice) ``` Appends bytes. ### TextWriter.text ```ceesafe public fn text(borrow mut this, s: StringView) ``` Appends text. ### TextWriter.decimal ```ceesafe public fn decimal(borrow mut this, value: u64) ``` Appends an unsigned integer in decimal. ### TextWriter.signed ```ceesafe public fn signed(borrow mut this, value: i64) ``` Appends a signed integer in decimal, with a `-` if it is negative. ### TextWriter.hex ```ceesafe public fn hex(borrow mut this, value: u64, width: u32) ``` Appends an unsigned integer in lowercase hexadecimal, with at least `width` digits (zeros before it), as `%0*llx`; no `0x`. ### TextWriter.written ```ceesafe public fn written(borrow this) -> Slice ``` The bytes written, without the terminating zero. ### TextWriter.length ```ceesafe public fn length(borrow this) -> usize ``` How many bytes were written (at most the buffer's length less one). ### TextWriter.wanted ```ceesafe public fn wanted(borrow this) -> usize ``` How many bytes were asked for: more than `length()` when the text was cut, as `snprintf`'s result. ### TextWriter.truncated ```ceesafe public fn truncated(borrow this) -> bool ``` Whether some text did not fit. ### TextWriter.cText ```ceesafe public fn cText(borrow this) -> *CChar ``` The text as a zero-terminated C string, valid while the writer's buffer is. An empty buffer, which can hold no terminator, gives an empty string that is not in it. ## enum TextAlign ```ceesafe public enum TextAlign { Left, Right, Center, } ``` Where `appendPadded` puts text in a wider field (ADR 0087 §2.9): Rust's `{:w}` and `{:^w}`. A centred text's extra fill character goes after it. ## struct Formatted ```ceesafe public struct Formatted ``` The formatting helpers of `String` and `VectorBytes` (ADR 0087 §2.9): what Rust's `{:x}`, `{:08X}`, `{:o}`, `{:.3}` and `{:>8}` write. Each appends to a byte vector; static functions, the struct is never made. ### Formatted.hex ```ceesafe public fn hex(out: borrow mut Vector, value: u64, minWidth: u32, upper: bool) -> Result ``` `value` in hexadecimal, at least `minWidth` digits (zeros before it), uppercase when `upper`; no `0x`. ### Formatted.octal ```ceesafe public fn octal(out: borrow mut Vector, value: u64) -> Result ``` `value` in octal; no `0o`. ### Formatted.fixed ```ceesafe public fn fixed(out: borrow mut Vector, value: f64, digits: u32) -> Result ``` `value` in decimal with exactly `digits` digits after the point (none, and no point, for 0), rounded half to even on its exact binary value, as Rust's `{:.N}` and C's `%.Nf` round; `NaN`, `inf` and `-inf` as Rust writes them. A negative zero keeps its sign. ### Formatted.padded ```ceesafe public fn padded( out: borrow mut Vector, text: StringView, width: usize, align: TextAlign, fill: u32, ) -> Result ``` `text` in a field of `width` characters (Unicode scalar values, as Rust counts them), filled with the character `fill` on the side `align` leaves; text at least as wide is appended as it is. ## interface AppendFormatted ```ceesafe public interface AppendFormatted { fn appendHex(borrow mut this, value: u64, minWidth: u32, upper: bool) -> Result; fn appendOctal(borrow mut this, value: u64) -> Result; fn appendFixed(borrow mut this, value: f64, digits: u32) -> Result; fn appendPadded(borrow mut this, text: StringView, width: usize, align: TextAlign, fill: u32) -> Result; } ``` `s.appendHex(…)` and the other helpers (ADR 0087 §2.9), through `Formatted`: implemented here for a `Vector`, and in lib/std for a `String`. Each appends ASCII, or for `appendPadded` the text and its fill, so a string stays UTF-8. ## interface Iterator ```ceesafe public interface Iterator { fn next(borrow mut this) -> T?; } ``` A source of values, one at a time: `next()` gives each, then null. `for (x in e)` calls it on a struct that has such a method (the shape is enough, ADR 0074 §2.8), on a type parameter bounded by `Iterator`, and on a `borrow mut dyn Iterator` or a `Box>` (type-system.md §16.4). The item is a parameter, so callers fix it directly (`I: Iterator`) and `dyn Iterator` has a table. ## struct Volatile ```ceesafe @noncopy public struct Volatile ``` A cell read and written only by volatile accesses: a hardware register, or memory shared with a device. Not Copy: a copy would read the register with an ordinary access, once, instead of each time it is used. ### Volatile.read ```ceesafe public fn read(borrow this) -> T ``` Safety: `this.value` is a live, aligned `T` (it is a field), and the read is a single volatile access. ### Volatile.write ```ceesafe public fn write(borrow mut this, v: T) ``` Safety: as `read`; `borrow mut this` makes the write exclusive. ## struct MmioPtr ```ceesafe @noncopy public struct MmioPtr ``` The address of a memory-mapped `T`, typically a `@repr(C)` struct of `Volatile` registers. Not Copy: two copies would be two owners of the same registers. ### MmioPtr.at ```ceesafe public unsafe fn at(address: usize) -> MmioPtr ``` Safety: `address` is mapped, aligned for `T`, and no other `MmioPtr` or reference aliases the registers. ### MmioPtr.get ```ceesafe public fn get(borrow this) -> borrow T ``` Safety: `at`'s contract makes `base` a live `T` reached only through this value; the result borrows `this`. ### MmioPtr.getMut ```ceesafe public fn getMut(borrow mut this) -> borrow mut T ``` Safety: as `get`; `borrow mut this` makes the access exclusive. ### MmioPtr.address ```ceesafe public fn address(borrow this) -> usize ``` The address of the registers, as given to `at`. ## struct NumberParsing ```ceesafe public struct NumberParsing ``` Parsing integers and floats from bytes: static functions; the struct has no fields and is never made. ### NumberParsing.parseU8 ```ceesafe public fn parseU8(s: Slice) -> u8? ``` Decimal text as a `u8`. ### NumberParsing.parseU8Radix ```ceesafe public fn parseU8Radix(s: Slice, radix: u32) -> u8? ``` Text in `radix` (2 to 36; null otherwise) as a `u8`. ### NumberParsing.parseU16 ```ceesafe public fn parseU16(s: Slice) -> u16? ``` Decimal text as a `u16`. ### NumberParsing.parseU16Radix ```ceesafe public fn parseU16Radix(s: Slice, radix: u32) -> u16? ``` Text in `radix` (2 to 36; null otherwise) as a `u16`. ### NumberParsing.parseU32 ```ceesafe public fn parseU32(s: Slice) -> u32? ``` Decimal text as a `u32`. ### NumberParsing.parseU32Radix ```ceesafe public fn parseU32Radix(s: Slice, radix: u32) -> u32? ``` Text in `radix` (2 to 36; null otherwise) as a `u32`. ### NumberParsing.parseU64 ```ceesafe public fn parseU64(s: Slice) -> u64? ``` Decimal text as a `u64`. ### NumberParsing.parseU64Radix ```ceesafe public fn parseU64Radix(s: Slice, radix: u32) -> u64? ``` Text in `radix` (2 to 36; null otherwise) as a `u64`. ### NumberParsing.parseUsize ```ceesafe public fn parseUsize(s: Slice) -> usize? ``` Decimal text as a `usize`. ### NumberParsing.parseUsizeRadix ```ceesafe public fn parseUsizeRadix(s: Slice, radix: u32) -> usize? ``` Text in `radix` (2 to 36; null otherwise) as a `usize`. ### NumberParsing.parseI8 ```ceesafe public fn parseI8(s: Slice) -> i8? ``` Decimal text as an `i8`. ### NumberParsing.parseI8Radix ```ceesafe public fn parseI8Radix(s: Slice, radix: u32) -> i8? ``` Text in `radix` (2 to 36; null otherwise) as an `i8`. ### NumberParsing.parseI16 ```ceesafe public fn parseI16(s: Slice) -> i16? ``` Decimal text as an `i16`. ### NumberParsing.parseI16Radix ```ceesafe public fn parseI16Radix(s: Slice, radix: u32) -> i16? ``` Text in `radix` (2 to 36; null otherwise) as an `i16`. ### NumberParsing.parseI32 ```ceesafe public fn parseI32(s: Slice) -> i32? ``` Decimal text as an `i32`. ### NumberParsing.parseI32Radix ```ceesafe public fn parseI32Radix(s: Slice, radix: u32) -> i32? ``` Text in `radix` (2 to 36; null otherwise) as an `i32`. ### NumberParsing.parseI64 ```ceesafe public fn parseI64(s: Slice) -> i64? ``` Decimal text as an `i64`. ### NumberParsing.parseI64Radix ```ceesafe public fn parseI64Radix(s: Slice, radix: u32) -> i64? ``` Text in `radix` (2 to 36; null otherwise) as an `i64`. ### NumberParsing.parseIsize ```ceesafe public fn parseIsize(s: Slice) -> isize? ``` Decimal text as an `isize`. ### NumberParsing.parseIsizeRadix ```ceesafe public fn parseIsizeRadix(s: Slice, radix: u32) -> isize? ``` Text in `radix` (2 to 36; null otherwise) as an `isize`. ### NumberParsing.parseF64 ```ceesafe public fn parseF64(s: Slice) -> f64? ``` Text as an `f64`, correctly rounded, or null. `[+-]digits[.digits][(e|E)[+-]digits]` with a digit on at least one side of the point, or `inf`, `infinity` or `nan` in any case, with an optional sign. Too large is ±infinity. ### NumberParsing.parseF32 ```ceesafe public fn parseF32(s: Slice) -> f32? ``` Text as an `f32`, correctly rounded (once: not through an `f64`), or null; the grammar of parseF64. ## struct ResultMethods ```ceesafe public struct ResultMethods ``` `Result`'s methods. ### ResultMethods.isOk ```ceesafe public fn isOk(r: borrow Result) -> bool ``` Whether `r` is `Ok`. ### ResultMethods.isErr ```ceesafe public fn isErr(r: borrow Result) -> bool ``` Whether `r` is `Err`. ### ResultMethods.ok ```ceesafe public fn ok(r: Result) -> T? ``` The `Ok` value, or null; an `Err` value is destroyed. ### ResultMethods.err ```ceesafe public fn err(r: Result) -> E? ``` The `Err` value, or null; an `Ok` value is destroyed. ## struct OptionalResults ```ceesafe public struct OptionalResults ``` `o.okOr(e)`: an optional as a `Result`. ### OptionalResults.okOr ```ceesafe public fn okOr(o: T?, e: E) -> Result ``` `Ok` with the value, or `Err(e)` for null; `e` is destroyed when there is a value. ## struct SliceHalves ```ceesafe public struct SliceHalves { public left: Slice; public right: Slice; } ``` The two parts of a slice that `splitAt` divides (type-system.md §16.5): the elements before the split, and from it on. ## struct SliceHalvesMut ```ceesafe public struct SliceHalvesMut { public left: SliceMut; public right: SliceMut; } ``` The two parts of a mutable slice that `splitAtMut` divides: disjoint, so both may be written at once. ## struct SliceSplitting ```ceesafe public struct SliceSplitting ``` Splitting a slice in two (type-system.md §16.5): `s.splitAt(mid)` is `SliceSplitting.splitAt(s, mid)`. The struct's region ties the halves to the slice they come from; it has no fields and is never made. ### SliceSplitting.splitAt ```ceesafe public fn splitAt(s: Slice, mid: usize) -> SliceHalves ``` The elements before `mid`, and from `mid` on; panics if `mid` is past the end. ### SliceSplitting.splitAtMut ```ceesafe public fn splitAtMut(s: SliceMut, mid: usize) -> SliceHalvesMut ``` As splitAt, for a mutable slice: two mutable slices that do not overlap. Safety: slicing `s[mid..]` checks `mid` against the length (it panics past the end), so `[0, mid)` and `[mid, length)` are inside `s` and disjoint, and each is lent for as long as `s` is. ## struct SliceAlgorithms ```ceesafe public struct SliceAlgorithms ``` The algorithms over a slice of `T`, as static functions: the struct has no fields and is never made. ### SliceAlgorithms.sortBy ```ceesafe @effectsOf(cmp) public fn sortBy(s: SliceMut, cmp: borrow mut fn(borrow T, borrow T) -> i32) ``` Orders `s` so that `cmp` of each element and the next is at most zero, for a consistent `cmp`. Not stable. An introsort (ADR 0057): quicksort with a median-of-three pivot and a partition without a branch on `cmp`'s result, the elements equal to an earlier pivot set aside at once, insertion sort for short runs, and heapsort past a depth of 2·log₂ n, so O(n log n) calls of `cmp` in the worst case. A slice that is one ascending run is left as it is, and one strictly descending run is reversed. Allocates nothing; the shorter side is sorted first, so at most 64 ranges wait. An inconsistent `cmp` leaves some permutation of `s`. ### SliceAlgorithms.forEach ```ceesafe @effectsOf(visit) public fn forEach(s: Slice, visit: borrow mut fn(borrow T)) ``` Calls `visit` on each element, in index order. ### SliceAlgorithms.any ```ceesafe @effectsOf(test) public fn any(s: Slice, test: borrow mut fn(borrow T) -> bool) -> bool ``` Does `test` return true for some element? Stops at the first that does. ### SliceAlgorithms.all ```ceesafe @effectsOf(test) public fn all(s: Slice, test: borrow mut fn(borrow T) -> bool) -> bool ``` Does `test` return true for every element? Stops at the first that does not. ### SliceAlgorithms.retain ```ceesafe @effectsOf(keep) public fn retain(s: SliceMut, keep: borrow mut fn(borrow T) -> bool) -> usize ``` Moves the elements for which `keep` returns true to the front, in order, calling it once on each element in index order; returns how many there are. `v.retain(keep)` then truncates the vector to that length. ## struct VectorEditing ```ceesafe public struct VectorEditing ``` `v.insert(at, x)`, `v.remove(at)` and `v.swapRemove(at)` (ADR 0075 §2.12). The index is checked at the call, so a panic names the program's line: `at <= length` for `insert`, `at < length` for the others. The elements move by `swap`, so they may be of any type. A vector's allocator is `SystemHeap` or `FailingAllocator` (type-system.md §23.1); the `…Failing` functions are the latter's. ### VectorEditing.insert ```ceesafe public fn insert(v: borrow mut Vector, at: usize, x: T) -> Result ``` Puts `x` at `at`, moving the elements from `at` on one place up. O(length - at). If growing fails, `v` is unchanged and `x` is destroyed. ### VectorEditing.remove ```ceesafe public fn remove(v: borrow mut Vector, at: usize) -> T ``` Takes the element at `at` out, moving the later elements one place down. O(length - at). ### VectorEditing.swapRemove ```ceesafe public fn swapRemove(v: borrow mut Vector, at: usize) -> T ``` Takes the element at `at` out and puts the last element in its place. O(1); the order is not kept. ### VectorEditing.insertFailing ```ceesafe public fn insertFailing(v: borrow mut Vector, at: usize, x: T) -> Result ``` `insert` for a vector of `FailingAllocator`. ### VectorEditing.removeFailing ```ceesafe public fn removeFailing(v: borrow mut Vector, at: usize) -> T ``` `remove` for a vector of `FailingAllocator`. ### VectorEditing.swapRemoveFailing ```ceesafe public fn swapRemoveFailing(v: borrow mut Vector, at: usize) -> T ``` `swapRemove` for a vector of `FailingAllocator`. ## struct TextHalves ```ceesafe public struct TextHalves { public left: StringView; public right: StringView; } ``` The parts of a text before and after a separator: what `s.splitOnce(sep)` finds. ## struct ByteHalves ```ceesafe public struct ByteHalves { public left: Slice; public right: Slice; } ``` The parts of bytes before and after a separator: what `s.splitOnce(sep)` finds on a `Slice`. ## struct TextSplit ```ceesafe public struct TextSplit ``` The pieces of a text between separators, in order: `s.split(sep)`. `next()` gives each piece, then null. `"a,,b"` split by `","` is `"a"`, `""`, `"b"`; an empty text is one empty piece; an empty separator gives the text whole. Implements `Iterator>`. ## struct ByteSplit ```ceesafe public struct ByteSplit ``` The pieces of bytes between separators, in order: `s.split(sep)` on a `Slice`, as TextSplit. Implements `Iterator>`. ## struct TextSearch ```ceesafe public struct TextSearch ``` The methods of a `StringView` that search it (ADR 0071 §2.4), as static functions: the struct is never made. ### TextSearch.indexOf ```ceesafe public fn indexOf(s: StringView, needle: StringView) -> usize? ``` ### TextSearch.lastIndexOf ```ceesafe public fn lastIndexOf(s: StringView, needle: StringView) -> usize? ``` ### TextSearch.indexOfByte ```ceesafe public fn indexOfByte(s: StringView, b: u8) -> usize? ``` ### TextSearch.contains ```ceesafe public fn contains(s: StringView, needle: StringView) -> bool ``` ### TextSearch.startsWith ```ceesafe public fn startsWith(s: StringView, prefix: StringView) -> bool ``` ### TextSearch.endsWith ```ceesafe public fn endsWith(s: StringView, suffix: StringView) -> bool ``` ### TextSearch.equalsIgnoreAsciiCase ```ceesafe public fn equalsIgnoreAsciiCase(s: StringView, other: StringView) -> bool ``` ### TextSearch.trim ```ceesafe public fn trim(s: StringView) -> StringView ``` ### TextSearch.trimStart ```ceesafe public fn trimStart(s: StringView) -> StringView ``` ### TextSearch.trimEnd ```ceesafe public fn trimEnd(s: StringView) -> StringView ``` ### TextSearch.splitOnce ```ceesafe public fn splitOnce(s: StringView, separator: StringView) -> TextHalves? ``` The text before the first `separator` and after it, or null if it does not occur. An empty separator is found at the start. ### TextSearch.split ```ceesafe public fn split(s: StringView, separator: StringView) -> TextSplit ``` ## struct ByteSearch ```ceesafe public struct ByteSearch ``` The methods of a `Slice` that search it (ADR 0071 §2.4), as TextSearch's. ### ByteSearch.indexOf ```ceesafe public fn indexOf(s: Slice, needle: Slice) -> usize? ``` ### ByteSearch.lastIndexOf ```ceesafe public fn lastIndexOf(s: Slice, needle: Slice) -> usize? ``` ### ByteSearch.indexOfByte ```ceesafe public fn indexOfByte(s: Slice, b: u8) -> usize? ``` ### ByteSearch.contains ```ceesafe public fn contains(s: Slice, needle: Slice) -> bool ``` ### ByteSearch.startsWith ```ceesafe public fn startsWith(s: Slice, prefix: Slice) -> bool ``` ### ByteSearch.endsWith ```ceesafe public fn endsWith(s: Slice, suffix: Slice) -> bool ``` ### ByteSearch.equalsIgnoreAsciiCase ```ceesafe public fn equalsIgnoreAsciiCase(s: Slice, other: Slice) -> bool ``` ### ByteSearch.trim ```ceesafe public fn trim(s: Slice) -> Slice ``` ### ByteSearch.trimStart ```ceesafe public fn trimStart(s: Slice) -> Slice ``` ### ByteSearch.trimEnd ```ceesafe public fn trimEnd(s: Slice) -> Slice ``` ### ByteSearch.splitOnce ```ceesafe public fn splitOnce(s: Slice, separator: Slice) -> ByteHalves? ``` ### ByteSearch.split ```ceesafe public fn split(s: Slice, separator: Slice) -> ByteSplit ``` # lib/std - [struct AsyncConsole](#struct-asyncconsole) - [struct AsyncTcpListener](#struct-asynctcplistener) - [struct AsyncTcpStream](#struct-asynctcpstream) - [struct AsyncUdpSocket](#struct-asyncudpsocket) - [struct AsyncSockets](#struct-asyncsockets) - [struct AsyncFiles](#struct-asyncfiles) - [enum SendError](#enum-senderror) - [struct Channel](#struct-channel) - [struct AsyncMutex](#struct-asyncmutex) - [struct AsyncMutexGuard](#struct-asyncmutexguard) - [struct Crc32c](#struct-crc32c) - [struct Crc64Nvme](#struct-crc64nvme) - [struct Md5](#struct-md5) - [struct Sha256](#struct-sha256) - [struct HmacSha256](#struct-hmacsha256) - [interface HashKey](#interface-hashkey) - [struct Hasher](#struct-hasher) - [fn deriveHash](#fn-derivehash) - [struct HashMap](#struct-hashmap) - [enum PathKind](#enum-pathkind) - [struct ProcessOutput](#struct-processoutput) - [struct Environment](#struct-environment) - [struct VolumeSpace](#struct-volumespace) - [struct Directory](#struct-directory) - [struct HostServices](#struct-hostservices) - [struct FileMetadata](#struct-filemetadata) - [struct UnixTime](#struct-unixtime) - [struct Clock](#struct-clock) - [struct Random](#struct-random) - [enum SocketAddress](#enum-socketaddress) - [interface SocketEndpoint](#interface-socketendpoint) - [struct SocketAddresses](#struct-socketaddresses) - [struct ListenOptions](#struct-listenoptions) - [struct IoParts](#struct-ioparts) - [struct UdpOptions](#struct-udpoptions) - [struct Datagram](#struct-datagram) - [struct UdpReceived](#struct-udpreceived) - [struct UdpMessage](#struct-udpmessage) - [struct AsyncDns](#struct-asyncdns) - [interface OrderKey](#interface-orderkey) - [fn deriveCompare](#fn-derivecompare) - [struct OrderedCursor](#struct-orderedcursor) - [struct OrderedMap](#struct-orderedmap) - [struct Mutex](#struct-mutex) - [struct MutexGuard](#struct-mutexguard) - [struct Arc](#struct-arc) - [enum SpawnError](#enum-spawnerror) - [struct TestFailure](#struct-testfailure) - [struct Check](#struct-check) - [struct CheckResult](#struct-checkresult) - [struct TestRun](#struct-testrun) - [enum WindowError](#enum-windowerror) - [struct WindowSize](#struct-windowsize) - [struct WindowBox](#struct-windowbox) - [enum WindowButton](#enum-windowbutton) - [struct WindowModifiers](#struct-windowmodifiers) - [enum WindowKey](#enum-windowkey) - [enum WindowCursor](#enum-windowcursor) - [enum WindowEvent](#enum-windowevent) - [struct Window](#struct-window) ## struct AsyncConsole ```ceesafe public struct AsyncConsole ``` Console output for tasks: in order with Console's, and never blocking the thread (ADR 0031 §3.5). ### AsyncConsole.write ```ceesafe public async fn write(text: StringView) -> Result ``` Writes `text` to standard output. ### AsyncConsole.writeLine ```ceesafe public async fn writeLine(text: StringView) -> Result ``` Writes `text` and a newline to standard output. ### AsyncConsole.writeError ```ceesafe public async fn writeError(text: StringView) -> Result ``` Writes `text` to standard error. ## struct AsyncTcpListener ```ceesafe @noncopy public struct AsyncTcpListener { drop { … } } ``` A TCP listener for tasks (type-system.md §40.4): `accept` parks the task until a connection arrives. Its socket is non-blocking; the drop closes it. ### AsyncTcpListener.bind ```ceesafe public fn bind(address: StringView, port: u16) -> Result ``` Listens on `address` (IPv4 or IPv6 text, without a zone) and `port` (0: any free port), with `ListenOptions.defaults()`. Safety: the runtime reads the address only during the call. ### AsyncTcpListener.bindWith ```ceesafe public fn bindWith(address: SocketAddress, options: ListenOptions) -> Result ``` Listens on `address` with `options` (ADR 0044 §3.2). Safety: the runtime reads the address only during the call. ### AsyncTcpListener.localAddress ```ceesafe public fn localAddress(borrow this) -> Result ``` The address it listens on. ### AsyncTcpListener.port ```ceesafe public fn port(borrow this) -> Result ``` The port it listens on. Safety: the descriptor is this listener's. ### AsyncTcpListener.accept ```ceesafe public async fn accept(borrow this) -> Result ``` The next connection; the task parks until one arrives. Safety: the descriptor is this listener's; a new one becomes the stream's. ## struct AsyncTcpStream ```ceesafe @noncopy public struct AsyncTcpStream { drop { … } } ``` A TCP stream for tasks (type-system.md §40.4): `read` and `write` park the task until the socket is ready. Its socket is non-blocking; the drop closes it. ### AsyncTcpStream.connect ```ceesafe public async fn connect(address: StringView, port: u16) -> Result ``` Connects to `address` (IPv4 or IPv6 text, without a zone) and `port`; the task parks until the connection is made or refused. Safety: the runtime reads the address only during the call; the descriptor becomes the stream's at once, so an error closes it. ### AsyncTcpStream.connectTo ```ceesafe public async fn connectTo(address: SocketAddress) -> Result ``` Connects to `address` (ADR 0044 §3.3); the task parks until the connection is made or refused. Safety: the runtime reads the address only during each step; the descriptor becomes the stream's at once. ### AsyncTcpStream.setNoDelay ```ceesafe public fn setNoDelay(borrow this, on: bool) -> Result ``` TCP_NODELAY: send small writes at once instead of gathering them (Nagle's algorithm off). ### AsyncTcpStream.setKeepAlive ```ceesafe public fn setKeepAlive(borrow this, idleSeconds: u32?) -> Result ``` SO_KEEPALIVE: probe an idle connection, after `idleSeconds` of quiet (0: the system's time); null: off. ### AsyncTcpStream.setSendBuffer ```ceesafe public fn setSendBuffer(borrow this, bytes: u32) -> Result ``` SO_SNDBUF: the kernel's send buffer, in bytes (it may round the size, and Linux doubles it). ### AsyncTcpStream.setRecvBuffer ```ceesafe public fn setRecvBuffer(borrow this, bytes: u32) -> Result ``` SO_RCVBUF: the kernel's receive buffer, in bytes (as setSendBuffer). ### AsyncTcpStream.setLinger ```ceesafe public fn setLinger(borrow this, seconds: u32?) -> Result ``` SO_LINGER: how long the close waits for unsent data, in seconds; 0 makes the close reset the connection; null restores the default (the close returns at once and the kernel sends the rest). ### AsyncTcpStream.peerAddress ```ceesafe public fn peerAddress(borrow this) -> Result ``` The peer's address (an IPv4 peer of a dual-stack listener as V4). ### AsyncTcpStream.localAddress ```ceesafe public fn localAddress(borrow this) -> Result ``` This end's address. ### AsyncTcpStream.shutdownRead ```ceesafe public fn shutdownRead(borrow this) -> Result ``` Ends this side's reading: what arrives afterwards is discarded, and a read returns 0. Safety: the descriptor is this stream's. ### AsyncTcpStream.read ```ceesafe public async fn read(borrow this, buffer: SliceMut) -> Result ``` Reads into `buffer`: the count, 0 at the end of the stream; the task parks until there is something to read. Safety: the runtime writes at most the buffer's length, during the call only. ### AsyncTcpStream.write ```ceesafe public async fn write(borrow this, bytes: Slice) -> Result ``` Writes every byte; the task parks whenever the socket cannot take more. Safety: the runtime reads at most the slice's length, during the call only. ### AsyncTcpStream.writeVectored ```ceesafe public async fn writeVectored(borrow this, parts: borrow IoParts) -> Result ``` Sends from `parts` in one step (ADR 0045 §3.2): the task parks until the socket takes at least one byte, and the result is how many it took, which may stop inside any part. Empty parts send nothing and return 0. ### AsyncTcpStream.writeAllVectored ```ceesafe public async fn writeAllVectored(borrow this, parts: borrow IoParts) -> Result ``` Sends every byte of `parts`, in as many steps as the socket needs. ### AsyncTcpStream.sendFile ```ceesafe public async fn sendFile(borrow this, file: borrow File, offset: u64, length: u64) -> Result ``` Sends from `file` at `offset`, at most `length` bytes, in one step (ADR 0051): the task parks until the socket takes at least one byte, and the result is how many went, 0 if the file ends at `offset`. The file's own position is neither used nor moved, so one file may be sent on several streams at once. Where the system cannot send a file (ceesafe_async_op_sendfile's CEE_ASYNC_NO_SENDFILE), the step reads at most 64 KiB at `offset` on a worker thread and writes them: the same results, through a copy. Safety: the runtime reads the file's descriptor during the call only, and lends the kernel no buffer. ### AsyncTcpStream.sendFileAll ```ceesafe public async fn sendFileAll(borrow this, file: borrow File, offset: u64, length: u64) -> Result ``` Sends `length` bytes of `file` from `offset`, in as many steps as the socket needs. If the file ends first, the result is `Io(InvalidInput)`, with the bytes before its end sent. ### AsyncTcpStream.shutdownWrite ```ceesafe public fn shutdownWrite(borrow this) -> Result ``` Ends this side's writing, so the peer reads the end of the stream. Safety: the descriptor is this stream's. ## struct AsyncUdpSocket ```ceesafe public struct AsyncUdpSocket { drop { … } } ``` A UDP socket for tasks (ADR 0050): datagrams sent whole or not at all, and received one at a time, a longer one than the buffer reported as truncated. `sendTo` and `recvFrom` are the common case; `sendMsg` and `recvMsg` add the local address, ECN bits and segments that QUIC needs. ### AsyncUdpSocket.bind ```ceesafe public fn bind(address: SocketAddress, options: UdpOptions) -> Result ``` A socket bound to `address` (port 0: the system chooses), with `options`. Safety: the runtime reads the address only during the call; the descriptor becomes the socket's at once, so an error closes it. ### AsyncUdpSocket.localAddress ```ceesafe public fn localAddress(borrow this) -> Result ``` The address it is bound to. ### AsyncUdpSocket.sendTo ```ceesafe public async fn sendTo(borrow this, bytes: Slice, to: SocketAddress) -> Result ``` Sends `bytes` as one datagram to `to`: whole, or not at all (a datagram too long for the path is `Io(InvalidInput)`). An empty slice sends an empty datagram. ### AsyncUdpSocket.recvFrom ```ceesafe public async fn recvFrom(borrow this, buffer: SliceMut) -> Result ``` Waits for one datagram, into `buffer`. ### AsyncUdpSocket.sendMsg ```ceesafe public async fn sendMsg(borrow this, message: borrow UdpMessage) -> Result ``` Sends `message` (ADR 0050 §3.1): one datagram of its parts, or with `segmentSize` > 0 datagrams of that many bytes each, the last one shorter, in one call where the system can (Linux's UDP_SEGMENT) and one by one elsewhere. The result is the bytes sent: always whole segments. If a segment fails after others went, the result is the bytes of those before it; if the first fails, its error. ### AsyncUdpSocket.recvMsg ```ceesafe public async fn recvMsg(borrow this, buffer: SliceMut) -> Result ``` Waits for one datagram, into `buffer`, with what `UdpOptions` asked to be reported about it. Safety: the runtime writes at most the buffer's length; the buffer stays borrowed until the operation is complete, which this frame outlives (an operation abandoned by a cancellation is waited for, §40.7). ## struct AsyncSockets ```ceesafe public struct AsyncSockets ``` The non-blocking socket calls under AsyncTcpListener and AsyncTcpStream, one at a time, on descriptors: a value >= 0, `wouldBlock()`, or a negated errno. For code that runs its own wait loop, as the interpreter does; programs use the async types, which own their descriptors. ### AsyncSockets.wouldBlock ```ceesafe public fn wouldBlock() -> i64 ``` ### AsyncSockets.listen ```ceesafe public fn listen(address: StringView, port: u16) -> i64 ``` Safety: the runtime reads the address during the call only. ### AsyncSockets.port ```ceesafe public fn port(fd: i32) -> i64 ``` Safety: on a descriptor the runtime only asks the kernel. ### AsyncSockets.accept ```ceesafe public fn accept(fd: i32) -> i64 ``` Safety: as port. ### AsyncSockets.connect ```ceesafe public fn connect(address: StringView, port: u16) -> i64 ``` Safety: as listen. ### AsyncSockets.connected ```ceesafe public fn connected(fd: i32) -> i64 ``` Safety: as port. ### AsyncSockets.read ```ceesafe public fn read(fd: i32, buffer: SliceMut) -> i64 ``` Safety: the runtime writes at most the buffer's length, during the call only. ### AsyncSockets.send ```ceesafe public fn send(fd: i32, bytes: Slice) -> i64 ``` Safety: the runtime reads at most the slice's length, during the call only. ### AsyncSockets.shutdownWrite ```ceesafe public fn shutdownWrite(fd: i32) -> i64 ``` Safety: as port. ### AsyncSockets.listenAt ```ceesafe public fn listenAt(family: u32, bytes: Slice, port: u16, scope: u32, backlog: u32, flags: u32) -> i64 ``` ADR 0044's calls on descriptors, for the interpreter's models: a listener at an address's fields (`bytes` has 16, of which IPv4 uses 4), with a backlog and rt.h's CEE_LISTEN_* flags. Safety: the runtime reads the 16 bytes during the call only (checked: fewer is -EINVAL). ### AsyncSockets.connectAt ```ceesafe public fn connectAt(family: u32, bytes: Slice, port: u16, scope: u32) -> i64 ``` A connection started to an address's fields, as `connect`. Safety: as listenAt. ### AsyncSockets.name ```ceesafe public fn name(fd: i32, peer: u32, bytes: SliceMut, scope: borrow mut u32) -> i64 ``` The socket's own (`peer` 0) or its peer's (1) address: its 16 bytes and scope written; (family << 16) | port, or a negated errno. Safety: the runtime writes 16 bytes (checked) and the scope during the call only. ### AsyncSockets.setOption ```ceesafe public fn setOption(fd: i32, option: u32, value: i64) -> i64 ``` Sets rt.h's CEE_SOCKET_OPTION_* `option` to `value`: 0 or a negated errno. Safety: as port. ### AsyncSockets.shutdownRead ```ceesafe public fn shutdownRead(fd: i32) -> i64 ``` Safety: as port. ### AsyncSockets.sendVectored ```ceesafe public async fn sendVectored(fd: i32, parts: borrow IoParts) -> Result ``` One vectored send on a descriptor (ADR 0045 §3.2): the task parks until the socket takes at least one byte; the count may stop inside any part. Empty parts send nothing and return 0. Safety: the parts stay borrowed until the operation completes, and its record is in this task's frame. ### AsyncSockets.udpBind ```ceesafe public fn udpBind( family: u32, bytes: Slice, port: u16, scope: u32, flags: u32, sendBuffer: u32, receiveBuffer: u32, ) -> i64 ``` A UDP socket bound to the address's fields (ADR 0050), with UdpOptions' flags and the buffer sizes: the descriptor, or a negated errno. Safety: the runtime reads the 16 address bytes during the call only. ### AsyncSockets.udpSend ```ceesafe public fn udpSend( fd: i32, data: Slice, socketFamily: u32, family: u32, bytes: Slice, port: u16, scope: u32, ) -> i64 ``` One datagram of `data` to the address's fields, from a socket of `socketFamily`: the count, `wouldBlock()`, or a negated errno. Safety: the runtime reads the data and the 16 address bytes during the call only. ### AsyncSockets.udpRecv ```ceesafe public fn udpRecv( fd: i32, buffer: SliceMut, bytes: SliceMut, scope: borrow mut u32, info: borrow mut u32, ) -> i64 ``` One datagram into `buffer`: its length there, `wouldBlock()`, or a negated errno; the peer's 16 bytes and scope, and in `info` (family << 24) | (truncated << 16) | port. Safety: the runtime writes at most the buffer's length and 16 address bytes, during the call only. ### AsyncSockets.interfaceIndex ```ceesafe public fn interfaceIndex(name: Slice) -> u32 ``` An interface's index by name (an IPv6 zone), 0 if there is none (lib/std's net.cee declares the call). Safety: the runtime reads the name during the call only. ### AsyncSockets.close ```ceesafe public fn close(fd: i32) ``` Safety: as port. ### AsyncSockets.poll ```ceesafe public fn poll(fds: Slice, events: Slice, ready: SliceMut, timeout: i64) -> i64 ``` Waits up to `timeout` ms (-1: no limit) for any descriptor: `events[k]` 1 readable, 2 writable; `ready[k]` receives each one's readiness. The count ready, 0 on timeout, or a negated errno. Safety: the three slices have `fds.length` elements (checked), read and written during the call only. ## struct AsyncFiles ```ceesafe public struct AsyncFiles ``` Files for tasks (type-system.md §40.4): a regular file is always ready to the kernel's readiness calls, so each operation runs on a worker thread (Async.blocking) while the task waits. ### AsyncFiles.open ```ceesafe public async fn open(path: StringView) -> Result ``` Opens `path` for reading. ### AsyncFiles.openRegular ```ceesafe public async fn openRegular(path: StringView) -> Result ``` Opens `path` for reading if it is a regular file (ADR 0078 §3): never waits on a FIFO; `IsDirectory` for a directory, `InvalidInput` for anything else that is not a regular file. ### AsyncFiles.modified ```ceesafe public async fn modified(file: borrow mut File) -> Result ``` The open file's last modification time, read from the file (ADR 0078 §3). ### AsyncFiles.kind ```ceesafe public async fn kind(file: borrow mut File) -> Result ``` What the open file is, read from the file (ADR 0078 §3). ### AsyncFiles.create ```ceesafe public async fn create(path: StringView) -> Result ``` Creates or truncates `path` for writing. ### AsyncFiles.read ```ceesafe public async fn read(file: borrow mut File, buffer: SliceMut) -> Result ``` Reads into `buffer`: the count, 0 at the end of the file. ### AsyncFiles.write ```ceesafe public async fn write(file: borrow mut File, bytes: Slice) -> Result ``` Writes every byte. ### AsyncFiles.openReadWrite ```ceesafe public async fn openReadWrite(path: StringView) -> Result ``` Opens an existing `path` for reading and writing (ADR 0075 §2.1). ### AsyncFiles.openOrCreate ```ceesafe public async fn openOrCreate(path: StringView) -> Result ``` Opens `path` for reading and writing, creating it if missing; never truncates (ADR 0075 §2.1). ### AsyncFiles.createNew ```ceesafe public async fn createNew(path: StringView) -> Result ``` Creates `path` for reading and writing; `AlreadyExists` if it exists (ADR 0075 §2.1). ### AsyncFiles.readAt ```ceesafe public async fn readAt(file: borrow mut File, buffer: SliceMut, offset: u64) -> Result ``` One read at `offset` into `buffer`: the count, 0 at or past the end (ADR 0075 §2.1). ### AsyncFiles.length ```ceesafe public async fn length(file: borrow mut File) -> Result ``` The file's length in bytes (ADR 0075 §2.1). ### AsyncFiles.writeAt ```ceesafe public async fn writeAt(file: borrow mut File, bytes: Slice, offset: u64) -> Result ``` Writes every byte at `offset` (ADR 0075 §2.1). ### AsyncFiles.syncData ```ceesafe public async fn syncData(file: borrow mut File) -> Result ``` Makes the data, and the metadata needed to read it, durable (ADR 0075 §2.1). After a failure the durable contents are unknown. ### AsyncFiles.syncAll ```ceesafe public async fn syncAll(file: borrow mut File) -> Result ``` `syncData` with all metadata (ADR 0075 §2.1). ### AsyncFiles.setLength ```ceesafe public async fn setLength(file: borrow mut File, length: u64) -> Result ``` Truncates or zero-extends the file to `length` bytes (ADR 0075 §2.1). ### AsyncFiles.rename ```ceesafe public async fn rename(from: StringView, to: StringView) -> Result ``` Renames `from` to `to`, atomically replacing it (ADR 0075 §2.1). ### AsyncFiles.syncDirectory ```ceesafe public async fn syncDirectory(path: StringView) -> Result ``` Makes a directory's entries durable (ADR 0075 §2.1). ## enum SendError ```ceesafe public enum SendError { Closed(T), Cancelled(T), } ``` Why `sendAsync` gave a value back: the channel is closed, or the task was cancelled (ADR 0032 §3.3). ## struct Channel ```ceesafe @threadSafe @noncopy public struct Channel { drop { … } } ``` A bounded channel (ADR 0032): every handle may send and receive; `clone` makes another handle to the same channel, and the last handle's drop drops the values still in it. ### Channel.create ```ceesafe public fn create(capacity: usize) -> Result, AllocError> ``` A channel of `capacity` values, its buffer allocated once. A capacity of 0 panics: rendezvous channels are not in v1 (ADR 0032 §5). Safety: the runtime's channel is reference counted; this handle holds one count until its drop. ### Channel.clone ```ceesafe public fn clone(borrow this) -> Channel ``` Another handle to the same channel. Safety: it takes a count of the runtime's channel, given back by its drop. ### Channel.close ```ceesafe public fn close(borrow this) ``` Every later send fails; receivers take what is buffered, then see the end. Safety: the runtime's channel is alive while this handle is. ### Channel.isClosed ```ceesafe public fn isClosed(borrow this) -> bool ``` Whether `close` has been called (ADR 0060). Values sent before it may still be buffered: the channel is drained when `isClosed()` is true and a `tryRecv()` after it is `null`, in that order. Safety: as `close`. ### Channel.send ```ceesafe public fn send(borrow this, value: T) -> Result ``` Sends `value`, waiting while the channel is full; `Err(value)` if it is closed. Safety: a slot from begin_send is the channel's, under its lock until end_send, and holds no value. ### Channel.trySend ```ceesafe public fn trySend(borrow this, value: T) -> Result ``` Sends `value` if there is room now, without waiting: `Err(value)` if the channel is full or closed. It has no effect, so a `drop` block may call it. Safety: as `send`. ### Channel.tryRecv ```ceesafe public fn tryRecv(borrow this) -> T? ``` The next value if one is buffered, else `null` at once, whether the channel is empty or closed and empty (ADR 0060; `isClosed` tells the two apart). It has no effect, so async code and a `drop` block may call it. Safety: as `recv`. ### Channel.recv ```ceesafe public fn recv(borrow this) -> T? ``` The next value, waiting while the channel is empty; `null` once it is closed and empty. Safety: a slot from begin_recv is the channel's, under its lock until end_recv, and holds a value. ### Channel.sendAsync ```ceesafe public async fn sendAsync(borrow this, value: T) -> Result> ``` Sends `value` from a task, parking it while the channel is full: other tasks of its thread run meanwhile. A closed channel or a cancelled task gives the value back. Safety: as `send`; the wait node is in this task's frame and leaves the queue before the task returns. ### Channel.recvAsync ```ceesafe public async fn recvAsync(borrow this) -> Result ``` The next value for a task, parking it while the channel is empty; `null` once it is closed and empty; `Err(AsyncError.Cancelled)` only while it has taken nothing, so no value is lost. Safety: as `recv`; the wait node is in this task's frame and leaves the queue before the task returns. ## struct AsyncMutex ```ceesafe @threadSafe @noncopy public struct AsyncMutex ``` Mutual exclusion for tasks (type-system.md §40.4): `await lock()` parks the task while another holds it, and the guard may be held across `await`. Its token is the one value of a channel of capacity 1: taking it locks, and the guard's drop puts it back without waiting. ### AsyncMutex.create ```ceesafe public fn create(data: T) -> Result, AllocError> ``` A mutex holding `data`, unlocked. ### AsyncMutex.lock ```ceesafe public async fn lock(borrow this) -> Result, AsyncError> ``` Waits for the lock; `Err(AsyncError.Cancelled)` if the task is cancelled first. ## struct AsyncMutexGuard ```ceesafe public struct AsyncMutexGuard { drop { … } } ``` Holding an AsyncMutex: `data()` lends the protected value; dropping the guard unlocks. ### AsyncMutexGuard.data ```ceesafe public fn data(borrow mut this) -> borrow mut T ``` Safety: the guard holds the token, so no other task or thread reaches the data, and the loan ends before the guard's drop. ## struct Crc32c ```ceesafe public struct Crc32c ``` CRC-32C (Castagnoli: polynomial 0x1EDC6F41, reflected, initial value and final XOR all ones), the checksum of iSCSI, ext4, Btrfs and the S3 `x-amz-checksum-crc32c` header. `Crc32c.of("123456789".bytes())` is 0xE3069283. ### Crc32c.create ```ceesafe public fn create() -> Crc32c ``` A checksum of no bytes yet. ### Crc32c.update ```ceesafe public fn update(borrow mut this, bytes: Slice) ``` Feeds `bytes`: eight at a time through the slicing tables, then the rest one at a time. ### Crc32c.finish ```ceesafe public fn finish(borrow this) -> u32 ``` The checksum of the bytes fed so far. It does not change the state: more bytes may be fed after it. ### Crc32c.of ```ceesafe public fn of(bytes: Slice) -> u32 ``` The checksum of `bytes`, in one call. ## struct Crc64Nvme ```ceesafe public struct Crc64Nvme ``` CRC-64/NVME (polynomial 0xAD93D23594C93659, reflected, initial value and final XOR all ones), the checksum of the S3 `x-amz-checksum-crc64nvme` header. `Crc64Nvme.of("123456789".bytes())` is 0xAE8B14860A799888. ### Crc64Nvme.create ```ceesafe public fn create() -> Crc64Nvme ``` A checksum of no bytes yet. ### Crc64Nvme.update ```ceesafe public fn update(borrow mut this, bytes: Slice) ``` Feeds `bytes`: eight at a time through the slicing tables, then the rest one at a time. ### Crc64Nvme.finish ```ceesafe public fn finish(borrow this) -> u64 ``` The checksum of the bytes fed so far. It does not change the state: more bytes may be fed after it. ### Crc64Nvme.of ```ceesafe public fn of(bytes: Slice) -> u64 ``` The checksum of `bytes`, in one call. ## struct Md5 ```ceesafe public struct Md5 ``` MD5 (RFC 1321): a 16-byte digest. **MD5 is broken as a signature**: collisions are cheap to make, so it must never authenticate anything or stand for a value an attacker chooses. It is here for S3 ETags and `Content-MD5` only, which detect accidental corruption. For anything else use Sha256 or HmacSha256. ### Md5.create ```ceesafe public fn create() -> Md5 ``` A digest of no bytes yet. ### Md5.update ```ceesafe public fn update(borrow mut this, bytes: Slice) ``` Feeds `bytes`. Whole blocks are compressed straight from `bytes`; only a partial block is copied. ### Md5.finish ```ceesafe public fn finish(borrow this) -> [u8; 16] ``` The digest of the bytes fed so far. It does not change the state: more bytes may be fed after it. ### Md5.of ```ceesafe public fn of(bytes: Slice) -> [u8; 16] ``` The digest of `bytes`, in one call. ## struct Sha256 ```ceesafe public struct Sha256 ``` SHA-256 (FIPS 180-4): a 32-byte digest. ### Sha256.create ```ceesafe public fn create() -> Sha256 ``` A digest of no bytes yet. ### Sha256.update ```ceesafe public fn update(borrow mut this, bytes: Slice) ``` Feeds `bytes`. Whole blocks are compressed straight from `bytes`; only a partial block is copied. ### Sha256.finish ```ceesafe public fn finish(borrow this) -> [u8; 32] ``` The digest of the bytes fed so far. It does not change the state: more bytes may be fed after it. ### Sha256.of ```ceesafe public fn of(bytes: Slice) -> [u8; 32] ``` The digest of `bytes`, in one call. ## struct HmacSha256 ```ceesafe public struct HmacSha256 ``` HMAC-SHA-256 (RFC 2104, FIPS 198-1): a 32-byte message authentication code under a key of any length. A key longer than SHA-256's 64-byte block is first replaced by its SHA-256. Compare two codes in constant time, not with an early exit, where an attacker can time the comparison. ### HmacSha256.create ```ceesafe public fn create(key: Slice) -> HmacSha256 ``` A code under `key` of no bytes yet. ### HmacSha256.update ```ceesafe public fn update(borrow mut this, bytes: Slice) ``` Feeds `bytes` of the message. ### HmacSha256.finish ```ceesafe public fn finish(borrow this) -> [u8; 32] ``` The code of the bytes fed so far. It does not change the state: more bytes may be fed after it. ### HmacSha256.of ```ceesafe public fn of(key: Slice, bytes: Slice) -> [u8; 32] ``` The code of `bytes` under `key`, in one call. ## interface HashKey ```ceesafe public interface HashKey { fn hash(borrow this, h: borrow mut Hasher); fn sameAs(borrow this, other: borrow K) -> bool; } ``` A key of a HashMap (ADR 0052 §3.1): `K: HashKey`, since an interface has no `Self`. ## struct Hasher ```ceesafe public struct Hasher ``` SipHash-1-3 (one compression round, three finalisation rounds), keyed by 128 bits: a hash an attacker cannot aim collisions at without the key. ### Hasher.create ```ceesafe public fn create(k0: u64, k1: u64) -> Hasher ``` A hasher keyed by `k0` and `k1`. ### Hasher.bytes ```ceesafe public fn bytes(borrow mut this, data: Slice) ``` Feeds `data`. While nothing is pending, whole eight-byte words are compressed as they are read (the same message blocks, little-endian, as byte by byte). ### Hasher.u64 ```ceesafe public fn u64(borrow mut this, value: u64) ``` Feeds `value`'s eight bytes, little-endian. With nothing pending they are one message block. ### Hasher.finish ```ceesafe public fn finish(borrow this) -> u64 ``` The hash of what was fed. ## fn deriveHash ```ceesafe public fn deriveHash>(key: borrow K, h: borrow mut Hasher) ``` Feeds `key` to `h` through its `HashKey`: what a derived `hash` does for each field (ADR 0087 §2.3). A field type without one is N0268 at the `@derive`. ## struct HashMap ```ceesafe public struct HashMap, V> ``` A map from keys to values (ADR 0052 §3.2). Iteration (`keyAt`, `valueAt` for 0..length) is in insertion order, except that a removal moves the last entry into the removed one's place. Only `put` allocates; a failed `put` leaves the map as it was. ### HashMap.create ```ceesafe public fn create() -> HashMap ``` An empty map keyed by the process's random key. Safety: the runtime writes two words into the array, which outlives the call. ### HashMap.withSeed ```ceesafe public fn withSeed(k0: u64, k1: u64) -> HashMap ``` An empty map keyed by `k0` and `k1`: for tests, and for keys no one chooses against it. ### HashMap.length ```ceesafe public fn length(borrow this) -> usize ``` How many entries. ### HashMap.get ```ceesafe public fn get(borrow this, key: borrow K) -> borrow(this) V? ``` The value of `key`, if it is there. ### HashMap.getMut ```ceesafe public fn getMut(borrow mut this, key: borrow K) -> borrow mut(this) V? ``` The same, to change it. ### HashMap.contains ```ceesafe public fn contains(borrow this, key: borrow K) -> bool ``` Whether `key` is there. ### HashMap.keyAt ```ceesafe public fn keyAt(borrow this, i: usize) -> borrow(this) K ``` The key of entry `i` (0 ≤ i < length). ### HashMap.valueAt ```ceesafe public fn valueAt(borrow this, i: usize) -> borrow(this) V ``` The value of entry `i` (0 ≤ i < length). ### HashMap.put ```ceesafe public fn put(borrow mut this, key: K, value: V) -> Result ``` Puts `value` at `key`: the old value if the key was there, else null. A failure leaves the map as it was. A map holds at most 2^32 − 1 entries (a slot's entry field); one more is OutOfMemory. ### HashMap.remove ```ceesafe public fn remove(borrow mut this, key: borrow K) -> V? ``` Takes `key` out: its value, or null. The last entry moves into its place (ADR 0052 §2). ### HashMap.clear ```ceesafe public fn clear(borrow mut this) ``` Takes every entry out; the table keeps its size. ## enum PathKind ```ceesafe public enum PathKind { File, Directory, Other, } ``` What is at a path (`File.kind`, type-system.md §26.8). ## struct ProcessOutput ```ceesafe public struct ProcessOutput { public status: i32; public stdout: Vector; public stderr: Vector; } ``` A finished program's exit status and what it wrote (`Process.capture`, type-system.md §26.7). ## struct Environment ```ceesafe public struct Environment ``` The environment the program started with (type-system.md §26.9). ### Environment.variable ```ceesafe public fn variable(name: StringView) -> StringView? ``` The value of the variable `name` as static text, or null if it is not set or not valid UTF-8. Safety: the runtime returns getenv's text, which stays valid and unchanged: CeeSafe has no way to change the environment (C code that calls setenv breaks this, as it does in C). ## struct VolumeSpace ```ceesafe public struct VolumeSpace { public total: u64; public free: u64; public available: u64; } ``` Directories (type-system.md §26.10). A volume's bytes (`Directory.space`, ADR 0077). ## struct Directory ```ceesafe public struct Directory ``` ### Directory.create ```ceesafe public fn create(path: StringView) -> Result ``` Makes one directory; `AlreadyExists` if something is there already. Safety: the runtime reads the path's bytes during the call only. ### Directory.remove ```ceesafe public fn remove(path: StringView) -> Result ``` Removes an empty directory. Safety: the runtime reads the path's bytes during the call only. ### Directory.space ```ceesafe public fn space(path: StringView) -> Result ``` The bytes of the volume that holds `path` (ADR 0077): `total`, `free`, and `available` to this process, which is at most `free` where the system keeps a reserve for its administrator. POSIX `statvfs`; Windows `GetDiskFreeSpaceExA`. A storage engine stops writing before `available` runs out. The interpreter answers `Unsupported`. Safety: the runtime reads the path's bytes and writes the three values during the call only. ### Directory.sync ```ceesafe public fn sync(path: StringView) -> Result ``` Makes the directory's entries durable: the files created, renamed or removed in it survive a power cut once this returns (ADR 0075 §2.1). POSIX: `fsync` of the directory, `F_FULLFSYNC` on Apple systems. Windows journals directory entries and cannot flush a directory: there it checks the path is a directory and does nothing else. Safety: the runtime reads the path's bytes during the call only. ### Directory.list ```ceesafe public fn list(path: StringView) -> Result, IoError> ``` The names in a directory, without `.` and `..`, sorted by byte value so programs stay deterministic. A name that is not valid UTF-8 is `InvalidInput`: skipping it could make a program miss a file without a word. Safety: the listing is closed on every path out (`defer`), and each name is copied before the next call reuses the runtime's buffer. ### Directory.temporary ```ceesafe public fn temporary() -> Result ``` A new, empty directory that only this user can enter, under the platform's temporary directory. The caller removes it. Safety: the runtime writes at most the buffer's length. ### Directory.current ```ceesafe public fn current() -> Result ``` The working directory. Safety: the runtime writes at most the buffer's length. ## struct HostServices ```ceesafe public struct HostServices ``` The functions behind `Process.capture` and `File.kind`. ### HostServices.capture ```ceesafe public fn capture( program: StringView, arguments: borrow Vector, input: Slice?, ) -> Result ``` `Process.capture(program, arguments, input)` (type-system.md §26.7): runs `program`, found on `PATH`, with `arguments`, `input` as its standard input (empty if null), and returns its exit status and what it wrote to standard output and standard error. The streams go through files in a private temporary directory, removed before this returns, so a program that writes much to both cannot block (ADR 0028). ### HostServices.kind ```ceesafe public fn kind(path: StringView) -> Result ``` `File.kind(path)` (type-system.md §26.8): what is at `path`, following symbolic links, or null if nothing is. Safety: the runtime reads the path's bytes during the call only. ### HostServices.flush ```ceesafe public fn flush() ``` `Console.flush()` (ADR 0077): writes what standard output holds buffered, so it survives the process being killed. Standard error is never buffered. Safety: the runtime only flushes the C library's standard output. ### HostServices.modified ```ceesafe public fn modified(path: StringView) -> Result ``` `File.modified(path)` (ADR 0077): when the file or directory at `path` was last modified, as a `UnixTime` (`stat`'s `st_mtime`, with nanoseconds where the system keeps them). The interpreter answers `Unsupported`. Safety: the runtime reads the path's bytes and writes the two values during the call only. ### HostServices.setPermissions ```ceesafe public fn setPermissions(path: StringView, mode: u32) -> Result ``` `File.setPermissions(path, mode)` (ADR 0077): POSIX `chmod`, the low 12 bits of `mode` (`0o640`: the owner reads and writes, the group reads). Windows has only a read-only attribute, set when the owner may not write. A file created by `File.createNew` and given its permissions before anything is written to it never holds data others could read. The interpreter answers `Unsupported`. Safety: the runtime reads the path's bytes during the call only. ### HostServices.rename ```ceesafe public fn rename(from: StringView, to: StringView) -> Result ``` `File.rename(from, to)` (ADR 0075 §2.1): gives the file at `from` the name `to`, atomically replacing a file there (POSIX `rename`; Windows `MoveFileExW` with `MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH`). The new name is durable once its directory is synced (`Directory.sync`). Safety: the runtime reads both paths' bytes during the call only. ## struct FileMetadata ```ceesafe public struct FileMetadata ``` An open file's metadata (ADR 0078 §3), read from the file (`fstat`), not its path: it describes the file that is read, even if another has been renamed into its place since it was opened. `f.modified()` and `f.kind()` are the checker's names for FileMetadata.modified(f) and FileMetadata.kind(f). ### FileMetadata.modified ```ceesafe public fn modified(file: borrow File) -> Result ``` `f.modified()`: when the open file was last modified, with nanoseconds where the system keeps them. The interpreter answers `Unsupported`, as for `File.modified(path)`. Safety: the runtime reads the file's descriptor and writes the two values during the call only. ### FileMetadata.kind ```ceesafe public fn kind(file: borrow File) -> Result ``` `f.kind()`: what the open file is. A directory opened for reading is `Directory`; a device, FIFO or socket is `Other`. Safety: the runtime reads the file's descriptor during the call only. ## struct UnixTime ```ceesafe public struct UnixTime { public seconds: i64; public nanoseconds: u32; } ``` A point in real time (ADR 0055): seconds since 1970-01-01T00:00:00Z, leap seconds not counted (POSIX time), negative before it, and the nanoseconds after them. ### UnixTime.millis ```ceesafe public fn millis(borrow this) -> i64 ``` The milliseconds since 1970, rounded toward minus infinity (the nanoseconds are never negative). ## struct Clock ```ceesafe public struct Clock ``` The system's real time (ADR 0055): the date, which may jump backward or forward (NTP, the user). Measure and wait with `Timer`, whose monotonic clock never jumps. The interpreter answers 2000-01-01T00:00:00Z plus its virtual clock, so its runs stay reproducible. ### Clock.now ```ceesafe public fn now() -> UnixTime ``` Now. Safety: the runtime writes the two values during the call only. ### Clock.unixMillis ```ceesafe public fn unixMillis() -> i64 ``` Now, in milliseconds since 1970. ### Clock.monotonicNanos ```ceesafe public fn monotonicNanos() -> u64 ``` Nanoseconds on the monotonic clock, from an arbitrary start (ADR 0075 §2.6): for measuring durations and latencies, never for the date. It never decreases within a process (`CLOCK_MONOTONIC`; `CLOCK_UPTIME_RAW` on Apple systems; `QueryPerformanceCounter` on Windows). The interpreter answers its virtual clock. Safety: the runtime only reads the clock. ## struct Random ```ceesafe public struct Random ``` Random bytes from the system's cryptographic source (ADR 0060): `getrandom` (or `/dev/urandom`) on Linux, `arc4random_buf` on macOS and the BSDs, `BCryptGenRandom` on Windows; for keys, nonces, masks and tokens. Each call reads the source, which is cheap enough to call per message. The interpreter answers a fixed sequence instead, so its runs stay reproducible: its bytes are not secret. ### Random.fill ```ceesafe public fn fill(into: SliceMut) -> Result ``` Fills `into` with random bytes. `Err(Unsupported)` where the system has no source (CosmoOS userland); another error if the source fails, with `into` partly written. It has no effect, so async code and a `drop` block may call it. Safety: the runtime writes at most the slice's length, during the call only. ## enum SocketAddress ```ceesafe public enum SocketAddress { V4([u8; 4], u16), V6([u8; 16], u16, u32), } ``` An IPv4 or IPv6 endpoint: the address bytes in network order and the port; an IPv6 address also has its scope id (0: none), which a link-local address needs to say which interface it is on. Implements `SocketEndpoint`. ## interface SocketEndpoint ```ceesafe public interface SocketEndpoint { fn port(borrow this) -> u16; fn withPort(borrow this, p: u16) -> SocketAddress; fn isLoopback(borrow this) -> bool; fn isUnspecified(borrow this) -> bool; fn write(borrow this, w: borrow mut TextWriter); } ``` What every SocketAddress answers (ADR 0044 §3.1). ## struct SocketAddresses ```ceesafe public struct SocketAddresses ``` The ways to make a SocketAddress: from text, and the well-known ones. (An enum has no functions of its own, type-system.md §15.1, so they are here.) ### SocketAddresses.parse ```ceesafe public fn parse(text: StringView) -> SocketAddress? ``` `a.b.c.d:port`, `[v6]:port` or `[v6%zone]:port`, where the zone is a number or an interface's name; null if the text is not one. ### SocketAddresses.parseIp ```ceesafe public fn parseIp(text: StringView, port: u16) -> SocketAddress? ``` An address without a port (`127.0.0.1`, `::1`, `fe80::1%2`, `fe80::1%en0`) and `port`; null if the text is not one. ### SocketAddresses.v4Any ```ceesafe public fn v4Any(port: u16) -> SocketAddress ``` ### SocketAddresses.v6Any ```ceesafe public fn v6Any(port: u16) -> SocketAddress ``` ### SocketAddresses.v4Loopback ```ceesafe public fn v4Loopback(port: u16) -> SocketAddress ``` ### SocketAddresses.v6Loopback ```ceesafe public fn v6Loopback(port: u16) -> SocketAddress ``` ## struct ListenOptions ```ceesafe public struct ListenOptions { public backlog: u32; public reuseAddress: bool; public reusePort: bool; public v6Only: bool; } ``` How a listener is made (ADR 0044 §3.2). `defaults()`: the platform's largest backlog, the address reusable (as the runtime always did), no port sharing, and dual stack for an IPv6 address. ### ListenOptions.defaults ```ceesafe public fn defaults() -> ListenOptions ``` ## struct IoParts ```ceesafe public struct IoParts ``` A list of up to eight byte parts sent as one (ADR 0045 §3.1): an HTTP response's head and body, or a chunk's size line, data and CRLF, in one system call and without a copy. It is a value built by copying, since a slice cannot be an array element: `IoParts.of(head).then(body)`. Empty parts are allowed and send nothing. ### IoParts.of ```ceesafe public fn of(part: Slice) -> IoParts ``` One part. ### IoParts.then ```ceesafe public fn then(this, part: Slice) -> IoParts? ``` These parts and `part` after them, or null when there are eight already. ### IoParts.count ```ceesafe public fn count(borrow this) -> u32 ``` How many parts (0 to 8; 0 only from `after`). ### IoParts.part ```ceesafe public fn part(this, i: u32) -> Slice ``` Part `i` (from 0); empty past the last. ### IoParts.totalLength ```ceesafe public fn totalLength(borrow this) -> usize ``` The number of bytes in every part. ### IoParts.after ```ceesafe public fn after(this, skip: usize) -> IoParts ``` The bytes after the first `skip`, as parts: whole parts dropped, the part where byte `skip` falls cut. Empty (no parts) from `totalLength()` on. What a write loop sends after a partial send. ## struct UdpOptions ```ceesafe public struct UdpOptions { public reuseAddress: bool; public reusePort: bool; public v6Only: bool; public sendBuffer: u32; public recvBuffer: u32; public receiveEcn: bool; public receiveLocal: bool; } ``` How a UDP socket is made (ADR 0050 §3.1). `defaults()`: no option set, the system's buffer sizes, and dual stack for an IPv6 address. ### UdpOptions.defaults ```ceesafe public fn defaults() -> UdpOptions ``` ## struct Datagram ```ceesafe public struct Datagram { public length: usize; public from: SocketAddress; public truncated: bool; } ``` What `AsyncUdpSocket.recvFrom` gives: the datagram's length in the buffer, where it came from, and whether it was longer than the buffer (the rest is gone). ## struct UdpReceived ```ceesafe public struct UdpReceived { public length: usize; public truncated: bool; public from: SocketAddress; public to: SocketAddress?; public ecn: u8; } ``` What `AsyncUdpSocket.recvMsg` gives (ADR 0050 §3.1). ## struct UdpMessage ```ceesafe public struct UdpMessage { public parts: IoParts; public to: SocketAddress; public from: SocketAddress?; public ecn: u8; public segmentSize: u16; } ``` A datagram for `AsyncUdpSocket.sendMsg` (ADR 0050 §3.1): its parts (ADR 0045), where it goes, and optionally the local address it leaves from, its ECN bits and a segment size. ## struct AsyncDns ```ceesafe public struct AsyncDns ``` Name resolution for tasks (ADR 0048). ### AsyncDns.resolve ```ceesafe public async fn resolve(host: StringView, port: u16) -> Result, AsyncError> ``` The addresses of `host`, each with `port`: both families, in the resolver's order, duplicates removed, never empty. A literal address is itself, at once. Otherwise the system's resolver runs on one of at most four resolver threads while the task waits; a cancelled task returns `Err(Cancelled)` at once, and the lookup is left to its thread. Errors are `AsyncError.Io`: `HostNotFound` (no such name, or no address for it; ADR 0060), `TimedOut` (a temporary failure: try again), `InvalidInput` (an empty name, one over 253 bytes or with a NUL), `OutOfMemory`, `Unsupported` (no resolver: CosmoOS, the interpreter for names other than `localhost` and the reserved `.invalid` ones). Safety: the runtime copies the name during the start; the lookup's record is the runtime's, ended once by `DnsLookup`'s drop, so no path leaves it or ends it twice. ## interface OrderKey ```ceesafe public interface OrderKey { fn compareKey(borrow this, other: borrow K) -> i32; } ``` A key of an OrderedMap (ADR 0075 §2.4): `K: OrderKey`, since an interface has no `Self`. The order must be total and consistent: `a.compareKey(b)` is negative, zero or positive as `b.compareKey(a)` is positive, zero or negative. ## fn deriveCompare ```ceesafe public fn deriveCompare>(a: borrow K, b: borrow K) -> i32 ``` `a.compareKey(b)`: what a derived `compareKey` does for each field (ADR 0087 §2.3). A field type without an `OrderKey` is N0268 at the `@derive`. ## struct OrderedCursor ```ceesafe public struct OrderedCursor ``` A position in an OrderedMap: a node, a slot in it, and the map's version when the cursor was made. A cursor is read through its map (`keyAt`, `valueAt`); one used after the map changed panics. ## struct OrderedMap ```ceesafe public struct OrderedMap, V> ``` A map whose iteration order is its keys' order (ADR 0075 §2.4): a B-tree of minimum degree 16 (at most 31 entries a node), its nodes in one arena vector and linked by index, so it needs no `unsafe`. Ordered access is through cursors (`first`, `last`, `seek`, `seekAfter`, `next`, `previous`); a put, a removal or a clear changes the map's version, and a cursor made before it panics when used ("ordered map changed under a cursor"). Only `put` allocates; a failed `put` leaves the map as it was. ### OrderedMap.create ```ceesafe public fn create() -> OrderedMap ``` An empty map. It allocates nothing until the first put. ### OrderedMap.length ```ceesafe public fn length(borrow this) -> usize ``` How many entries. ### OrderedMap.isEmpty ```ceesafe public fn isEmpty(borrow this) -> bool ``` Whether there are none. ### OrderedMap.get ```ceesafe public fn get(borrow this, key: borrow K) -> borrow(this) V? ``` The value of `key`, if it is there. ### OrderedMap.getMut ```ceesafe public fn getMut(borrow mut this, key: borrow K) -> borrow mut(this) V? ``` The same, to change it. Changing a value does not change the map's version. ### OrderedMap.contains ```ceesafe public fn contains(borrow this, key: borrow K) -> bool ``` Whether `key` is there. ### OrderedMap.put ```ceesafe public fn put(borrow mut this, key: K, value: V) -> Result ``` Puts `value` at `key`: the old value if the key was there, else null. A failure leaves the map as it was. The nodes a put may need are allocated before anything changes. ### OrderedMap.remove ```ceesafe public fn remove(borrow mut this, key: borrow K) -> V? ``` Takes `key` out: its value, or null. The descent keeps every node it enters above the minimum, borrowing from a sibling or else merging (CLRS's B-TREE-DELETE), so the tree stays balanced. Nothing allocates. ### OrderedMap.clear ```ceesafe public fn clear(borrow mut this) ``` Takes every entry out and frees the nodes. ### OrderedMap.first ```ceesafe public fn first(borrow this) -> OrderedCursor? ``` The least entry, or null if the map is empty. ### OrderedMap.last ```ceesafe public fn last(borrow this) -> OrderedCursor? ``` The greatest entry, or null if the map is empty. ### OrderedMap.seek ```ceesafe public fn seek(borrow this, key: borrow K) -> OrderedCursor? ``` The first entry whose key is not before `key`, or null if there is none. ### OrderedMap.seekAfter ```ceesafe public fn seekAfter(borrow this, key: borrow K) -> OrderedCursor? ``` The first entry whose key comes after `key`, or null if there is none. ### OrderedMap.next ```ceesafe public fn next(borrow this, at: OrderedCursor) -> OrderedCursor? ``` The entry after `at`'s, or null at the end. ### OrderedMap.previous ```ceesafe public fn previous(borrow this, at: OrderedCursor) -> OrderedCursor? ``` The entry before `at`'s, or null at the start. ### OrderedMap.keyAt ```ceesafe public fn keyAt(borrow this, at: OrderedCursor) -> borrow(this) K ``` The key at `at`. ### OrderedMap.valueAt ```ceesafe public fn valueAt(borrow this, at: OrderedCursor) -> borrow(this) V ``` The value at `at`. ### OrderedMap.valueAtMut ```ceesafe public fn valueAtMut(borrow mut this, at: OrderedCursor) -> borrow mut(this) V ``` The value at `at`, to change it. Changing a value does not change the map's version. ## struct Mutex ```ceesafe @threadSafe @noncopy public struct Mutex { drop { … } } ``` Mutual exclusion: the data is reached only through a guard, which one thread holds at a time. ### Mutex.create ```ceesafe public fn create(data: T) -> Result, AllocError> ``` A mutex holding `data`; its platform lock is allocated. Safety: a null lock is reported, never stored. ### Mutex.lock ```ceesafe public fn lock(borrow this) -> MutexGuard ``` Waits for the lock; the guard lends the data until it is dropped. Safety: `raw` is the lock made in create, destroyed only in drop, when no guard can be live (B-rules). ## struct MutexGuard ```ceesafe public struct MutexGuard { drop { … } } ``` Holding a Mutex: `data()` lends the protected value; dropping the guard unlocks. ### MutexGuard.data ```ceesafe public fn data(borrow mut this) -> borrow mut T ``` Safety: the guard holds the lock, so no other thread reaches the cell's contents, and the loan ends before the guard's drop (a RawCell's contents may change while the mutex is shared, §41.1). ## struct Arc ```ceesafe @threadSafe @noncopy public struct Arc { drop { … } } ``` Shared ownership across threads: each clone is an owner, and the last owner's drop destroys the value. ### Arc.create ```ceesafe public fn create(value: T) -> Result, AllocError> ``` Safety: the pointer comes from Box.intoRaw and is given back once, by the last owner's drop. ### Arc.clone ```ceesafe public fn clone(borrow this) -> Arc ``` Another owner of the same value. Safety: this owner keeps the value alive while the count is raised. ### Arc.get ```ceesafe public fn get(borrow this) -> borrow T ``` The shared value, for as long as this owner is borrowed. Safety: the value lives while any owner does; T is Share, so shared references to it may cross threads. ## enum SpawnError ```ceesafe public enum SpawnError { Resources, } ``` Why Thread.spawn failed: the thread, its record or its closure could not be allocated (type-system.md §38.2). ## struct TestFailure ```ceesafe public struct TestFailure { public message: Vector; } ``` What a failed check returns (ADR 0049 §3.2): a message, owned, so it borrows nothing from the test. `ceesafec test` shows it with the test's name. ### TestFailure.create ```ceesafe public fn create(message: StringView) -> TestFailure ``` A failure with `message`. If the message cannot be allocated, the failure has an empty one: it still fails. ## struct Check ```ceesafe public struct Check ``` The checks of a test (ADR 0049 §3.2): each is `Ok` when it holds, and otherwise `Err` with what was expected, for `?`. The comparisons are typed: CeeSafe has no equality interface yet for a generic one. ### Check.isTrue ```ceesafe public fn isTrue(condition: bool, what: StringView) -> Result ``` `condition` holds; `what` says what it is. ### Check.isFalse ```ceesafe public fn isFalse(condition: bool, what: StringView) -> Result ``` `condition` does not hold. ### Check.equalU64 ```ceesafe public fn equalU64(expected: u64, actual: u64) -> Result ``` `actual` is `expected`. ### Check.equalI64 ```ceesafe public fn equalI64(expected: i64, actual: i64) -> Result ``` `actual` is `expected`. ### Check.equalText ```ceesafe public fn equalText(expected: StringView, actual: StringView) -> Result ``` `actual` is the text `expected`. ### Check.equalBytes ```ceesafe public fn equalBytes(expected: Slice, actual: Slice) -> Result ``` `actual` has the bytes of `expected`; else where they first differ. ### Check.fail ```ceesafe public fn fail(message: StringView) -> Result ``` A failure with `message`, always. ## struct CheckResult ```ceesafe public struct CheckResult ``` The checks of a `Result` (ADR 0049 §3.2), a generic struct's since a method declares no type parameters of its own: `CheckResult.isOk(borrow parsed, "the line parses")?`. ### CheckResult.isOk ```ceesafe public fn isOk(r: borrow Result, what: StringView) -> Result ``` `r` is `Ok`. ### CheckResult.isErr ```ceesafe public fn isErr(r: borrow Result, what: StringView) -> Result ``` `r` is `Err`. ## struct TestRun ```ceesafe public struct TestRun ``` The generated harness's side of the runner protocol, version 1 (ADR 0049 §3.4). Not for tests to call. ### TestRun.begin ```ceesafe public fn begin() ``` Before the test: the arguments after the test's name, ` ` (0: none), turn the runtime's reports and hooks on and arm the timeout. Safety: the runtime only records the numbers, and the watchdog only waits and ends the process. ### TestRun.passed ```ceesafe public fn passed() -> i32 ``` The test passed: its line, and the exit status 0. ### TestRun.failed ```ceesafe public fn failed(message: Slice) -> i32 ``` The test failed with `message`: its lines, and the exit status 1. ### TestRun.failedWith ```ceesafe public fn failedWith(message: StringView) -> i32 ``` The same with a fixed message. ### TestRun.unknown ```ceesafe public fn unknown() -> i32 ``` No test has the name the runner asked for. ## enum WindowError ```ceesafe public enum WindowError { Unsupported, NoDisplay, NotMainThread, Resources, } ``` Why a window could not be opened or drawn (§26.13). ## struct WindowSize ```ceesafe public struct WindowSize { public width: u32; public height: u32; } ``` A window's content size in points. ## struct WindowBox ```ceesafe public struct WindowBox { public x: u32; public y: u32; public width: u32; public height: u32; } ``` A part of an image that changed (`Window.presentChanged`), in the image's pixels. ## enum WindowButton ```ceesafe public enum WindowButton { Primary, Secondary, Middle, } ``` A pointer button. ## struct WindowModifiers ```ceesafe public struct WindowModifiers { public shift: bool; public control: bool; public alt: bool; public command: bool; } ``` The modifier keys held during a key event. `command` is ⌘ on macOS and the Windows key elsewhere. ## enum WindowKey ```ceesafe public enum WindowKey { Tab, Enter, Escape, Backspace, Delete, Left, Right, Up, Down, Home, End, PageUp, PageDown, Space, Character(u32), Function(u8), Other(u32), } ``` A key (§26.13): the named keys a UI needs, letters and digits as `Character`, F1 to F12 as `Function`. ## enum WindowCursor ```ceesafe public enum WindowCursor { Default, Pointer, Text, Crosshair, Grab, ResizeHorizontal, ResizeVertical, } ``` The pointer's shape over the content. ## enum WindowEvent ```ceesafe public enum WindowEvent { Closed, Resized(WindowSize), Focus(bool), PointerMoved(f32, f32), PointerButton(WindowButton, bool, f32, f32), Wheel(f32, f32), Key(WindowKey, bool, bool, WindowModifiers), Text(u32), } ``` What happened to a window (§26.13). Positions are points from the content's top left. ## struct Window ```ceesafe public struct Window { drop { … } } ``` A native window (§26.13): a move type that closes the window when dropped. It is used on the program's first thread only. ### Window.open ```ceesafe public fn open(title: StringView, width: u32, height: u32) -> Result ``` Opens a resizable window of `width` × `height` points titled `title`. Safety: the runtime reads the title's bytes during the call only, and the handle it returns is this window's until the drop closes it. ### Window.nextEvent ```ceesafe public fn nextEvent(borrow mut this) -> WindowEvent? ``` The next pending event, or null; it never blocks. Safety: the runtime writes four fields and two coordinates, the sizes of the arrays it is given. ### Window.present ```ceesafe public fn present(borrow mut this, pixels: Slice, width: u32, height: u32) -> Result ``` Shows a `width` × `height` image of premultiplied RGBA8 pixels (bytes R, G, B, A), rows top to bottom, scaled to the content. Safety: the runtime reads `width × height` pixels, which `pixels` is checked to hold, during the call only. ### Window.presentChanged ```ceesafe public fn presentChanged( borrow mut this, pixels: Slice, width: u32, height: u32, changed: Slice, ) -> Result ``` Like `present`, but only the pixels inside `changed` differ from the last frame presented (§26.13, ADR 0070): the window shows just those, which saves copying the rest. Without a last frame of this size, with a box outside the image, or with more than 64 boxes, it is `present`. An empty `changed` shows nothing new. Safety: the runtime reads `width × height` pixels, which `pixels` is checked to hold, and the boxes, during the call only. ### Window.size ```ceesafe public fn size(borrow this) -> WindowSize ``` The content's size in points. Safety: the runtime writes the two sizes. ### Window.scale ```ceesafe public fn scale(borrow this) -> f32 ``` Pixels per point: 2.0 on a Retina display. Safety: the runtime writes the scale. ### Window.setCursor ```ceesafe public fn setCursor(borrow mut this, cursor: WindowCursor) ``` The pointer's shape while it is over the content. Safety: the runtime only reads the cursor's number. ### Window.setTitle ```ceesafe public fn setTitle(borrow mut this, title: StringView) ``` Sets the title. Safety: the runtime reads the title's bytes during the call only. ### Window.waitFrame ```ceesafe public fn waitFrame(borrow mut this) ``` Returns about 1/60 s after the previous call, or earlier when an event arrives. Safety: the runtime only waits on the window it was given.