Operators
Operators, precedence, and conditional expressions
Arithmetic
| Op | Name | Operand types | Notes |
|---|---|---|---|
+ | Addition | number / string | On strings, concatenates. No mixing of types. |
- | Subtraction | number | |
* | Multiplication | number | |
/ | Division | number | Division by zero yields undefined. |
% | Modulo | number | Operates on truncated integers; mod 0 is undefined. |
- | Unary minus | number |
total = input.qty * input.price
greeting = "hello, " + input.nameComparison
| Op | Meaning | Operand types |
|---|---|---|
< | Less than | number |
<= | Less or equal | number |
> | Greater than | number |
>= | Greater or equal | number |
Comparisons return a bool. If either operand is undefined, the
result is undefined.
Equality
| Op | Meaning |
|---|---|
== | 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
| Op | Meaning |
|---|---|
&& | 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.bannedMembership
| Op | Meaning | Right operand |
|---|---|---|
in | Element is in array | array<any> |
high_risk_country = input.country in input.restricted_countriesin returns a bool. The right operand must be an array; the left
operand is compared structurally against each element.
Field and index access
| Form | Meaning |
|---|---|
a.b | Field 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 : 1000Both 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:
| Pattern | Matches |
|---|---|
| number literal | The scrutinee equals that number (0, -1, 3.5) |
| string literal | The scrutinee equals that string ("free") |
true / false | The scrutinee equals that bool |
| identifier | Anything — 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.
| Level | Operators |
|---|---|
| 1 | . [] f(...) (calls, access) |
| 2 | unary !, unary - |
| 3 | * / % |
| 4 | + - |
| 5 | < <= > >= |
| 6 | in |
| 7 | == != |
| 8 | && |
| 9 | || |
| 10 | ?: (right-assoc), if/then/else, match |
Parentheses can always be used to disambiguate.