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

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 =

: 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

= 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

@ 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.

Casts and sizeof

# does an explicit type coercion — widening/narrowing integers, reinterpreting signedness, or casting pointers:

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

Z type returns the byte size of a type as an i64. Note that Z u is 1, not 8u 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/mem.nu` m
( m::alloc 16 )     // calls the imported file's alloc, renamed m__alloc

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

Next

Last updated on

On this page