Nodora
Language

Operators

Operators, precedence, and conditional expressions

Arithmetic

OpNameOperand typesNotes
+Additionnumber / stringOn strings, concatenates. No mixing of types.
-Subtractionnumber
*Multiplicationnumber
/DivisionnumberDivision by zero yields undefined.
%ModulonumberOperates on truncated integers; mod 0 is undefined.
-Unary minusnumber
total = input.qty * input.price
greeting = "hello, " + input.name

Comparison

OpMeaningOperand types
<Less thannumber
<=Less or equalnumber
>Greater thannumber
>=Greater or equalnumber

Comparisons return a bool. If either operand is undefined, the result is undefined.

Equality

OpMeaning
==Equal
!=Not equal

Equality is structural — arrays and objects compare element- and key-wise. It works on any pair of values of the same type.

same  = input.role == "admin"
ok    = input.tags == ["a", "b"]

Logical

OpMeaning
&&And
||Or
!Not (unary)

Both operands must be bool. The operators are not short-circuit in a way that hides side effects — Nodora has no side effects in expressions — but undefined propagates through both.

eligible = is_adult && allowed_country
blocked  = !eligible || input.banned

Membership

OpMeaningRight operand
inElement is in arrayarray<any>
high_risk_country = input.country in input.restricted_countries

in returns a bool. The right operand must be an array; the left operand is compared structurally against each element.

Field and index access

FormMeaning
a.bField on object
a["b"]Field on object (computed key)
a[i]Element of array (integer index)

Accessing a missing field yields undefined. Indexing an array out of range is an error. Indexing an array with a non-integer is an error.

Conditional expressions

Nodora supports two equivalent ternary forms.

if / then / else

limit = if input.plan == "free" then 100 else 1000

?:

limit = input.plan == "free" ? 100 : 1000

Both branches are type-checked. The condition must be bool.

Match expression

match compares a single value (the scrutinee) against a list of patterns and evaluates the body of the first arm that matches:

out limit = match input.plan {
    "free" => 100,
    "pro"  => 1000,
    "ent"  => 10000,
    _      => 500,
}

Each arm is written pattern => expression. Arms are separated by commas; a trailing comma is allowed. Arms are tried top-to-bottom and the first match wins, so order matters.

Patterns

A pattern is one of:

PatternMatches
number literalThe scrutinee equals that number (0, -1, 3.5)
string literalThe scrutinee equals that string ("free")
true / falseThe scrutinee equals that bool
identifierAnything — binds the scrutinee to that name
_Anything — the wildcard, binds nothing

A literal pattern must have the same type as the scrutinee. An identifier pattern always matches and makes the scrutinee value available under that name inside the arm body and guard:

out num = match input.n {
    0 => 0,
    1 => 2,
    n => n * 10
}

Guards

An arm may add a when guard, a bool expression that must also hold for the arm to be chosen. Guards are typically paired with an identifier pattern so the bound value can be tested:

out tier = match input.requests {
    n when n > 1000 => "critical",
    n when n > 500  => "warning",
    n when n > 100  => "elevated",
    _               => "normal",
}

A guarded arm does not count toward exhaustiveness, since the guard might be false at runtime.

Exhaustiveness

A match must cover every possible value of the scrutinee. Provide a catch-all arm (an unguarded identifier or _ wildcard) to satisfy this:

match input.plan {
    "free" => 100,
    _      => 1000,   // covers everything else
}

A match on a bool is also exhaustive when it has unguarded true and false literal arms:

out label = match is_positive {
    true  => "yes",
    false => "no"
}

A non-exhaustive match is a compile-time error.

Result type

Every arm body is type-checked and they must all share a compatible type, which becomes the type of the match expression. Because a match is an expression it can be nested or used anywhere a value is expected:

out label = match input.category {
    "size" => match input.value {
        "s" => "small",
        "m" => "medium",
        _   => "unknown-size"
    },
    _ => "unknown-category"
}

Function calls

A call is written as a name followed by a parenthesized, comma-separated argument list. Names may be qualified with a namespace using the :: separator.

n      = len(input.items)
upper  = strings::upper(input.name)
hashed = crypto::sha256(input.password)

Arguments are evaluated left-to-right and the result is the function's return value. See Built-in functions for the full set of names you can call.

Lambdas

positive = |x| x > 0
has_any  = some(input.numbers, |x| x > 0)
keyfn    = |item| item.kind
groups   = arrays::group_by(input.items, |it| it.category)

Lambdas are values; they can be bound to names but cannot be returned as outputs or accepted via input.

Precedence

From highest to lowest. Operators on the same row are left-associative unless noted.

LevelOperators
1. [] f(...) (calls, access)
2unary !, unary -
3* / %
4+ -
5< <= > >=
6in
7== !=
8&&
9||
10?: (right-assoc), if/then/else, match

Parentheses can always be used to disambiguate.

On this page