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/user/reference/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. A rowless closure declared outside is also fine when its body is only pure computation — the handle still sees the arguments. What fails is a call with no visible body: an immediately-applied closure, or a lambda whose body performs or calls something this pass cannot name. The diagnostic names the edit and does not cite an ADR. The full measured table is in docs/user/reference/cheatsheet.md.
// skip: eligibility rejection — the callee is an expression, not a name
effect Ask {
Get() -> Int
}
fn ask() -> Int with Ask {
perform Ask::Get()
}
fn main() -> Int {
handle { ((n: Int) -> Int { n })(ask()) } with {
Ask::Get() => resume(0)
}
}
handle of effect 'Ask' cannot be compiled here: the handled body calls through an expression rather than a name, and this handle cannot see what that call performs. Give the callee a name it can see -- a top-level `fn`, or a `let` bound to the closure literal inside the handled body -- or move the `handle` into the function that performs.
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
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 was retired
(#1461): with Error is refused, and the message names Exception.
You may still see Error::Throw as an operation qualifier in older
code; it is accepted, but 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's syntax does not say the
element type. vibe run and vibe test still answer by content when
that element is one == can compare, including Double, Bytes and
arrays. let xs: Array[Int] = [] is the form that does not depend on
that. A generic struct is compared at each concrete instantiation, so
Box[Double] and Box[Array[Int]] compare by content too. What fails
is an element with no structural comparator, and it fails as a build
error, not at run time. vibe check does not report it.
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-leading - is a prefix only when it touches its operand
A newline does not end an expression when the next line begins with an
operator, with one exception: -, ! and ~ at the start of a line
are prefix operators when nothing separates them from their operand
(#3041). So -1 on its own line starts a new expression, while - 1
(a space after the -) continues the line above as a subtraction:
fn main allows Console {
let a = 10
let fresh = {
println("computing")
-1
}
let continued = a
- 1
let trailing = a -
1
println("\{fresh} \{continued} \{trailing}")
}
computing
-1 9 9
The space after the - is what decides, so write a subtraction that
wraps as a - 1 on one line, as - 1 at the start of the next, or with
the - ending the previous line. Every other line-leading operator
(+, *, |>, &&, ...) always continues the previous line. A spaced
- 1 after a statement that returns Unit is a type error whose
message names both edits: drop the space, or write (-1).
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.