pith diagnostics use stable error codes grouped by compiler phase. codes are never reused — if a code is retired, it stays retired.
use pith check --json <file> or pith lint --json <file> for
machine-readable output that includes the error code, location, message,
and fix suggestion (if available).
the lexer and parser do not have their own numeric ranges. every diagnostic they produce lands in the E24x block alongside the checker's, and the four codes below are the whole set:
| code | phase | what it covers |
|---|---|---|
E240 |
parser | an expected token was missing |
E241 |
parser | the file ended in the middle of a construct |
E242 |
parser | the tokens do not form a valid construct here |
E243 |
lexer | an invalid token: a stray character, an unterminated string, or a bad indentation change |
each is documented in full below. codes in the E0xx and E1xx ranges have never been emitted and are not reserved for these phases.
an expression's type doesn't match what was expected. this is the most common checker error. includes a fix suggestion when the mismatch is in a return type.
error[E200]: type mismatch: expected String, got Int
3 | x * 2
^^^^^ this is Int, but the function returns String
fix: change the return type to Int
a bare none reports here too. none is the empty case of an optional and
satisfies nothing else, so it is accepted only where the target is already
a T? — a binding, an assignment, a return, an argument, a struct field, a
collection element, a tuple element, or the other side of == / !=.
error[E200]: return type mismatch: expected Int, got none
8 | return none
^
fix: return a value, or declare the return type Int?
a variable or function name was used but never defined.
error[E201]: undefined variable 'foo'
5 | print(foo)
^^^
mod.name(...) reaches only what mod itself declares. every module's
top-level names live in one shared table, and without this check the lookup
falls through to a function some unrelated module declared, then emits a call
that resolves to nothing.
a method is an easy way to land here. a method is not a module function and has
no free-function spelling: wait on Process is written proc.wait(), never
process.wait(proc). when the first argument's type has a method by that name,
the error says so.
error[E201]: undefined function: process.wait
fix: 'wait' is a method on Process; call it as receiver.wait(...)
a type name was used in an annotation but doesn't exist.
error[E202]: undefined type 'Foob'
1 | fn bar(x: Foob) -> Int:
^^^^
a name was defined more than once in the same scope.
error[E203]: duplicate definition of 'x'
3 | x := 10
^
not currently emitted. rebinding a name in the same scope is accepted; there is no duplicate-definition check. rebinding it inside a nested block reports E262.
a match expression doesn't cover all possible values. includes a fix
suggestion listing the missing patterns. a guarded arm never counts toward
coverage, since its guard can be false: _ if n > 0 => ... alone is not
exhaustive. a binding on an optional subject covers the present values only,
so the match also needs a none arm.
error[E204]: non-exhaustive match: missing variant 'Circle'
5 | match shape:
^^^^^
fix: add missing arm: Circle(..)
a match arm can never be reached because an earlier arm without a guard is a
wildcard, a binding, or a tuple of those, and matches every value. a binding
on an optional subject matches only present values, so a none arm after it
is still reachable. a repeated arm that is not a catch-all is not reported.
error[E205]: unreachable match arm: an earlier arm without a guard matches every value
4 | 1 => 1
^
fix: remove this arm, or move it above the catch-all arm
a function needs a return type annotation but doesn't have one.
not currently emitted. a function with no return type annotation is treated as returning nothing.
a function call has too many or too few arguments.
error[E207]: expected 2 arguments, got 3
5 | add(1, 2, 3)
^^^^^^^^^^^
an expression was used as a function call but its type isn't callable.
a field access references a field that doesn't exist on the struct.
error[E209]: field 'z' not found on type 'Point'
3 | p.z
^
the same code covers a missing method, and a method call that writes type
arguments on a name the receiver's type does not declare as a method with
type parameters — a builtin such as s.len[Int](), or a field.
error[E209]: String has no method 'len' that takes type arguments
a field access or struct constructor was used on a non-struct type. the same
code reports an explicit type argument that is not a type at all, such as
Channel[1](2) or Holder[f()](); a type written in expression position may
be a name, a parameterized type, an optional, a tuple of those, or an
associated type of a type parameter (engine[A.Msg]()).
an enum variant pattern was used on a non-enum type.
not currently emitted. matching an enum pattern against a non-enum reports E228 instead.
an enum variant name doesn't exist on the enum type.
a pattern has the wrong number of fields for the enum variant.
reserved for future checker diagnostics. not currently emitted.
a break or continue statement was used outside of a loop body.
a variable was assigned to but wasn't declared with mut. includes
a fix suggestion.
error[E216]: cannot assign to immutable variable 'x'
3 | x = 10
^
fix: declare with 'mut': mut x := ...
an operator was used with types that don't support it.
error[E217]: operator '+' not supported for types Bool and Bool
2 | true + false
^^^^^^^^^^
the if guard on a match arm doesn't evaluate to Bool.
a function argument has the wrong type.
error[E219]: argument type mismatch: expected String, got Int
3 | greet(42)
^^
a bare none passed to a parameter that is not an optional reports here.
error[E219]: argument type mismatch: expected Int, got none
11 | print(takes_int(none).to_string())
^
the reverse is fine: a plain value passed to an optional parameter widens to
Some(v), so f(3) against fn f(x: Int?) is accepted. it does not run
backwards — an Int? argument to an Int parameter still reports here, and
so does an inner type that does not match (f("three") against Int?).
a builtin container position that looks a value up rather than stores it —
xs.contains(3), m.contains_key(3), a map key — reports here too, because
a freshly built Some(3) would not compare equal to the one the container
holds; see docs/limitations.md.
the right side of a pipe operator (|) is not a valid function.
not currently emitted. a bad right-hand side to | reports E208 instead.
a generic type was used with the wrong number of type arguments. the channel
constructor counts too: a channel carries exactly one payload type, and
Channel[Int, String](1) used to build a Channel[Int] silently, dropping
the rest. send a struct when one message should carry several fields.
error[E221]: Channel expects 1 type argument, got 2
type arguments written at a method call, x.describe[Int](v), count
against the method's own bracket list the same way; a method that declares
no type parameters expects zero.
error[E221]: describe expects 1 type arguments, got 2
the compiler couldn't infer the type arguments for a generic type.
the compiler couldn't determine the element type of a collection literal,
or the elements disagree. a none element is folded in rather than compared
— [none, 1] is a List[Int?] — but a set element and a map key are hashed
and compared, so neither accepts none.
error[E223]: set element cannot be none
4 | zs: Set[Int] := {none}
^
the ? (unwrap) operator was used on a non-optional type, or the !
(try) operator was used on a non-result type. includes a fix suggestion.
error[E224]: try requires a result type, got Int
3 | x!
^^
fix: use ? for unwrapping optional types
if/elif/else branches, match arms or select arms produce different types when
the whole is used as an expression. a none beside a value is not a mismatch:
the branches settle on the value's optional, so match v: none => none; x => x
is a T?, and a value beside its own optional settles on the optional the same
way.
a type doesn't satisfy the interface bounds required by a generic parameter.
a method call references a method that doesn't exist on the type.
not currently emitted. a missing method reports E209 instead, which covers both fields and methods.
a pattern in a match arm doesn't match the type being matched on. a bare name that equals a variant of the matched enum is also E228: a bare name is a binding that matches every value, so the variant has to be written qualified.
error[E228]: 'Red' is a variant of Color, but a bare name in a pattern is a binding that matches every value
8 | Red => 1
^
fix: write Color.Red to match the variant
self was used outside of a method body. includes a fix suggestion.
error[E229]: 'self' can only be used inside a method body
1 | fn foo(): self.x
^^^^
fix: define methods inside an 'impl' block with 'self' as the first parameter
a type annotation is required but wasn't provided.
a return statement was used outside of a function body.
error[E231]: return statement outside of function
1 | return 42
^^^^^^
fix: 'return' can only be used inside a function body
a spawn or await expression has a type error.
a spawn's arguments and a method call's receiver are evaluated on the new task, so
! among them has no function to return from. bind the value before the spawn:
value := load()!
t := spawn work(value)
type declarations are nested too deeply (exceeds the compiler's recursion limit). this usually happens with deeply nested generic types or recursive type definitions.
error[E233]: type nesting too deep
the fail statement was used in a function that does not return a result type.
two or more modules import each other, forming a cycle. break the cycle by restructuring the code or extracting shared types into a third module.
a from ... import refers to a name that doesn't exist in the module.
error[E236]: name 'subtract' not found in the imported module
1 | from math import subtract
^^^^^^^^
not currently emitted. importing a name a module does not declare reports E246 instead.
a from ... import refers to a name that exists but isn't marked pub.
error[E237]: 'secret' is not public in the imported module
1 | from math import secret
^^^^^^
not currently emitted, and there is no check behind it: from mod import name currently succeeds whether or not name is pub. only the mod.name(...) call form is gated, by E251.
the ? operator was used outside a function that returns a result type.
? propagates none as a fail, so the enclosing function must be able
to carry that error. Either change the function to return T!, or replace
? with match/unwrap_or for an in-place default.
error[E238]: unwrap (?) can only be used in functions that return a result type
3 | print("got: {value?}")
^^^^^^
fix: wrap main or the enclosing fn return in T! and propagate, or replace ? with `match`/`unwrap_or` for an in-place default
the ! operator was used inside a function that does not itself return a result type.
the parser expected a specific token but encountered a different one.
the parser reached the end of the file before a required token or block terminator appeared.
the parser encountered a token sequence that does not form a valid expression, pattern, or declaration.
the lexer produced an invalid token — an unterminated string literal, a
stray character that starts no valid token, or a bad indentation change.
every lexer error is reported, even one that lands between two otherwise
valid tokens (1 € + 2), so an invalid character can never be silently
dropped.
if let and while let take a variant pattern (which destructures like a
match arm) or a bare binding (which unwraps an optional subject). anything
else — a literal pattern, or a bare binding against a non-optional — is
rejected.
error[E245]: if let with a bare binding needs an optional subject, got Int
a from import must name something its source module actually declares
at the top level. a wrong module name in an import otherwise compiles
clean and dies much later as a silent unknown call in the backend.
error[E246]: 'imaginary_helper' is not declared by module 'helper'
an import or from ... import names a module whose file cannot be
found. standard-library modules are imported with the std. prefix
(import std.fs as fs, not import fs); local modules resolve
relative to the importing file.
error[E247]: cannot find module 'fs'
1 | import fs
^
a spawned task runs concurrently with the scope that spawned it, on a pool of
os threads. reference counts are atomic, but a list, map, or set is an
unsynchronized buffer behind a handle, so passing one the current scope still
holds lets both mutate the same buffer. the same holds for a value sent
through a channel: the receiver gets the handle, not a copy, so send and
try_send are checked like a spawn's arguments.
hand the task a copy, build the value in the call, or use an atomic cell for
a shared scalar. a fresh collection — a literal, a copy, a call result — is
not shared and is allowed.
error[E248]: a spawned task cannot take a list this scope still holds; both threads would mutate the same buffer
6 | t := spawn worker(data)
^
a struct argument is refused the same way when one of its fields reaches a
list, a map or a set outside a guard block, directly or through a nested
struct or optional struct field. move the field into a guard block, or build
the struct in the spawn's arguments, or the send's, so the other task holds
the only reference.
error[E248]: a spawned task cannot take this Account: its field 'inbox.messages' is a collection outside a `guard` block, and both threads would reach the same buffer
16 | t := spawn count(acct)
^
fix: move the field into a `guard` block, or hand the task its own copy
error[E249]: weak field 'parent' must be an optional struct type (T?)
a weak field points at a struct without keeping it alive, so it must be able
to read back as "gone" once the target is reclaimed — which means it has to be
an optional struct type, T?. weak parent: Node is rejected; write
weak parent: Node?. weak is for breaking reference cycles (a parent/child back
pointer, a doubly-linked list's back link); see docs/ownership.md.
a struct or enum cannot reuse the name of a builtin type (Channel, Task,
List, Map, Mutex, WaitGroup, Semaphore, AtomicInt, or a primitive
like Int). the declaration would shadow the builtin and silently miscompile
anything that uses it — a struct Channel, for instance, corrupts every
Channel[T] in scope, so an imported module's own channels stop working with
no error. rename the type.
error[E250]: 'Channel' is a builtin type name and cannot be used as a struct name
2 | n: Int
^
a module function declared with a bare fn belongs to the module that declared
it; only a pub fn can be called through an import. the two live in one shared
name table, so before this check the lookup for mod.helper would fall through
to a bare-name match and return something the caller had no business holding.
mark the function pub if it is meant to be part of the module's surface, or
call the public entry point that wraps it.
error[E251]: 'frame_message' is not public in module 'grpc'
11 | framed := grpc.frame_message(bytes.from_string_utf8("hi"))
^
pith build and pith run make an executable, and an executable needs an
entry point. a file without a top-level fn main is a library: it can be
imported, checked and linted, but not built on its own. before this check the
build sailed through to the system linker, which answered with an
undefined-reference dump instead of a diagnosis.
error[E252]: this file has no main function
fix: `pith build` and `pith run` make an executable. a module without main can be imported, checked and linted, but not built on its own
the block form of catch runs on the error path and produces no value, so
falling out of its end would leave the surrounding binding with nothing.
end the block with return, fail, continue or break.
error[E253]: a catch block must end with return, fail, continue or break; it produces no value for the surrounding expression
a Set element or a Map key must hash, and a set and a map hash exactly
three flavors: the int family, String, and Bytes, which is hashed and
compared by content. every other type used to fall into the string flavor
anyway and misbehave silently — an optional's shell and a list's allocation
header were read as a c-string, collapsing distinct values into one entry; a
struct compared raw memory bytes; a plain enum inserted nothing at all; a
float crashed the process. the type is rejected wherever it is formed: a
written annotation, an inferred literal, an empty literal's first typed
store, and a generic instantiation. store List[T?] instead of Set[T?],
and key by a string, integer or bytes encoding of the value otherwise (an id
field for a struct, a serialized form for a tuple). an optional map value
stays legal; only the key is hashed.
error[E254]: an optional cannot be a set element type; store List[T?] or key by the payload instead
error[E254]: a list cannot be a map key type; a set element and a map key hash int, string and bytes flavors only
defer and errdefer appear outside a function, or the deferred statement is
not a plain side effect. a deferred statement may not return, fail, break,
continue, use ! or ?, bind a name, or nest another defer — move the
control flow into a helper and defer the call to it. errdefer additionally
requires a function that returns a result (T!), since there is no error case
for it to run on otherwise.
error[E260]: errdefer is only meaningful in a function returning a result (`T!`)
fix: use `defer` for cleanup that must always run, or give the function a `T!` return type
see defer.md for the full ordering rules.
a weak binding broke one of its restrictions. a weak binding cannot be mut,
cannot carry a type annotation (it takes its type from its value), must hold a
struct value, cannot be declared inside a closure, and cannot reuse a name
already bound in the same function.
error[E261]: a weak binding cannot be 'mut'
see ownership.md for why each restriction exists.
a payload-free variant of a generic enum carries nothing to infer the type
argument from. bound without an annotation, the value would have no concrete
type — and a later match on it could not be checked. annotate the binding;
a constructor with a payload argument (Opt.Some(5)) infers its instance
and needs no annotation.
error[E262]: cannot infer the type arguments of generic enum 'Opt' from this constructor; annotate the binding
the same code reports a binding in a nested block that reuses a name an enclosing block of the same function already bound. a function has one slot per name, so the inner binding would overwrite the outer value instead of shadowing it. sibling blocks may reuse a name.
error[E262]: 'x' is already bound in this function; a nested binding overwrites it rather than shadowing it
two impl blocks gave the same declaration the same method name with the same kind of declaration, so one body would silently shadow the other and which one answered depended on module order. remove or rename one of the two. an interface impl re-declaring a method the inherent impl also has is not a duplicate — that pair is the conformance idiom std/io uses — and an impl in another module adding new method names to a type it can see stays legal too.
error[E263]: duplicate method 'read' on 'FileStream': another impl block already declares it
a where clause put a bound on a name that is not one of the function's
declared type parameters. the clause form and the inline form
([T: Display]) are two spellings of the same bounds, so a clause can
only constrain names the bracket list declares. declare the parameter,
or fix the spelling.
error[E264]: where clause names 'U', which is not a type parameter of 'f'
a method of a generic type declared a type parameter with the same name as
one of the type's own. the two lists compose by name — the type's parameters
are fixed by the receiver's instance, the method's by the call — so a
method parameter spelled T on a Box[T] would resolve to the box's T
everywhere it is written, never to the call's type. give the method's
parameter a name of its own.
the code used to refuse every generic method on a generic type; that shape is supported now (docs/generics.md), and only the name clash is reported.
error[E265]: generic method 'describe' on generic type 'Box' reuses its owner's type parameter name 'T'; a method's own type parameters need names of their own
a function's storage is flat: one slot per name for the whole body. a local, a
parameter or a match / if let payload therefore takes its name for the
entire function, and the module global of that name is unreachable from inside
it. that is legal on its own (the linter mentions it as
E308). what has no answer is a
body that wants both — reads the global somewhere above and binds a local of the
same name below.
rename the binding, or move the reads of the global into a function that does not bind the name.
error[E266]: 'items' names a module global that is read earlier in this function, and this binding takes the name for the whole function
10 | mut items: List[Int] := [7]
^
fix: rename the binding, or move the reads of the global into a function that does not bind the name
a for variable is exempt. it is the one binding whose storage is scoped to a
block rather than the function, so the global is still reachable on either side
of the loop and nothing is ambiguous.
a generic body is checked once per distinct set of concrete types the program
asks for (see docs/generics.md). a generic that calls itself at a larger type
(fn grow[T](x: T) calling grow([x])) asks for T, then List[T], then
List[List[T]], without end. the checker stops queueing the declaration after
64 sets and reports it once.
error[E267]: 'grow' is instantiated at more than 64 distinct type sets; a generic that calls itself at a larger type never stops
not reported when the check is switched off (PITH_CHECK_GENERIC_BODIES=off
or silent); the declaration is then left unchecked past the cap, as every
generic body was before the check existed.
impl Drop for T asks the compiler to call T's drop method from the
destructor it attaches to every T it builds, when the last value naming the
struct goes away (see docs/ownership.md, "destructors"). the impl has to be a
shape that destructor can call: T must be a struct, declared in the module
the impl is written in, and not generic — the destructor is generated per
declaration and spells the method symbol from the declaring module, and a
generic struct's destructor is generated per instance with no place for the
call. the impl must declare fn drop() with no parameters and no return
value, since the runtime hands it the bare pointer and reads nothing back, and
nothing else beside it.
error[E268]: impl Drop for Guard: a generic struct cannot implement Drop
error[E268]: impl Drop for Shape: only a struct can implement Drop
error[E268]: impl Drop for Slot: drop() takes no parameters
error[E268]: impl Drop for Slot: the impl may only declare drop(), not 'close'
error[E268]: impl Drop for Slot: the impl must declare fn drop()
Drop is a language-level interface: nothing declares it and nothing imports
it. put the type's other methods in an impl T: block of their own, and have
drop call the explicit closer so the two stay idempotent.
a closure captures the outer variables it uses by value, when it is created.
an assignment to one inside the closure would change only the closure's copy:
mut n := 0; c := fn() -> Int: n = n + 1; return n returned 1 on every call
and n never changed. the checker rejects the assignment. return the new
value from the closure and assign it outside, or keep the state in a struct
field or a collection, whose handle the closure shares with the caller. a
module global is not captured, and a closure can assign its own locals.
error[E269]: cannot assign to 'n' inside a closure: a closure captures a copy, so the variable outside would not change
fix: return the new value from the closure, or keep the state in a struct field or a collection the closure can update
a function that declares a return type reaches the end of its body on some
path without returning a value. that path used to return a zero: 0 for an
Int, [] for a list, none for an optional, a failed result with no
message for T!, and a null struct that crashed on its first field read.
there is no implicit none or ok, so -> T? and -> T! follow the same
rule.
a path returns when it ends in return or fail, in a call to exit, or in
a while true loop with no break for it. an if returns when it has an
else and every branch returns, and a match when every arm does. a match
that is the last statement of the body returns its value: an inline arm
(Color.Red => "red") returns its expression, and a block arm has to return
on its own. a closure with a block body follows the same rule when it
declares a return type or returns a value on some path. pith lint reports
the same code as a warning.
error[E270]: 'mk' declares a return type, but a path through its body reaches the end without returning a value
fix: end every path with return or fail
error[E270]: this closure returns a value, but a path through its body reaches the end without returning one
data declared in a guard block can only be read or written with its mutex
held: inside a locked block for it, or in a function marked holding it.
a read counts, since reading a map while another task inserts into it is as
unsafe as the write. a struct field under guard pick: needs pick held on
the same value, so p.next needs p.pick, and the value has to be reached
through a path the checker can compare: a variable, self, or a field read
off one. a call to a function marked holding m needs m held at the call;
a path rooted at self is read off the receiver, and one rooted at a
parameter off that argument. a closure body runs when it is called, so the
locks held where it is made do not count inside it. code outside a function
runs at module init, before any task, and needs no lock.
error[E271]: 'cache' is guarded by cache_mu, which is not held here
fix: wrap the access in `locked cache_mu:`, or mark the function `holding cache_mu`
error[E271]: 'p.next' is guarded by p.pick, which is not held here
error[E271]: 'evict' must be called holding cache_mu, which is not held here
locked m: takes m on entry and releases it at every exit from the block,
which means evaluating m again at each exit. so m has to be a path that
names the same Mutex every time: a variable, self, a field read off one,
or a list element indexed by a variable or a literal. the body may not
assign the path, a part of it, or an index variable in it, and may not
rebind one of those names, since the unlock would then reach a different
mutex. a locked block outside a function is rejected the same way.
error[E272]: locked needs a Mutex, got Int
error[E272]: a locked block needs a variable, a field or an indexed element that holds a Mutex
fix: bind the mutex to a name first: `m := pick_lock()`, then `locked m:`
error[E272]: the body of this locked block changes 'i', so the unlock at its exit could reach a different mutex
fix: change 'i' outside the locked block
a Mutex is not reentrant. a locked block nested inside one that already
holds the same mutex would park the task on itself for good, so the checker
rejects it. the check compares the two paths as written, inside one function
or closure body: it does not follow a call into a function that takes the
same lock.
error[E273]: 'm' is already held by an enclosing locked block, and locking it again would deadlock
fix: remove the inner locked block; the outer one already holds the mutex
a module guard block holds only mut globals, and none of them may be pub,
since another module could not take the mutex. a sharded guard,
guard m[n]:, needs a count of at least 1 and is only allowed on module
globals. a guard block is not allowed in a generic struct.
error[E274]: a guarded global cannot be pub, since another module could not take its mutex
error[E274]: a guard block holds only `mut` globals
error[E274]: a sharded guard is only allowed on module globals
error[E274]: a guard block is not allowed in a generic struct
reported by pith lint. all of them are warnings: pith lint reports them
without failing a build.
function names, variable names, and method names must use snake_case.
error[E300]: function name 'GetUser' should be snake_case
1 | fn GetUser():
^^^^^^^
type names (structs, enums, interfaces, type aliases) must use PascalCase.
error[E301]: struct name 'my_point' should be PascalCase
1 | struct my_point:
^^^^^^^^
a public function or method has no doc comment. every pub fn should have a
# comment on the line directly above it explaining its purpose. there is no
/// form — that is a parse error.
warning[E304]: public function 'serve' is missing a doc comment
5 | pub fn serve():
^^^^^
code is indented more than 4 levels deep. consider extracting a helper function to reduce complexity.
warning[E305]: indentation depth 5 exceeds maximum of 4
12 | if x > 0:
^^
a struct holds a strong reference back to itself, directly (next: Node?) or
through a chain of fields across other structs in the same module. a cycle of
strong references keeps its own count above zero, so it is never reclaimed;
mark the back edge weak (see ownership.md). a weak field
is not an edge, and neither is a function-typed field. a cycle that runs
through another module's structs is not visible to the linter and passes.
warning[E306]: struct 'Node' holds a strong reference to itself through field 'next'; a cycle of strong references is never reclaimed
3 | next: Node?
^
fix: mark the back edge with `weak` (`weak next: ...`), see docs/ownership.md
a local binding holds a resource its caller owns and nothing in the function
closes it. reference counting reclaims the handle's memory but not the registry
slot behind it, so the slot is held until the process exits with no other
diagnostic. pair the build with a defer close (see defer.md), or
close it explicitly.
the constructor list is empty at the moment. it held the three std.net.tls
config builders, and a tls config now implements Drop (docs/ownership.md,
"destructors"): a config built and never closed gives its slot back when the
last value naming it goes away, so the shape this rule reported is no longer a
leak. the rule stays for the std types that still close by hand, which
docs/destructors_roadmap.md lists; an entry is added when one of them is
audited onto the list rather than the destructor.
the rule is deliberately narrow, because a false positive on a resource that is
closed elsewhere is worse than no rule. it reports only when every later mention
of the name is the receiver of a method that is not a closer. passing the value
as an argument, returning it, assigning it, storing it, reading a field off it,
or capturing it in a lambda all silence the rule — each of those can hand the
resource to something the linter cannot see. it also follows ! and a chain of
builder methods back to the constructor, so
tls.client_config()!.with_alpn(["h2"]) is recognized.
the constructor list is short on purpose. an entry has to be a call whose result
the caller owns, on a type with no method that consumes the receiver instead of
closing it — bytes.ByteBuffer is excluded for exactly that reason, since
take_bytes() frees the buffer as it takes its contents. the survey behind the
list is in destructors_roadmap.md.
the message names the constructor, as it did for a tls config before the destructor:
warning[E307]: 'config' is built by tls.client_config() and never closed; the resource behind the handle is not reclaimed when the binding goes out of scope
7 | config := tls.client_config()!
^
fix: close it on every exit -- `defer config.close()` next to the build; see docs/destructors_roadmap.md
a local, a parameter or a match / if let payload is spelled like one of the
module's globals. the binding reads its own storage, so the global is
unreachable for the rest of that function — every mention of the name inside is
the binding, whichever was declared first. a for variable is exempt: its
storage lasts the loop and the global is still reachable on either side of it.
that is legal and sometimes deliberate, which is why it is a warning. it is worth a look anyway: a reader cannot tell a function that means to read the global from one that means to hold its own value, and the two are one rename apart.
it used to be a miscompile rather than a readability problem. the ir namespace
is flat and the consumer resolved a bare load/store operand to a global's
data slot whenever one existed, so the binding wrote the GLOBAL's storage: the
global's own next read came back as whatever the binding had left there, and a
container global reached list indexing on invalid list handle once the
binding's cleanup freed what the slot still named. globals now carry a storage
symbol of their own (__g_<name>) and a binding keeps its written name, so the
two can no longer meet.
warning[E308]: binding 'items' shadows the module global declared on line 1; the global is unreachable for the rest of this function
4 | mut items: List[Int] := [7, 8, 9]
^
fix: rename the binding, or drop the global if the binding is what you meant
a mut module global that holds a list, a map, a set or a struct is one buffer
every task can reach, and two tasks writing it at once corrupt it. in a program
that spawns tasks, pith check, build, run and test refuse each such
global that is written after module init and is neither declared in a guard
block nor threadlocal, and pith lint reports it as well. the error names
one spawn the program reaches.
the rule covers the whole program the file loads, so a library global is
reported when a program that spawns uses it. a program spawns when a function
in the entry file holds a spawn, or calls one that does. a call is resolved
to its module's function, its import or, for a method, its receiver's type; a
method on a receiver whose type does not say whose method it is, such as an
interface, counts every method of that name. a call through a function value
is not followed. a write is an assignment rooted at the global (g = x,
g[k] = x, g.f = x) or a call of a mutating method on it (push, insert,
remove, clear and the like). a write made through a function that takes the
collection as an argument is not seen.
a global that is built once and only read afterwards needs no lock. build it in
its own initializer, catalog := seeded(), rather than in a function called
from main, and drop the mut.
error[E309]: 'hits' is a mut module global holding a shared buffer, written after module init, in a program that spawns tasks (app.pith:7)
1 | mut hits: Map[String, Int] := {}
^
fix: declare it in a `guard` block, mark it `threadlocal`, or stop writing it after init
a task that parks inside a locked block, or anywhere in a holding
function, keeps the mutex for as long as it is parked, and every task that
needs the mutex waits for the park too. pith lint reports these calls when
the mutex is held:
await- a
selectwith nodefaultarm sleepandtime.delay- a channel
recv fs.readand the otherstd.fsreadstcp.accept,tcp.connectand thestd.net.tcpreads- the reads, receives and accepts of
tls.Conn,tls.Listenerandwebsocket.Conn
a channel send is not reported. it parks only when the channel is full, and
sending under the lock is how a writer keeps its frames in order. a closure or
a spawn inside the block runs later or on another task, so neither is
checked, and a call to a function that parks somewhere inside it is not
followed.
warning[E310]: a channel `recv` runs with a mutex held, so the task parks holding it and every task that needs the mutex waits for the park too
7 | job := jobs.recv()
^
fix: park before taking the lock or after releasing it, and copy what the call needs while the lock is held
a task that holds mutex A and waits for B, while another task holds B and
waits for A, deadlocks both. pith lint records every place in the program a
mutex is taken while another is held and reports a pair taken in both orders.
a mutex taken inside a function called while another is held counts, through
calls the checker resolved; a call through a function value is not followed,
and a closure or a spawn inside the block is not walked.
a mutex is named by where it lives: a module global by its module and name, a
struct field by the struct's type and the field, and every element of a
sharded guard by the guard's name. two values of one struct share a name, so
nesting them, as a transfer between two accounts does, is not reported, and a
mutex reached through a local or a parameter of type Mutex is skipped.
warning[E311]: 'ledger.b_mu' is taken here while 'ledger.a_mu' is held, and ledger.pith:16 takes 'ledger.a_mu' while 'ledger.b_mu' is held; two tasks taking these in opposite orders can deadlock
10 | locked b_mu:
^
fix: take the mutexes in one fixed order everywhere, or release the first before taking the second
a mutex is not reentrant, so a task that holds one and calls a function that
takes it again waits for itself forever. E273 refuses a locked block nested
in a block on the same path; pith lint reports the same mistake made
through a call. a module global is matched through the callee and everything
it calls, except a sharded guard, whose elements share one name. a struct's mutex is matched when a method called on the receiver
whose mutex is held locks that field on self, so self.encode() inside
locked self.mu: is reported when encode takes self.mu, while a method
called on a different value of the same struct is not.
warning[E312]: this call takes 'self.mu', which is already held here; a mutex is not reentrant, so the task deadlocks
21 | self.bump()
^
fix: call a version that expects the lock held, marked `holding`, or release the mutex before the call
a String and a Bytes value are immutable, so s[i] = v and b[i] += 1
are refused. build a new value instead, or keep bytes that change in a
ByteBuffer.
error[E313]: cannot assign into an index of a Bytes: the value is immutable
3 | b[0] = 70
^
fix: build a new value, or use a ByteBuffer for bytes that change
arguments bind by position. a label on an argument (scale(by: 2)) is
checked against the name of the parameter in that position, so a label that
names a different parameter, or none, is refused rather than bound to the
wrong one. a call through a function value takes no labels, since the value
carries no parameter names.
error[E314]: argument label 'b' is in the position of parameter 'a'; arguments bind by position
5 | print("{scaled(b: 1, a: 5)}")
^
an interface bounds a type parameter. it has no values of its own, so it cannot be the type of a parameter, a return or a field. take a type parameter bounded by the interface instead.
error[E315]: interface 'Shape' is not a value type
11 | fn show(x: Shape) -> Int:
^
fix: take a type parameter bounded by it, as in `fn f[T: Shape](x: T)`
a field declared frozen is set when its struct is built and never changed
after (see docs/ownership.md, "frozen fields"). reassigning it, assigning to
one of its elements, or calling a method that changes it in place (push,
insert, remove and the like) through a path that runs through it is
refused. build a new struct with the changed value, or drop frozen and keep
the field in a guard block.
error[E275]: 'routes' is frozen, so it cannot be changed in place
8 | r.routes.push("/admin")
^
fix: build a new value with the change, or drop `frozen` and keep the field in a guard block
a guarded field or global holding a collection, or a struct that reaches one
outside a guard block, is reached safely only while its mutex is held. inside
locked, its value may not be stored in a variable declared outside the
block, returned from the block, or captured by a closure made in the block:
each keeps a handle to the buffer after the lock is released.
a block that moves the value out and gives the field a new one, out = self.items then self.items = [], is a take and is allowed, since the guard
no longer reaches the old buffer. a value that never changes after it is
built belongs in a frozen field instead of a guard.
error[E276]: 'messages' is guarded, and this returns its value out of the block, so the buffer is reached after the lock is released
11 | return inbox.messages
^
fix: copy what is needed inside the block, or declare the field `frozen` outside the guard block if it is never changed