NURL

Control flow and pattern matching

Conditionals, loops, and match, all in prefix form.

Like every NURL expression, control flow uses prefix form: the keyword comes first, then its operands.

Conditionals: ?

? cond then else

Both then and else are full expressions. A block { ... } is the usual form. Both branches must give the same type.

? > x 0 { ( puts `positive\n` ) } { ( puts `non-positive\n` ) }

Chained conditionals read top to bottom. Each else slot holds the next ?:

? div3
{ ( nurl_print `Fizz\n` ) }
? div5
{ ( nurl_print `Buzz\n` ) }
{ ( nurl_println_int i ) }

Count the operands

? takes exactly three operands. It is easy to get a chain of & a b c d inside a condition wrong. See Binary operators below.

Loops

While

~ cond { body }
: ~ i i 1
~ <= i 10 {
    ( nurl_println_int i )
    = i + i 1
}

Foreach

~ item collection { body }

collection must be a slice. The element binding is a borrow. It is not owned, the runtime does not drop it at the end of the iteration, and you cannot reassign it.

: [i nums [i | 1 2 3 4 5]
~ n nums {
    ( puts ( nurl_str_int n ) )
}

A foreach can walk a struct's field directly (grammar v2.10):

~ x . bag items { ( puts ( nurl_str_int x ) ) }

The loop body may not mutate collection itself. The borrow checker rejects push, remove, clear, and similar calls on it — by path, so a loop over . s a may still change . s b. See Memory and ownership.

Break and continue

break exits the innermost ~ loop immediately. continue jumps to the next iteration (advancing the index in a foreach loop). Both run scope-exit drops and ; defers for bindings created inside the loop body up to that point:

~ n nums {
    ? < n 0 { continue } {}
    ? == n 99 { break } {}
    ( nurl_println_int n )
}

Defer: ;

; { cleanup_body }

; schedules a block to run when the enclosing lexical scope exits — whether by reaching the closing }, returning via ^, propagating an error via \, or exiting a loop with break / continue. Multiple ; statements in the same scope run in reverse (LIFO) order, and only defers whose ; statement was actually reached at runtime execute.

@ process_item → !i s {
    ( acquire_trace_span )
    ; { ( release_trace_span ) }
    : i v \ ( step_that_may_fail )
    ^ @ !i s { T v }
}

Binary operators

Arithmetic, comparison, shift, and logical operators are all strictly binary — they take exactly two operands. For a chain of more than two, write one operator per pair, n−1 operators for n values:

& a & b c        // a && b && c
| | a b c d      // a || b || c || d

The compiler rejects the common mistake of writing a flat n-ary chain inside ? (where extra values would otherwise become the ?'s then/else operands) as a compile error by default (demoted to a warning only with --no-strict-arity).

Match: ??

?? expr { arm* }

Matches an enum value, an ?T option, or a !T E result. You must cover every variant. Use an explicit arm or a _ wildcard. The compiler checks this at compile time.

?? val {
    JNull        → `null`
    JNum n       → ( nurl_str_int n )
    KeyPress c m → ( key_event c m )   // binds two payload values
    Ok 200       → `ok`                 // matches only when payload == 200
    _            → `other`
}

A pattern can be a variant name, _ for wildcard, or A | B (an or-pattern that covers several variants with no payload binding). A payload slot can be an identifier that binds the value, or an integer literal that must match exactly.

An arm may add a guard — ? condition between the payload and →. The runtime checks the guard after it binds the payload. A guarded arm does not by itself satisfy exhaustiveness for its variant. You still need a plain arm or _ to cover the case where the guard is false.

?? msg {
    Move n ? > n 0 → `forward`
    Move n         → `back-or-stay`
    _              → `other`
}

Channel select

A ?? immediately followed by { (no scrutinee expression) starts a different construct — a Go-style channel select. It runs the body of whichever channel becomes ready first. See Concurrency.

Next

Last updated on

On this page