NURL

Language basics

Prefix syntax, literals, variables, types, and functions.

NURL source files use the .nu extension and are UTF-8 text.

Prefix notation

Every expression is operator first, operands after. There is no operator precedence table to learn, because every operator has a fixed, known number of operands (its arity).

+ 1 2          // 3
* + 1 2 3      // (1 + 2) * 3 = 9
== a b         // a equals b, yields a bool

NURL does not use parentheses for grouping. They appear only around function calls, function-type literals, and generic-type arguments:

( puts `hello` )        // function call
(@ i i)                  // a function type: takes i, returns i
( Pair i s )              // a generic type instantiated with i and s

Why no grouping parentheses?

It keeps the grammar regular and cheap to parse: at most 4 tokens of lookahead, no precedence climbing. A call still needs parens; a bare operator chain does not.

Comments

// a line comment — runs to end of line

There are no block comments.

Literals

KindFormExample
Integer-?DIGIT+42, -7
Float-?DIGIT+.DIGIT+([eE][+-]?DIGIT+)?3.14, -2.5e10
Stringbacktick-delimited`hello\n`
BooleanT / FT

String literals recognize four escapes: \n, \t, \r, \\. Any other \X passes through unchanged (\d stays as the two characters \ and d, which helps with regex source strings). A string cannot contain a literal backtick.

A - glued directly to a digit, with no space, is a negative literal. A - with space on either side is the binary minus operator.

Base types

TypeMeaning
vunit / no value
isigned 64-bit integer
uunsigned 8-bit byte
f64-bit float
bboolean
sUTF-8 C-string (i8*, NUL-terminated)
i8 i16 i32 i64signed sized integers
u16 u32 u64unsigned sized integers
f3232-bit float
v128128-bit SIMD vector (4×i32)
v256256-bit SIMD vector (8×i32)
v512512-bit SIMD vector (16×i32), the AVX-512 crypto tier's

There are no implicit conversions. A width or sign change needs an explicit cast with # (see below).

s is a raw C string, and the standard library's String (from stdlib/core/string.nu) is a separate, managed, length-tracked type. They are not interchangeable — convert with string_data (String → s) or string_from (s → String).

Variables and globals: : and =

: declares a new binding. Bindings are immutable by default; ~ makes one mutable. You can write the type explicitly, or leave it for the compiler to infer from the right-hand side.

: i n 0                      // explicit type
: ~ i x 0                    // mutable
: String s ( string_new )    // named type
: total ( add 1 2 )          // inferred

At top level, : declares a module-wide global variable:

: i MAX 100
: i LIMIT * MAX 4            // compile-time constant folding in global initializers
: ~ i counter 0              // mutable global
: ( Locked State ) g ( locked_new [State] @ State { 0 } )   // call-initialized owning global

Inside a package build, ( pkg_version ) evaluates to the version string from nurl.toml (or "0.0.0" outside a package).

= reassigns an existing mutable binding, or writes into a struct field, array slot, or slice element:

= x 5                        // reassign x
= . point x 10               // write field x of struct point

Reassigning an immutable binding is a compile-time error.

Functions

modifier* @ name param* → return_type { body }

Parameters are type IDENT, separated by whitespace, with no commas. ^ returns a value from the enclosing function.

@ add i a i b → i {
    ^ + a b
}

@ main → i {
    ( puts `hello\n` )
    ^ 0
}

Every program has exactly one main. It returns either i for the process exit code or v (exits 0).

Calls always use parentheses, with the function name first: ( add 1 2 ). A bare identifier is a name lookup, never a call — writing add 1 2 without parentheses is a compile error that names the missing parens.

Default parameters and named arguments

Parameters may declare a compile-time default with = expr. Callers can omit trailing defaulted parameters or pass arguments by name (name: value) in any order after positional arguments:

@ connect s host i port = 80 b tls = F → i {
    ^ ? tls + port 363 port
}

( connect `example.com` )                 // port = 80, tls = F
( connect `example.com` tls: T )          // port = 80, tls = T
( connect `example.com` tls: T port: 443 )

Function modifiers and visibility

Functions can be prefixed with one or more modifiers:

ModifierMeaning
pubExported as part of the module's public surface (nurldoc, lint roots)
inlineHint to inline across call sites
simdEnable vectorization hints (vectorize(enable)) on loops inside the body
unsafeMay dereference raw *T pointers, cast pointers, call extern C functions, and call raw functions
rawLow-level stdlib primitive that wraps raw pointers/FFI — callable only from unsafe or raw functions

Functions whose name begins with __ (such as @ __helper i x → i) are file-private: the compiler scopes them to the defining .nu file so two modules can define __helper without colliding.

Casts and sizeof

# does an explicit type coercion — widening/narrowing integers, reinterpreting signedness, or casting pointers. A pointer cast makes a raw pointer, so it compiles only inside an unsafe function:

# i ( some_f32_returning_call )   // widen to i64
# *Point ( nurl_alloc 16 )        // reinterpret i8* as *Point (unsafe only)

Z type returns the byte size of a type as an i64. Note that Z u is 1, not 8 — u is a single byte, not the default integer.

Imports

$ "path" inline-compiles another .nu file into the current module. NURL imports the same file only once. An alias scopes the imported names:

$ `stdlib/core/char.nu` ch
( ch::is_digit 55 )   // the imported file's is_digit, renamed ch__is_digit

Without an alias, the imported file's top-level names join the current file's namespace directly.

Next

Last updated on

On this page