Known Limitations
Limitations of the language and the compiler (compiler/nurlc.nu) — the fixed rules a program must work within. The authoritative grammar is spec/grammar.ebnf; the normative language reference is docs/spec.md.
This page is deliberately scoped to language + compiler. Standard-library feature coverage and gaps (databases, TLS, MQTT, …) are not language limitations — they live with each module (the stdlib/** file headers and docs/NETWORKING.md) and on the ROADMAP.md.
For active compiler quirks (binary & / | arity, bare @-fn closure coercion, same-line parameter shadowing, ternary cascading, : ~ closure-borrow escape) see docs/GOTCHAS.md. The memory model and the borrow checker's not-yet-checked list live in docs/MEMORY.md.
Type system
| Limitation | Workaround |
Single-letter type keywords (i u f b s v) cannot be used as variable names with type inference | Use an explicit type annotation: : i n expr |
Functions and calls
| Limitation | Workaround |
Calls require explicit parens — ( f a b ) is the only call form; a bare identifier is always a name lookup, never a call | Wrap every callsite: ( puts s ) |
Struct parameters are passed by value by default (C/Go/Zig semantics) — = . p field val inside the callee writes a local copy; the caller's struct is unchanged | Mark the parameter inout (@ bump inout Counter c → v) — an exclusive mutable borrow, the callee mutates the caller's binding in place (see docs/MEMORY.md). Or return the modified struct (= c ( inc_returning c )); or use a *T parameter; or wrap state in a single-handle struct ({ ( Vec i ) slots }) |
Closures capture by value (snapshot at construction) by default. The : ~ mutable-struct byref capture path (stdlib/std/panic.nu recover-with-typed-result) shares the caller's alloca — see docs/MEMORY.md §2.3 for the lifetime rule | Use : ~ MultiFieldStruct for shared-mutation closures; for value semantics keep the binding immutable |
A sink parameter takes a manually-managed handle (Vec, single-pointer struct) that the callee frees explicitly. A compiler-auto-dropped value (an owned string, owned slice, Drop value, or struct with owned fields) is rejected at the call site by design — its auto-drop obligation can't be transferred without risking a double-free across ?/??/loop scope restores (see docs/MEMORY.md §1) | Wrap it in a handle ({ s data } / Vec), or pass it by value as an ordinary parameter and let the caller's scope drop it |
Imports
| Limitation | Workaround |
import_decl is a static inline-include (like #include) — the imported file is compiled into the same LLVM module | Avoid importing files that define main; avoid circular imports |
Import alias ( $ path alias ) rewrites top-level @-functions, struct/enum types, enum variants, and global : constants to alias__name. FFI decls (& "lib" @ name) and trait/impl methods are intentionally NOT renamed — FFI symbols resolve at the linker by literal C-ABI name, and trait methods are mangled by the impl-target type | Use pub to scope FFI declarations to the importing file if collision is a risk |
pub visibility covers @-functions, struct/enum types, enum variants (inheriting their enum's flag), and global : constants. Files with no pub decl stay in legacy mode (everything public, backwards-compat). FFI and trait/impl decls accept pub forward-compat but do not enforce | Mark each cross-file API entry with pub; the diagnostic private X 'Y' is not visible across files points at the leaked-private use site |
$-import dedup is keyed on the path string with a small normalisation (leading ./ is stripped). Symlink-equivalent paths still collide as separate imports | Stick to the project-root-relative form (stdlib/foo.nu, no ./ prefix) |
Grammar
| Limitation | Workaround |
Import is inline-include only: no namespaces. Alias rewriting covers @-functions, struct/enum types, enum variants, and global : constants; FFI decls and trait/impl methods are deliberately not renamed | Stick to a single canonical import path per file; prefix FFI names manually when collisions matter |
Every operator has fixed arity (prefix notation has no closing token — a locked 1.0 design decision, see docs/spec.md §6; not a deferred gap). & / `\ | / ^^ / + / - / ` / `/` / `==` / `!=` / `<` / `>` / `<=` / `>=` / `<<` / `>>` are all binary* (OP A B); ? ternary is ? cond then else; ^ / ! / ~ are unary. A missing or extra operand silently consumes the next token, so the diagnostic can land on the following line | Count operands left-to-right when "unexpected token" fires on a line that looks fine. For n-ary &/`\ | chains write & A & B C or & & & A B C D (n−1 operators for n atoms), or factor a predicate helper (is_alpha-style); the compiler warns on the common ? & A B C D { … } { … }` shape |
^ is the return keyword, not XOR — but ^^ (two adjacent carets) is the native XOR operator. ^ a b parses as return (a b …) | Use ^^ for XOR. The lexer pairs ^^ only when the carets are adjacent, so a stray space (^ ^) still means two returns |