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.
namespacedeclarations 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<u8>; - in an expression,
geometry.area(p),geometry.LIMIT,geometry.Shape.Circle(2), a struct literalgeometry.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<u8>.make()), sincegeometry.Pair<u8>.make()does not parse (grammar.md §2.1: in an expression, generic arguments follow a simple name only).- in a type,
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.Xis resolved in. A parameter, local, loop variable, closure parameter or pattern binding may (ADR 0074 §2.10):- in its scope,
geometry.xin 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.Pointin a type, a struct literalgeometry.Point { … }and a patterngeometry.Shape.Circle(r)keep naming the package's items; - where
xis also a public item of the dependency,geometry.xis warning N0401, since the item is hidden.
Elsewhere
geometry.xmeans the dependency's item, and the compiler resolves it before type checking (PackageNames), with a syntactic scope walk to find the locals.- in its scope,
Imports.
import geometry;at the top of a file brings every public item of the dependencygeometryinto 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
publicitems are visible to other packages, and of a public struct only itspublicfields and methods;internal, the default, keeps an item within its package, andprivatewithin 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. Animpl I for Tapplies whereverIandTare 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 asceeT_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. Fora.b: ifais a local or an item of the package or oflib/core,.bis a member as before; otherwise,abeing a dependency (and so not an item, N0315),a.bis that dependency's itemb, unless a localais in scope there (ADR 0074 §2.10).T.NAME, withTa 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 _andlet _: Tare 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
voidis an error (N0224). - Parameters and
constitems are immutable. Assigning to them, or to alet, 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
-128i8and-9223372036854775808(asi64) 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, soc == 'k'withc: u8compares bytes, and'0'..'9'is a range. With no integer expectation it is au32. In an 8-bit type it must be ASCII (at most0x7F), 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 nochartype: 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;
returnvalue → the return type;constvalue → its declared type;if/whilecondition →bool; each value branch of aniformatchexpression → 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 aniformatchexpression whose expression branches are all flexible (§15.3). Since M11 (G7), unsuffixed float literals combined the same way are flexible too; they take an expectedf32orf64instead (§35.2). For a binary operator whose operands must have the same type:- if the operator's result type is the operand type and an integer type is expected, both operands are checked against it;
- 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;
- otherwise both are checked without expectation (flexible ones default to
i32).
So
x + 1withx: u8types1asu8, andlet y: u64 = 2 * 3;types both literals asu64.
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<u32>(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 Tis allowed only when every value of the source type is representable inT(N0208 otherwise):- identity;
uN → uMandiN → iMfor M ≥ N;uN → iMfor M > N;bool →any integer type (false→0,true→1).isize/usizeuse the target width. Floats (M11, §35.4):f32 → f64, and the integers that a float type holds exactly.
truncate<T>(e):eof any integer type,Tany integer type. The result isemodulo 2^bits(T), reinterpreted inT(two's complement). It is always defined. Use it for narrowing and for signed↔unsigned reinterpretation.checkedTruncate<T>(e)andsaturatingTruncate<T>(e)(ADR 0074 §2.9):eof any integer type,Tany integer type.checkedTruncategivesT?:e's value whenTholds it, elsenull.saturatingTruncategivesT:e's value whenTholds it, elseT's least value for anebelowT'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:
asnever loses information;truncatewraps;checkedTruncateandsaturatingTruncatecheck or clamp (checkedTruncate<i64>(n)!for ausizen).
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)andConsole.writeLine(x):xis an integer, abool, or a string literal.writeLine()with no argument writes a newline. Both returnvoid.Process.exit(code: i32): has type never.truncate<T>(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: <message> at <file>:<line>:<column>. In a freestanding program the environment'sceesafe_panicgetsCEE_PANIC_EXPLICIT("explicit panic") and the message is not shown.assert(condition)andassert(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)anddebugAssert(condition, message)(ADR 0087 §2.5):void. They are checked in every build, like any call, but evaluated only when debug assertions are on: underbuild -g(unless--no-debug-assertions) or--debug-assertions(build,run), underceesafec test, and in the interpreter (unlessrun --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 asassertdoes. Since the call may vanish, its arguments may change nothing that exists before it (N0360):- no assignment,
mutargument orborrow mut thiscall 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 (?), nobreakorcontinueto 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
sleeporsuspendeffect (§32.1): no I/O, noawait. 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 adynmethod. lib/core's atomic loads (AtomicU32.load, …) andCell.getonly read and are allowed; atomic stores, swaps, fetches and compare-exchanges, lock acquisition andRefCell.read/writeare not; - a call is allowed when its callee's body, and everything it calls or drops, keeps these rules, writes through its own
borrow mutparameters aside (the caller'smutargument is what is checked). Drops count: a temporary the arguments make is dropped inside the call, so itsdropblock is checked as a call. The checker infers this per function with §32.1's machinery, as an internal fourth fact that no@effectslist names.
To assert through something the rules refuse, bind the value to a local before the call, or put an
assertinif (cfg("NAME")) { … }(ADR 0080 §3.3).- no assignment,
replace(mut place, value) -> T(ADR 0075 §2.11): putsvaluein the place and returns the old value.swap(mut a, mut b)exchanges two places of one type (N0205 otherwise). Each argument ismutand a place (N0249, N0247). Neither leaves a place moved-from, so both work for any type, a field of a struct with adropblock included:replace(mut this.buffer, Vector<u8>.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/continuemust be insidewhileorloop(N0214).return;only in avoidfunction.return e;only in a non-voidfunction (N0215), except thatreturn v;of a unit local isreturn;(§20.4). Neither is allowed in aneverfunction (§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) readsx. 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,breakorcontinue, or a call of type never. It also may not complete if it is a block containing such a statement, anifwith anelsewhose two branches both cannot complete, or aloopwith nobreakthat targets it. Awhileloop may always complete, whatever its condition: writeloopfor infinite loops. Aniformatchexpression (§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-
voidfunction body may be reached, that is N0213. For a function returningnever(§1), the end must be unreachable too: every path ends in a call of type never or aloopwithoutbreak.
10. Constants
const N: T = e;whereeis a constant expression: literals, other constants, and unary, binary,asandtruncateapplied to constant expressions. Calls are not constant (N0217), exceptembedandembedText(§10.1) and a constant view's.bytes()(below).==and!=on constant views compare their bytes ("a" + "b" == "ab"istrue).- 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/coreCStr(§22.3, ADR 0087 §2.4), whose value is a string literal or ac"…", or aSlice<u8>of static bytes (below); other struct types are N0216. - Constant arrays (C9, ADR 0014):
Tmay be[E; N]withEan integer type,bool,StringVieworSlice<u8>(static bytes, below), andean 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.lengthread (ADR 0072), and bound to a local to be sliced or borrowed. A table to be sliced where it is named is astaticinstead (§39.2, ADR 0066). Arrays of other types, and of arrays, are N0216. - Constant views (ADR 0064 §2):
Tmay beStringView(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 areCStr,Slice<u8>and an array ofSlice<u8>, all holding static data; any other is N0246. - Constant bytes (ADR 0087 §2.8, §9). A constant's
Slice<u8>is static bytes: anembed(path)(§10.1), or the.bytes()of a constant view (a string literal, aStringViewconstant, anembedTextor a constant+), which is CeeSafe's spelling of a byte-string literal (const MAGIC: Slice<u8> = "\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), andascannot make one (N0208). - Constant concatenation (ADR 0087 §2.7). In a constant's value or a static initialiser (§39.4),
a + bwhose operands are both constant views (a string literal, a constant's name, anembedText, §10.1, or another such+) is a constant view:a's bytes, thenb's.A + B + Cfolds left to right, and a constant of another package (dep.PREFIX) is a constant view like any. With ac"…"(or aCStrconstant) as the right operand, the result is aCStr<static>whose text isa's then the literal's: a NUL byte inais N0006 at the+, as inside ac"…". ACStron 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 aString. @exporton 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 inherentimplblock (§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 asType.NAME, and from a dependent package asdep.Type.NAMEwhen public (N0312 otherwise). A bareNAMEdoes 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
implhas 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
usizeconstant, 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<u8>, and embedText(path) the same bytes as a StringView, read when the package is checked: static ROOT_CERTS: Slice<u8> = 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-
mutstatic may have the typeSlice<u8>for it, or an array of them; §10, §39.2), is used anywhere a literal is, andembedText(…)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 holdingceesafe.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, orcheckrun 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_GETPATHon macOS,/proc/self/fdon Linux,GetFinalPathNameByHandleWon 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).
- 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 (
- 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.
embedTextalso 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 betweencheckandbuildis read again bybuild, which compiles the new contents.embedis 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)
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-
voidM1 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 functionfofS. - Copy. In M2a every struct is a Copy type: its fields are Copy and
dropdoes not exist yet. Assignment, argument passing and return copy the value. OD-7's rule ("Copy iff all fields Copy and nodrop") 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. Eacheiis checked against the field's type. Initialisers are evaluated in source order (intermediate-representation.md), not in declaration order. - Field access.
e.fwhereehas struct typeSandfis a field ofShasf's type (unknown field: N0230;eis not a struct: N0234). An access chaina.b.cis a place ifais a local orthis. - Field assignment.
a.f = eanda.f op= erequire the root localato be avar(N0211).thisis 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
thisis a method.thishas the struct's type and is an immutable binding (a copy for Copy structs). It is called ase.m(args), whereeis evaluated first and passed asthis. A static function (nothis) is called asS.f(args). Calling a method asS.m(), or a static function ase.f(), is N0233.thisanywhere 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).publicandinternalfields, and the default, stay visible throughout the package. - Operators. No operator applies to struct values in M2a (
==included: N0207).Console.writecannot 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 fromTargetInfo. 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) andborrow mut T(exclusive), whereTis any non-reference, non-voidtype.borrow borrow Tis rejected (N0246). At runtime a reference is a non-null pointer.Where references may appear (v0):
- parameter types;
letlocals with an initialiser;- return types (with a source list, §13.5).
They may not appear in struct fields (E3), constants, or
varlocals (N0246). Struct fields of reference type need lifetime parameters (OD-3).borrow Tis Copy. Aborrow mut Tvalue is never copied: every use reborrows it (§13.3).
13.2 Creating references
borrow p/borrow mut p, wherepis a place: a local,this, a field path, or a path through a reference such asr.f((*r).f). Borrowing anything else is N0247; bind the value to a local first.borrow mut prequirespto be mutable: rooted at avarlocal, or reached through aborrow mutreference (N0248). Aletbinding, a parameter, a by-valuethis, and anything reached through a shared reference are immutable. That holds through aborrow mutorSliceMutstored where a shared reference leads: inh.vwithh: borrow Holderandv: borrow mut Vector<T>, the vector cannot be changed, as with Rust's& &mut T.Call sites.
A
borrow Tparameter accepts a place or value of typeT, borrowed implicitly. A value becomes a temporary that lives until the end of the statement.A
borrow Tparameter also acceptsborrow Torborrow mut T; the latter is coerced by a shared reborrow.A
borrow mut Tparameter acceptsmut p, wherepis a mutable place of typeT, or aborrow mut Tvalue (reborrowed).A
borrow T?parameter accepts a place of typeT?orT, and a(borrow mut T)?parametermut pof one (§22.1, ADR 0074 §2.5).
The
mutmarker is required for a plain place and is rejected where the parameter is notborrow mut(N0249). It makes mutation visible at the call site, as C#'srefdoes.Receivers.
fn m(borrow this)andfn m(borrow mut this)declare reference receivers;thisthen has typeborrow S/borrow mut S. Ine.m(…)the receiver is borrowed implicitly, with no marker. Forborrow mut this,emust be a mutable place or aborrow mut Svalue (N0248).A temporary as a receiver (ADR 0073 §2.1). A receiver of a
borrow thisorborrow mut thismethod that is not a place,make().view()ortext.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 aletused after it, it is N0243. A path through a reference is not copied: it is borrowed as a place. An explicitborrow eormut eof a value is still N0247.
13.3 Using references
- Auto-deref. Where a value of type
Tis needed (an operand, a by-value argument, an initialiser of typeT, a return of typeT,Console.write), aborrow Torborrow mut Toperand is read through:*r. Field accessr.fand method callsr.m()also look through references. - Writes through references.
r = v,r op= vandr.f = vwrite to the referent. This requiresborrow mut(N0211: "cannot assign through a shared reference"). A reference local itself can never be re-pointed. - Reborrow of
borrow mut. Using aborrow mut Tvalue 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 typeborrow mut TorSliceMut<T>(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 au64. The referent must be a Copy type (N0251 otherwise: borrow it, orreplaceit);*on anything but a reference is N0205, and a raw pointer is read withp.read()(§27).*r = vand*r op= varer = vandr 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 Twith 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 aborrow mutresult naming a shared parameter. Afterborrow, 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), soborrow (T?)is a reference to an optional andborrow(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, andborrow T. A struct is Copy iff it has nodropblock and every field is Copy. - Move types: structs with a
dropblock or a move-type field, andborrow 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
dropblock, 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
struct File {
fd: i32;
drop { Sys.close(this.fd); } // `this` is `borrow mut File`
}- At most one
dropblock per struct (N0228). Inside it,thishas typeborrow mut S. The block cannot be called explicitly. It runs exactly once when a value ofSis 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
dropblock is N0252: thedropblock needs the whole value. Moving out ofthisinsidedropis 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
thisreceiver; - a struct-literal field;
- a
returnvalue.
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 pexplicitly moves out of the placep, even for a Copy type. Afterwardspis uninitialised, which ends a value's use early.Partial moves (O2): moving
p.fout of a local is allowed if no struct on the path fromptofhas adropblock.pis then partially initialised.p.fmay be reassigned, after whichpis whole again. Readingpas 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
thisinborrow/borrow mutmethods; - moving out of a
usingbinding (N0253).
- moving out of a place reached through a reference (N0251), because the referent must stay valid; this includes
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.frequires the rest ofpto be initialised (no piecewise construction of an uninitialised struct). Re-initialising a moved-out field is allowed.- If some enclosing struct of
p.fhas adropblock, that whole struct must be initialised,fincluded: 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 isf(found by the mutation suite, M2d).
- If some enclosing struct of
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/continueleaving the block, or areturn; - the end of its statement, for a temporary. An
iforwhilecondition 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.
- the end of its variable's scope: the block end, a
- Order: locals in reverse declaration order; then a struct's
dropblock; 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.exitand 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/continueout 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)
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-voidtype (E3 applies to payloads too). An enum must not contain itself by value (N0227).- Values.
E.Vfor a variant without payload;E.V(e1, …)with exactly the payload arity (N0209). Eacheiis checked against its payload type. - Equality (ADR 0072). For an enum none of whose variants has a payload (a fieldless enum),
a == banda != bcompare the variants; both operands have that enum type and are only read. An enum with payloads is N0207 (compare withmatch), 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
dropblock (v0). - Layout.
- A tag (
u8for ≤ 256 variants, elseu16/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).
- A tag (
15.2 match
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<u8>: 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
matchis 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 guardgis abool(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 isfalse, 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 notawait(the loan would cross a suspension): N0355. This holds anywhere in the guard: in its blocks, loops (over ranges or iterators) anddeferblocks, and in a closure it makes, whichmut-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").
- 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
- 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; aSlice<T>(orSliceMut<T>), an array[T; N]or a reference to one: slice patterns, and on aSlice<u8>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).boolmatches 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..=himatcheslotohi, both included;lo..hiexcludeshi, 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 au8. 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; aboolbound is N0108). - String literal patterns (ADR 0085 §2.7). On a
StringView,"get"matches equal bytes, as==does (§22.2). On aSlice<u8>, 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 aStringViewneeds_or a binding); a repeated one is N0257. - Slice patterns (ADR 0085 §2.7).
[],[a, b],[first, ..],[.., last],[x, rest @ ..],['"', inner @ .., '"']match aSlice<T>, an array or a reference to one, and a payload that is a slice or aStringView(Some([a, ..]),Some("yes"); also an or-pattern of these,Some("x" | "y"), orname @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 aSlice<T>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 bevar). 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 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
- 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 aborrow T/borrow mut Tto the payload, and the borrow checker treats it like any reborrow of the scrutinee. _binds nothing.
- Scrutinee of type
- 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
matchstatement. - 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 orboolpayload (Some(3)), ornullon an optional payload (Ok(null)). Each is checked against the payload's type exactly as a wholematchis 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 | Bmatches 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 samevar-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 firsttruewins. - 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 aconstin scope that no local hides: it matches that value and counts as that literal for coverage. The constant must be an integer orbool(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), andvar na mutable one.Someon an optional, andOkorErron aResult, 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 animplof an interface gives the type, its own or the interface's default (ADR 0086 §2.4):A.CODE =>.Selfmay 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, orSelf.NAMEin 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 thatPmatched, as a payload binding would (a copy, a move or a reference, by the rules above), and testsP; it does not change coverage.@binds tighter than|:d @ ('0' | '1')needs the parentheses. A value moves at most once: whennamebinds by move (a non-Copy value of a consuming match, §18.4), no binding underPmay also move, soe @ Err(inner)with a non-Copy payload is N0250 with its own message ("moves the value thatinneralso 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 separatematchon that reference (N0205 otherwise). A slice orStringViewpayload 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/SomeandOk/Erras 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..=0x7Fand0x80..=0xFFcover au8). N0256's witness is then a value no arm holds: the lowest, written as a character literal for a printable byte of au8('{') and in decimal otherwise (128); a position no arm constrains is_. Strings are constructors of which there are infinitely many, so they never cover aStringView(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 aSlice<u8>is the slice of its bytes. N0256's witness names a length and its elements:[],[_, _, ..].
15.3 if and match expressions (M11, G5)
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; },
};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
returnvalue, …). Aniformatchthat begins a statement is the statement form (§9, §15.2), as before. Anifexpression needs itselse(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,continueor a call of type never, such as{ return; },{ Process.exit(1); }or{ die("bad number"); }withfn 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. Anif(with itselse) ormatchthat ends a value block, directly before its}, is read as an expression. Only branches ofifandmatchexpressions have value blocks: in a function body, a statement-formiformatchor 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 + 3isif (c) 1 else (2 + 3). Write parentheses to use anifexpression 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
nullis 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:f64if any is a float literal (§35.2), elsei32. 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.
matchexpressions 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
ifstatement (§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)
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:
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 Pis the test ofmatch (e) { P => true, _ => false }(§15.2):eis evaluated once, any pattern of §15.2 may follow, and testing only reads. Afteristhe pattern grammar reads as far as it can, so|there is an or-pattern. The scrutinee's type is one amatchtakes (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 theif(statement or expression) or the body of thewhilewhose condition is that chain. A pattern with bindings anywhere else (under||or!, as a wholeletinitialiser 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 amatch's do (§15.2). - Moves happen only on success.
ismoves 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 consumingmatchconsumes 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 }leavesewhole inB. 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
whilebody for that pass;breakandcontinueend them. When a later operand of the chain isfalse, those made so far are destroyed at once, in reverse order, before theelsebranch runs or the loop exits: inx is Some(v) && g, a falsegdestroysvthere. - Temporaries. The condition's temporaries end with the condition, before the then-branch, the
elsebranch 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, ..]), orrest @ ..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 theelsebranch or the next test (Rust 2024'sif letscope, applied towhiletoo). So aRefCellguard (§41.3) used to copy a value out is gone before the body, and a.write()in the body or inelsedoes not panic. Awhileevaluates 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: inlet ok = c.tryRead() is Some(g) && g.data() == 5;the guardgis released before the next statement, so atryWrite()there succeeds.
16. Arrays, slices and for (M2d)
16.1 Types
[T; N]is a fixed array ofNelements.Nis an integer literal (no suffix orusize), ausizeconstant (LENGTH, or an associated one,Oid.LENGTH; §10), or ausizeconst 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 asNconsecutiveTs: sizeN × size(T), the alignment ofT. The same holds for theNof a repeat[v; N](§16.2).- v0 restriction:
Tmust 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).
- v0 restriction:
Slice<T>/SliceMut<T>are borrowed views(pointer, length)of a run ofTs in an array or another slice. They follow every rule for references (§13), with the same scoping:- only in parameters,
letlocals with an initialiser, and return types with a source list; - never in fields, payloads, array elements, constants or
varlocals (N0246); Slice<T>is Copy, and each use of aSliceMut<T>reborrows it;SliceMut<T>coerces toSlice<T>;- 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.
- only in parameters,
- Internally,
Slice<T>isborrow [T]andSliceMut<T>isborrow 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 ofv.[](ADR 0075 §2.9) is the empty slice, where aSlice<T>orSliceMut<T>is expected (an initialiser, an argument, a return): no array is behind it, and its address isT'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, isa[..].m(…), mutably for the methods that write or move elements (writeLE,copyFrom,fill,swap,sortBy,splitAtMut, …), which need a mutable place asborrow mut a[..]does. a.lengthis ausize: the constantNfor arrays, the stored length for slices. For an array,amust be a place (a variable, field or element), because the constant would skip evaluating it (N0216), unless there is nothing to evaluate: aconstarray (§10) or a literal of constants (ADR 0072).- Equality (ADR 0072).
a == banda != bcompare two arrays of the same type[T; N], or two slices of the sameT(aSliceMut<T>, and a vector's elements, lent asSlice<T>): equal lengths and equal elements in order.Tis an integer type,boolor a fieldless enum (§15.1); other element types, floats included (NaN != NaN), are N0207, and so are arrays of different lengths.Slice<u8> == StringViewis N0205: compares == t.bytes(). The operands are only read. The comparison is not constant-time.
16.3 Indexing and slicing
a[i]:ais an array or slice place, or a reference to an array;iis ausize(a flexible literal becomesusize);i ≥ a.lengthpanics ("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 avaror reached throughborrow mut, or aSliceMut), and borrowed.
a[lo..hi],a[lo..],a[..hi],a[..]give aSlice<T>of elementsloup to but not includinghi(default0anda.length).lo ≤ hi ≤ a.lengthis checked; otherwise the program panics.borrow mut a[lo..hi]gives aSliceMut<T>and needsato be mutable. As a call argument for aSliceMut<T>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..lengthof 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, thenhi. In an assignmenta[i] = v, the target's indexes are evaluated and checked beforev. - The borrow checker treats all elements of an array as overlapping (no index reasoning in v0). Borrowing
a[0]and then writinga[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) { … }:itakes each value fromloup to but not includinghi. Both bounds are evaluated once, have the same integer type (flexible literals adapt), andiis immutable in the body.- Index ranges (ADR 0063). When both bounds are flexible literals, the range is
usizeif the body indexes withi, and the literals' default (i32, §5) otherwise. The body indexes withiwhereiis 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<usize>(i)]keepsi32). The body also indexes withiwhere it passesiitself as an argument for ausizeparameter (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 onthis, 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 meansithroughout.
- Index ranges (ADR 0063). When both bounds are flexible literals, the range is
for (x in seq) { … }:seqis an array place, a slice, a reference to an array, or an iterator (below).xis 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
Tis not Copy,xis aborrow Tto the element, in place, instead of a copy (which could not be made: N0251). This is the rule of amatchon a reference (§15.2). The borrow checker treats it as a shared borrow of that element for as long asxis used. While it lives, the sequence cannot be changed (§13.4, §23.1). A Copy element is still copied, as before.breakandcontinuebehave as inwhile.continueproceeds to the next element.Iterators (ADR 0074 §2.8).
for (x in e) { … }, wheree's type is a struct, or aborrow mutof one, with a methodnext(borrow mut this) -> T?(its own or an impl's, with no other parameter and no type parameter), callsnextuntil it returnsnull:eis evaluated once, before the first pass, into a hidden local of the loop's header. That local ande's temporaries live until the loop exits (ADR 0073 §2.1), and are destroyed then, the iterator first. A valueemoves into the loop; aborrow muteis 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 (textinfor (part in text.split(",")) { … }), but not change it while the iterator lives (N0241). - On
Some(v),xtakesv(moved:xowns a non-CopyT), with typeTand the region ofnext's result. continueproceeds to the next call ofnext;breakleaves the loop.- No interface is needed: the method's shape is the rule. A struct without such a
next, or whosenexthas another shape, is N0205, naming the expected signature. A shared reference to an iterator is N0248.
Iterator<T>(ADR 0086 §2.7). lib/core declarespublic interface Iterator<T> { 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 aneof typeI(orborrow mut I), whereIis a type parameter whose bound declares such anext(I: Iterator<T>), aborrow mut dyn Iterator<T>, and a mutable place ofBox<dyn Iterator<T>>, iterated in place throughborrow mutof its contents (N0248 if the place is not mutable).xhas typeT; 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<u8>), anddyn Iterator<T>is dyn-compatible. There is noIntoIteratorand 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), andnext'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.
fn copy(out: SliceMut<u8>, to: usize, src: Slice<u8>, from: usize, n: usize) {
out[to..to + n].copyFrom(src[from..from + n]); // two range checks, one length check, one copy
}d.copyFrom(s), withd: SliceMut<T>ands: Slice<T>(aSliceMut<T>coerces): ifs.length != d.lengththe program panics ("slice lengths differ", reported at the call) and writes nothing. Otherwise eachd[i]becomess[i], as ifswere first copied to a temporary. In safe code the two cannot overlap, becauses's loan would conflict withd's (N0241, N0242); ifunsafecode makes them overlap (§27.3,Slice.fromRaw), the result is still defined.d.fill(v), withv: T: every element ofdbecomesv. Nothing is checked.Tmust be Copy (N0259), because both methods copy elements.- The receiver is a
SliceMut<T>(a parameter, a local or a call result; reborrowed, like any use of aSliceMut), or a slicing expressiona[lo..hi]in any of its forms, which is then taken asborrow mut a[lo..hi]. That needsato be mutable (N0248), asmut a[lo..hi]does for aSliceMutargument. ASlice<T>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 thismethods of a place). So the argument cannot read the destination:s.fill(s[0])is N0242, andlet 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), withi, j: usize: exchanges elementsiandjof aSliceMut<T>(or of a vector, which lends its elements mutably).Tmay 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 == jchanges 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<u8>, SliceMut<u8>, a Vector<u8>'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<T>(at)andb.readLE<T>(at)(at: usize) are theTin thesize(T)bytes atat, big- or little-endian, in two's complement for signedT.Tis a fixed-width integer type,u8tou64ori8toi64(N0207 otherwise,usizeandisizeincluded); a missing type argument is N0265.d.writeBE(at, x)andd.writeLE(at, x)writexthere. The receiver is aSliceMut<u8>, a mutable vector's elements, or a slicing expression taken mutably (N0211 for aSlice<u8>).x's type gives the width: a type argument (writeBE<u32>(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 au32code point, so'P'alone would write four bytes (pushBE<u8>('P')writes one; 0.15.2). They are not two-phase, likecopyFrom(§16.5).v.pushBE(x)andv.pushLE(x)on aVector<u8, A>appendx's bytes with one growth:Result<void, AllocError>, the vector unchanged on failure. The width is chosen as forwriteBE. They are two-phase (B5), likepush.- A read or write checks
at < lengthandat + size(T) - 1 < lengthat 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<u8> arguments and results. Results that are views borrow the receiver, like slicing it.
s.indexOf(t)ands.lastIndexOf(t):usize?, wheretfirst or last starts; an emptytis at0, and ats.lengthforlastIndexOf.s.indexOfByte(b)(b: u8):usize?.s.contains(t),s.startsWith(t),s.endsWith(t)ands.equalsIgnoreAsciiCase(t):bool; onlyA–Zanda–zfold.s.trim(),s.trimStart(),s.trimEnd(): the view without leading and trailing ASCII whitespace (space,\t,\n,\v,\f,\r).s.splitOnce(sep):TextHalves<r>?(ByteHalves<r>?on bytes) with the parts before and after the firstsep, ornull; an emptysepis found at the start.s.split(sep): aTextSplit<r>(ByteSplit<r>) whosenext()is each piece between separators in order, thennull:"a,,b"gives"a","","b", an empty text one empty piece, and an emptysepthe 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
StringViewthese 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<T>()ands.parseIntRadix<T>(radix)(radix: u32, 2 to 36):T?for any integer typeT. The text is an optional+(or-for a signedT), then one or more digits0–9,a–z,A–Zbelow the radix, and nothing else. A value outsideT, any other text, and a radix outside 2–36 arenull.s.parseFloat<T>(),Tf32orf64:T?. The text is[+-]digits[.digits][(e|E)[+-]digits]with a digit on at least one side of the point, orinf,infinityornanin any case after an optional sign; anything else isnull. The result is the nearestTto the decimal value, ties to even, rounded once (anf32is not rounded through anf64); past the largest finite value it is ±infinity, and below the smallest subnormal ±0. For every finitex, parsing the textConsole.writeLine(x)prints givesxback (§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<f64>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)
enum ParseError { Empty, BadDigit(u8), Overflow }
fn digit(c: u8) -> Result<u32, ParseError> {
if (c < 48u8 || c > 57u8) { return Err(ParseError.BadDigit(c)); }
return Ok((c - 48u8) as u32);
}
fn twoDigits(a: u8, b: u8) -> Result<u32, ParseError> {
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 aSliceHalves<T>whoseleftiss[..mid]andrightiss[mid..].s.splitAtMut(mid), on aSliceMut<T>or a vector, gives aSliceHalvesMut<T>of twoSliceMut<T>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") ifmidis past the end; the panic names lib/core's line, not the caller's. The halves borrowsfor as long as either lives. They are lib/core code (SliceSplitting<T>,lib/core/slices.cee).
17.1 Types
Result<T, E>is a built-in enum with two variants, in this order:Okwith payloadT, andErrwith payloadE. Everything in §15.1 applies: layout, Copy iffTandEare Copy, needs drop iff either does, and only the active variant's payload is destroyed.Tmay bevoid, in which caseOkhas no payload.Emust not bevoid. Neither may be a reference or slice (N0246, E3); arrays, structs, enums and results are fine.- A type contains
TandEby value:struct S { r: Result<S, i32>; }is recursive (N0227). AllocErroris a built-in enum{ OutOfMemory }(Copy). The namesResult,Ok,ErrandAllocErrorcannot be redeclared as items (N0201).- Two
Resulttypes are equal iff theirTand theirEare equal.
17.2 Values
Ok(e),Ok()(forT = void) andErr(e)need an expectedResulttype that fixesTandE. 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
TorElike a call argument against its parameter (§5).
17.3 Matching
- Patterns
Ok(x),Ok(_),Ok(forT = void),Err(x)andErr(_)are written without a type prefix. The bindings and exhaustiveness rules of §15.2 apply unchanged: a non-Copy payload is bound by matching onborrow r(N0258 otherwise).
17.4 e? and e!
Postfix = Primary ( … | '?' | '!' )* -- '!' is postfix only when it follows an operande?(propagate).emust have aResult<T, E>type (N0262). The enclosing function must returnResult<U, E>with the sameE, orResult<U, F>whereFis an enum with an@fromvariant whose payload isE(N0261 otherwise, naming the@fromvariant 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@fromvariant per payload type (N0262).e?then returnsErr(F.Io(x))forErr(x). The conversion is one step, and only?makes it:return Err(x)still names the variant.- If
eisOk(v), the value ofe?isv(of typeT;voidifTisvoid). - If
eisErr(x), the function returnsErr(x)at once, exactly like areturnstatement. The statement's temporaries end, then every scope is left (drops and deferred statements), innermost first. Likereturn,?is not allowed in a deferred statement (N0254).
e!(unwrap).emust have aResult<T, E>type (N0262). The value isvforOk(v);Err(_)panics with "unwrapped an Err value". Panic is abort, so nothing is destroyed.- In both cases
eis evaluated once and consumed. The payload moves out of it (or is copied); nothing else oferemains to be destroyed. - As an expression statement:
f()?;andf()!;are allowed when the operand is a call, like calls themselves (N0225 otherwise). - Precedence: postfix, like a call or member access:
parse(a)?.valueand-x?work as expected (-applies tox?).!directly followed by=lexes as!=. Writer! == 1with a space.
17.5 Discarded values (correction to §14.5)
- An expression statement whose value needs drop (for example a call
make();returning aRes) 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<T, E> (ADR 0075 §2.3)
r.isOk()andr.isErr()(borrow this):bool.r.ok():T?, andr.err():E?. Both consumer; the other side is destroyed.o.okOr(e)on aT?:Result<T, E>, consumingo;eis destroyed when there is a value.- They are lib/core's ResultMethods<T, E> and OptionalResults<T, E>.
mapandmapErrare not provided: they need generic methods of generic structs, which the checker does not have yet.
18. Boxes and allocators (M3b)
struct Account {
id: u64;
balance: i64;
fn deposit(borrow mut this, amount: i64) { this.balance += amount; }
}
fn open(id: u64) -> Result<Box<Account>, 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 freedBox, SystemHeap and FailingAllocator are built in (ADR 0003).
18.1 Allocator types
SystemHeapandFailingAllocatorare 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 {}.SystemHeapis the platform heap (malloc/freein hosted and CosmoOS userland builds).FailingAllocatorallocates from the platform heap, but can be told to fail, for testing error paths in the language itself:FailingAllocator.failAfter(n: u64): the nextnallocations throughFailingAllocatorsucceed, 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 throughFailingAllocator, exactlynof the allocations that followfailAfter(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 throughFailingAllocatorfrom 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<T, A>
Box<T, A>owns exactly one value of typeTin memory obtained from allocator typeA(H1).Box<T>meansBox<T, SystemHeap>.Amust be an allocator type (N0263).Tis any value type exceptvoidand 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 toA. - A type does not contain the target of a
Boxby value, so recursive types are built with boxes:enum List { Nil, Cons(i32, Box<List>) }is allowed (no N0227). - Creation (built-in functions):
Box.create(v):Result<Box<T>, AllocError>;Box.createIn(a, v)withaa value of allocator typeA:Result<Box<T, A>, AllocError>.Tis the type ofv. An expected typeResult<Box<X, _>, _>provides the type hint forv(soBox.create(5)can make aBox<u8>).vis moved into the call. If the allocation fails,vis destroyed and the result isErr(AllocError.OutOfMemory).
- Access through a box works like access through a reference (§13.3), with the box's own mutability:
- fields
b.f, elementsb[i]and method callsb.m()reachT(auto-deref); - the contents are mutable iff the box is: the box variable is
var, or the box itself is reached throughborrow mutor another mutable place.b.f = 1needs avar b; - a
Box<T>argument for aborrow Tparameter borrows the contents, andmut blends them for aborrow mut Tparameter; - where a Copy
Tis expected, aBox<T>is read through (auto-deref); - a
borrow Box<T>converts toborrow T, and aborrow mut Box<T>toborrow mut Torborrow T: a reborrow of the contents (for example, a payload bound by reference in a match); borrow bborrows the box itself (borrow Box<T>).
- fields
- 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
Boxis 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
Twith adropblock 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, andThas nodropblock by the rule above. - There is no way to move the whole contents out in v0 (
Twould need an unbox operation, M6).
18.4 Moving payloads out in match (M3c; replaces N0258)
Binding = Identifier | 'var' Identifier | '_'var xmakes a mutable binding;xan 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; ausingbinding 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.Vwithout a list, a variant without payload), the value is destroyed when the match statement ends.
- the scrutinee is moved when the match starts. A place scrutinee is left uninitialised, as by
- 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
Tdestroys 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 aBox<T>again. If there is one,Box<T>'s glue becomes a loop:- destroy everything of the current
Texcept 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.
- destroy everything of the current
- 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
dropblock runs in the order §14.5 defines. - Consequence: lists (
Cons(x, Box<List>)) 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<T> and PoolBox<T>, 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<T, E>whoseTorEis scoped (a scoped result);- the scoped region values:
FixedBufferAllocator(§19.4),ScratchFrame(§19.5) andPoolBox<T>(§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)
fn process(input: Slice<u8>) -> Result<u64, AllocError> {
using arena = Arena.create(SystemHeap {}, 65536u64);
let counts = arena.allocArray(256usize, 0u32)?; // SliceMut<u32>, 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 hereArenais 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), withaan allocator value (SystemHeap {},FailingAllocator {}) andchunkSize: u64, makes an empty arena. It cannot fail: memory is obtained in chunks ofchunkSizebytes when needed. An allocation larger than a chunk gets a chunk of its own.Allocation (R1). Both methods take
borrow thisand return a scoped result that carries a shared loan of the arena:arena.alloc(v):Result<borrow mut T, AllocError>, withvmoved into fresh arena memory;arena.allocArray(n, fill)withn: usize:Result<SliceMut<T>, AllocError>,ncopies offill.Tmust be Copy.nmay be 0 (an empty slice);n × size(T)beyond the address space isErr(OutOfMemory).
The returned references are mutable: each allocation is fresh memory that nothing else refers to.
R2:
Tmust not need drop, and must not be scoped (no references or slices in arena memory, E3). Both are N0264.arena.reset()takesborrow mut thisand 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)
fn countWords(text: Slice<u8>) -> Result<u32, AllocError> {
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.sis a mutable place of type[u8; N], or aSliceMut<u8>. 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.FixedBufferAllocatoris scoped (§19.1): aletlocal, a parameter or a return type with sources, never stored. It is a move type without drop; nothing is freed.alloc(v)andallocArray(n, fill)work as forArena(§19.2), including R2 (N0264). Each allocation is aligned within the storage. When the remaining bytes are not enough, the result isErr(AllocError.OutOfMemory); the storage is never grown. There is noreset: 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)
fn render(n: usize) -> Result<u64, AllocError> {
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 hereScratch 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.
ScratchFrameis scoped, so neitherSendnorShare(§38.1), andThread.bothrefuses 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
Sendeither. It reaches another thread only as aThread.bothcapture: aborrow mut Torborrow TwhoseTisSendorShare(§38.6). The borrow checker keeps the frame live untilbothreturns, 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/allocArraycall 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.ScratchFrameis 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)andallocArray(n, fill)work as forArena, including R2. A failure to obtain a chunk isErr(AllocError.OutOfMemory).Frames are bound with
using(R3; §14.6): nesting borrows the frame mutably, and a scoped value cannot be avar. Since frames are scoped, they cannot be returned from the function that entered them.
19.6 Pool<T> and PoolBox<T> (M4c)
struct Particle { x: i32; v: i32; }
fn simulate() -> Result<i32, AllocError> {
using pool: Pool<Particle> = Pool.create(SystemHeap {}, 64u32);
var a = pool.take(Particle { x: 0, v: 2 })?; // PoolBox<Particle>: 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 destroyedPool<T>is a built-in allocator for up tocapacityvalues of one typeT.Pool.create(a, capacity), withaan allocator value andcapacity: u32, makes an empty pool.Tcomes from the expected type (let pool: Pool<T> = …); without one it is N0260.- It cannot fail: the slots are allocated from
aat the firsttake. Pool<T>is a move type that needs drop; destroying it returns the slots toa. It is not scoped.
pool.take(v)takesborrow thisand returnsResult<PoolBox<T>, AllocError>:vmoves into a free slot. The result isErr(AllocError.OutOfMemory)when all slots are taken, or when the slots cannot be allocated;vis then destroyed.PoolBox<T>owns one slot and its value, like aBox(§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, soTmay 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); Tmust not be scoped (E3; N0264).
- auto-deref, the box's own mutability, borrowing its contents, and moving fields out of it (§18.3) all work as for
- Destroying the pool while a
PoolBoxfrom it is live is rejected (B2), so every slot is free when the pool is destroyed.
20. Generics (M6a)
struct Pair<T> {
first: T;
second: T;
fn swap(this) -> Pair<T> => Pair { first: this.second, second: this.first };
}
enum Maybe<T> { Nothing, Just(T) }
fn largest<T>(p: borrow Pair<T>, pick: bool) -> borrow T {
if (pick) { return borrow p.first; }
return borrow p.second;
}
fn main() {
let p = Pair { first: 1, second: 2 }; // Pair<i32>, inferred from the fields
let q = p.swap();
let m: Maybe<u8> = Maybe.Just(7u8);
let r = largest(p, true); // T = i32, inferred from the argument
let s = largest<i32>(q, false); // explicit type argument
}20.1 Declarations
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<S: Shape>(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<Square>(s),Pair<i32>.make<u8>(…). 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 nodyntype (N0331): adyntable 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<i32>,Maybe<Pair<u8>>. A generic struct or enum must always be given all its type arguments in a type (N0204).Calls:
largest<i32>(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 isi32unless 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 }infersTfrom the field values, or takes it from the expected type.Pair<u8> { … }gives it. LikewiseMaybe.Just(7)infers from the payload, andMaybe.Nothingneeds an expected type or explicit arguments:Maybe<u8>.Nothing.Static functions:
Pair<i32>.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…>whenI's argument implements exactly one instantiation ofJ(its impls; for adyntype, its interface and supertraits; for a type parameter, its bounds). Otherwise it is N0265, as before. Sofn count<T, I: Iterator<T>>(it: I) -> usizeis called ascount(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
Ttwice is N0250). - Generic declarations that are never used produce no C code.
20.4 Instantiation (monomorphisation)
- Every distinct use (
largest<i32>,Pair<Res>) 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<i32>is Copy;Pair<Res>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<T>callingf<Box<T>>) is N0266. - Unit payloads (ADR 0074 §2.1).
Result<void, E>.Okhas no payload (§17.1), but a pattern may bind one name to it, andOk(e)may pass one argument of typevoid, soOk(v) => Ok(v)means the same atT = voidas at any otherT, in a generic body and written out by hand:- A name bound so is a unit local: it has type
voidand holds nothing.let v = r?;(orr!) withr: Result<void, E>declares one too, as doeslet w = v;of one. Aletof a call of avoidfunction is still N0224, and avarcannot hold a unit. - Reading a unit local is a unit value. It may be passed where
voidis expected: asOk's payload, and asreturn v;in avoidfunction, which isreturn;. Elsewhere it is avoidvalue of the usual rules: N0205 where another type is expected, N0211 when assigned. Ok(e)with anyvoideevaluatesefor its effects, then builds the payload-lessOk. 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), andSome(v)withvoid.
- A name bound so is a unit local: it has type
- An instantiation that does not check is an error at its use (M15 S1). §20.3's checking covers every type argument; a
voidone'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)
enum Method { Filter, All }
struct Grouped<const ENDS: usize> {
starts: [u32; ENDS];
}
struct Parser<const TS: bool, const SCAN: bool> {
pos: usize;
fn step(borrow mut this) {
if (TS) { this.pos += 2; } else { this.pos += 1; }
}
}
fn hoist<const M: Method>(n: u32) -> u32 => match (M) { Method.Filter => n, Method.All => n + 1 };
fn run<const TS: bool>() -> usize {
var p = Parser<TS, false> { pos: 0 };
p.step();
return p.pos;
}
fn parse(ts: bool) -> usize => if (ts) run<true>() else run<false>();- 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<W: Writer, const STYLE: Style>).conststays a reserved word; this is its one place in a parameter list.Tisbool, 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),
Nis a value of typeT, 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 itborrow mutis N0211. Both branches ofif (TS)are checked, and code afterif (TS) { return 1; }stays reachable. A closure readsNwithout capturing it. A bareNin a pattern is N0358: test it in a guard,x if (x == N) =>(§15.2). - Array lengths. A
usizeconst 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), anda.lengthisN. 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 ausize), the name of a constant (an associated one,Tags.COUNT, included), an enum variant (hoist<Method.Filter>(n)), or inside a generic item one of its own const parameters, bare (Parser<TS, false>). 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;thencounted<ENDS>(). - 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<true>() else run<false>(). So is an expression over a const parameter (run<!TS>(),[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.NAMEis 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<const N: usize>(a: [u8; N])called with a[u8; 8];l: borrow Lexer<E>with aLexer<Encoding.Ascii>), 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<const SSL: bool>), 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 nodyntype (N0331). A generic impl names its parameters in its target (ADR 0054):impl<const E: Encoding> CharIter for ShellCharIter<E>; an inherent impl names the struct's, in order:impl<const TS: bool, const SCAN: bool> Parser<TS, SCAN>. 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<true>withimpl<const B: bool> I for S<B>) overlaps (§21.1). - Instantiation (§20.4). Each distinct argument list is an instance, checked again and compiled to C:
Parser<true, false>andParser<false, false>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,Nis its literal, soif (TS)compiles toif (1)orif (0), which the C compiler removes at-O2. The C names are in c-backend.md, "Generic code". - Not included: default values (
<const X: bool = false>), parameters of other types, expressions over a parameter in arguments or array lengths, a const parameter as a constant pattern, andconst 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)
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<S: Shape>(shapes: Slice<S>) -> i64 {
var t = 0i64;
for (i in 0..shapes.length) { t += shapes[i].area(); }
return t;
}21.1 Declarations
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 structS, 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 declaresS. Its target namesSwithS's own parameters, in order:impl<T: B> S<T> { … }forstruct S<T: B>,impl<region r> V<r> { … }forstruct V<region r>. 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<T>). - 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 asT.create(o), in a default asSelf.create(o), and on a concrete type asRule.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.thisisSelf,borrow Selforborrow mut Self, as the receiver says. The body may call the interface's methods, required or default, but not use fields or other methods ofSelf(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 adyntable (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
@effectslist bounds a default as it bounds every impl (§37.3). There is no call from an override to the default it replaces.
- A default is checked once, at the interface, as generic code (§20.3) whose one type parameter is
Self(ADR 0086 §2.2). In an interface,Selfis the implementing type: opaque in its signatures and defaults, and the target in each impl, which writes either the target orSelfwhere the interface hasSelf. In animplblock (inherent or for an interface) and in a struct's or enum's body,Selfis the target type with its parameters (Pair<T>,View<r>), 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 namedSelfis N0100.- Constants (ADR 0086 §2.4). An interface may declare
const NAME: T;orconst NAME: T = v;(a default). An impl givesconst 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 useSelf. Each becomes an associated constant of the target, read asType.NAME; a name the target already has as a constant is N0228. In generic code and defaults it is read asT.NAMEorSelf.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, soif (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;ortype Name: B1 + B2;(bounds as in §21.2). Each impl definestype Name = T;(N0267 if it is missing, or the interface has no such type); the impl's type and region parameters may appear inT(impl<region r> Source for Words<r> { type Item = StringView<r>; }), andTmust meet the bounds (N0268, at the impl).- Projection. In a type,
P.Namenames the associated type, wherePisSelfinside the interface or a type parameter whose bounds declare exactly one associated typeName: none is N0200, two or more N0267 (ambiguous, as for bound methods). In generic codeP.Nameis an opaque type with the declared bounds (§20.3); an instance substitutes the impl's type. WherePis a concrete type (Selfin an impl),P.Nameis the impl's type. - There are no defaults for associated types, no bounds that fix one (
E: Encoding<Unit = u16>) and no associated types with parameters of their own: an interface whose callers fix a type takes it as a parameter, asIterator<T>does (§16.4).
- Projection. In a type,
- Supertraits (ADR 0086 §2.6).
interface B: A + Copy { … }: every supertrait is an interface instantiation, which may useB's parameters but notSelf, orCopy.impl B for TrequiresTto implement each supertrait (N0268, at the impl). A boundT: BimpliesT: A(andT: Copy): the methods, defaults, constants and associated types ofAare reachable through it, it meets a boundA, and the ambiguity rule of §21.2 applies to the union. A cycle of supertraits is N0227.dyn B's table holdsA's methods afterB's, in declaration order (each interface once), so they can be called through it, anddyn BimplementsAfor bounds as well asB(§37.2); there is no conversion fromdyn Btodyn A.Bis dyn-compatible only if its supertraits are, and not if it requiresCopy(N0331).Sized,SendandSyncare not supertraits. impl I for T { … }implements interfaceI(with its type arguments,I<X>) for a typeT: a struct, an enum, or a built-in type such asi32. A generic impl (ADR 0054),impl<T: B, U> I<…> for C<…T…, …U…> { … }, implementsIfor every instance of the generic struct or enumCthat 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 adyntable all use the instance. A type with region parameters is implemented with them named,impl<region r> I for S<r>(§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<u8>andimpl<T> I for Wrap<T>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
implblock's methods are ordinary methods ofT.x.area()works on anySquare, and their names must not clash withT's other methods (N0228).
- 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
- Interfaces are not types: an interface is a bound (§21.2), and since M13 also the
Iof adyn Itype 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 everyimplof it (§37.3).
21.2 Bounds
- A type parameter may have bounds:
<T: Shape>,<T: Comparable<T> + Copy>. - Inside generic code, a value whose type is
T(or a reference to one) may call the methods ofT'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). Copyis a built-in bound: aT: Copyis 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 forCopy. Otherwise it is N0268, reported where the instantiation is written. - Dispatch is static: an instantiation calls
T'simplmethod directly (monomorphisation).
21.3 Slices of move types (correction to §16.1)
Slice<T>andSliceMut<T>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?
fn find(xs: Slice<i32>, 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<i32>) -> 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 optionalT, for any non-voidT. It is a built-in enum with two variants, in this order:null(no payload) andSome(T). Everything in §15.1 applies: it is Copy iffTis, needs drop iffTdoes, and is scoped iffTis (borrow T?).Optional references and references to optionals (M11, G6).
?applies to the whole type before it, soborrow T?is an optional reference,(borrow T)?. A reference to an optional is written with parentheses:borrow (T?),borrow mut (Box<Node>?). A type may be parenthesised anywhere a type is written (grammar.md §2). Diagnostics print the two as written:borrow i32?andborrow (i32?). A reference to an optional is a reference like any other (§13): a match on it binds references to the payload,r == nullreads the referent, andr = vthrough aborrow mut (T?)replaces the optional, as inceesafefn append(slot: borrow mut (Box<Node>?), value: i32) { match (slot) { Some(node) => { append(mut node.next, value); } null => { slot = Some(Box.create(Node { value: value, next: null })!); } } }Values.
nullneeds an expected optional type (N0260).Some(e)makes a present value; with an expectedT?,eis checked againstT, otherwiseTise's type.- Where a
T?is expected (an initialiser, an argument, a return value, a field), a value of typeTconverts toSome(value)implicitly.
Matching. Patterns
null,Some(x)andSome(_)are written without a type prefix; the rules of §15.2 and §18.4 apply.Comparisons.
o == nullando != nulltest for absence, for anyT(the value is only read, not consumed). So doo.isNull()ando.isSome()(ADR 0072),bools. Other comparisons of optionals are not defined (N0207).Optional borrows (ADR 0074 §2.5).
o.asBorrow(), on a place of typeT?or on aborrow (T?), is aborrow T?:Someborrows the payload, andnullisnull.o.asBorrowMut(), on a mutable place of typeT?or on aborrow 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 asmatchwould be: a tag test, then the payload's address ornull. Any other method on an optional is N0234.Optional borrow arguments. At an argument whose parameter is
borrow T?, a place of typeT?is borrowed as byasBorrow(), and a place of typeTasSome(borrow t), as a place ofTis borrowed for aborrow Tparameter (§13.2). At a(borrow mut T)?parameter the same holds withmut pandasBorrowMut()(N0249 withoutmut). Sof(relay.tlsConfig)is accepted forfn f(c: borrow TlsConfig?). Aborrow (T?)stays a different type: it converts withasBorrow().a ?? b(ADR 0072) isa's value, orbwhenaisnull.ais aT?(N0262 otherwise) andbis checked againstT; the whole is aT. Ifbis itself aT?ofa's type, the whole is thatT?, so a chain may end in an optional (find(a) ?? find(b)).bis evaluated only whenaisnull, so?? fail("…"), a call of type never, stops with a message.ais consumed, as bymatch (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 returnsnullfrom the enclosing function, which must return an optional (N0261).?on an optional in a function returning aResultis N0261 in v0 (language-spec.mdEx11).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 isnull, and any other value isSome. That coversBox<T, A>?, except a pool box (two pointers), andborrow T?andborrow 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_allocgives 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<T>??) 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<Node>?is 8 bytes rather than 16 on a 64-bit target.- allocation returns a non-null pointer or fails (
22.2 StringView
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<u8>
}StringViewis a shared, read-only view of UTF-8 text(pointer, length in bytes). It is scoped likeSlice<T>(§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 aStringViewwithout scoped parameters (likegreetingabove). The return check still rejects any view that borrows a local (N0244). - Operations.
s.lengthis the length in bytes (usize);s[i]is the byte ati(u8, bounds-checked);s.bytes()is the same bytes as aSlice<u8>;s[lo..hi](any bound may be omitted) is aStringViewof bytesloup tohi. 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)andConsole.writeLine(s)print it;- searching, trimming, splitting and parsing numbers: §16.6.
for (b in s)is not defined: iterate overs.bytes()(characters come withStringmethods later).- From bytes (M15 S6):
StringView.fromUtf8(bytes)(bytes: Slice<u8>, or aVector<u8>lent as one) is aStringView?.- 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.readorConsole.readbecomes aStringView, and throughappendaString, 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<region r> and c"…" (ADR 0087 §2.4)
lib/core's CStr<region r> is a Copy view of UTF-8 text with a NUL byte after its end, valid for region r as a StringView<r> is (§25): what a C function taking a const char * needs.
c"…"(grammar.md§1.6) is aCStr<static>. The compiler appends the NUL; a\0inside is N0006.s.ptr() -> *CChar, the first byte's address;s.view() -> StringView<r>ands.length() -> usize, the text without the NUL;s.bytesWithNul() -> Slice<r, u8>, the text and its NUL.CStr.fromBytesWithNul<region r>(b: Slice<r, u8>) -> CStr<r>?takes bytes whose last byte is their only NUL and whose text before it is UTF-8 (asview()returns aStringView); otherwise it isnull. TheCStris a view ofb: returning one made from a localVector's bytes is N0244, as for any view.- Its fields are private to lib/core (N0296): only
c"…"andfromBytesWithNulmake one, so its NUL is always there. lib/core'sTextWriter, whosecText()relies on the zero after what it wrote, keeps its fields private for the same reason. - A struct field of type
CStrneeds a region parameter, as aStringView's does (N0270). - Like a
StringView, aCStrmay be a constant's type (§10) and a static's (§39.2), initialised by ac"…". "…\0".ptr as *CCharstays valid, but nothing checks that its\0is there, and its view's length counts it.
Diagnostics: none new.
23. Vector<T, A> and String<A> (M6d)
23.1 Vector<T, A>
fn sum(xs: Slice<i32>) -> i32 {
var total = 0;
for (x in xs) { total += x; }
return total;
}
fn main() -> i32 {
var v = Vector<i32>.create(); // no allocation until the first push
v.push(1)!; // Result<void, AllocError>
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<T, A>owns a growable, contiguous buffer ofTvalues obtained from allocator typeA(H2).Vector<T>meansVector<T, SystemHeap>.Amust be an allocator type (N0263).Tis any value type exceptvoidand 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<T, A>orResult<Vector<T, A>, _>; N0260 otherwise):Vector<T>.create(): an empty vector. It allocates nothing;Vector<T>.withCapacity(n)(n: usize):Result<Vector<T, A>, AllocError>, an empty vector with room fornelements (n == 0allocates nothing).
- Members:
v.lengthandv.capacity(usize);v[i]is elementi, bounds-checked (panic "index out of bounds"). It is a place, like an element of a slice: it can be read (CopyT), borrowed (borrow v[i],borrow mut v[i]) and assigned (v[i] = xdestroys the old element). Writing needs a mutable vector: avar, or one reached throughborrow mut. Moving an element out is rejected (N0251), as for a slice;v.push(x):Result<void, AllocError>. It appendsx, growing the buffer when it is full (new capacity: twice the old, at least 4). If the allocation fails,xis destroyed, the vector is unchanged, and the result isErr(AllocError.OutOfMemory);v.pop():T?, the last element moved out, ornullif the vector is empty;v.clear(): destroys every element; the capacity stays;v.truncate(n)(M11 G8c,n: usize): ifn < v.length, destroys the elements from indexnup, lowest first, and the length becomesn; otherwise nothing changes. The capacity stays;v.extend(s)(s: Slice<T>),v.resize(n, fill)(n: usize,fill: T) andv.reserve(extra)(extra: usize), ADR 0071 §2.1:Result<void, AllocError>. Each grows the buffer at most once (aspushgrows it, to at least the length needed) and leaves the vector unchanged if that fails.extendappends a copy ofs;resizeappends copies offillup to lengthn, or truncates ton;reservemakes room forextramore elements and changes nothing else.extendandresizeneed a CopyT(N0259). They takeborrow mut thisand are two-phase (B5), sov.resize(v.length + 1, 0)is allowed;v.extend(v.view())is not (N0241);v.insert(at, x):Result<void, AllocError>, putsxat indexatand moves the later elements up (at == lengthappends);v.remove(at):T, takes the element atatout 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 <= lengthforinsertandat < lengthfor the others, and panics there ("index out of bounds"). They are lib/core's VectorEditing<T>: the elements move byswap, so they may be of any type. Ifinsertcannot grow the vector, it is unchanged andxis destroyed;- for a
Vector<u8, A>, 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 aSlice<T>, andv.viewMut()as aSliceMut<T>;- a
Vector<T>argument for aSlice<T>parameter is borrowed asv.view(), andmut vfor aSliceMut<T>parameter asv.viewMut()(like a box's contents, §18.2); for (x in v)isfor (x in v.view())(§16.4).
- Calls take
thislike methods:push,pop,clearandtruncatetakeborrow mut this, and their arguments are evaluated before the mutable loan starts (two-phase, B5,lifetimes.md§5).length,capacity,viewand element reads takeborrow 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
==, noCopy. - 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<A>
fn greet(name: StringView) -> Result<String, AllocError> {
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<A>owns UTF-8 text in a growable buffer (StringmeansString<SystemHeap>). It is a move type that needs drop, likeVector<u8, A>, but a distinct type.- UTF-8 invariant: text only enters a string through
appendof aStringView, 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] = bis rejected (N0211), andborrow mut s[i]and mutable slices of a string (N0248). - Creation:
String.create()(no allocation) andString.withCapacity(n):Result<String<A>, AllocError>;String<A>.create()for another allocator. - Members:
s.append(text)(text: StringView):Result<void, AllocError>, two-phase likepush. Growth is as for vectors. On failure the string is unchanged;s.appendNumber(n)(M11, G4c;nof any integer type, or a reference to one): appendsnin decimal, with a-before a negative value and no leading zeros or+:0,-128,18446744073709551615. It isResult<void, AllocError>, two-phase and unchanged on failure, as forappend. 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<void, AllocError>:s.appendHex(n, minWidth, upper)(n: u64; at leastminWidthhexadecimal digits, zeros before them, uppercase whenupper; no0x),s.appendOctal(n),s.appendFixed(f, digits)(f: f64; exactlydigitsdigits 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-), ands.appendPadded(text, width, align, fill)(textin a field ofwidthUnicode scalar values,alignaTextAlign(Left,Right,Center, whose extra fill goes after),filla Unicode scalar value, else a panic). They are lib/core'sAppendFormattedinterface, implemented forVector<u8>in lib/core and forStringin lib/std (hosted), over lib/core'sFormatted, 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 aStringView, a loan ofs; aStringargument for aStringViewparameter is borrowed the same way;- read-only access as for a view:
s[i](u8),s[lo..hi](aStringView, 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 ofswhen the call's mutable loan starts. - Layout (v0): as
Vector<u8, A>.
Diagnostics: none new. The errors reuse N0205, N0207, N0209, N0211, N0240–N0251, N0260 and N0263.
24. SlotMap<T, A>, Handle<T> and StableVector<T, A> (M6e)
24.1 SlotMap<T, A> and Handle<T>
struct Node { value: i32; next: Handle<Node>?; }
fn main() {
var nodes = SlotMap<Node>.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<T, A>owns values ofTin reusable slots from allocator typeA(SlotMap<T>meansSlotMap<T, SystemHeap>; N0263).Tis any value type exceptvoidand 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<T>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 byinsert.- Operations:
SlotMap<T>.create(): an empty map; it allocates nothing (type arguments as for vectors, §23.1);m.insert(v):Result<Handle<T>, AllocError>, takingborrow mut this(two-phase, B5). If the storage cannot grow,vis destroyed and the result isErr(AllocError.OutOfMemory);m.get(h):borrow T?(a loan ofm),nullifhdoes not name a live value;m.getMut(h):borrow mut T?(a mutable loan ofm; needs a mutable map);m.remove(h):T?, the value moved out (ornull), takingborrow 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
Tis memory-safe: it yields that map's value in that slot, ornull(a logic error, as with any index). - Invalidation: references from
get/getMutare loans of the map, soinsertandremoveare 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<T>is{ u32 index; u32 generation }.
24.2 StableVector<T, A>
fn main() {
let names = StableVector<i32>.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<T, A>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 forVector(§23.1).sv.push(x):Result<usize, AllocError>(the new element's index). It takesborrow 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 failurexis destroyed and the vector is unchanged.sv.length(usize) andsv[i](a bounds-checked place, as for vectors: read,borrow,borrow mutand assignment, the last two needing a mutable vector).- No
pop,clear, views orfor(the elements are not contiguous): iterate withfor (i in 0..sv.length). Aborrow mut sv[i]is a mutable loan ofsv, sopushis rejected while one is live (N0241). Sendwith aSendelement, but neverShare(§38.1): one sharedStableVectorcannot be pushed to from two threads. Share it behind aMutex. Layout (v0):{ void *chunks[32]; u64 length }(264 bytes); elementiis in chunk⌊log2(i + 8)⌋ − 3. At most8 · (2³² − 1)elements; beyond thatpushfails withOutOfMemory.
Diagnostics: none new. The errors reuse N0205, N0209, N0240–N0251, N0260 and N0263.
24.3 HashMap<K, V> (lib/std, ADR 0052)
- Keys.
K: HashKey<K>, a lib/std interface:hash(borrow this, h: borrow mut Hasher)feeds what identifies the key, andsameAs(borrow this, other: borrow K)compares two keys. lib/std implements it for the integer types,bool,StringandVector<u8>; a program implements it for its own types, or derives it (@derive(Equal, HashKey), §28.7, throughderiveHash<K: HashKey<K>>(key: borrow K, h: borrow mut Hasher)). A key type without it is N0268. - The hash.
Hasheris 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<V?, AllocError>, the old value if the key was there;get(borrow key),getMut: an optional reference;contains,remove(borrow key)(V?),clear,length();keyAt(i)andvalueAt(i)foriin0..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
putallocates. A failedputleaves 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
StringandVector. A call naming no built-in method reaches an impl's method (ADR 0052 §3.3), soimpl I for Stringis usable. - The allocator. The system heap.
25. Region parameters (M6x; OD-3, ADR 0006)
enum Kind { Word, Number }
struct Token<region src> { kind: Kind; text: StringView<src>; }
struct Lexer<region src> { input: StringView<src>; position: usize; }
fn next<region src>(lexer: borrow mut Lexer<src>) -> Token<src>? { … } // the token views the input
fn split<region r>(line: StringView<r>) -> Result<Vector<StringView<r>>, AllocError> { … }25.1 Declaring regions
- Structs, enums and functions may declare region parameters before their type parameters:
struct Lexer<region src, T>,fn f<region a, region b, T>(…). In a parameter list,regionfollowed by a name declares a region;regionalone is still an ordinary name. - A method may use its struct's regions.
staticis 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<region a>(borrow this, other: borrow(a) Vector<u8>) -> Slice<a, u8>. Its region names may not repeat the struct's or the impl's (N0202). An interface's methods, and the methods of animplof 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<src>,Pair<r, i32>. The built-in scoped types take one region optionally:StringView<r>,Slice<r, T>,SliceMut<r, T>, and referencesborrow(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.staticin 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<T> is not scoped (it holds no loan).
A struct or enum with region parameters implements an interface as impl<region r> I for S<r> { … } (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: Body>(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 ofT, …), the instantiation is rejected where it is written: N0274. - Collections:
Vector,StableVectorandSlotMapelements may be scoped; so may the value passed to a region'salloc, 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<T>) and structs or enums holding them, but not bareborrow Treferences. All elements share the element type's regions, so[StringView<r>; N]andSlice<a, StringView<r>>hold loans ofr. An array of scoped elements is itself scoped, like aVectorof 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 Tarray and slice elements, and written references to references. Region values and pool boxes (FixedBufferAllocator,ScratchFrame,PoolBox<T>) 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<a> and Token<b> 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 contributesp'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 Torborrow mut T(or an optional of one) with no sources or regions written on the reference itself, whose pointee names regions (-> borrow HeaderList<r>): 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: writeborrow(p). - for a result whose written regions are all
static(StringView<static>,Slice<static, T>,S<static>,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
iiff they share a flow name: the result may carryi's loans (this generalises v0's return sources, §13.5); - deep results (ADR 0074 §2.4). A reference parameter
ithat 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 withibut names none ofi's outer names, and is not tied toiby elision orborrow(i), is a deep result ofi: it names only regions thati's referent holds (fn token(borrow mut this) -> Slice<r, u8>instruct Lexer<region r>). A result whose type is a type parameter (fn take<T>(v: borrow mut Vector<T>) -> T) is a deep result of each reference parameter it would be tied to by elision, sinceTcannot name a reference's elided region (ADR 0075 §8). It carries the referent's loans, not the loan ofiitself:- 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
iwithout passing another reference (this.buf[0..2]for abuf: [u8; 4]), noriitself: N0245. Such a function namesi's own region instead (-> Slice<u8>, elided); - caller: for a deep result, an argument
borrow pcontributesp's loans (whatpholds), not the new loan ofp; a reference parameter passed on contributes what it refers to. Solet t = lexer.token(); lexer.skipSpace();is accepted, andtis tied to the text the lexer views. An argument that holds the result of a callf(a)whose result passes throughacontributes whatawould as that argument (B18): aborrow Tresult tied to one reference parameter alone, whose pointee names only regions that parameter's referent holds (none of its own). Soo.inner().first()withinner(borrow this) -> borrow Inner<r>is tied tor, not too. A pointee that names no region but may hold loans (-> borrow Inner) is not followed.
- 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
- parameter
i← parameterj, foriof typeborrow mut TwhoseTshares a flow name withj: the callee may storej's loans into whatirefers 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:
- Writing a scoped value into a place of a local (a field, an element, a payload) adds its loans to that local.
- 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 mutparameterpof the current function, the value's loans must all come from parameters that flow intop(§25.6): otherwise N0272. Whatp's own referent held (its deep loans, not the loan ofpitself) may always go back into it (ADR 0075 §8):let x = take(mut this.a); this.b.push(x)!;. push,insertandappendwrite their argument through their receiver (rule 2).- At a call, an argument that flows into a
borrow mutparameter adds its loans to what that argument refers to. - In generic code, values whose type mentions a type parameter carry loans like scoped values, unless it mentions it only inside a
dyntype (an interface's arguments, adyn fn's signature): adynvalue carries no loans of its own (§37.2), soBox<dyn Handler<S>>is stored whereSis a type parameter as it is for a concreteS. - Destroying a value that is scoped and needs drop is a use of its loans: its
dropmay read them, so they must still be live (N0243 otherwise).
Diagnostics: N0270, N0272, N0273, N0274 (diagnostics.md).
26. Program arguments, files and sockets (C3)
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?, argumentias a view of static text (no loans), ornullifiis out of range or the argument's bytes are not valid UTF-8 (theStringViewinvariant, §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
Fileowns 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<File, IoError>for reading;File.create(path): for writing, created or truncated;File.remove(path):Result<void, IoError>.pathis aStringView; a path that contains a zero byte or is longer than 4095 bytes isInvalidInput.- A file may be removed or renamed while a
Fileholds it open, natively on every system: the name goes, and the openFilekeeps reading and writing the file until it is dropped. Windows opens every file sharing delete (FILE_SHARE_DELETE), which its C runtime's_opendoes not. - Durable files (ADR 0075 §2.1), each
Result<…, IoError>:File.openReadWrite(path)opens an existing file for reading and writing (NotFoundif missing);File.openOrCreate(path)creates a missing one;File.createNew(path)creates one and isAlreadyExistsif 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 positionreadandwriteuse.f.writeAt(bytes, offset: u64): writes every byte atoffset, or fails; it does not move the position.f.syncData(): the data, and the metadata needed to read it back, reach stable storage (fdatasyncon Linux;fcntl(F_FULLFSYNC)on Apple systems, whosefsyncdoes not flush the drive's cache, falling back tofsynconly where the file system refuses it;FlushFileBufferson 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 tookborrow mut this): none moves the positionreadandwriteuse, so a file shared by reference can be read and synced. Interrupted calls are retried. On Windows, anOVERLAPPEDoffset would move the handle's own position, soreadAtandwriteAttransfer through a second handle on the same file (ReOpenFile), which has its own position: the descriptor's position never moves, even for areadorwriteon another thread during the call. The descriptor must still be open for reading (readAt) or writing (writeAt), as withpreadandpwrite, 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<UnixTime, IoError>, the last modification time.File.openRegular(path)(ADR 0078 §3): openspathfor reading if it is a regular file, never waiting on a FIFO (POSIXO_NONBLOCK | O_NOCTTY, thenfstat, thenO_NONBLOCKcleared); a directory isIsDirectory, anything else that is not a regular fileInvalidInput.f.modified()andf.kind()(ADR 0078 §3, lib/std'sFileMetadata;borrow this): the open file'sResult<UnixTime, IoError>andResult<PathKind, IoError>, fromfstat(_fstat64on Windows), not its path, so they describe the file that is read even if another was renamed into its place.kindisFile,Directory, orOtherfor a device, FIFO or socket. The interpreter answersUnsupportedformodified, and takeskindandopenRegular's check from the path.File.setPermissions(path, mode)(ADR 0077, lib/std):chmodof 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 answersUnsupportedfor this andFile.modified.File.rename(from, to)(lib/std):rename(2), which atomically replacesto; WindowsMoveFileExWwithMOVEFILE_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<u8>,ftaken asborrow mut this):Result<usize, IoError>, the number of bytes read,0at the end of the file. Interrupted reads are retried.f.write(bytes)(Slice<u8>):Result<void, IoError>; it writes all the bytes or fails.Console.read(mut buffer): reads standard input likef.read.Console.writeBytes(bytes)writes raw bytes to standard output, in order withConsole.write.Console.writeError(text)andConsole.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'sHostServices.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,0for any free port):Result<TcpListener, IoError>, listening, with the platform's largest backlog (SOMAXCONN),SO_REUSEADDRon POSIX, and an IPv6 socket dual stack (IPV6_V6ONLYoff).listener.port():Result<u16, IoError>, the port it is bound to.listener.accept():Result<TcpStream, IoError>.TcpStream.connect(address, port):Result<TcpStream, IoError>.s.read(mut buffer)ands.write(bytes)as for files;s.shutdownWrite():Result<void, IoError>, 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<void, IoError>and not blocking:s.setNoDelay(on: bool)(TCP_NODELAY),s.setKeepAlive(idleSeconds: u32?)(SO_KEEPALIVEand the idle time,0the system's; null turns it off),s.setSendBuffer(bytes: u32)ands.setRecvBuffer(bytes: u32)(SO_SNDBUF,SO_RCVBUF; the kernel may round, and Linux doubles),s.setLinger(seconds: u32?)(SO_LINGER:0makes 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 onPATH;arguments: borrow Vector<StringView>, passed after the program name):Result<i32, IoError>. 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 isInvalidInput; 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<ProcessOutput, IoError>.program: StringViewis found onPATH, as forProcess.run;arguments: borrow Vector<StringView>.input: Slice<u8>?is the child's standard input (empty ifnull).- 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, asProcess.runreports it;stdout: Vector<u8>andstderr: Vector<u8>: 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
capturereturns. A child that writes much to both streams therefore cannot block, withoutpollor threads, which CosmoOS's libc does not offer.- POSIX:
posix_spawnfile actions. - CosmoOS:
spawnvp's handle mapping (itsspawn.h). - Windows:
CreateProcess, quoting the arguments by the C runtime's rules.
- POSIX:
- Errors. As for
Process.run: a program that cannot be started isNotFoundand the like, and a failed allocation isOutOfMemory. A mistake in the call's arguments is reported againstHostServices.capture, lib/std's function behind it.
26.8 What is at a path
File.kind(path):Result<PathKind?, IoError>, following symbolic links:PathKind.File,PathKind.DirectoryorPathKind.Other(lib/std's enum), ornullif nothing is there.- ADR 0028 called this
Path.kind. It isFile.kindbecausePathis 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, ornullif 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 callssetenvbreaks that, as it would in C.
26.10 Directories
Directory.create(path)makes one directory:AlreadyExistsif something is there,NotFoundif its parent is missing.Directory.remove(path)removes an empty directory.Directory.list(path):Result<Vector<String>, 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.
- It gives the entries' names, without
Directory.temporary():Result<String, IoError>, the path of a new, empty directory that only this user can enter.- It is created under the platform's temporary directory (
TMPDIRor/tmpon POSIX,GetTempPathon Windows) by trying names untilmkdirmakes one, which is atomic. - The caller removes it.
- It is created under the platform's temporary directory (
Directory.current():Result<String, IoError>, the working directory.Directory.space(path)(ADR 0077):Result<VolumeSpace, IoError>, the bytes of the volume that holdspath:total,free, andavailableto this process, which is at mostfreewhere the system keeps a reserve (statvfs; WindowsGetDiskFreeSpaceExA). The interpreter answersUnsupported, 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)andfsyncon POSIX. Windows journals directory entries and cannot flush a directory, so there it only checks thatpathis a directory.- Common rules for §26.7–§26.10.
- Paths are
StringViews, underFile.open's rules: a zero byte or more than 4,095 bytes isInvalidInput. - All of these are hosted-only. In freestanding code,
ProcessandFileare N0293, andEnvironmentandDirectory, being lib/std's, are not defined. - They are lib/std code (
lib/std/host.cee) over the runtime'sceesafe_host_*functions (runtime/hosted/ceesafe_rt_io.c). - The interpreter models them, and its sandbox (
ceesafec mcp,ceesafe_run) refuses them all.
- Paths are
26.11 The wall clock (ADR 0055)
Clock.now()(lib/std) is aUnixTime:seconds: i64since 1970-01-01T00:00:00Z, leap seconds not counted (POSIX time), negative before it, andnanoseconds: u32, 0 to 999,999,999.t.millis()isseconds * 1000 + nanoseconds / 1,000,000, rounded toward minus infinity;Clock.unixMillis()isClock.now().millis().- It is the system's real time (
clock_gettime(CLOCK_REALTIME); WindowsGetSystemTimePreciseAsFileTime), which may jump backward or forward: waits and measurements useTimer'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
ClockandUnixTime, 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):u64nanoseconds on the monotonic clock from an arbitrary start, for durations and latencies. It never decreases within a process (CLOCK_MONOTONIC;CLOCK_UPTIME_RAWon Apple systems;QueryPerformanceCounteron Windows). The interpreter answers its virtual clock in nanoseconds.
26.12 Random bytes (ADR 0060)
Random.fill(into)(lib/std;into: SliceMut<u8>) isResult<void, IoError>: it fillsintofrom the system's cryptographic random source, read on every call:getrandom(…, 0)on Linux (retried onEINTRand for short reads;/dev/urandomwheregetrandomis missing orENOSYS),arc4random_bufon macOS and the BSDs,BCryptGenRandom(NULL, …, BCRYPT_USE_SYSTEM_PREFERRED_RNG)on Windows. An empty slice isOk.- Errors:
Unsupportedwhere there is no source (CosmoOS userland); the source's error otherwise (IoErrorof its errno;Other(EIO)'s kind on Windows), withintopartly 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
Randomis 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: u32in points) isResult<Window, WindowError>.- 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, orwidth/height0 or over 16384;NoDisplay: no window server, such as noDISPLAYor 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.
Windowis a move type, neitherSendnorShare. Its drop closes the window.
Events.
w.nextEvent()isWindowEvent?: 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)andPointerButton(WindowButton, down, x, y).WindowButtonisPrimary,SecondaryorMiddle. Positions aref32points from the content's top left; a pointer outside the window during a drag gives coordinates outside the size;Wheel(dx, dy), in lines, positivedyaway from the user (scrolling up);Key(WindowKey, down, repeat, WindowModifiers).WindowModifiershasshift,control,altandcommand:commandis ⌘ on macOS and the Windows key elsewhere;Text(u32): a Unicode scalar the user typed, after the key's ownKeyevent; control characters are not reported asText.
Keys.
WindowKeyis one of:Tab,Enter,Escape,Backspace,Delete,Left,Right,Up,Down,Home,End,PageUp,PageDownandSpace;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()isWindowSize { width, height }in points, andw.scale()is pixels per point as anf32, such as 2.0 on a Retina display.w.present(pixels: Slice<u32>, width, height)isResult<void, WindowError>. It shows awidth × heightimage, rows top to bottom, scaled to fill the window's content.- Pixels. Each
u32is 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, isResources. - 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<WindowBox>)(ADR 0070) ispresentwith a promise: only the pixels inside thechangedboxes differ from the last frame presented.WindowBoxis{ x, y, width, height }in the image's pixels. The window may then show just those pixels.- It is a plain
presentwhen 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
changedshows nothing new, and a box of zero size is skipped. - Pixels outside the boxes may be shown from
pixelsor from the window's copy of the last frame, so a program that breaks the promise sees either. A scripted window shows the copy.
- It is a plain
Settings and waiting.
w.setCursor(WindowCursor)takesDefault,Pointer,Text,Crosshair,Grab,ResizeHorizontalorResizeVertical, 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 previouswaitFrame, or earlier when an event arrives, so an idle loop does not spin.
Effects.
open,present,presentChanged,waitFrameandsetTitlemay sleep (@effects(sleep)).nextEvent,size,scale,setCursorand the drop have no effect.Backends (
runtime/hosted/ceesafe_rt_window.c):- macOS: AppKit through the Objective-C runtime, loaded with
dlopenat the firstopen, so no program links it; - Windows: Win32 and GDI;
- Linux: X11,
libX11.so.6loaded withdlopen.
The runtime file is part of a build only when the program calls a window function.
- macOS: AppKit through the Objective-C runtime, loaded with
Scripted windows (for tests, ADR 0069 §2.3). When the environment variable
CEESAFE_WINDOW_SCRIPTnames a file,openmakes 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 whatnextEventreturns then, in the script's order. - Script grammar. The file holds
;-separatedFRAME:WHATitems:move:X,Y;down:B,X,Yandup: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:NAMEandkeyup:NAME;type:TEXT: oneTextper character;resize:W,H,focus:0|1andclose.
- 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_DUMPnames a file, eachpresentreplaces it with the lineceesafe-window W H, then the W×H pixels as RGBA bytes. - Waiting.
waitFramereturns at once. - Errors. A script that cannot be read or parsed makes
openResources.
- Size and scale. Its size is the requested one, until a
Hosted only. In freestanding code these types are not defined (N0200). The interpreter answers
Unsupportedtoopen.
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.
/// 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 toT.Tis any value type, orvoidfor an untyped pointer (*void, C'svoid *). It may not be a reference, slice,StringViewor other scoped type (§19.1, §25.3), ornever(N0281). Since M8d it may be a type parameter: an instantiation with a scoped type argument is then N0274, as for a box ofT.- 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 Tcoerces to*Twherever a*Tis expected (likeSliceMut<T>toSlice<T>).==and!=compare two pointers of the same type (after that coercion) by address. Ordering comparisons are not defined on pointers (N0207); comparep as usizeinstead.
27.2 Making pointers (safe)
Making and converting a pointer is safe; only using one is not (§27.3).
r as *Tfor a referencer: borrow Torborrow mut T, andr as *mut Tforr: borrow mut T.borrow x as *Tparses as(borrow x) as *T(grammar §2.1). The borrow ends at once: the pointer holds no loan, so later uses ofxare not checked against it.s.ptrfor a slices:*TforSlice<T>,*mut TforSliceMut<T>; for aStringView,*u8. It is the address of element 0 (of where it would be, for an empty slice).p as *Uandp as *mut Ufor any raw pointerp, whatever its pointee and mutability.p as usizefor any raw pointer, andn as *T/n as *mut Tforn: usize(also*void). An integer literal converts too:0 as *mut Tis the null pointer (there is nonullpointer literal:nullis 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(): theTatp, moved out (copied, for a CopyT). Afterwards the memory holds noT: 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.mdU2). A type holding C state (§33) is N0282, since C state never moves. Since ADR 0027; before it,Thad to be Copy.p.write(v): movesvtop(copies, for a CopyT) without reading or destroying what was there.pmust be a*mut T(N0282). It returnsvoid. For a type with@placedmethods (§33.3) the store is a placement: they run atpafterwards, as forBox.create, andvmay be the struct literal without its C state thatBox.createtakes (§33.2).p.readVolatile()andp.writeVolatile(v)(§29.1) still need a CopyT.p.add(n)andp.sub(n)(n: usize) andp.offset(n)(n: isize): the pointernelements (n × size(T)bytes) after (sub: before)p, with the type ofp. The arithmetic wraps modulo 2^64; it never panics.Ptr.toBorrow(p): aborrow Tto theTatp;Ptr.toBorrowMut(p): aborrow mut T, forp: *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): aSlice<T>of thenelements fromp;SliceMut.fromRaw(p, n): aSliceMut<T>, forp: *mut T. LikePtr.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 ise'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.mainmay not beunsafe(N0219). Methods may beunsafe; since C5, interface methods may be too (§30.1), and animplmust match (N0267).- Everything lexically inside an unsafe block, an unsafe expression or the body of an
unsafe fnis in an unsafe context, including deferred statements. A nestedunsafeis allowed. - The Safety rule (U2): an
unsafe fn, and every function whose body contains anunsafeblock or expression, must carry a doc comment (grammar §1.2) with a line that starts withSafety:. It states the contract: what callers must guarantee (for anunsafe 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()andp.write(v)are a copy from, and an assignment to, the place(*t)of a temporarytholdingp: the sameDerefprojection as references, through a pointer.Ptr.toBorrow,Ptr.toBorrowMutand the pointer casts areCasts; the arithmetic,isNull,.ptrandfromRaware 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 onuintptr_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 callsPtr.toBorrow/toBorrowMutorfromRawkeeps 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)
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<i32>(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<void, E> | 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 withceesafe_,cee_,CEE_, orceeand 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 ispublic(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, whichsys.strlenneeds) and both are extern declarations, the message says to callsys.strlenand remove this declaration, or, when sys's is notpublic, to make itpublicthere; and if the two are identical in CeeSafe (the same parameter and result types, none of them in error, variadic-ness and attributes,@cnameand@effectsamong them; an identical C signature is not enough), a fix removes this declaration: its lines, with its doc comment and a trailing//comment, and itsextern "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_statsandcosmofs_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'sNAME(an object-like macro or an enumerator):_Static_assert((NAME) == v, "…"), naming the declaration's file and line.Tis an integer type (an imported enum among them) orbool(N0291).@cvalue("expression")names a C constant expression inNAME's place:@cvalue("sizeof(struct stat)") const STAT_SIZE: usize = 144;, or anoffsetof. 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 isNAME(or the@cvalueexpression), so it is read at run time and is not a constant expression.Tis an integer,boolor*CChar(a string macro; N0291). The interpreter cannot run a program that has one (a fault at start).
- A function that a
@headerdefinesstatic inlinemay 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'slist.hhelpers are called so (lib/kernel'sList). - 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 anextern "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,f64or a raw pointer,*CCharamong them, passed as its C type (§28.3). A narrower integer,bool,f32or anything else is N0347, whose message says how to widen it withas. 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
@reprenum, 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<region r>(lib/core/format.cee, every profile) writes into a caller'sSliceMut<u8>assnprintfdoes, with no allocation:create(buffer), thentext(s),bytes(b),byte(b),decimal(u64),signed(i64)andhex(u64, width); what does not fit is cut, the text stays zero-terminated in a buffer of at least one byte, andwritten(),length(),wanted()(every byte asked for, assnprintf's result),truncated()andcText()(a*CCharto it) read it. lib/kernel'sKernelLog(lib/kernel/log.cee,--kernel cosmoos) sends one:send(level, line)isklog(level, "%s", text),write(level, message)a fixed message, andpanic(line)is CosmoOS'spanic("%s", text);LogLevelis log.h's levels,LOG_LINE_MAXits 256. These are the only variadic calls, with a fixed format, so a port's messages are safe code.
- Like every C call, it is unsafe (N0280), and here the
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, withnani64, is accepted on macOS and Linux and is N0208 on Windows, whereaswould lose information; a@repr(C)struct with aCLongfield has another size and layout there. Code meant for both converts withtruncate<CLong>orcheckedTruncate<CLong>(or fromi32), 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 usesCLongorCULongcarries ("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
CLongstops at the same assertion there.
28.4 Exports
@exportmakes a top-level, non-generic, non-method function callable from C, with the C types of §28.3 (N0291 otherwise).maincannot 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@exportfunction puts its C definition, its prototype and its line inceesafe_exports.hinside#if NAME…#endif: the symbol exists in the builds where the C macroNAMEis non-zero, as C's#if NAMEaround 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.NAMEis the macro's name as a string, a valid C identifier (N0292), or"!NAME"for the builds where it is zero (ADR 0080);@cfgwithout 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 anextern "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_Genericandsizeofchecks (§31.3) are inside#if NAME.if (cfg("NAME")) { … } else { … }, whose condition is the callcfgof one string literal ("NAME"or"!NAME", a C identifier: N0292) while nothing the program declares is namedcfg, 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 andcfgblocks nest. It is a statement:cfgas 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
cfgblock 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@exportfunction's own body (N0340; not in a closure inside it), is ausize: 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
@reprenum parameter that is not one of its variants panics (invalid enum value from C) before the body runs. - Exported constants (C9, ADR 0014).
@exportor@export("symbol")on a constant (§10) of integer orbooltype, 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;orconst CE symbol[N] = { … };, declared inceesafe_exports.hasextern 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), orAtomicU32,AtomicU64andAtomicUSize(ADR 0041; notAtomicBool, au32where C'sboolis a byte), and their names valid, unreserved C identifiers (N0291, N0292). In C it isstruct Swith members named as in CeeSafe, of the types CeeSafe uses inside its own C (usizeisuint64_t, pointers have noconst), so that the generated code and C code that includesceesafe_exports.hdeclare the same struct type. It is otherwise an ordinary struct.- C names. A
@repr(C)struct's C tag, and a@reprenum's C type name (typedef uint8_t Shade;and its constantsShade_Light, …, inceesafe_exports.h), are the name it is declared with, in every package: a dependencygeo'sPoint, which CeeSafe callsgeo.Point, isstruct Pointin C, in prototypes and in the exports header, as ingeo'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@reprenums, or a@reprenum 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@reprtype 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, anduint64_tforAtomicUSize, as forusize. CeeSafe reaches it only through the atomic type's methods, which are the__atomic_*operations on the member's address, so C code that includesceesafe_exports.hshares 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 arraysCE 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.createand its@placedinitialiser, 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 adrop(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 buildaccepts 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)andceesafe_free(p, CEE_ALLOC_FOREIGN), which followmalloc,reallocandfree(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 throughCRYPTO_set_mem_functionsin 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 withCEE_ALLOC_FOREIGN's (either number may reallocate or free them). With them, a program may have itsmainin C instead of in CeeSafe. - A program without a CeeSafe
main(a C, C++ or Rust host; ADR 0088) is linked frombuild --emit lib's archive (tooling.md §4.1) or--emit obj's object, and starts the runtime withceesafe_initfromceesafe/embed.h, which every build that writes headers writes besideceesafe/rt.h, and--emit libbeside its output (with the exports header). The archive has no Cmainunless--lib-mainasks 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 withCEESAFE_INIT_ENVIRONMENT), and a panic hook.ceesafe_initmust happen before every call into CeeSafe on every thread, once; then exports may be called from any host thread at once.ceesafe_shutdownprints the report when one was asked for and returns the live blocks. A host that never callsceesafe_initgets what 0.27.0 gave: no program arguments, the environment's variables ignored, and the printed panic. The generatedmaincalls the same two functions, so a CeeSafemainand 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.runWorkersworker, or one C calls a@callbackon) with the kind, the reason, the message and the CeeSafe location; the executor's own aborts arrive asCEE_PANIC_ASYNCwith 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@callbackentry 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
@exportfunctions or@reprtypes, ceesafec also writesceesafe_exports.h: the@reprstructs and enums, and a prototype for each export. C files include it as#include "ceesafe_exports.h". run --interpruns 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.
Equalwritesimpl DerivedEqual<S> for S(lib/core).derivedEqual(borrow this, other: borrow S) -> boolcompares the fields in declaration order, joined by&&; for an enum, the variant, then its payloads.a == bonSisa.derivedEqual(b)anda != bits 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), anotherEqualtype and slices, arrays and vectors of one (the same length and equal elements in order, through lib/core'sDerivedEqualElements<T>;==on them works outside derives too), a cell whose contents have it (§41.2), and throughborrow TandBox<T>the referent. A field writtenT?compares as twonulls 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 opaqueT,==stays N0207, and so does a[T; 2]orVector<T>field's, each instance checking its element type.DerivedEqualis implemented only by@derive(Equal): animplof it in source is N0290, so==keeps one meaning (language-spec.md §6: no operator overloading).HashKeywritesimpl HashKey<S> for S(§24.3) and needsEqual(N0290 otherwise).hashfeeds each field in order through lib/std'sderiveHash(field, mut h), which needs the field's type to implementHashKey(N0268), except for a fieldless enum without one, whose variant index is hashed as au32; an enum's variant index, as au32, comes before its payloads.sameAsisderivedEqual.OrderKeywritesimpl OrderKey<S> for S(lib/std, ADR 0075 §2.4):compareKeycompares the fields lexicographically, in declaration order, throughderiveCompare(a, b)(N0268 for a field type withoutOrderKey, 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:
impl DerivedEqual<Point> for Point {
fn derivedEqual(borrow this, other: borrow Point) -> bool => this.x == other.x && this.y == other.y;
}
impl HashKey<Point> 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<Point> 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)
@repr(C)
struct UartRegs { data: Volatile<u32>; status: Volatile<u32>; }
struct Uart {
regs: MmioPtr<UartRegs>;
/// 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<UartRegs>.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<T: Copy> | 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<T> | The address of a memory-mapped T (typically a @repr(C) struct of Volatile registers). unsafe MmioPtr<T>.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<T>.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 (pauseon x86-64,yieldon 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()andTarget.isAArch64():bool, the architecture the generated C is compiled for. They are decided by the C compiler, not byceesafec(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) -> Ris the type of a pointer to a C function (grammar §2). Its parameter and result types must be FFI-safe (§28.3) and not@reprenums (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@exportfunction or a@callbackfunction (§34.1) is a value of that type where such a type is expected (a typedlet, 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(…) -> Ris the null pointer,f.isNull()tests for it, andf as usizeis 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(…) -> Rforn: usizeis the function pointer to that address, asn as *Tis a raw pointer (§27.2), so a hook can be published in anAtomicUSizeand 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), withfa 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)
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<Box<u64, KernelHeap>, 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 leastlayout.sizebytes aligned tolayout.align, orErr(never a nullOk);unsafe fn deallocate(borrow this, p: *mut u8, layout: Layout): returns a block. Its contract:pcame fromallocateof this allocator type with the same layout, and is returned once.
- Interface methods may be
unsafe(lifting §27.4's earlier rule); animplmust match the interface'sunsafe(N0267). layoutOf<T>()(ADR 0027):T's size and alignment as aLayout, fromTargetInfo. For a type holding C state (§33), whose size only the C compiler knows (§31.2), it is C'ssizeof/_AlignofofT's C type, soceesafec run --interpcannot evaluate it. It takes one type argument and no arguments (N0209), and is a static initialiser (§39.4). ADR 0027 named itLayout.of<T>(); CeeSafe has no type arguments on a member call, so it is a function, liketruncate<T>.offsetOf<T>(field)(ADR 0030): the offset in bytes offieldin the struct typeT, asusize. FromTargetInfofor a CeeSafe layout; for a C-laid-out or importedT(§31, §33), C'soffsetof, soceesafec run --interpcannot evaluate it. It takes one type argument and a field's name (N0209); aTthat 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'scontainerOf). ADR 0030 wrote itoffsetOf(T, field); a type is no expression in CeeSafe, so it takesTas a type argument, likelayoutOf<T>().
30.2 Allocator types
- A user-defined allocator is a non-generic struct with no fields that implements
Allocator. It may be theAofBox<T, A>, andBox.createIn(a, v)takes a value of it. Having no fields, it has no state that a box would need to carry: aBox<T, A>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
SystemHeapandFailingAllocator.Vector,String,SlotMap,StableVector,ArenaandPooltake 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
Allocatoris N0263.
30.3 Semantics
Box.createIn(a, v)with a user allocator callsallocate(Layout { size: size(T), align: align(T) })once. OnErr, it destroysvand returnsErr(AllocError.OutOfMemory); onOk(p), it movesvtop. Destroying the box destroys the contents, then callsdeallocate(p, the same layout)once (H1). The compiler only callsdeallocatewith blocksallocatereturned, so the contract holds for every safe program.- A
size(T)of 0 is never requested: such boxes still allocate one byte (asSystemHeapdoes). - User allocators are not placement-transparent (M5): their boxes stay on the heap.
- They are allowed in the freestanding profile (
profiles.md§4.1):Boxwith 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)
@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'sExternStruct. @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-Ipaths, 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 iscnameinstead, for a C typedef (typedef struct {…} cname;). The name, the@ctypename 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 andextern "C" fntypes, for C array membersCE name[N](struct dev *spare[4]isspare: [*mut dev; 4]; its elements are read and assigned), or (ADR 0041)AtomicU32,AtomicU64orAtomicUSizefor auint32_t,uint64_torsize_tmember, and (ADR 0080 §3.5)AtomicU8for auint8_tone; otherwise N0291. An embedded member is checked with_Genericonly, 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 (u32isuint32_t,usizeissize_t,*CCharisconst char *,*mut TisT *,[u8; 16]isuint8_t[16]). This is checked when the C is compiled (§31.3), not by ceesafec. A Cchar,signed charorunsigned charmember is declaredu8, 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 onchar's signedness. (Acharparameter or result isCChar, §28.3.) The address of au8member, when it is borrowed, converts touint8_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 asr.aandr.b(C'sp->a). In the block,union name { … }declares an imported union, spelledunion namein 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'sExternUnionandAnonymousUnion).unionis 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).
- 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;
- 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_Genericcheck 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.AtomicBoolis refused (N0291): it is au32, and C'sboola 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).@cnameanywhere else is N0290.- An imported struct has no methods,
dropblock, 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
Tappears 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 astatic mutitem'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.fis a place of the declared field type, as for any struct (§12): read it, assign it throughborrow mut, or borrow it. The borrow checker treats it like any field place. A field of raw pointer type,usizeorisizecan 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 againstN,r.f[a..b]is a slice (mutable throughborrow mut), and when the element is an integer orboolthe 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.toBorrowMutmake references from them (unsafe, §27.3).add,subandoffset(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, andSlice.fromRaw/SliceMut.fromRaw. - Across the boundary,
*Tand*mut Tare FFI-safe (§28.3):struct name const *andstruct name *(or the@ctypename) in prototypes. The type itself is not, by value (N0291).
31.3 Lowering
- The generated C includes every
@header, afterceesafe/rt.h, once each, in the order the blocks appear (files in command-line order);ceesafe_exports.hincludes 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 throughuintptr_twhen read and written, as at exports (c-backend.md §2.3), so thatconstin the header's member type causes no warning. - For every declared field
fof typeF(C typeCF), the generated C asserts both_Generic(((S *)0)->f, CF: 1, default: 0)andsizeof(((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 readsenum 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_Genericof the member itself would see a decayed pointer, and, for integer andboolelements, with the size of CeeSafe's array. Its place is(*(A *)(uintptr_t)&(*p).f)(throughuintptr_t, aspmay be aconstpointer), whereAis 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'sExternEnum), whereIisu8,u16,u32,u64,i8,i16,i32ori64: the integer type the C compiler gives the enum. Any other type is N0291. Its C spelling isenum 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 fromIand from every other.asconverts between it and integers under §13's rules, sov as u32andn as vnode_typeare lossless forI = u32. It isCopyand 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
Iis (§28.3) and is spelled with its C spelling in prototypes,@exportand@callbackdefinitions,extern "C" fntypes, imported struct fields (whose_Genericcheck 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=functionrefuses a call whose pointer type namesunsigned intwhere the function's saysenum, 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 spellingTandI's C typeCIwhenIis 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._Genericcannot 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-negativeunsigned int(u32) and one with a negative enumeratorint(i32); MSVC, and Clang targeting Windows, give every enumint. 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)
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 allocate32.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: anawait, §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,
dropblocks, 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
checkdoes 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
dynmethod 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 eacheone ofsleep,allocandsuspend, and@effects()for none:- on an
externfunction, 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).
- on an
- An unknown effect name, or
@effectstwice, is N0290.
32.3 Contexts
@context(atomic)on a function forbidssleepandsuspendin its body.@context(irq), code that may run in a hardware interrupt, forbidssleep,allocandsuspend(cosmoos/kernel-memory-and-lifetimes.mdKQ-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,sleepandsuspendare forbidden (N0295): a spin guard may not be held across anawait. 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)
@effectsOf(test)
fn count(values: Slice<i32>, 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<i32>, 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 holdsp(copied, cast or reborrowed from it and assigned nothing else), is not one of the function's own effects. So is passingpas the argument for another function's@effectsOfparameter. Every other use is as before:ppassed 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
@effectsOfparameters, 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 @effectsOfparameter, or a local that holds itnone: 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
@effectsOffunction (§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 throughpduring a call runs the argument the caller passed, whose effects the caller counted. Anything made frompother than a copy or reborrow is counted by its type, where it is called.lib/core's
sortBy,forEach,any,allandretain(§36.4) are@effectsOftheir closure, and so aresortBy's helpers.Async arguments (ADR 0037). An
@effectsOfparameter 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 lackssuspend(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 runsf's async instance for those arguments, one per (f, the closures bound to its parameters), whetherfis sync or async: a copy offin which each call through such a parameter, or a local holding it, awaits the closure, and passing it on to another function's@effectsOfparameter 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 (suspendamong them) and the instance's. Cancellation is the closure's (§40.7):fsees only what its awaits return. fsees 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).
- 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
33. C state embedded by value (C7b; ADR 0009 §2)
@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<T> {
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<Box<SpinLock<u64>, KernelHeap>, AllocError> =>
Box.createIn(KernelHeap {}, SpinLock { name: name, data: 0 }); // `raw` is left out33.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.createorBox.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 anasyncfunction, 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, throughPtr.toBorrowof 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
@placedon a methodfn 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.createInhas placed a C-laid-out value in its box, and before it returns, the compiler calls the@placedmethods 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@placedmethod runs C's initialisers (spinlock_init,mutex_init,kobject_init) at the value's final address. - A box whose allocation fails runs no
@placedmethod: the value is dropped, as in §18.2. - The effects of
Box.create/Box.createIn(§32.1) include those of the@placedmethods 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)
@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<T>` made by `KRef<T>.create`, and this runs once.
@callback
unsafe fn release<T>(obj: *mut kobject) {
let object: Box<KObject<T>, KernelHeap> = Box.fromRaw(obj as *mut KObject<T>);
} // dropping `object` drops the data and frees it
// … this.ktype.release = release<T>; in KObject<T>'s @placed method34.1 @callback: C addresses of CeeSafe functions
@callbackon a top-level function gives it a C address, as@exportdoes (§29.5), without a symbol. Where anextern "C" fntype 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<T>inside generic code,release<u64>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: itsSafety:line states what the C caller guarantees.@callbackwith@exportis N0290. - Generated C gives each instance used as a value a
staticfunction 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 thesleepandalloceffects (§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 boxb: Box<T, A>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) turnsp: *mut Tback into a box. The box type, and with it the allocator, comes from the expected type (let b: Box<T, KernelHeap> = unsafe { Box.fromRaw(p) }); without one it is N0205. Contract:pcame fromBox.intoRawof 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)
fn mean(values: Slice<f64>) -> f64 {
var sum = 0.0;
for (v in values) { sum += v; }
return sum / round<f64>(values.length);
}
fn main() {
let samples = [1.5, 2.25, 4.0];
let m = mean(samples[..]); // 2.5833…
Console.writeLine(truncate<i64>(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
f32andf64are IEEE 754 binary32 and binary64. Their layout comes fromTargetInfo(size and alignment 4 and 8 on both supported targets). They are Copy, need no drop, hold no loans, and are FFI-safe (Cfloatanddouble, §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 typef32orf64. Its value is the decimal value rounded to nearest (ties to even) in that type, once:0.1f32is thef32nearest 0.1, not thef64nearest 0.1 rounded again. - With a suffix, the type is the suffix's. Without one, the literal is flexible (§5): it takes an expected
f32orf64(looking through an optional, as integer literals do) and otherwise defaults tof64. 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 (write1.0, orround<f64>(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.0is 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
matchscrutinees 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
asstays 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,u8andu16asf32orf64;i32andu32asf64.
Any other
asfrom or to a float type is N0208.round<T>(e):eof any integer or float type,Ta float type. The result iserounded to nearest (ties to even) inT. It is always defined: anf64too large forf32becomes ±infinity, and NaN stays NaN.truncate<T>(e)witheof a float type andTan integer type rounds toward zero. It panics with "float out of range" ifeis NaN or ±infinity, or if the rounded value does not fit inT. (truncatebetween integer types is unchanged, §7.) A floatTis N0207: useround.checkedTruncate<T>(e): astruncate, but the result isT?,nullwheretruncatewould panic.saturatingTruncate<T>(e): astruncate, but a value belowT's range gives its minimum and one above gives its maximum. NaN gives 0. It never panics.checkedTruncateandsaturatingTruncateneed a float or integere(an integereconverts as in §7, ADR 0074 §2.9) and an integerT, androunda floatT(N0207 otherwise). All four take exactly one type argument (N0216).
35.5 Representation
f.bits()is the IEEE representation off: au32forf32, au64forf64.f32.fromBits(u)(u: u32) andf64.fromBits(u)(u: u64) make a float from a representation; every pattern is a value, some of them NaNs.- The names
bitsandfromBitsare reserved on float types, as the overflow methods are on integers (§6).
35.6 C and the interpreter
f32andf64are Cfloatanddouble. 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.hrequiresFLT_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 generatedmain.casks for this with#define CEE_FLOATSwhen 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, andFLT_EVAL_METHODis not 0. gcc ignores the standard pragma, but does not contract under-std=c11, which CeeSafe's builds use.ceesafecalso passes-ffp-contract=offto 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:
truncateand its variants compare the value with the target's exact bounds before converting (C11 6.3.1.4);round<f32>of anf64outsidef32's range gives the infinity directly, not through a C conversion (C11 6.3.1.5).
- The interpreter computes in CeeSafe's own
f32andf64(ceesafec's Cfloatanddouble), 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.writeLineandString.appendNumberacceptf32andf64(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
f32prints withf32's shortest digits:0.1f32prints as0.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.
- if k ≤ n ≤ 21, the digits and n − k zeros:
NaN,Infinityand-Infinity. Both zeros print as0, 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 gives4.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,minandmaxare exact portable C over integers and bits inrt.h. On baseline x86-64 there is no instruction forfloororceil;roundsdneeds SSE4.1.sqrtis the hardware square root, which is correctly rounded on both targets. It is reached through__builtin_elementwise_sqrtwith clang and__builtin_sqrtwith gcc, built with-fno-math-errno(ceesafec passes it), so it never setserrnoor callssqrt. MSVC uses its C runtime'ssqrt.
- 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,minandmaxare 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-floatandceesafec check --no-floatreject every float type, float literal andf32.fromBits/f64.fromBitswith 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.pytoc8.py), andcheck-fpregs.shremains 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<f64>orparseFloat<f32>, 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-floatchecks 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)
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<i64>; // a generic function, named with its type arguments
Console.writeLine(id(7));
}
fn identity<T>(x: T) -> T => x;The type
fn(T1, …, Tn) -> R(fn(T1, …, Tn)whenRisvoid) 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 stayextern "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<i64>), 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):
externandunsafefunctions, 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.
- wherever a value can be (
Calls.
f(args), withfa 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), elsesleepandalloc(§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)
fn countIf(values: Slice<i32>, keep: borrow fn(i32) -> bool) -> i32 {
var n = 0;
for (v in values) { if (keep(v)) { n += 1; } }
return n;
}
fn forEach(values: Slice<i32>, 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) => expressionor(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
voidfor 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,
thisincluded, 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 aborrow mut thismethod or mutating collection method on it, or, for aborrow mutvariable or field, uses it where aborrow mutis expected, which reborrows it (§13.3):() => grow(s)withs: borrow mut Stringcapturessmutably, 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.
- mutably if the body assigns to it or to a place rooted at it, passes it as
- Types.
- A literal that captures nothing is a plain function value,
fn(…) -> R(§36.1). - One that captures is a closure:
borrow fn(…) -> Rif every capture is shared,borrow mut fn(…) -> Rif any is mutable. - A closure type is a reference (§13):
borrow fnis Copy, andborrow mut fnis unique and reborrowed at each call. Both are scoped, and allowed where references are (§13.1, §25): parameters,letlocals, 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.
- A literal that captures nothing is a plain function value,
- 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 fnconverts to aborrow mut fn: calling a shared closure changes nothing. - A
borrow mut fnwhere aborrow fnis 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 plainfnis expected.
- Calls
f(args)are as for function values (§36.1): safe, with the effects of the type's effect list (§36.3), elsesleepandalloc. - In the body,
returnreturns from the closure.breakandcontinuecannot 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 astaticfunction takingconst void *envfirst. A call is(*f)(f, args…). The code pointer's member isf_0code, and a field-precise capture'sf_0<local>__<field>: the digit, which starts no identifier, keeps them apart from a captured local'sf_<local>, whatever its name.
36.3 Effect lists on function and closure types (G8b′; ADR 0012 §3.7)
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<i32>, 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<i32>) {
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 beforefnin a function type, with eachesleep,allocorsuspendas in §32.2:@effects() fn(i32) -> bool, and for closuresborrow @effects() fn(i32)andborrow mut @effects(alloc) fn(i32). No other attribute is allowed there (N0290), and onlyfnmay 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)andfn(i32)are different types. A type without a list may havesleepandalloc, sofn(i32)is the same type as@effects(sleep, alloc) fn(i32). It never hassuspend: only an async closure's call suspends, and that is a task (§40.3), not an effect of a sync call. A list may namesuspend: an@effectsOfparameter 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
sleepandalloc. - 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
@effectsis 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 tofn(i32), andborrow @effects() fn(i32)toborrow 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 afn(i32) -> bool, which cannot be passed where an@effects() fn(i32) -> boolis 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)
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<String>.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<T>, or a vector (lending its elements mutably) | borrow mut fn(borrow T, borrow T) -> i32 | void |
s.forEach(visit) | Slice<T> (a SliceMut<T> 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<T, A> (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).
sortByorders the elements so thatcmp(borrow s[i], borrow s[i + 1]) <= 0for eachi, whencmpis 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
cmpO(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 oncmp'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 ofcmp. - If
cmpis 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.
- It is not stable: equal elements may end in any order. The order chosen is a deterministic function of the input and of
forEachcallsvisiton each element in index order.anycallstestin index order until it returnstrue(thentrue;falsefor an empty slice), andalluntil it returnsfalse(thenfalse;truefor an empty slice).retaincallskeeponce on each element in index order. It keeps the elements for which it returnedtrue, 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,swapandretain), so the closure cannot capture it:v.retain((x) => x < v.length)is N0241, as for any other conflicting loan. - Effects. The methods are
@effectsOftheir closure (§32.5): a call has the closure's effects, plusretain's destruction of the elements it removes. Sos.sortBy((a, b) => a - b)may be used in@context(atomic)and@context(irq)code, ands.sortBy(compare)may too ifcompareneither sleeps nor allocates. - Implementation. The methods are CeeSafe code in
lib/core/slices.cee: static functions of the generic structsSliceAlgorithms<T>andVectorAlgorithms<T, A>, which the method calls name (soSliceAlgorithms<i32>.sortBy(s, cmp)is the same call). They use only safe CeeSafe andswap(§16.5).
36.5 Owning closures: move literals and dyn fn (M13 G3; ADR 0015 §3.6)
fn adder(n: u64) -> Box<dyn fn(u64) -> u64> {
return Box.create(move (x: u64) => x + n)!; // n is moved (here, copied) into the closure
}
fn counter() -> Box<dyn mut fn() -> 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<String>.create();
names.push(String.create())!;
let size: Box<dyn fn() -> usize> = Box.create(move () => names.length)!; // a closure owning the vector: Box<dyn fn() -> usize>
Console.writeLine(size()); // 1
}- Types.
dyn fn(T…) -> Randdyn mut fn(T…) -> R(with an optional effect list,dyn @effects() fn(…)) aredyntypes (§37): unsized, so they appear only behind a reference or in a box.Box<dyn fn(T…) -> 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 writtenborrow dyn fn. - Move literals.
move (x: T, …) => bodyis 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,breakand?are those of §36.2. - Where. A move literal may only be the value argument of
Box.create(…)orBox.createIn(a, …)whose expected result is aBox<dyn fn…, A>or aBox<dyn mut fn…, A>. 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 adyn fnis 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 movechild (§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)wherefis a place of typeBox<dyn fn…>,borrow dyn fn…, or a reference to such a box, calls the closure. Adyn mut fnneeds mutable access: avarbox, or aborrow mut, else N0248. A call has the effects of the type's effect list, elsesleepandalloc(§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
dynpair (§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)
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<Box<dyn Shape>>) -> u64 {
var t: u64 = 0;
for (s in shapes) { t += s.area(); } // a call through the table
return t;
}
fn main() {
var shapes = Vector<Box<dyn Shape>>.create();
shapes.push(Box.create(Square { side: 3 })!)!; // Box<Square> coerces to Box<dyn Shape>
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<i32>),dyn Iis the type of "a value of some type that implementsI". Its size is not known, so, like[T](§16), it is unsized: it appears only behind a pointer.borrow dyn Iandborrow mut dyn Iare references (§13). They are scoped, follow every reference rule, andborrow dyn Iis Copy.Box<dyn I, A>(andBox<dyn I>forSystemHeap) owns its value (§18.2). It is a move type that needs drop.- Anywhere else,
dyn Iis N0330: a local, parameter or result of typedyn I, a field, an array or vector element, an optional, a type argument (Vector<dyn I>;Vector<Box<dyn I>>is fine), or an element of a slice type.
- Dyn-compatible interfaces (amended by ADR 0086 §2.3).
dyn Iis N0331, naming the member, if a method ofItakesthisby value, has no receiver (a static function), has type or const parameters of its own (§20.5), or namesSelfoutside its receiver, or ifIhas an associated type or a constant, or requiresCopy; 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 keepsT = dyn Isound (dyn IimplementsIfor bounds, §37.2): generic code may callT.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). dynis a keyword in types only (grammar.md §2).dyn Ibinds tighter thanborrow, soborrow dyn Iis a reference to adyn I.
37.2 Coercions
- A value converts to a
dyntype only where that type is expected: an annotatedlet, an argument, a field initialiser, areturn, or an argument of a collection method such aspush. Inference never invents adyntype.borrow T→borrow dyn I, andborrow mut T→borrow mut dyn I(orborrow dyn I). Aborrow mut Tvariable or field converts through a reborrow, as every use of aborrow mutdoes (§13.3): it stays usable after the reborrow ends, and is frozen while it lives (B4);Box<T, A>→Box<dyn I, A>: a move of the box, with no new allocation. The allocator type must be the same.
Tmust implementI(§21.1), else N0268, as for an unsatisfied bound.- Scoped
Tbehind a reference (ADR 0064 §3.2, OD-10a). A type with region parameters that implementsI(§25.3), such asView<r>, coerces toborrow dyn Iandborrow mut dyn I. Thedynreference carries the loan of the reference it came from. As for anyborrow(a) View<r>,routlivesa, so the referent's loans outlive thedynreference, and borrowing the value keeps its loans alive (B4). No region is written on thedyntype. Other scoped types (references, closures, views, slices) are notdyn(they implement no interface), andBox<dyn I>takes no scopedT(N0268): a box owns its value, and its type keeps no region. Tmust be a sized type: a struct, an enum, or a built-in type such asi32. Adyn Idoes not coerce to anotherdyntype, and there is no conversion back toT(no downcasting; OD-10b).dyn IimplementsI(ADR 0064 §3.1). For bounds,dyn I(anddyn I + Send) implements its own interface, with the same type arguments: each method calls through the value's table. So a genericfn layout<M: Metrics>(m: borrow M)takes aborrow dyn Metricsas well as aborrow Fixed, withM = dyn Metrics.dyn Iis 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
dynvalue by value is N0222 at the use, with the reason. - A user struct or enum instantiated with
dyn I(ByRef<dyn I>, written or inferred) is N0330 if it holds the value by value (struct Wrap<T> { inner: T }).
- A function instance whose parameters, locals or result hold a
37.3 Calls
x.m(args), wherexis aborrow dyn I, aborrow mut dyn I, aBox<dyn I, A>, or a place of one of these types, calls the methodmofIthrough the value's table. The method is that ofT'simpl I for T.The receiver rules are those of a reference to
T(§13, §21.1):- a
borrow thismethod may be called through any of the three; - a
borrow mut thismethod needs mutable access: aborrow mut dyn I, or a mutable place ofBox<dyn I, A>. Through aborrow dyn Iit is N0248, as for any value behind a shared reference.
- a
Only
I's methods can be called.T's other methods and fields are not visible throughdyn 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 throughT: 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
implof it:ceesafeinterface Visitor { @effects() fn visit(borrow mut this, value: u64); }An
implmethod whose effects exceed its interface's list is N0295, reported at theimplmethod. A call throughdyn Ihas the effects ofI's list, orsleepandallocwithout one, as for a closure without a list (§36.3); an async method without one hassuspendandalloc, since an async body cannot sleep (ADR 0036). A call on a genericT: Iis unchanged: it has the effects of the instantiated method (§21.2).Drop. Dropping a
Box<dyn I, A>destroys its value withT's drop glue, then frees it withA.
37.4 Representation (c-backend.md §2)
- A
borrow dyn I, aborrow mut dyn Iand aBox<dyn I, A>with a zero-sizedAare each two words:{ data, vt }.datapoints at the value, andvtpoints at the table for (T,I). - The table is a
static conststruct per pair (T,I) that a coercion names. It holds the drop glue, the value's size and alignment (for a user allocator'sdeallocate), then one function per method ofI, in declaration order. Each function is a generated adapter takingvoid *as its receiver, so no function is called through a pointer of another type. An async method's entry startsT's method in the frame of a call through the table, and the table also holds the size of that frame forT(§40.6, c-backend.md §3a). dyn Ivalues have no address identity beyonddata. Twoborrow dyn Ivalues 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)
struct Counter { n: u64; }
fn main() {
let counter = Arc<Mutex<Counter>>.create(Mutex<Counter>.create(Counter { n: 0 })!)!;
var workers = Vector<Thread<u64>>.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<Mutex<Counter>>.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):<T: Send>,<T: Send + Share>.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 aSendelement; Box<T>,Vector<T>,String,SlotMap<T>,StableVector<T>andHandle<T>, withT: Sendand the system heap (orFailingAllocator);File,TcpListener,TcpStream;Box<dyn I + Send>andBox<dyn fn(…) -> R + Send>, and with+ Share(§38.4);- the library's thread-safe types (§38.3):
Arc<T>,Mutex<T>, the atomics.
- integers, floats and
It fails for:
- references, slices, views and every other scoped type (§19.1);
- closures (
borrow fn), function values aside (fn(…)isSend: a code address); - raw pointers and imported C types;
- a box or collection with a user allocator;
- regions and pools;
Thread<T>(§38.2);Box<dyn I>andBox<dyn fn…>without+ Send.
Share: shared references to a value may be used from several threads at once. It holds for aSendtype 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<T, A>, whosepushtakesborrow this(§24.2); - a
RawCell<T>(§41.1), so every cell (Cell<T>,RefCell<T>) and every type holding one (ADR 0084 §2.2); - a
dynbox,Box<dyn I + Send>orBox<dyn fn… + Send>: what it holds is not known, and may be such a value (adyn … + Sharebox is not, §38.4: the walk stops there); - a type parameter without a
Sharebound.
The walk stops at the library's thread-safe types (§38.3), which are
Shareby design:Mutex<StableVector<T>>isShare.Thread<T>is notSend, so notShare. (Until 2026-11-09, everySendtype wasShare, which let two threads push to one sharedStableVector, or share adynbox, in safe code: a data race. ADR 0084 added the cells.) A type that holds a cell is stillSendwhen its contents are: a cell may move to another thread, not be used from two at once.- a
Inference is structural and recursive. A recursive type is
Sendunless a path through it reaches a type that is not. Generic code knows only its bounds: aTisSendonly under aT: Sendbound.
38.2 Spawning and joining
Thread.spawn(move () => body)starts a thread that runs the move literal's body (§36.5) and returnsResult<Thread<T>, SpawnError>, whereTis the body's result type.SpawnErroris the enumSpawnError.Resources(the thread or its closure could not be allocated).- The literal takes no parameters. Its captures must be
Send(N0334), and so mustT(N0334). Thread.spawnalso takes aBox<dyn fn() -> T + Send>.- A move literal is allowed here as well as in
Box.create(amending §36.5).
- The literal takes no parameters. Its captures must be
Thread<T>is a move type that needs drop.t.join()consumes it, waits for the thread to finish, and returns itsT. Dropping aThread<T>that was not joined waits for it and destroys its result. There are no detached threads: every thread a program starts has ended beforemainreturns.- A panic in any thread aborts the process (OD-5).
- Effects (§32):
spawnhassleepandalloc.join, and dropping aThread<T>, havesleep.
38.3 Shared state (library types: lib/std/sync.cee, and the atomics in lib/core/atomics.cee)
Arc<T: Share>: shared ownership.Arc.create(v)returnsResult<Arc<T>, AllocError>.a.clone()returns another owner of the same value, anda.get()returns aborrow T(shared only).- The count is atomic. The last owner's drop destroys the value and frees it.
Mutex<T: Send>: mutual exclusion.Mutex.create(v)returnsResult<Mutex<T>, AllocError>; the platform lock is allocated.m.lock()waits for the lock and returns aMutexGuard<T>, scoped tomlike C7's guards. Itsdata()lendsborrow mut T, and its drop unlocks. The data is held in aRawCell<T>(§41.1), since the guard changes it while the mutex is shared.T: Sendalready refuses every loan-holdingT, so N0351 cannot arise there.lockhas thesleepeffect.
- Atomics:
AtomicU32,AtomicU64,AtomicUSizeandAtomicBool, 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), thenload(),store(v),swap(v)andcompareExchange(expected, new)(returnsResult<T, T>,Errwith the current value).- The integer types also have
fetchAdd(v)andfetchSub(v), which wrap, and (ADR 0041)fetchOr(v)andfetchAnd(v); each returns the value before the change. - Layout.
AtomicU32andAtomicU64are laid out exactly asu32andu64(size and alignment, checked by_Static_assertin every unit), andAtomicUSizeasusize; so an atomic may be a field of a@repr(C)or imported struct, as its integer member (§28.5, §31.1; ADR 0041).AtomicBoolis au32. - Every operation takes
borrow this, is sequentially consistent, and has no effect. - With an ordering (ADR 0021):
loadWith(o: LoadOrdering),storeWith(v, o: StoreOrdering), andswapWith,compareExchangeWith,fetchAddWith,fetchSubWith,fetchOrWith,fetchAndWithtakingo: Ordering(AtomicBoolhas the first four). The enums areLoadOrdering { Relaxed, Acquire, SequentiallyConsistent },StoreOrdering { Relaxed, Release, SequentiallyConsistent }andOrdering { 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:AcquireReleaseasAcquire,ReleaseasRelaxed. - 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
SendandShareby the library's declaration (@threadSafe, accepted only in the library's own files). Theirunsafeinternals are argued inownership.mdT1–T3.
38.4 dyn … + Send
dyn I + Sendanddyn fn(…) -> R + Send(and withmut) aredyntypes (§37, §36.5) whose boxes areSend, but notShare(§38.1): a box may move to another thread, not be read from two at once. A coercion to one needsT: Send, which for a move literal meansSendcaptures (N0334). Adyn … + Sendconverts to the samedyntype without+ Send(the same value: they share their C representation and tables), never the other way.Thread.spawn(f)withf: Box<dyn mut fn() -> T + Send>runs a closure made elsewhere.dyn … + Share(ADR 0084 §2.5):dyn I + Shareanddyn fn(…) -> R + Share(and withmut) aredyntypes whose boxes areSendandShare;+ Shareimplies+ Send, asShareimpliesSend, and+ Send + Shareis the same type. A coercion to one needsT: Share(N0334 otherwise: a value holding a cell, §41, or a closure capturing one, cannot become one). ReachesSharedChange stops at such a box, soArc<Box<dyn I + Share>>is allowed and bothThread.bothchildren may read one. It converts todyn I + Sendand todyn I(the same value and tables), never the other way.
38.5 Profiles
- Threads,
ArcandMutexare in the hosted profile: hosts with pthreads or Win32 threads, and CosmoOS userland overcosmo_thread_*andcosmo_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 inlib/core, in every profile (ADR 0026). lib/std/is the hosted library: compiled into hosted packages only. Likelib/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)runsaon a new thread andbon the calling thread, waits until both have finished, and returnsvoid. It is in the hosted profile, withThread(§38.5).- Each argument is a closure literal with no parameters and a
voidresult (§36.2), or a function value of typefn()(§36.1). A closure that is not a literal (a variable or a parameter of typeborrow 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); });
- Each argument is a closure literal with no parameters and a
- Captures.
aruns on another thread whilebruns 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
Sharetype, where, for this rule only,borrow T,Slice<T>andStringViewareSharewhenTis; - a capture used mutably needs a
Sendtype, where, for this rule only,borrow mut TandSliceMut<T>areSendwhenTis; - a captured closure is refused;
- otherwise N0334, naming the capture and its type. These extended rules apply here only: outside
Thread.bothreferences are neitherSendnorShare(§38.1).
- a capture used shared needs a
- 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
bothreturns. - No failure. If the new thread cannot be started (its runtime record cannot be allocated, or the platform refuses),
bothrunsaand thenbon the calling thread. The result is the same either way, sobothreturnsvoidand a program cannot tell whetheraran in parallel. - A panic in either closure aborts the process (OD-5).
- Effects (§32):
sleepandalloc.
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)
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;andstatic mut NAME: T = e;at package level, with an optional visibility, likeconst(§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
Twith adropis allowed, and its drop never runs.
39.2 static
Tmust beShare(§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:Tmay beStringView, and in astatic(not astatic 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"];thenf(LEVELS[..]). A lib/coreCStr(§22.3) initialised by ac"…"is static string data too (ADR 0087 §2.4), and so is aSlice<u8>in astaticinitialised byembed(§10.1), by a constant view's.bytes()(§10) or by a constant holding one, and, in astatic, an array of them (static FILES: [Slice<u8>; 2] = [embed("a.bin"), embed("b.bin")];).NAMEin an expression denotes the place*S, whereSis a reference to the static of typeborrow(static) T.staticis 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
TisCopy, borrowed (borrow NAME,borrow NAME.field), and its methods takingborrow thiscalled; - 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
staticregion. It may be returned from a function whose return type namesstatic(borrow(static) T, §13.5), stored, and sent to another thread.
- it can be read when
- Mutation goes through
T's own synchronisation: an atomic's operations, a lock's guard.
39.3 static mut
Tneed not beShare.NAMEdenotes the place*SwithS: 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.mdU3). - What may be written (ADR 0067).
Srefers 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 onlystatic(§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).
Tmay 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 astatic mut: a plainstaticof 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,
boolor 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 aStringViewor, with ac"…"right operand, aCStr;embed(path)as aSlice<u8>,embedText(path)as aStringView(§10.1), and a constant view's.bytes()as aSlice<u8>(§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
@constantconstructors (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 nodropblock, such as atomics (ADR 0075 §2.15): a static is never moved out of or destroyed, and[e; N]there evaluateseN times at compile time, sostatic 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
@constantfunction whose arguments are static initialisers;a call of a C constant macro whose arguments are static initialisers or
here(below);layoutOf<T>()(§30.1).For C objects (ADR 0042), where the expected type is an imported C type, an
extern "C" fnor 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 Cchararray, ADR 0034) may be given a string literal of at mostNbytes, which C zero-pads (longer: N0338); - the name of an
extern "C",@exportor@callbackfunction: its C address (§29.5); borrow S as *T,borrow mut S as *mut T, and the same of a field ofSor an element at a constant index, for a staticS(any static, declared before or after): its address, an address constant (C11 §6.6p9). Taking astatic mut's address is not an unsafe use.S's value, or a field's, is N0338;0 as extern "C" fn(…) -> Rand0 as *T: null.
- 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
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<Table> = Mutex<Table>.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
@constantfunction'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_<name>(with the package's name for a dependency's, as in §2.1 ofc-backend.md),staticunless--splitplaces it inceesafe_instances.cand renames it (c-backend §2.4). It is notconstin 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 aStringView.hereis(&ceeS_<name>.<field>…), an address constant (C11 §6.6p9), andlayoutOf<T>()of a C-laid-outTis{ .f_size = sizeof(…), .f_align = _Alignof(…) }. Sis&ceeS_<name>.- 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'sceeW_function, declared before the statics); a static's address is((T)&ceeS_<name>.<member>…). 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
maininto 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.
@exportor@export("symbol")on astaticorstatic mutmakes it C data with external linkage undersymbol(default: its name), declared inceesafe_exports.h. Its type is one C spells (§28.5's member types: integers,bool,CChar, atomics, raw pointers,extern "C" fntypes,@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 mutis C'sT symbol = …;: C may read and write it, and every CeeSafe use stays unsafe (§39.3). - A plain
staticholding no atomic is C'sconst T symbol = …;, matching a header'sextern const struct ops data_ops;, and may be of an imported C type (which §39.3 otherwise refuses): neither side writes it.
- A
- Imported statics. In an
extern "C"block,static NAME: T;andstatic mut NAME: T;declare data its headers define (extern T NAME;), namedNAMEin C or its@cname("symbol"), of a type as above; arrays included (static names: [*CChar; 8];forextern const char *const names[8];). Astatic mutis used as one (unsafe); a plainstaticis read-only, forconstC data and for data C changes only atomically (an atomic type). - Lowering. The C object is named by its symbol instead of
ceeS_<name>. No definition is emitted for an imported static. A use takes its address throughuintptr_tto CeeSafe's pointer type, as for a C array member (§31.3), which also drops aconst. - 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)
async fn fetchBoth(net: borrow Net, a: StringView, b: StringView) -> Result<Pair, NetError> {
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. Anexternfunction, adropblock and a@constantfunction cannot be async (N0290).asynccomes afterunsafe.- 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. Throughdyn I(§37.3, ADR 0036) the awaiter's frame has room for the largest frame ofI.mamong the types coerced todyn Ianywhere in the program: the program is compiled whole, so that set is known.dyn Ianddyn I + Sendhave one such frame, with room for the types coerced to either, since+ Sendchanges no layout (bug B15). An await through the table that may reach its own frame is recursion throughawait(below).Async.boxedtakes 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 mayawait(§40.2); it is a context that forbidssleep(§32.3), so a blocking call is N0295 with its chain of calls; and its effects (§32.1) are those of its operations,suspendamong them if it awaits. - Recursion through
await(an async fn that awaits itself, directly, through other async fns, or throughdyn) 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-framesnames the stacked ones. N0343 remains where there is no frame stack (the freestanding profile), for a cycle through araceorallchild, 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 fromalloc(an allocator type, as forBox.createIn, §30) and is given back when it returns; the awaiting frame holds only a pointer, so recursion through it is allowed. Its value isResult<T, AllocError>:Err(AllocError.OutOfMemory)if the frame cannot be had, and then the task does not run. N0343's sure fix isAsync.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:emust be a task (a call of an async fn or an async closure), or one of theAsyncoperations that produce one (§40.4); otherwise N0342. Its value is the task's result, of the task's result type.awaitis allowed only in the body of an async fn or async closure: anywhere else, including a sync closure inside an async fn, adropblock and a deferred statement, it is N0341.- What it means. The current task runs
eto completion; whileewaits, other tasks of the same thread may run. Everyawaitis 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 anawaitwith no extra rule. The borrow checker treatsawait eas a call: whateborrows is borrowed until it returns. - Effects:
suspend(§32.1), so anawaitwhile 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.returnand?in the body return from the closure's task.- It is accepted only as the argument of
spawn,race,allandshield(§40.4, §40.5), or of an@effectsOfparameter (§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@effectsOfparameter. 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<void, E> | §40.5 |
Async.race(a, b) | Result<Raced<T>, E> | Two async closures of result Result<T, E>, run at once. The first to return decides: an Ok wins and the other is cancelled and awaited; Raced<T> (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<Raced<T>, E>, else from a's expression body |
Async.all(a, b) | Result<void, E> | Two async closures of result Result<void, E>, 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<void, AsyncError> |
Async.timeout(ms, timedOut, body), Async.deadline(at, timedOut, body) | Result<T, E> | ADR 0046. Runs body, an async closure of result Result<T, E>, 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<T, E>, else from its expression body |
Async.checkpoint(cancelled) | Result<void, E> | 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<E>.checkpoint |
Async.isCancelled() | bool | Whether the current task is cancelled and not shielded (AsyncControl<E>.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<void, AsyncError> | 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<T> (a guard that may be held across await, its waiters served first come, first served), and channels (Channel<T: Send>, 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<T, AsyncError>, where AsyncError has Cancelled, TimedOut and Io(IoError).
Socket addresses (lib/std's net.cee, ADR 0044):
SocketAddressis an enum:V4([u8; 4], u16)andV6([u8; 16], u16, u32), the address bytes in network order, the port, and an IPv6 scope id (0: none). Through the interfaceSocketEndpointit hasport(),withPort(p),isLoopback(),isUnspecified()andwrite(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), eachSocketAddress?; andv4Any(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. ListenOptionshasbacklog: u32(0:SOMAXCONN),reuseAddress(SO_REUSEADDR; ignored on Windows, whose option lets another socket take the port),reusePort(SO_REUSEPORT;Unsupportedwhere the platform has none, Windows), andv6Only(IPV6_V6ONLY, always set explicitly on an IPv6 socket);ListenOptions.defaults()is{ backlog: 0, reuseAddress: true, reusePort: false, v6Only: false }.AsyncTcpListener.bindWith(address, options)andlistener.localAddress(), bothResult<_, 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 isV4, not::ffff:a.b.c.d.
Name resolution (lib/std, ADR 0048): await AsyncDns.resolve(host, port) is Result<Vector<SocketAddress>, 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(GetAddrInfoWon 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
Iokinds:HostNotFound(no such name, or no address; ADR 0060, before which it wasNotFound),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 areHostNotFound).
Vectored sends (lib/std, ADR 0045):
IoParts<region r>(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), thenparts.then(b), which isIoParts<r>?, null when eight are there already.count(),part(i)(empty past the last),totalLength(), andafter(n): the bytes after the firstn, as parts, whole parts dropped and the one where bytenfalls cut; no parts fromtotalLength()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<usize, AsyncError>, is how many it took, which may stop inside any part. Empty parts returnOk(0)without a system call.await stream.writeAllVectored(borrow parts)sends every byte, aswritedoes for one slice.await AsyncSockets.sendVectored(fd, borrow parts)is the one step on a raw descriptor. Errors and cancellation arewrite'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,offsetandlengthu64) is one step: the task waits until the socket takes at least one byte of the file fromoffset, and the result,Result<u64, AsyncError>, is how many went; 0 if the file ends atoffset. Alengthof 0 isOk(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)sendslengthbytes in as many steps as the socket needs; if the file ends first, the result isErr(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 arewrite's (§40.7). - A send to a connection the peer has reset is an error (
BrokenPipeorConnectionReset), never a signal (ADR 0078 §2): on Linux, wheresendfilehas noMSG_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) isResult<AsyncUdpSocket, IoError>, 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<void, AsyncError>. A datagram too long for the system isIo(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, andtruncatedsays the rest is gone.await s.sendMsg(borrow message)sends aUdpMessage { parts, to, from, ecn, segmentSize }: the parts (anIoParts, §40.4) as one datagram, from the local addressfromif given (packet info), with the ECN bitsecn(0: not set). WithsegmentSize > 0the bytes are sent as datagrams of that size, the last shorter: in one call where the system can (Linux'sUDP_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)isUdpReceived { length, truncated, from, to, ecn }:tois the local address the datagram arrived at, with the socket's port (withreceiveLocal; else null),ecnits ECN bits (withreceiveEcn; else 0).- An IPv4 peer of a dual-stack IPv6 socket is reported as
V4, and aV4destination is sent IPv4-mapped. A port unreachable is not reported on a later receive (on WindowsSIO_UDP_CONNRESETturns its report off). Windows refuses a sender's ECN value 3 (CE) asIo(InvalidInput). CosmoOS userland has no UDP yet:bindisIo(Unsupported). - Cancellation is
read's andwrite's (§40.7).AsyncSockets.udpBind,udpSendandudpRecvare 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) => { … }), andAsync.scope(oom, body)with the system heap (and thealloceffect). WithscopeIn, each child's task and frame come fromallocand go back to it when the child returns; the spawn has the effects of itsallocateanddeallocate.bodyis a sync closure literal taking the scopesand returningvoid.oom: Eis 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 beResult<void, E>, the sameEfor every child of the scope. It returnsvoid. 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 isErr(oom).s.spawnSend(async () => …)(ADR 0038) isspawnfor a child that any worker ofAsync.runWorkersmay 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 followThread.both's rule (§38.6), so one it reads needsShareand one it changesSend, references counting by their target, and a captured closure is refused. Its error type must beSend, and its frame comes from the system heap (notAsync.scopeIn's allocator). Each of these is N0334. A child's error cancels its siblings wherever they run. UnderAsync.run, which has one worker,spawnSendisspawn.- The scope returns only after every child has returned. Its result is
Ok()if every child returnedOk; otherwise the firstErr(in the order the children returned), after the firstErrhas cancelled the others. - The body is a block of the awaiting function, not a closure (ADR 0031 §6.6):
sis not a local, and names the scope only ins.spawn(…).return,?,breakandcontinuecannot 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
spawnuntil 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 amoveliteral 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).
- every child's captures are borrowed from its
- No
SendorSharecheck 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.runreturns.
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) => …)isResult<void, E>,Ebeingoom's type, which must beCopy(N0268). It is a call of lib/core'sAsyncTasks<E>.run(ADR 0037): the body runs in the awaiting task, as the set's owner, withset: borrow TaskSet<E>, 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 fromalloc's allocator type instead (an allocator, N0263), asAsync.scopeIndoes, in every profile: lib/core'sAsyncTasksIn<E, A>.rungives the set its allocator when it starts, so a spawn in any function uses it.await set.spawn(async move () => …)starts a child of resultResult<void, E>and isResult<void, E>: whilelimitchildren run, the task waits for one to return.await set.spawnSend(child)is the same, but the child may run on another worker ofAsync.runWorkers(ADR 0038): each capture andEmust beSend(N0334); a set fromtasksInwith 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 isErr(oom); the set goes on.set.trySpawn(child)isbool, false instead of waiting (or once cancelled, or with no frame); it is not awaited.setis a local or parameter of typeTaskSet<E>or a reference to one. Not awaitingspawnis N0342; a child that is not anasync moveliteral 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 throughArcor aChannel, or values are passed down. - Errors. A child's
Errcancels no sibling; the set keeps the first. The body'sErrcancels every child. The set's result, once the body and every child have returned, is the body'sErr, else the first child's, elseoomif a child's frame could not be allocated, elseOk. 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-framesreports it (c-backend.md §3a). - A frame is placed once and never moves. The root frame of
Async.runis 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, andrace,allandshieldembed 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) andc-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),
raceandallcancel the other child,timeoutanddeadlinecancel their body at the deadline (ADR 0046), and the runtime may cancel a root task (theCEESAFE_CANCEL_ATsweep,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.checkpointandAsync.isCancelled: a cancelled task's next such operation returnsErr(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 anawait. - Cleanup is ordinary code:
?returns the error, and the frame's locals are dropped as the task returns. Async.shield(ms, body): withinbody, the library's operations ignore the flag. Oncemsmilliseconds have passed since the shield began, they returnErr(AsyncError.TimedOut), neverCancelled. 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, …)isAsync.run, andspawnSendisspawn(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<Cell<u32>> (Arc's T: Share, N0334) and static C: Cell<u32> (N0337, §39.2) are refused.
41.1 RawCell<T>: the primitive
- A struct declared
@interiorMutableand@noncopyin lib/core.@interiorMutableis accepted only in lib/core's own files (N0290 elsewhere), and user code may not claim@threadSafefor a type holding one. ARawCell<T>hasT'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 aCellisCell<T>.create(c.get()). RawCell<T>.create(v);c.pointer()(borrow this) returns*mut T, safe to take andunsafeto use;c.getMut()(borrow mut this) returnsborrow mut T;c.intoInner()(this) returnsT.- The rule it changes (amending
ownership.mdB2,unsafe.md§2'sp.writeandPtr.toBorrowMutrows and its pointers made from references, and ADR 0010): theTinside aRawCellmay be read and written throughc.pointer()while shared loans of the cell, or of a value that holds it, are live. Aborrow mutof the cell still excludes every other access (B1). Unsafe code still must not make two liveborrow mut Tfrom the pointer, nor write while aborrow Tmade from it is live. Tmay 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 Tstays aconst T *, which C allows to be written through when the object is notconst; no value that holds aRawCellin its own storage is declaredconst(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<T: Copy>
Cell<T>.create(v);c.get()returns a copy of the contents;c.set(v)storesv;c.replace(v)storesvand returns the old contents; these takeborrow this, so any number of shared loans of the cell coexist with them.c.getMut()(borrow mut this) lendsborrow mut T, andc.intoInner()(this) returns the contents.Tis boundedCopyon the struct (an inherent impl cannot add a bound, §21.1):Cell<Box<u32>>is N0268. A non-Copy value that is replaced or taken through a shared reference is aRefCell<T?>(§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
Thas nodrop, sosetruns no user code. ==and!=. TwoCell<T>compare their contents,a.get() == b.get(), whenThas the operator (numbers,bool, fieldless enums, raw pointers, arrays and cells of them): N0207 otherwise.getis one load andsetone 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<T>: checked shared and exclusive access
RefCell<T>.create(v). Its state is oneCell<isize>beside the contents: 0 free,n > 0readers, −1 writing.c.read()returns aCellRef<T>andc.write()aCellRefMut<T>(borrowis a reserved word, so Rust'sborrow()andborrow_mut()areread()andwrite()). Both takeborrow this. Each guard is a scoped struct over a region of the cell, written likeMutexGuard(§38.3):CellRef'sdata()(borrow this) lendsborrow T,CellRefMut'sdata()(borrow mut this) lendsborrow mut T. Dropping a guard gives the access back.- On conflict it panics with "cell already borrowed":
writewhile any guard is live,readwhile aCellRefMutis (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()andc.tryWrite()return the guard, ornullin the same cases.c.replace(v)storesvand returns the old contents, andc.take(), on aRefCell<T?>only, returns them and leavesnull(lib/core'sRefCellTaking<T>.take). Both panic aswritedoes. The old value is moved out before they return, so itsdropruns in the caller with no guard live, and may use the cell again.c.getMut()(borrow mut this) andc.intoInner()(this) need no check.RefCellhas 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 referencedata()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
iforwhilecondition is its own statement (§14.5): a guard that is one of its temporaries ends once the condition is evaluated, before the body or theelseblock runs, and on every pass of awhile. Soif (c.read().data().isEmpty()) { … } else { c.write()… }does not panic, nor does anifexpression whose branch writes. Amatchscrutinee's temporaries live until the end of thematch(§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 thewhilebody, never in theelseblock. - 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, aBox, aVector, aStableVector, aSlotMap, or a closure's environment. Tasks that cross threads already needSendcaptures, and a guard is notSend. - A
RefCellin a task frame is fine (frames never move,ownership.mdA1).