Common pitfalls
The handful of things worth knowing before the compiler tells you.
The compiler catches and explains every source-level trap. NURL's errors are the first place to look. Do not memorize a list in advance. This page shows the few patterns worth learning up front. You can see the fix more easily when you know the rule.
Calls always need parentheses
nurl_print `hello` // error: bare identifier as a statement has no effect
( nurl_print `hello` ) // correctA bare identifier is always a name lookup, never a call. If you write a call without parentheses, it is a dead statement with no effect. The compiler rejects it.
^ is return, not XOR
^ a b // "return a", then a separate, likely-unintended statement "b"
^^ a b // XOR of a and b^ is the unary return operator. ^^ — two carets with no space between
them — is binary XOR. A stray space between two carets silently becomes
two return statements instead of one XOR; the compiler warns when this
shape appears on one line.
Struct parameters are copies by default
NURL copies struct parameters into the callee. If you write to a field
inside the callee, you do not change the caller's value. Declare the
parameter inout to change the caller's value. See
Memory and ownership.
A closure that changes a capture cannot leave its frame
A closure snapshots what it only reads when you create it: later changes
to the outer variable do not show inside the closure. A : ~ binding the
closure assigns, or hands to an inout parameter, it captures by
reference instead, so the write lands in the caller's binding — and such
a closure cannot be returned, stored in a container or handed to a
thread. State a closure changes after its frame is gone belongs in an
Atomic, a Locked or an Rc. See
Memory and ownership and
docs/MEMORY.md §2.3.
Rc cannot cross a thread boundary
Capturing an Rc (a non-atomic reference count) in a thread_spawn or
spawn closure is a compile error. If two threads update the count at
the same time, the behavior is undefined. Use Arc for anything shared
across threads or fibers. See Concurrency.
Single-letter type keywords cannot be inferred variable names
i u f b s v are type keywords, not ordinary identifiers. : n 0 infers
n's type from 0. If you name the binding i the same way, the
compiler rejects it. i is a reserved type name. Give the binding an
explicit type annotation: : i n 0 works as a type annotation on a
differently-named variable. But a binding named i must have a different
spelling.
Import paths: beside the importer first, then the working directory
$ "path" is tried relative to the file that contains the $ first,
then the compiler's current working directory, then $NURL_STDLIB.
Stdlib imports (stdlib/core/vec.nu) resolve through the last two. A
package's own modules import each other by their path from the
importing file — store.nu from src/service.nu, not src/store.nu:
the src/ form only resolves while the working directory is the
package root, so it breaks once the package is installed under a
consumer's deps/<pkg>/.
Next
- Memory and ownership — the borrow checker rules behind several of these.
- Cheat sheet — the syntax these pitfalls are exceptions to.
- The full, compiler-level limitations list is in
docs/LIMITATIONS.md.
Last updated on