Reference
Gojo language reference
The rules the compiler implements, section by section, with the tests that cover each one.
This document describes the language that the current compiler implements. It is the precise version of design.md: the design document explains why; this one states the rules. The "Tests" line at the end of each section lists the test files that cover its rules:
run/: runnable programs; the output must match the.outfile of the same name;errors/: must fail to compile with the message written after// error:in the file;panic/: must terminate at run time with exit code 2 and the message written after// panic:;projects/: multi-package projects;testblocks/:gojo test;examples/(at the repository root): example programs, whose output is compared too.
All tests live in tests/ and run with cargo test. cargo test -- --include-ignored also reruns every run/ program under AddressSanitizer / UBSan, and reruns the concurrent ones under ThreadSanitizer. Two other kinds of tests:
fuzz/: inputs (reduced) that fuzzing found and that once crashed the compiler. Each must get a diagnostic or compile successfully;unsafe/: programs written specifically to get around the safety checks (dangling views, views escaping locks and loops, moves hidden indeferand closures, data races…). Each must be rejected. Many of them compiled when they were written, and really fail under AddressSanitizer;nesting_limitsincargo test: programs nested close to the various limits must compile and run; programs far beyond the limits must get an error, not a crash (section 15).
The fuzzing tool is gojo-fuzz (section 15).
1. Lexical structure
- Source files are UTF-8. Comments are
// line commentsand/* block comments */; block comments can span lines. - Automatic semicolons: a statement terminator is inserted when the last token on a line is an identifier, a literal,
true,false,none,),],},return,breakorcontinue. So a line that ends with an operator continues on the next line. Newlines inside parentheses and brackets do not end a statement.;puts several statements on one line. - Leading-dot lines: a line that starts with
.namecontinues the expression on the line before (a method chain written one step per line), unless it is amatcharm, that is, it has a=>outside brackets (.Circle(r) => ...). Comments and blank lines may sit between the steps. A block whose last line is a bare.Variantafter another expression statement therefore needs the variant's type:Color.Red. - Keywords:
use extend pub fn struct enum interface type copy let var if else for in match return break continue defer errdefer go try catch with yield unsafe extern none true false inout sink where test const select scope isolate.self,asandlibare contextual keywords. - Integer literals: decimal,
0x,0b,0o, optionally separated with_. Floats:1.5,2.5e3,1e-3. - Strings
"..."support\n \t \\ \" \$ \xNN \u{...}. Backquoted strings are raw and process no escapes (and no interpolation). - Interpolation:
"x = ${expr}"embeds the value ofexpr, shown asprintshows it;${expr:spec}formats it with afmt.formatspec (${n:>8},${f:.2},${b:08b}), checked at compile time. A string with an interpolation is a newString; one without stays a literal. The expression stays on the string's line, and\${is a literal${. A character literal'é'is arune(ani32code point); when the expected type isu8it is converted to a byte. - An untyped integer literal defaults to
intand a float literal tof64. When there is an expected type, the literal takes it; a value out of range is a compile error. In a binary operation, a side whose value can only be a literal (5,-1.5,(1 + 2), anif/matchwhose branches are all literals,{ 3 }) takes the type of the other side: in(if c { 5 } else { 7 }) +% vthe literals have the type ofv.
Tests: run/lexical, run/patterns, run/modern, errors/int_mismatch, errors/literals, errors/interp_empty, errors/interp_unclosed, errors/modern, errors/chan_renamed.
2. Packages and imports
- A directory is a package. The package name is the directory name; files have no package declaration. The
mainfunction in the entry package is the program entry point and does not needpub. use std/osimports a standard library module;use std/{fmt, os}imports several at once.use ./geomanduse ../util/textimport packages of this project, with paths relative to the directory of the current package. The start of the path names the source:std/is the standard library;./and../are this project. The standard library takes only the namestd, so modules added later will never clash with any package name.use std/osbinds the nameos: code still writesos.readText(...). Writing justuse osis a compile error that suggestsuse std/osinstead.- The default bound name is the last segment of the path;
use ./util/text as tpicks another name. use ./geom.{Vec2, Shape}brings the names directly into scope and does not bindgeom. The imported names must exist and must bepub.- An unused import (including an unused name inside
.{...}) is a compile error. Imports between packages cannot form a cycle. - Only
pubdeclarations can be used by other packages. Functions, types, methods, associated functions and struct fields are all invisible outside the package by default. print,println,eprint,eprintln,assert,assertEq,drop,ok,err,someandnoneare in the prelude and need no import.
2.1 Package manifest gojo.toml
A directory with a gojo.toml is a package. Its subdirectories are its subpackages (except subdirectories that have their own gojo.toml; those are separate packages). Programs without a gojo.toml still run, but they cannot have dependencies. See packages.md for the design and rationale.
[package]
name = "@ann/notes" # lowercase letters, digits, `-`, `_`; may have a scope `@scope/name`
version = "0.3.0" # MAJOR.MINOR.PATCH, default 0.1.0
edition = "2026" # default: the latest edition
exports = [".", "schema"] # subpackages other packages can import; default: only the root "."
[dependencies]
yaml = { path = "../yaml" } # the key is the name code uses in `use`
uuid = { path = "../uuid", package = "@carol/uuid" }
[workspace] # optional: members checked and tested together
members = ["app", "libs/*"]- The form of an import names its source:
std/...is the standard library,./...and../...are subpackages of this package, and a name without a prefix is a dependency from[dependencies](use yaml,use yaml/schema). A dependency can have the same name as a standard library module; onlystdis reserved. - A relative path cannot leave this package (the directory that holds
gojo.toml), and cannot point into another package that has agojo.toml: such a package must go in[dependencies]. A subpackage of a dependency must be listed in that dependency'sexports. - Only
pathdependencies are supported for now.version(registry) andgitdependencies are recognized, but report an error saying they are not available yet. - A directory that several packages depend on is compiled once (diamond dependencies).
gojo.tomlis a strict subset of TOML (tables, strings, integers, booleans, arrays, inline tables). Unknown keys, misspelled names and paths that don't exist are errors with a location; they are never ignored.gojo init [name]writes agojo.tomlin the current directory. In a workspace root without[package],gojo checkandgojo testprocess each member in turn, andgojo run/buildrequire a member to be named.gojo doc yaml/schemashows the API of a dependency.
Tests: projects/app, projects/cycle, projects/private, projects/groups, packages/*, errors/bad_use, errors/bad_use_names, errors/unused, errors/use_groups.
3. Declarations
3.1 Variables and constants
let x = eis immutable;var x = eis mutable. A type annotation follows a colon:let n: u8 = 1. A string literal is astrview unless aStringis expected, so a string to build on is declaredvar s: String = "".var x: TrequiresTto satisfydefault(section 4.6); its value is the default value.- Assigning to a
letbinding is a compile error. - No shadowing within a function: an inner block cannot declare a variable with the same name as a local variable of an outer block.
- A local variable that is declared but never used is a compile error. Names that start with
_, and_itself, are exempt. const Name = constant expression. It can refer to other constants. Integer constants can be used as array lengths.- The order of package-level declarations (functions, types, aliases, constants, values) does not matter; a declaration can use declarations written after it. An alias or constant that refers to itself, directly or indirectly, is a compile error (recursive types need a struct or enum).
- Package-level values:
let name = e(orlet name: T = e) at the top level of a file,pub letto export it.eis any expression; it is computed once, on first use, by whichever task gets there first (the others wait), so a program pays only for the values it uses. The type must besendand can't be or contain a view: every task can see the value. It is read-only: no assignment, noinoutborrow, no move out of it; state that changes goes inside, in aMutex,RwLockorAtomic(let hits = Atomic(0)). A value whose computation needs itself panics:the value of x depends on itself. There is no top-levelvar.
Tests: run/basics, run/decls, run/forward_decls, run/globals, errors/let_immutable, errors/shadowing, errors/unused, errors/constraints, errors/decl_cycles, errors/const_cycle, errors/globals, errors/global_move, panic/global_cycle.
3.2 Type declarations
struct,enumandinterfacedeclarations start with the kind, optionally preceded bypubandcopy.type A = Bis an alias, identical toB.type Celsius f64is a newtype: it is incompatible with its underlying type; convert explicitly withCelsius(x)andf64(c).- A newtype whose underlying type is a number keeps the number's operators (
+ - * / %,+% -% *%, bitwise operators for integers, unary-, comparisons). Both sides must be the same newtype, and the result is that newtype. A newtype satisfiesordered,defaultandhashwhen its underlying type does. Other newtypes (such astype Name String) have only comparisons, no operators. - Struct embedding: a field written as a bare type name (
struct Admin { Named }) promotes the fields and methods of the embedded type to the outer type. - A recursive type must go through an indirection (
Box,Vec,Rc, etc.); otherwise its size is infinite, which is a compile error.
Tests: run/decls, run/constraints, errors/infinite_type, errors/constraints.
3.3 Functions and methods
fn name(a: A, b: B) -> R. Every parameter has its ownname: Type; a mode goes before the type (u: inout User,x: sink T, section 5.2). Without->the function returns unit;fn save(c: Config) ! Ereturns nothing but may fail. Function types are written the same way:fn(int, inout Buf) -> bool.- Methods are written inside the braces of a
struct/enum. Their first parameter isself, which can have a mode:self,inout self,sink self(section 5.2). A function withoutselfis an associated function, called through the type:Point.new(),Stack[String].new(); type parameters can be inferred from the expected type. extend T { ... }adds methods to a type, and can target a specific instance (extend []String). Methods can only be defined in the package that declares the type.- A field and a method, or a method and an associated function, cannot have the same name.
selfcan only appear in methods. - Receivers take no
&:v.push(x)takes a mutable borrow automatically for ainout selfmethod. - Constructors: when a struct or enum has an associated function
new, the type can be called like a function:T(args)isT.new(args)(RwLock(m),Atomic(0),Stack[int](), orStack()when the type is expected). For a newtype,T(x)is the conversion (3.2). - A function body, like any block, has the value of its last expression:
fn area(self) -> f64 { self.w * self.h }.returnis for leaving early.
Tests: run/basics, run/generics, run/interfaces, errors/methods, errors/assoc_calls, errors/self_outside_method, errors/dup_members.
4. Types
4.1 Built-in types
| Type | Description |
|---|---|
bool, int/i8…i64, uint/u8…u64, f32, f64, byte (= u8), rune (= i32) |
scalars, copy |
String / str |
owned UTF-8 string / borrowed view (String(bytes) and file reads don't check the encoding; invalid bytes decode as U+FFFD) |
Vec[T] / []T |
owned dynamic array / borrowed slice view (read-only unless inout, 5.2) |
[N]T |
fixed-length array; N is an integer constant or an integer type parameter |
Map[K, V] |
hash table; iteration order is insertion order |
(A, B) … |
a tuple of 2 to 6 values (section 4.7) |
?T |
none or a T |
T ! E, ! E |
a success value T or an error E; ! E has no success value |
Box[T] |
exclusive ownership on the heap |
Rc[T], Arc[T], Weak[T] |
non-atomic / atomic reference counting, weak reference |
Cell[T], Mutex[T], MutexRef[T] |
single-threaded interior mutability; lock; lock that borrows outside data (section 10.1) |
Chan[T], Sender[T], Receiver[T] |
a channel and its two ends (section 10.3) |
Task[T], Scope |
a task handle and a scope handle; views (section 10.1) |
Atomic[T], Iso[T] |
an atomic integer; an isolated value (section 10.5) |
fn(A) -> R |
functions and closures, never null |
*T |
raw pointer, can be null |
Span |
copy struct { lo int, hi int }, a range within a string |
- There is no
nil. Only?Tcan be empty. - There are no implicit conversions between integer types, or between integers and floats; convert explicitly with
i64(x),f64(n). A narrowing conversion panics when the value is out of range. ?Box[T],?Rc[T],?Arc[T]and?Weak[T]are the size of one pointer;noneis the null pointer.
Tests: run/basics, run/niche, run/spans, errors/no_nil, errors/int_mismatch, panic/conversion.
4.2 Structs
- Fields can have default values:
port int = 8080. A?Tfield defaults tonone. Leaving out a field without a default in a literal is a compile error. Point{x, y}sets each field from the variable of the same name (Point{x: x, y: y}); it can be mixed withname: value..{x: 1, y: 2}is a literal of the struct the context expects (a parameter, a field, areturn, a typedlet, an element pushed into aVec[T],?Tof a struct), as.Variantis for enums:serve(ln, h, .{deadline: 5 * time.second}). Without an expected struct type it is an error.copy structdeclares a bitwise-copied type. All its fields must be copy, and the type cannot have adropmethod.- copy inference: structs and enums that are private to the package, not generic, have only copy fields and have no
dropmethod are copy automatically. Apubtype must writecopyexplicitly. - A type gets
==automatically when all its fields are comparable; it can be aMapkey when all its fields are hashable; it gets.clone()automatically when all its fields can be cloned. Definingequal,hash,compareorclonemethods replaces the generated implementations. A type that definesequalbut nothashcannot be a key. These methods have fixed signatures, and a wrong signature is a compile error:fn equal(self, other: T) -> bool,fn compare(self, other: T) -> Ordering,fn hash(self, h: inout Hasher),fn clone(self) -> T,fn drop(inout self).fn toString(self) -> String(orfn message(self) -> String) decides howprintshows the type; a method of the same name with a different signature is just an ordinary method. - Struct fields cannot have view types (
str,[]T,MutexRef).
Tests: run/basics, run/copyinfer, run/interfaces, errors/bad_copy, errors/copy_exported, errors/missing_field, errors/view_field, errors/protocol_methods.
4.3 Enums
- Variants can have named fields,
Circle(r f64), or none,enum Level { Debug, Info }. - When the expected type is known, you can write
.Warn,.Circle(1.0). - Enums without data satisfy
ordered(in declaration order) and can be converted to integers.
Tests: run/basics, run/generics, run/errors, run/patterns, errors/non_exhaustive.
4.4 Function types and closures
fn(int) -> int. Closure literals arex => x * 2,(x, y) => { ... }and(x int) => ...; parameter and return types are inferred from the expected type. You can also writefn(x: int) -> int { ... }. In amatchguard and the head of aselectarm,name =>is the arm's arrow; a closure there goes in parentheses.- Section 5.6 specifies how closures capture variables.
Tests: run/basics, run/generics, run/edge, run/patterns.
4.5 Interfaces
- Structural and implicitly satisfied: a type satisfies an interface when it has all the methods the interface requires (with the same names, parameters and
selfmodes). Exception: when the interface requiresinout self, a read-onlyselfmethod also satisfies it (whatever can be borrowed for writing can be borrowed for reading). So the read-onlyreadofos.Fileandnet.Connsatisfiesio.Reader. - A borrowed interface parameter (
r Reader,r: inout Reader) is a data pointer plus a method table and allocates no memory. - An interface value (a variable, field or collection element) is an exclusive box on the heap plus a method table; dropping it drops the value inside. An interface value is never empty; write
?Readerfor an optional one. match r { f File => ..., _ => ... }branches on the concrete type.- Interfaces can embed other interfaces. Methods with type parameters and conditional methods do not count toward the method set.
- Type sets:
interface Num { ~int | ~f64 }.~Talso accepts newtypes whose underlying type isT. An interface with a type set can only be used as a constraint, not as the type of a value. erroris a built-in interface with the single methodfn message(self) -> String.
Tests: run/interfaces, run/iface_modes, run/generics, run/constraints, errors/constraints, errors/rules.
4.6 Capabilities
Capabilities are built-in constraints that the compiler decides:
| Capability | Types that satisfy it |
|---|---|
copy |
scalars, Span, copy structs and enums, fixed-length arrays of copy elements |
clone |
copy types; String, Rc, Arc, Chan; containers whose elements can be cloned; types with a clone method or whose fields can all be cloned |
default |
scalars, String, Vec, Map, ?T; fixed-length arrays whose elements have a default; structs whose fields all have defaults; newtypes whose underlying type has a default |
send |
types that contain no Rc, Weak, Cell, raw pointer or function value (derived from the structure); interface values don't satisfy it for now (conservative) |
comparable |
built-in comparable types; types with an equal method or whose fields are all comparable |
ordered |
numbers, strings, bool, enums without data, types with a compare method, newtypes whose underlying type is ordered |
hash |
integers, strings, bool; types with a hash method or whose fields are all hashable |
Tests: run/generics, run/interfaces, run/constraints, errors/unordered_sort, errors/send_rc, errors/bad_copy, errors/rules.
4.7 Tuples
(A, B)is the type of a pair, up to six elements:(int, String, bool).(a, b)builds one; the element types come from the expected type, or from the elements (a string literal element is aString, as in a list).(x)is justxin parentheses, and()is unit.t.0,t.1read and assign elements (p.1.0 = 2), and can be moved out of an owned tuple like struct fields.- A tuple is copy when all its elements are, comparable and hashable when they are (so it can be a
Mapkey), andclonewhen they are. It prints as(1, "a"). Like a struct, it can't hold a view. - Patterns:
(p, q)matches the elements (match pair { (0, _) => ..., (x, y) if x == y => ... }), counts toward exhaustiveness, and works inif let, also through an optional:if let (k, v) = parse(s)whenparsereturns?(String, int). let (a, b) = eandvar (a, b) = ebind the elements (nested tuples and_too). The tuple is kept in a hidden variable, and each name takes its element aslet a = t.0would: moved, copied, or, wheneis a borrowed place, borrowed.- A tuple is a prelude struct (
Tuple2[A, B]with fields_0,_1, …), so it costs the same as a struct with those fields.
Tests: run/tuples, errors/tuples, errors/tuples_views, errors/tuples_moves.
5. Ownership
5.1 move and copy
- Every value has exactly one owner. Assigning, passing to a
sinkparameter, returning, storing in a collection and sending on a channel all transfer ownership; using the original variable afterwards is a compile error. - Copy types are copied bitwise and never moved. Other types are copied explicitly with
.clone(). - A variable that is moved on only some branches cannot be used afterwards: that is a "may have been moved" compile error. The compiler inserts a flag for dropping it.
- A loop body cannot move a variable from outside the loop (the next iteration would move it again). Likewise, a view assigned inside a loop to a view variable from outside the loop is still there when the next iteration starts: if the next iteration may read it before reassigning it (or the loop may exit), the borrowed value cannot be modified in the next iteration.
- A single field of a struct can be moved (a partial move). After that the struct cannot be used as a whole; the fields that were not moved are dropped as usual.
- You cannot move out of a borrowed place:
return v[0](wherevis a borrowed parameter) is a compile error; write.clone().
Tests: run/ownership, errors/use_after_move, errors/maybe_moved, errors/move_in_loop, errors/partial_move, errors/move_out_of_borrow.
5.2 Parameter modes
| Mode | Signature | Call site | Rules |
|---|---|---|---|
| read-only borrow (default) | fn show(u: User) |
show(u) |
the function cannot modify it or move it away |
| mutable borrow | fn rename(u: inout User) |
rename(&u) |
the argument must be a var place; the call site must write & |
| transfer | fn store(u: sink User) |
store(u) |
the argument is moved by the call |
- Exclusivity: at any moment a value has either any number of read-only borrows or exactly one mutable borrow.
both(&a, &a)andmixed(a, &a)are compile errors. - Slices: a
[]Tis a read-only view of elements, even in avar(other views of the same elements may exist). Only aninout []Tparameter can change them:v[i] = x,for x in &v,v.sort(),v.swap(i, j),v.reverse(),v.sortBy(less). Its argument is&xsor&xs[a..b]of avarVecor array, or anotherinoutslice (&v,&v[a..b]);xs[a..b].sort()works the same way. Aninoutslice can be re-sliced (v = v[1..]) but not pointed at other elements (5.4). - Ranges: a mutable borrow of
xs[a..b]covers only those elements when the checker can compare its bounds: integer constants, and immutable locals (let, read-only parameters) plus or minus a constant, with an omitted bound meaning the start or the end.split(&xs[..m], &xs[m..])is allowed, and so are two tasks holding&v[..p]and&v[p + 1..](10.1). Bounds that arevars, other expressions, or locals of a different loop iteration count as the whole value.
Tests: run/ownership, run/slices_mut, errors/borrowed_param_mutation, errors/inout_needs_amp, errors/exclusivity, errors/inout_param_invalidation, errors/slices_mut, errors/slices_tasks, unsafe/w7_*.
5.3 Dropping
- A value is dropped when its owner goes out of scope. Variables in the same scope are dropped in reverse order of declaration; struct fields are dropped in declaration order.
- A
fn drop(inout self)method is called when the value is dropped. A type with adropmethod cannot be copy. drop(x)drops early. It is an ordinarysinkfunction.defer stmtruns when the current block ends; several defers run in reverse order.errdefer stmtruns only when the function returns an error, and can only be used in functions that return a result or an optional value.break,continue,returnandtryalso run drops and defers when they leave a scope. A move inside deferred code happens where that code runs (at the end of the block, or at thebreak/continue/returnthat leaves the block), and is evaluated in execution order (reverse order): two defers that both move the same value, and a use after the block of a value moved in a defer, are compile errors.errdeferruns only when returning an error (ornone), or whentrypropagates an error.- Deferred code cannot leave itself: it cannot contain
return,tryoryield, and cannotbreak/continuean outer loop. Its own loops and closures are not affected. - Temporaries in a statement (for example the
VecinString(try readBytes())) live until the statement is done with them.
Tests: run/ownership, run/errdefer, run/labels, run/push_flow, run/temporaries, run/defer_moves, errors/defer_exit, errors/rules, errors/defer_moves.
5.4 Views and projections
- The view types
str,[]TandMutexRef[T], and references lent by projections, cannot be stored: they cannot be struct fields, collection elements or captures of escaping closures, and cannot be returned from functions (except through theyieldof a projection). - A projection function lends a reference with
yield:yield &T,yield inout T,yield ?&T.v[i],m.get(k),m.entry(k)andmu.lock()are all projections. let x = v[i]: when the element is copy it is copied out; otherwisexis a reference. To own the value, write.clone().if let u = m.get(k) { }andwith e = m.entry(k) { }bind the reference to the block; the reference cannot be taken out of the block. The block cannot modify the borrowed container. Awithbody runs exactly once, so a function can end with awithwhose body returns. Awithis an expression with its body's value (let n = with v = m.lock() { v.len() }, or the last expression of a function); that value can't be a view (what the block borrows, such as locked data, is reachable only inside it), and moving out of the borrowed value is an error as anywhere else.- Last use: a view or reference bound with
letstops holding the borrowed value after its last use in the block; after that the original value can be modified. - A view cannot outlive the value it borrows. You cannot take a view of a temporary and keep it (
let s: str = make()). - While a view exists, the borrowed
Vec/Stringcannot be modified, sopushnever leaves a view dangling. - Views live only in variables (possibly inside a variable's
?T/T ! E). No value can exist that puts a view into any other storage, whether its type is written or inferred:[s],Rc(s),Box(s),[2]str,Map[int, str],Vec[MutexRef[T]], and putting a view into an interface value (xs.push(s)wherexsis aVec[Len]). - Assigning to a view variable:
v = s.asStr()requires the borrowed value (s, or the final owner thatsborrows from) to live at least as long asv: it must be declared in an outer block, or in the same block beforev(values in the same block are dropped in reverse order of declaration, so the later one is dropped first, and adeferin between would see it already gone). After that, the borrowed value cannot be modified anywhere inv's scope (up to its last use).let v = { let s = ...; s.asStr() }is rejected for the same reason. - An
inoutview parameter can only be assigned a subview of itself (rest = rest[1..]) or a literal: the caller cannot see a new borrow. A closure cannot assign a view to a captured outer variable. - A view returned by a call is treated as borrowing all the arguments, including variables captured by function-value arguments (
run(() => s.asStr())borrowss;f()borrows the closurefitself) and values passed as interfaces. A reference lent by a projection function written in Gojo is treated as borrowing the parameters that itsyieldactually points into. - A reference into a temporary bound by
if let x = f()ormatch f() { }is valid only inside thatmatch; views of it cannot be taken out. - A function's
deferruns after the return value has been computed, so it cannot modify data that a returned view points to. - Data inside a lock: a view obtained inside
with s = mu.lock() { }(including locks fromMutex(&x)) cannot leave the block, cannot be assigned to a variable outside the block, and cannot be returned: the lock is released when the block ends, and other tasks may then modify the data. - Generics and views: if a generic function keeps a value of its type parameter
Tafter the call ends (captured by an escaping closure or by a task started through aScopevalue, or passed to another generic function that keeps it), thenTcannot be a view type at the call.
Tests: run/lastuse, run/generics, run/spans, run/mutexref, errors/view_field, errors/view_return, errors/view_temporary, errors/view_outlives_owner, errors/views_in_containers, errors/escaping_view, errors/projection_borrow, errors/mutable_ref_conflict, errors/iterator_invalidation, errors/move_while_borrowed, errors/view_assign_scope, errors/view_lock_escape, errors/stored_views, errors/generic_keeps_view, errors/closure_view, errors/defer_returned_view, errors/temp_and_iface_views.
5.5 Shared ownership
Rc(x)andArc(x)create reference-counted values that are read-only inside; to modify them, wrap the contents inCellorMutex..clone()increments the count.Weak[T]comes fromRc.downgrade();upgrade()returns?Rc[T]. The programmer breaks cycles withWeak.Arena[T]with generationalId[T]: accessing through a deleted Id returnsnone.
Tests: run/misc, run/interfaces, run/niche, run/arena.
5.6 Closure capture
- A non-escaping closure (passed as an ordinary parameter) borrows outer variables and can modify
varvariables. - An escaping closure (a
sink fn()parameter, a stored closure, one given togo) owns its captured variables: copy types are copied, other types are moved into the closure. Views cannot be captured by escaping closures. - A closure given to
gocannot modify outer variables.
Tests: run/basics, run/concurrency, errors/escaping_view, errors/go_closure_mutation.
6. Expressions
- Operator precedence from lowest to highest:
??(right-associative),||,&&, comparisons,|,^,&,<< >>,+ - +% -%,* / % *%, then unary- ! ~ & <- *and postfix.f,?.f,(...),[...]. - Both sides must have the same type; there is no implicit promotion.
+concatenates strings. + - *panic on integer overflow (in both debug and release);+% -% *%wrap./and%panic on division by zero. Out-of-bounds indexing panics.<<and>>panic when the shift amount is not between0and the bit width minus one.- A numeric conversion
T(x)uses the target type only to decide the type of literals (u64(18446744073709551615),u64(1 << 63)); other arguments are converted from their own type (whenbis au8,int(~b)first complements onu8). if,matchand blocks are expressions; their value is the last expression.- Operations on
?T:if let v = x { }(andif let x { }, which unwraps the variablexunder the same name inside the block),x ?? d,x?.field(the result is still optional, andfieldmust be a copy type; otherwise useif let),x?.method(args)(nonewhenxisnone; a method that returns?Ugives?U, not??U; the method may only read its receiver, and a view it returns borrowsx, soxmust be a variable to keep it),x.unwrap()(panics onnone),match. [a, b]is aVecliteral, or aSetwhere aSetis expected (let s: Set[String] = ["a", "b"], a field, a parameter).["a": 1, "b": 2]is aMapliteral and[:]an empty one, typed by the context or by the entries.Map[K, V]{k: v}andVec[T]{}are typed literals. String literals in a list or map areStrings.- List elements: besides values, a list literal can hold
if cond { x }(noelse:xis added whencondholds; alsoif let),for x in xs { f(x) }(one element per iteration) and..v(every element of theVecv, which is moved;..v.clone()keeps it). The block of anif/forelement may have statements before its value,continueskips an iteration, and the value may itself be anifwithoutelseor afor:[for r in rows { for c in r.cells { if c.shown { c.label.clone() } } }]. It costs what the pushes would: the list is built in a hiddenVec.if c { a } else { b }is an ordinary element. a..bis a half-open range anda..=ba closed range;s[a..b]takes a view of a string or slice.- A
T ! Eresult must be handled; discard it with_ = f().
Tests: run/basics, run/edge, run/misc, run/constraints, run/patterns, panic/overflow, panic/div_zero, panic/bounds, panic/shift, panic/unwrap_none, errors/unhandled_result, errors/rules.
7. Statements and control flow
if c { } else if d { } else { }; the condition must be abool.for x in xs,for i, x in xs(index and element),for i in 0..n,for k, v in m,for x in ch,while cond { },loop { }.for x in vborrows each element;for x in &viterates mutably, andvmust be avar. The collection being iterated cannot be modified during the iteration.- Labels:
outer: for ...(orouter: while,outer: loop);break outerandcontinue outerjump to that loop. A label cannot have the same name as the label of an enclosing loop; the label of abreakmust belong to an enclosing loop. Abreakinside aselectarm applies to the enclosing loop. matchis exhaustive. Patterns: literals (including negative ones), ranges1..=9, alternatives with|,_, bindings, enum variants (can nest; every field must be written),ok(x)/err(e),some(x)/none, tuples(a, b), and the concrete type of an interface value,f: File. An arm can have a guardif cond; guarded arms do not count toward the exhaustiveness check.let ... else:let pattern = e else { ... }matcheseagainst a pattern that may fail (let .Circle(r) = s else { return 0 },let ok(v) = r else { ... },let (k, 0) = pair else { ... });let x = opt else { ... }unwraps an optional. Theelseblock must leave (return,break,continue, a panic); the names it binds are in scope for the rest of the block. It is the same asmatch e { pattern => { rest of the block }, _ => { else } }, with the same binding rules.- When the matched value is a place, bindings are references by default. When an arm moves a binding away, that arm consumes the matched value.
- There is no
switch.
Tests: run/basics, run/labels, run/edge, run/misc, run/patterns, run/loop_views, errors/labels, errors/non_exhaustive, errors/iterator_invalidation, errors/iter_mutation, errors/iter_let, errors/rules, errors/loop_exits, errors/loop_carried_view.
7.1 Custom iteration
For a custom type, for x in t looks for a protocol in this order:
- Cursor (pull):
fn cursor(self) -> Candfn next(self, c: inout C) yield ?&T(or one that returns an owned?T, used as a generator).for x in &tusesnextMut. Theforis translated into "get a cursor, then callnextin a loop", with no closure. - Push:
fn each(self, body: fn(&T) -> Flow);for x in &tuseseachMut(inout self, body: fn(inout T) -> Flow). The loop body becomes a non-escaping closure; thebreak,continue,return,tryand labeled jumps in it are rewritten into control signals. WheneachreceivesFlow.Breakit should return as soon as possible; even if it keeps calling, the loop body does not run again, so a wrongeachonly runs a few extra rounds and cannot change the program's result. The loop body runs many times, so it cannot move outer variables.
When a type provides both protocols, the cursor is used. A type that satisfies neither protocol cannot be used in for. The value being iterated cannot be modified during the iteration.
Whether a view of an element can outlive the current iteration: with the cursor protocol, it can when next lends a part of self (as when iterating a Vec; the iterated value then cannot be modified); it cannot when next lends data in the cursor, because the next step changes the cursor. With the push protocol it cannot, because each may pass the body a value that is valid only for one call.
Tests: run/iterators, run/iterator_views, run/constraints, run/push_flow, errors/iter_misuse, errors/iter_mutation, errors/iterator_views.
8. Error handling
fn f() -> T ! E;return err(e),return x(wrapped as ok automatically).try e: on error, returns the error to the caller. The error types must be the same, or the outer error type must beerror; otherwise writetry e else Wrap, whereWrapis a function or enum variant that converts the inner error to the outer error.trycan only be used in functions that return a result or an optional value; on a?Tit propagatesnone.e catch err { block }: handles the error in place; the block's value is the value of the whole expression.match r { ok(v) => ..., err(.Kind(x)) => ... }.x.unwrap()panics on error.- In a function that returns
?T ! E,return noneis a successfulnone(wrapped as ok). When the expected type of a.Variantshorthand isE ! Error?E, it resolves againstE. - Error types: each module uses its own concrete error type, and they all satisfy the built-in interface
error(fn message(self) -> String), so a function written with! errorcantryerrors from any module. Errors from the operating system, files, processes and the network are allio.Error:kind(anio.ErrorKind:NotFound,PermissionDenied,AlreadyExists,Cancelled, etc., independent of the platform),message(),code(the system error number).os.Errorandnet.Errorare aliases of it. Errors of other modules carry their own information, such as the line and column ofjson.Error, ortime.ParseError.
A panic signals a bug in the program. A panic prints a message and a source location (pointing into user code, not the standard library) and terminates the whole process with exit code 2. It does not unwind the stack and does not run defers. There is no recover.
Tests: run/errors, run/errdefer, run/patterns, errors/try_error_mismatch, errors/try_outside_result, errors/unhandled_result, panic/unwrap_err, panic/unwrap_none, panic/no_defer, testblocks/blocks.
9. Generics
- Generic functions, structs and enums:
fn max[T: ordered](xs: []T) -> ?T,struct Stack[T]. Constraints are written next to the parameter, or withwhere T: copy + ordered. - Methods can have their own type parameters. A
whereon a method makes it a conditional method, which exists only when the condition holds. Calling a method whose condition does not hold is a compile error. - Type parameters are inferred locally at the call site (including closure return types). When they cannot be inferred, write them out:
fresh[int](). - A generic body can only use operations its constraints guarantee: call methods declared by an interface constraint, copy only with
copy, compare only withordered, do arithmetic only when the type set is all numbers. - Integer const generics:
struct Mat[R: int, C: int] { data: [R][C]f64 }. Only integer parameters are allowed, and there is no arithmetic at the type level. Mismatched dimensions are a compile error. - Implementations are monomorphized per type argument.
Tests: run/generics, run/constgen, run/constraints, errors/constgen, errors/constraints, errors/unordered_sort.
10. Concurrency
10.1 Scopes and tasks
scope { body }is an expression; its value is the value ofbody.go f(x)orgo () => { ... }inside it starts a task of the innermostscopeof the same function or closure.gooutside any scope is an error, and so is agoinside a closure whose scope is outside the closure (the closure needs its ownscope, or aScopevalue, below). The bodies ofmainand oftestblocks are scopes.- A
scopeends by waiting for every task started in it, before the values declared in its body are dropped. Leaving the body early (return,try,break,continue) first cancels its tasks (10.6), then waits. goas a statement starts a task whose result is not used.let t = go f()gives a handle of typeTask[R], whereRis what the task returns;Rcan't be a view. A handle is a view: it can't be stored, sent or returned out of the scope. On every path that reaches the end of its block it must be consumed, byt.join()(waits, then gives the result),t.cancel()(cancels just that task; the scope still waits for it, and drops its result), or aselectarm (10.4); otherwisetask t is never joined. A handle dropped when its block is left early cancels its task.- Arguments. In
go f(a, b), an argument that is a place (x,x.f,x[i],&x) is used by the task where it is: borrowed, or, for asinkparameter, moved into the task right away. Any other argument is evaluated before the task starts, and the task owns the result. - Captures. A task borrows the variables it uses that live until the scope ends: those declared outside the scope, and those declared directly in the scope's body (not in a block inside it). They stay borrowed until the scope ends, so the body can't modify them (or, if the task modifies them, use them) or move them meanwhile. A task may modify a variable that nothing else uses until then; two tasks can't both use a variable one of them modifies, and neither can the tasks a loop starts in different iterations. A task borrows only the fields it uses (
ch.rxleavesch.txfree), and only the range of elements it uses when that range is one the checker can compare (5.2):go psort(&v[..p])andgo psort(&v[p + 1..])run at once. Variables declared in blocks inside the body are moved (or copied) into the task, which then owns them; it can't modify a copied one. Everything a task captures must besend. - Failure. A task whose result type is
T ! Efails when it returnserr. A failure cancels the scope (its other tasks and its body). The first error of a task started with a plaingobecomes the scope's value: the scope then has typeB ! E(Bis the body's type), and is usually writtentry scope { }; all plain tasks of one scope must fail with the sameE. The error of a task with a handle goes to whoever joins it. Inmainand in tests, an error of a plain task is the function's error (it must return! E). scope s { }bindss, of typeScope(a view; copy).s.cancel()cancels every task of the scope and its body. A function can take aScopeparameter and start tasks withs.go(() => ...)(ors.go(f(x))); such a task may outlive the function, so it must own everything it captures, can't fail, and keeps any type parameter it captures (which then can't be a view at the call, 5.4).
Tests: run/scopes, run/task_move, run/concurrency, run/mutexref, run/cancel, errors/task_capture, errors/task_moved, errors/task_race, errors/task_shared_mutation, errors/task_never_joined, errors/task_results, errors/go_needs_scope, errors/scope_handle_escape, errors/scope_handle_chan, errors/scope_handle_field, errors/scope_value_go, errors/generic_keeps_view, unsafe/w1_*, unsafe/w3_*, unsafe/w6_*.
10.2 Data parallelism
par for x in &v { }(avarVecor array, aninoutslice, or a range of one),par for x in v(also any slice),par for i, x in &vandpar for i in a..brun the iterations on up tomaxProcstasks, in chunks handed out as tasks finish them. With&vthe body may modify its element. It may only read what it captures, which must besend.continueends an iteration;break,returnandtryare errors. The statement ends when every iteration has.send: fn(A) -> Ris the type of a function that any number of tasks may call at once. A closure is asend fnwhen everything it captures issendand it only reads it; functions declared withfnaresend fn. Asend fncan be used where afnis expected, not the other way round. Asend fnissend.task.map(xs, limit, f)callsfon each element in up tolimittasks at once (0: no limit) and gives the results in order;task.tryMapdoes the same forfreturningR ! E, and stops at the first error;task.eachgives no results;task.race(fs)runs the functions at once, gives the first success and cancels the rest (all fail: the last error).
Tests: run/parallel, errors/par_for, errors/send_fn.
10.3 Channels
Chan[T](n)makes a channel with room fornvalues (Chan[T]()andChan[T](0): none, so a send waits for a receiver). Its value is aChan[T]with two fields,tx Sender[T]andrx Receiver[T].Tmust besendand can't be a view.tx <- vsendsv(moving it), waiting while the buffer is full.tx.send(v)does the same and givesfalsewhen the value was not sent: every receiver is gone, or the task was cancelled; the value is dropped then.<-rxgives?T:noneonce every sender is gone and the buffer is empty (or when the task is cancelled).for x in rxreceives until then. Both ends can be cloned for more senders or receivers;.len()is the number of values in the buffer.- Closing. A channel closes when its last
Senderis dropped (drop(ch.tx)closes it early). When its lastReceiveris dropped, the values in the buffer are dropped, and sends fail. There is noclosemethod; nothing about channels panics.
Tests: run/concurrency, run/sched, run/niche, run/labels, errors/send_rc, unsafe/w1_chan_view, unsafe/w6_chan_of_rc, unsafe/w1_send_then_use.
10.4 select
- Arms:
let x = <-rx => ...(xis?T),<-rx => ...,tx <- v => ...,let r = <-t => ...(a task handle in a variable: the task has finished; the arm takes its result and the handle),time.after(d) => ...(dafter theselectstarts),time.at(t) => ...(at the instantt),cancelled => ...(the task has been cancelled) and_ => ...(nothing is ready: don't wait). Any arm can have a guard,... if cond =>; guards, channels, values and times are evaluated once, before waiting, and an arm whose guard is false takes no part. At most one_and onecancelledarm.time.afterandtime.atexist only as arms. - When several arms are ready, one is chosen at random; a
cancelledarm wins as soon as the task is cancelled. A send arm whose receivers are all gone is never chosen. The value of a send arm that is not chosen is dropped. Without acancelledarm, in a cancelled task the receive arms see their channels as closed, and with only send arms nothing runs.break/continuein an arm apply to the enclosing loop.
Tests: run/select, run/concurrency, run/sched, errors/select_arms, errors/select_v2, errors/select_task_join, errors/loop_exits, unsafe/w1_move_in_select, unsafe/w2_select_arm_view_escape, unsafe/w3_select_send_view_chan, unsafe/w5_move_in_select_loop.
10.5 Sharing
-
Mutex(x)owns its data;Arc(Mutex(x))is for long-term sharing.with v = mu.lock() { }locks; the lock is released when the block ends, and the reference can't leave the block.Mutex(&x)makes aMutexRef[T], a lock that borrowsx(avar); while it existsxis reachable only through it. -
with a, b = lock(m1, m2) { }takes two or more locks at once, in one order everywhere (by address), so code that always takes several locks this way can't deadlock on them. The same mutex twice is an error, or a panic when only known at run time. -
No waiting under a lock. A send, receive,
select,time.sleep,joinorscopewritten directly inside awith ... lock()body is an error: the tasks waiting for the lock would wait too. (Waits inside called functions are found at run time, 10.7.) -
RwLock(x)lets many tasks read at once, or one write:with v = rw.read() { }gives a read-only reference,with v = rw.write() { }a mutable one; neither can leave its block, and the rules for waiting under a lock apply to both. Waiters are served in order, a run of readers together, so a stream of readers can't starve a writer. -
Atomic[T], for integer types:Atomic(x),load,store,addandsub(wrapping; they give the new value),swap(gives the old one),compareSwap(old, new)andclone. Every operation is atomic and sequentially consistent; tasks share anAtomicby borrowing it. -
send interface Name { }is an interface that onlysendtypes implement, so its values aresend; a type parameter bounded by it issend. -
isolate { body }has typeIso[T](Tis the type ofbody, not a view). The body may use variables from outside only if their types aresend, so nothing outside points into what it builds.Iso[T]issendwhateverTis (it may holdRcandCell), can't be cloned, andiso.take()gives back theT. -
task.yieldNow()lets other tasks run. -
Task-local values:
let key = task.local[T]()(at package level) makes a key;task.with(key, value, () => body)binds it whilebodyruns, andkey.get()gives a copy (?T;Tmust besendand cloneable) in that code and in every task started inside it, through any number of scopes and shields. An innertask.withof the same key hides the outer one until it ends. Nothing else can change a binding, so tasks never race on one.
Tests: run/sharing, run/isolate, run/mutexref, run/rwlock, run/task_locals, errors/rwlock, errors/wait_under_lock, errors/send_interface, errors/isolate, errors/isolate_clone, errors/mutexref, errors/mutexref_borrow, errors/mutexref_immutable, panic/deadlock_locks.
10.6 Cancellation
- Cancellation propagates down the task tree: when a task fails,
s.cancel()ort.cancel()is called, a scope is left early, or the deadline of atask.timeoutpasses, every task below that scope, nested scopes included, is cancelled. A scope still waits for all its tasks, because they may be borrowing data from outside it. - In a cancelled task, blocking operations return early: a receive gets
none; a send does not happen and the value is dropped;selectbehaves as in 10.4;time.sleepwakes early;netreads and writes,acceptanddialreturn anet.ErrorwhoseisCancelled()is true. Locking andjoinare not cancelled. task.cancelled()reports whether the current task has been cancelled, andtask.reason()why:?task.Reason, one of.Failed(a task of the scope failed),.Deadline,.Explicit(cancel(), or a scope left early) and.Shutdown.try task.check()returnstask.Cancelled{reason}when it has, as a checkpoint in long computations. Cancellation does not interrupt code that doesn't block.task.timeout(d, () => body)returns?T:someif the body finishes within the deadline,noneif the deadline passes or the caller is cancelled. Deadlines nest;task.deadline()gives the nearest one of the current task (?int, on thetime.now()scale).task.shield(d, () => body)runs the body shielded: cancelling the caller doesn't affect it, only the shield's own deadlineddoes. A type'sdropmethod runs shielded the same way when its task is cancelled, for up to a second (GOJO_DROP_GRACE, in milliseconds), so cleanup that has to block still happens.- Ctrl-C (SIGINT) and SIGTERM cancel the scope of
main, with reason.Shutdown; the program then ends as usual (tasks wind down, values are dropped) with exit code 130. A second signal ends it at once. Code that belongs to no scope, likemain's own statements, is cancelled only this way.
Tests: run/cancel, run/cancel_reasons, run/http, run/net, examples/http, the shutdown_signal test (tests/signals).
10.7 Runtime
- Tasks are stackful coroutines, assigned to worker threads by an M:N scheduler. The number of tasks running Gojo code at the same time (
maxProcs) defaults to the number of CPU cores, and can be changed with the environment variableGOJO_MAXPROCSor withtask.setMaxProcs(n). Channels, locks,time.sleepand network operations suspend only the current task. - File I/O: an
os.Fileread or write of a pipe, terminal or socket that has to wait parks the task on the poller, as a network operation does: it takes no thread, and cancelling the task cancels it. On Linux, a read or write of a regular file first tries without waiting (RWF_NOWAIT); one that has to wait for the disk goes through an io_uring, where the task parks the same way (and cancelling it cancels the operation). Without io_uring (kernels before 5.6, seccomp profiles that block it such as Docker's default,GOJO_IO_URING=0), on filesystems withoutRWF_NOWAIT, and on macOS, regular-file I/O blocks its thread, which gives its CPU slot away first (below). - Preemption: when other tasks are waiting, a task that has run for about 10 ms in a row is asked to give up the CPU. It responds at its next checkpoint: the entry of a function that may recurse, each iteration of a loop that contains nested loops or calls, and every 65536 iterations of a purely computational counting loop. So computation that doesn't block cannot starve other tasks either. Other purely computational loops (
while cond {}) don't check; the enclosing loop takes care of it. If such a loop still doesn't respond, it is treated as a C call after 20 ms. What counts is the CPU time the thread actually used: when the machine is overloaded and the thread gets no CPU, it is not treated as unresponsive, so no extra threads are started to add to the overload. - C calls: while an
externfunction, a blocking file operation, or a DNS lookup runs, its thread gives its CPU slot to another thread (file I/O and DNS give it up immediately; other C calls give it up after about 1 ms). So the number of threads ismaxProcsplus the number of tasks inside C calls. A long loop without checkpoints (for example a large sort inside the runtime) is also treated as a C call after 20 ms. - Stacks: each task's stack stays at a fixed address (tasks can borrow data on other tasks' stacks). The default size is 2 MB (
GOJO_GOROUTINE_STACK);mainhas 1 GB. Only the pages that are used take memory. Stack overflow is caught by a guard page and ends with a panic. A task parked for more than 200 ms, among more than 256, whose scopes have no running tasks (that might be reading its stack), is hibernated: the used part of its stack is copied out and its pages are given back, and both come back at the same address before it runs again or before a channel writes into it.GOJO_HIBERNATE=0turns this off (do so for a program whose C libraries keep pointers into a task's stack while the task waits: the runtime can't see those),GOJO_HIBERNATE=nwaitsnms. It is off under sanitizers. task.stats()returns the number of live tasks, threads,maxProcsand hibernated tasks.- Diagnostics. When every task is blocked and no timer or network wait can wake any of them, the program ends with a panic,
all tasks are blocked: deadlock, followed by every task: where it was started, what it waits for (with where that channel, mutex or scope was made, and which task holds the mutex) and the locks it holds, and, when tasks wait for each other's locks, a line saying so.task.dump()prints the same list to stderr; SIGQUIT (Ctrl-\) prints it and ends the program. Debug builds also report, once per pair, a mutex taken while holding another one when somewhere else they are taken the other way round (warning: possible deadlock), and note a scope that has waited more than 10 s for its tasks, with what they wait for. - A panic in any task terminates the process.
Tests: run/sched, run/preempt, run/long_loops, run/ffi_block, run/file_io, panic/deadlock, panic/deadlock_locks, the stack_hibernation test (tests/hibernate), the stdin_pipe_cancel test (tests/stdin); on Linux, scripts/linux-test.sh runs them with and without io_uring.
10.8 Simulation
gojo testruns each test on a simulated scheduler: one thread; which runnable task goes next, and whether a task gives way at a channel operation, lock, spawn, join or atomic operation (and, now and then, at a preemption checkpoint), comes from a seed. Tasks affect each other only through these operations (there are no data races), so the seed decides the interleaving. The clock is virtual: monotonic time starts at 0 and wall-clock time at 2000-01-01 UTC; it advances 1 µs per scheduling step, and when every task waits it jumps to the next timer. While a task waits for the network the clock follows real time, and network events arrive when they do, so programs that use the network aren't fully repeatable.- The seed of a test comes from its name, so a test does the same thing every run.
--seed nuses seedn;--explore nruns each test undernseeds, stops at the first failure and prints its seed (FAIL name (seed 966961; rerun it with gojo test --seed 966961));--realuses the real scheduler.gojo run --seed n(orGOJO_SEED=n) runs a program the same way.
Tests: the simulated_scheduler test (tests/testblocks/sched.gojo).
10.9 Networking
net.listen("host:port")(":0"picks a free port) returns aListener;accept()returns aConn;net.dial(addr)connects.Conn:read(&buf, max)appends up tomaxbytes tobufand returns the number of bytes read; 0 means the peer has closed.write(data)andwriteText(s)write all the data.readAll()reads until the peer closes.closeWrite()closes the write direction.ListenerandConnare closed when they go out of scope.- Errors are
io.Error(net.Erroris an alias); for a cancelled call,isCancelled()is true.Connsatisfiesio.Readerandio.Writer.
Tests: run/net, run/http, examples/http.
11. unsafe and FFI
- Dereferencing a
*Tand calling anexternfunction must happen insideunsafe { }. Raw pointers can be created, passed around and stored in structs. extern "C" { ... }(with no library) calls libc functions as ordinary direct calls.extern "C" static "m" { ... }links that library.extern "C" lib "libz" { ... }loads the library at run time withdlopen, so neither the library nor its headers are needed at build time (statically linked binaries cannot uselib). None of the three needs C headers: the compiler generates the calls from the Gojo declarations.extern "C" fn name(...) { ... }is a Gojo function exported under a C symbol name. Calling it from Gojo does not needunsafe.- Parameters and return values of
extern "C"functions can only be numbers,bool, raw pointers and structs made only of these.String,str,Vec,?T, function values and the like have no C representation, and declaring them is a compile error. Gojo functions cannot yet be passed to C as callbacks. - A raw pointer makes a type fail
send; withunsafe send struct Handle { ... }the author vouches that it can cross threads.
Tests: run/ffi, run/unsafe_decls, run/ffi_block, errors/unsafe_extern, errors/ffi_types, errors/send_ptr.
12. Standard library
Except for the prelude, every module needs a use. Standard library modules can import each other; the compiler loads them in dependency order.
| Module | Contents |
|---|---|
| prelude | print/println (with spaces between arguments), assert, panic, drop, swap, replace, take, min/max; methods of ?T, T ! E, strings, slices, Vec, Map; Set[T] (insertion order, setOf, union/intersection/difference); Arena[T] |
fmt |
sprint; format(template, args...) and printf: {}, {:8}, {:<8} {:>8} {:^8}, {:*^8} (fill character), {:08}, {:.2}, {:x} {:X} {:o} {:b}, {{ }}. The template must be a string literal; the number of placeholders, and whether each format matches its argument's type, are checked at compile time. Numbers are right-aligned by default, everything else left-aligned |
io |
Error, ErrorKind, fromCode, newError; interfaces Reader (read(inout self, buf: inout Vec[byte], max: int) -> int ! Error, 0 means end) and Writer (write(inout self, data: []byte) ! Error); readAll, readText, copy, writeText; in-memory BytesReader (textReader, bytesReader) and Buffer |
bufio |
reader(r): readLine() (strips \n / \r\n, none at the end), readUntil(byte), readByte(), readAll(); it is itself an io.Reader. writer(w): write, writeText, flush; flushes automatically when it goes out of scope (errors are lost; call flush explicitly when they matter) |
os |
args, exit; File (open, create, append, stdin/stdout/stderr; read, write, writeText, readAll, readText, flush; closed when it goes out of scope; satisfies io.Reader/io.Writer); readFile, readText, writeFile, writeText; stat, lstat (Info: kind, size, modified, mode), exists, readDir (sorted names), mkdir, mkdirAll, remove, removeAll (does not follow symbolic links), rename, cwd, chdir, tempDir, joinPath, dirName, baseName; getenv, setenv, unsetenv; run(prog, args), runWithInput (capture output and return Output{status, stdout, stderr}; status is the exit code or 128 + the signal), call (shares this process's standard streams). During file and process operations the thread gives up the CPU and other tasks keep running. Writes to standard output and print share one buffer, so their order is consistent |
time |
monotonic clock now, since, sleep; wall-clock time unixNanos, unix; Date (year…nanosecond, offset; unixNanos(), weekday(), yearDay(), iso() (RFC 3339), format("%Y-%m-%d %H:%M:%S") and other strftime-style directives); utc, local, at(ns, offset), date(...), parseIso (! ParseError); formatDuration (1.5s, 250ms, 2h5m0s); dayName, monthName. Nanoseconds are held in an int, which covers about 292 years either side of 1970 |
json |
Value (Null, Bool, Int, Num, Str, Arr, Obj; objects keep the insertion order of keys, and integers that fit in an int stay exact); parse(text) Value ! json.Error (with line and column; at most maxDepth = 500 levels of nesting); projections get(key), at(i); str(), int(), num(), boolean(), isNull(), isObject(), isArray(), len(), keys(), set, push; encode(), pretty(); constructors object(), array(), string, integer, number, boolean, null; quoted(s). There is no automatic conversion between structs and JSON yet |
net |
TCP, see 10.5 |
http |
HTTP/1.1 server and client, plain-text HTTP only. Handler interface (fn serve(self, req: Request) -> Response); serve(ln, h, Options{deadline, maxBody}), serveCounting (also gives Stats): each connection is a task in the server's task group; each request has a deadline (504 when it passes); a client disconnect cancels the request; cancelling the task that runs serve cancels in-flight requests. Request: method, path, rawQuery, headers, body, header(name) (case-insensitive), param(name) (a decoded query parameter), segments(), text(); request bodies support Content-Length and chunked; over maxBody returns 413, headers over 64 KB return 431. Response: text(status, s), json(status, s), setHeader. Client: get, post, request(method, url, headers, body). encode/decode do percent-encoding. For now each connection handles one request (Connection: close) |
task, strings, math |
see section 10; strings.Builder; math functions |
The full API of each module is in docs/std (generated by gojo doc, section 17).
Examples: examples/todo (a service with a JSON API that stores its data as a JSON file) and examples/grep (a command-line tool; the tests run each command line in runs.txt and compare the output and exit code). Both use only the standard library and no unsafe.
Tests: run/files, run/buffered, run/format, run/dates, run/json, run/http, run/sets, run/iface_modes, errors/format, examples/todo, examples/grep, examples/http.
13. Test blocks
test "name" { ... }can be written in any file.gojo testruns them;gojo build/runignore them.- Each test runs in a separate process: a panic in one test fails only that test. When any test fails, the exit code is 1.
Tests: testblocks/blocks.
14. Execution model
- The compiler generates C and compiles it with
ccorzig cc. Debug and release (-O2) differ in optimization level and debug info: overflow checks and index checks stay in both. - Debug builds have debug info (
-g), and the generated C uses#lineto point at lines of the Gojo source, so lldb/gdb can set breakpoints likemain.gojo:12, and stack traces and sanitizer reports show Gojo lines. On macOS the debug info is in a.dSYMnext to the binary;gojo build -o progalso writesprog.dSYM.gojo emit-cand--keep-coutput C without#line, which makes it easier to debug the compiler itself. - Generics are monomorphized. Functions with at most 60 IR nodes get an
inlinehint. Box,Rc/Arcand interface boxes are allocated from thread-local free lists in size classes;Vec/Stringbuffers usemalloc.- Tasks are stackful coroutines with M:N scheduling; see 10.7.
Mapkeeps insertion order. Floats print in the shortest round-trip representation, without scientific notation when the exponent is in [-4, 21).- If a function that returns a value runs off its end (type checking proves this cannot happen), the program panics with an internal error instead of returning an undefined value.
- Linux targets are compiled with
-fstack-clash-protection: stack frames larger than the guard page (such as a very large local array) are touched page by page, so a stack overflow always lands on the guard page and panics, and never writes past it into other memory. Compilers on Darwin do this by default. - The C compiler runs with a 64 MB stack limit (clang also gets
-fbracket-depth), so the C generated for programs nested up to the limits also compiles.
15. Limits and fuzzing
Limits (exceeding them is a compile error, never a compiler crash):
- Nesting depth of expressions, blocks, types and patterns: at most 1000 parser levels (
parser::MAX_NEST). Each level of source nesting takes a few parser levels, so the practical limit is a few hundred parentheses or a few hundredelse ifs. Long chains likea + b + c ...also count toward the depth (one level per operator). The compiler runs on a 1 GB stack (only the used part takes memory); at this depth no phase overflows the stack, and no analysis grows exponentially with depth. - Array lengths are at most 2³² − 1; any value (after generics are instantiated) is at most 2⁴⁰ bytes. Use
Vecfor larger data. - Integer literals, integer patterns and integer constant arithmetic that exceed the range of every integer type are errors. Float literals that exceed the range of
f64(or off32when that is the expected type) are errors rather than becoming infinity. - Types after generic instantiation nest at most 400 levels. A generic function or type that instantiates itself with an ever larger type each time (
grow[T]callinggrow[Vec[T]],Node[T]with aNode[Vec[T]]field) never terminates; the compiler reports an error when it reaches this depth.
gojo-fuzz (cargo build --profile fuzz produces target/fuzz/gojo-fuzz; the fuzz profile is an optimized build with debug assertions and overflow checks):
gojo-fuzz [--jobs N] [--time SECS]: uses every Gojo program in the repository as the corpus and mutates them by token (delete, duplicate, replace, splice in pieces of other programs, rename, swap in boundary integers), running the front end and code generation in process. Compiler panics, crashes (stack overflows) and timeouts are recorded. Accepted mutated programs are compiled to C and run under AddressSanitizer / UBSan (except programs withunsafe, C calls, files or the network): if the generated C fails to compile, or a sanitizer reports an error at run time, that is a compiler bug or a hole in the safety checks. Mutations that produce a new error message or a new program are added to the corpus.gojo-fuzz diff: generates random programs that are well-typed, always terminate and have deterministic results, and builds and runs each with debug, release and the two sanitizers. All four must produce exactly the same output and end the same way. The programs also contain integer expressions (wrapping arithmetic, shifts, conversions, division) whose results the generator computes itself under Gojo semantics, and the program checks that the computed values agree: this also finds problems that are equally wrong in all four builds (literal types, conversions, integer promotion in the generated C). When the compiler rejects a generated program, that is recorded too (a generator bug, or a false positive in the compiler).gojo-fuzz borrow: generates small programs that perform random operations on a few owned values and their views (create, reassign, modify and move the owners,defer, nested blocks, loops, closures, locks andMapprojections), without guaranteeing they are valid. Most are rejected; each accepted one runs under AddressSanitizer, and an error there means the borrow checker let an unsafe program through.gojo-fuzz reduce <file>: deletes tokens and lines from the input while keeping the same failure signature, to get a minimal reproduction.gojo-fuzz fmt [N]: runs the formatter on generated programs, the corpus and mutated programs. For every input that parses, formatting must keep the tokens unchanged, the result must still parse, the type-checking result must be unchanged, formatting again must change nothing, and the result must not depend on the input's original indentation and trailing whitespace.gojo-fuzz toml [N]: mutates thegojo.tomlfiles intests/packages. Reading a manifest must never panic, and the location of every error must be displayable.gojo-fuzz msgs N: counts the most common first error messages over N mutated programs and gives examples, for reviewing the errors users see.
Findings go in fuzz-out/findings/; reduced regression cases go in tests/fuzz/.
16. Formatting
gojo fmt [--check] [path...] formats .gojo files in place (directories recursively, skipping hidden directories). --check only lists the files that need formatting, and exits with code 1 if there are any. Only files that parse are formatted: a file with a syntax error is reported and left unchanged. Rules:
- Indentation is 4 spaces, determined by bracket nesting: brackets opened on a line (however many) indent the next line by one level; after a line that ends with a binary operator, the next line is indented one more level.
- Existing line breaks are kept: lines are never joined or split. Runs of blank lines collapse to one; blank lines at the start of the file and at the start and end of blocks are removed; trailing whitespace is removed; the file ends with one newline.
- One space after binary operators,
=,=>,,and:; no spaces around..,..=,., unary operators, or the brackets of calls and indexing. Block braces have spaces inside (if c { x }); struct and map literals don't (P{x: 1}). - One space before a trailing comment. Comments are kept as written; only the indentation of their line changes. The inside of multi-line block comments is unchanged.
- The layout never depends on the spaces in the source: types follow
:or->, so whether*,-and[are prefixes, operators or indexing is clear from the tokens. - The formatted result has exactly the same tokens as the original file (otherwise it reports an internal error and does not write the file). All Gojo files in the repository are formatted (except the regression cases in
tests/fuzz), and a test checks this.
17. Documentation
gojo doc generates API documentation in Markdown from source: gojo doc is the index of the standard library, gojo doc os is one standard library module, gojo doc path is a package (directory) or file, and gojo doc --out dir writes the whole standard library into a directory (the docs/std in the repository is generated this way, and a test checks that it is up to date).
- Only
pubdeclarations are listed: constants, types (pubfields and variants; private fields are only noted as present), functions, and thepubmethods of types. Methods added to types from elsewhere (extend str) are listed separately. - A declaration's documentation is the comment lines directly above it (with no blank line in between). A function or constant without its own comment that directly follows the previous declaration shares that one's comment (
floor,ceilandroundshare one description). - A comment block at the start of a file, followed by a blank line or
use, is the package description. - Lines in a comment indented by two or more spaces are code examples.