20 — Pitfalls (measured)
Previous: Targeting wasm
日本語版: 20_pitfalls.vibe.md
These are rules that cost people an afternoon. Each one was measured on the current compiler. The full list lives in docs/cheatsheet.md. This chapter is the ones that bite first.
handle eligibility is not the type system
A program can type-check and still fail to compile if a handle cannot
see every perform it covers. Two things make a call visible: the
callee's type carries the effect row, or the callee is one the handle
can already see through — a top-level fn, a binding that aliases one,
or a closure declared inside the handled body.
So a call through a local binding or a parameter is fine when its type
carries the row; being a local binding is not itself the problem. What
fails is a rowless closure declared outside the handled body: nothing
tells the handle what that call performs. Give it the row, or move its
let inside the body — the diagnostic names both repairs. The full
measured table is in docs/cheatsheet.md.
// skip: eligibility rejection — the point is the diagnostic, not a run
effect Ask {
Get() -> Int
}
fn main with Exception {
let bump = (x: Int) -> Int {
x + 1
}
let n = handle {
bump(perform Ask::Get())
} with Ask {
Get() => resume(0)
}
()
}
handle of effect 'Ask' cannot be compiled here: this handle cannot see what
one call in its body performs (here: the call to 'bump'). Make that call
visible -- declare 'bump' as a top-level `fn`, give the binding or parameter
it arrives through an effect row (`with Ask`), or move its `let` inside the
handled body. Moving the `handle` into the function that performs works too.
(ADR-0076 evidence-passing migration.)
Note that bump carries no effect row. Giving its literal
with Ask is one of the four repairs the message lists, so that version
compiles — which is the point: the row on the binding is what makes the
perform visible. Lifting bump to a top-level fn works too.
Int width follows the tag bit
Int is 63-bit (one tag bit, ADR-0105). Literal max is
4611686018427387903. max + 1 wraps to -4611686018427387904 on
every backend. Text that still says 2^61-1 / 62-bit is stale.
Interpolation needs a renderer
A user struct interpolated with \{x} needs derive(Show) or
fn T::to_string(v) -> String. Scalars, Option, tuples, and arrays
already render. Missing Show used to print a pointer; it is now an
error (#1445).
s[i] is a byte, not a String
String is a byte string. s[i] is the byte at that offset, an Int.
'A' == 65. A one-byte String is String::from_char_code(s[i]) — a
byte write, alias String::from_byte (#2203) — or a slice. That byte
is a whole character only in ASCII.
Top-level is declarations only
A bare expression at the top level is rejected (ADR-0069). Put it in
fn main or a test block.
// skip: ADR-0069 — a file is a list of declarations
1 + 2
top-level expressions are not allowed; move it into fn main (ADR-0069)
test / bench take a string or nothing
test { } and test "name" { } work. test foo { } (a bare identifier)
does not.
Error vs Exception
Exception is the effect. Error as an effect spelling is deprecated
(ADR-0085). You will still see Error as an operation qualifier in
older rows; do not add new ones.
== on arrays
== compares by value, including arrays with non-scalar elements,
arrays returned from a function, and empty literals — annotated or not.
An unannotated let xs = [] takes its element type from the
Array::push calls that fill it (#2157), as long as the pushed value
says what it is: a literal, an array / tuple / struct of literals, or
an if whose branches agree.
Push a name or a call result and the binding gets no element type, and
comparing two such arrays once both are non-empty fails at run time
rather than answering. let xs: Array[Int] = [] is the fix. A struct
that takes type parameters counts as saying what it is only for the type
arguments whose == compares content — Box[Int] / Box[Bool] /
Box[Unit] / Box[String] resolve; Box[Double], Box[Bytes] and any
array or struct argument do not.
A generic T with no Eq witness is the boundary worth knowing, and it
is a compile error rather than a surprise — no impl Eq for
Array[Int]. See Equality.
A line that starts with an operator continues the previous line
A newline does not end an expression when the next line begins with an operator, so a negative literal meant as a block's final value glues onto the line above:
// skip: shown for the diagnostic — the `-1` parses as `println(...) - 1`
fn main with Console {
let v = {
println("failing")
-1
}
println("\{v}")
}
line 3:5: type mismatch in '-': operands must be Int or Double (the left operand is Unit -- a `-` at the start of a line continues the previous line's expression instead of starting a new one; parenthesize the negated value, e.g. `(-1)` or `(-x)`, or bind it with `let` if you meant a negative value)
Write (-1), or bind it first. A negative literal as a match arm's
direct body (None => -1) is fine. Since #2206 the diagnostic names
the continuation and the edit, as quoted above; the position still
anchors at the start of the glued expression.
fn is a keyword
let fn = 1 is a parse error. r#fn is not an escape hatch. Rename
the binding.
for does not always collect
let xs = for x in arr { x * 2 } works for Array. A pull iterator in
the same position is a located error. Accumulate with ArrayBuilder.
Next: Appendix.