Skip to content

Operators

Arithmetic Operators #

Operator Description Example
+ Addition 3 + 25
- Subtraction 10 - 46
* Multiplication 3 * 412
/ Division 10 / 33
% Modulo 10 % 31
~/ Floor division 7 ~/ 23
** Exponentiation 2 ** 8256

Floor Division ~/

~/ performs floor division (Lua // semantics): the result is always rounded toward −∞ (negative infinity), not toward zero.

print(7 ~/ 2)    // 3
print(-7 ~/ 2)   // -4  (floor toward −∞, not −3)
print(7 ~/ -2)   // -4
print(7.5 ~/ 2)  // 3   (float operand → floored quotient)

Type rules:

  • int ~/ intint (floor toward −∞, sign-corrected)
  • float ~/ * or * ~/ floatfloat (floor of the quotient)
  • int ~/ 0runtime error ("attempt to perform 'n~/0'")
  • float ~/ 0 → follows IEEE 754 (±inf / nan), then floor is applied

~/ is not the same as truncating division (/ truncates toward zero for floats; ~/ floors):

print(-7 ~/ 2)   // -4  (floor)  ← ~/
// vs. -7 / 2 in most languages → -3  (truncate)

Compound assignment ~/=:

var x = 20
x ~/= 6   // x == 3

Note: ~/ does not conflict with the // line-comment token. Zolo detects // as a comment during lexing before operator tokenisation; ~/ is a distinct two-character token (~ followed by /).

Comparison Operators #

Operator Description Example
== Equal x == 5
!= Not equal x != 0
< Less than x < 10
> Greater than x > 0
<= Less or equal x <= 100
>= Greater or equal x >= 1
~= Approximate equal (float) 0.1 + 0.2 ~= 0.3
!~= Approximate not equal (float) a !~= b within 1e-9

Approximate Equality (~= and !~=)

Direct == on float values is unreliable due to binary floating-point representation. Zolo's float-equality lint flags this and suggests ~= instead.

~= supports three within clauses to control the tolerance:

// Bare form — adaptive tolerance (default, covers ~80% of cases)
print(0.1 + 0.2 ~= 0.3)                          // true

// Absolute tolerance: |a - b| <= tol
print(velocity ~= 0.0 within 1e-6)               // true if velocity < 0.000001

// Relative tolerance: |a - b| / max(|a|, |b|) <= rtol
print(1e10 ~= 1.0000001e10 within 0.001 relative) // true (0.000001% < 0.1%)

// ULP tolerance: bit-pattern distance <= n (for testing math functions)
print((0.1 + 0.2) ~= 0.3 within 1 ulps)          // true (1 ULP of noise)

!~= is the exact negation of ~= and accepts all the same within clauses:

// Loop until convergence
while prev !~= x within 1e-10 {
    prev = x
    x = next_iteration(x)
}

For the complete reference — decision tree, all tolerance modes, NaN/infinity handling, decimal and bigdecimal types, and the float-equality lint — see Float Precision & Decimal Types.

Logical Operators #

Operator Description Example
&& Logical AND a && b
|| Logical OR a || b
! Logical NOT !flag

Assignment Operators #

Operator Description Example
= Assignment x = 10
+= Add and assign x += 5
-= Subtract and assign x -= 3
*= Multiply and assign x *= 2
/= Divide and assign x /= 4
%= Modulo and assign x %= 3
~/= Floor-divide and assign x ~/= 3

Pipe Operator |>

The pipe operator passes the left expression as the first argument to the right function. This creates a natural top-to-bottom data flow:

// Without pipe — hard to read (inside-out)
let result = collect(filter(map(list, |x| x * 2), |x| x > 5))

// With pipe — natural flow
let result = list
    |> map(|x| x * 2)
    |> filter(|x| x > 5)
    |> collect()

How It Works #

a |> f(b, c) transforms to f(a, b, c) — the left side becomes the first argument.

"  Hello World  "
    |> string.trim()
    |> string.split(" ")
    |> Array.map(|s| string.to_upper(s))
    |> Array.join(", ")

Pipe with Placeholder _

When the piped value shouldn't be the first argument:

users
    |> sort(_, by: .name)
    |> group(_, key: |u| u.age)

Tap Operator &.

Executes a side effect without breaking the chain — the original value passes through unchanged. Perfect for debugging and logging:

list
    |> sort()
    &. print()           // prints the sorted list, passes it along
    |> filter(|x| x > 0)
    &. |x| log(x)        // debug log
    |> collect()

How It Works #

a &. f() calls f(a) for its side effect, then returns a unchanged.


Optional Chaining ?.

Safely access fields on values that might be nil:

let city = user?.address?.city    // nil if any part is nil

Without optional chaining, you'd need nested nil checks:

// Equivalent without ?.
let city = if user != nil {
    if user.address != nil {
        user.address.city
    } else { nil }
} else { nil }

Force Chain !.

Force-unwrap a Result or Option, then immediately access a field or call a method. Panics if the value is Err or None, exactly like .unwrap():

let name = Result.Ok(user)!.name      // unwrap, then read .name; panics if Err
let up   = parse(s)!.to_upper()      // unwrap then call .to_upper()

a!.field is equivalent to a.unwrap().field; a!.method() is equivalent to a.unwrap().method().

Contrast with ?. and ?>

Operator On Err/None On Ok/Some
?. Returns nil (null-safe) Accesses field/method
!. Panics (force-unwrap) Accesses field/method
?> Propagates out of fn Pipes unwrapped value

Use !. only when you are certain the value cannot be an error — in tests, quick scripts, or after a prior validation. For safe access, prefer ?. (null-safe) or ?> (propagate).

Note: !. is currently supported on the VM backend. Native (Cranelift) support is planned.


Null Coalesce ??

Provide a default value when the left side is nil:

let name = user?.name ?? "Anonymous"
let port = config?.port ?? 8080

Combining with Optional Chaining #

let city = user?.address?.city ?? "unknown"

Error Propagation ?

Propagates errors up the call stack, similar to Rust:

fn read_config(path: str) -> Result<Config, Error> {
    let text = fs.read(path)?        // returns early on error
    let config = json.parse(text)?
    Result.Ok(config)
}

When ? is applied to a Result.Err, the function immediately returns that error. When applied to Result.Ok(value), it unwraps to value.


Fallible Pipe ?>

a ?> rhs is the propagate-then-pipe operator — the chainable sibling of ?. It combines error propagation with piping in a single step:

  • If a is Result.Err or Option.None, it propagates out of the enclosing function (exactly like ?).
  • If a is Result.Ok(v) or Option.Some(v), it pipes the unwrapped value v into rhs (exactly like |>).
fn list_users(db) -> Result {
    db.query("SELECT * FROM users") ?> .each(|u| print(u.name))
    Result.Ok(0)
}

The dot-method form a ?> .method(args) is the most common use: it unwraps the result and calls .method() on the inner value without a separate ? + |> step.

Propagation position #

Propagation happens at statement and let-binding position (the enclosing function returns the error). Inside a nested sub-expression, ?> performs a best-effort soft-unwrap instead of a hard early-return.

fn process(items) -> Result {
    let count = get_items()? |> Array.len()   // two steps: ? then |>
    get_items() ?> .each(|x| print(x))        // one step: ?> does both
    Result.Ok(count)
}

?> is to |> what ? is to an ordinary expression — it adds fallibility to the pipe chain without extra syntax.


Spread Operator ...

Spread elements into arrays or structs:

let a = [1, 2, 3]
let b = [0, ...a, 4, 5]   // [0, 1, 2, 3, 4, 5]

let base = { name: "Alice" }
let full = { ...base, age: 30 }

Range Operators #

Operator Description Example
.. Exclusive range 0..10 → 0 to 9
..= Inclusive range 0..=10 → 0 to 10
for i in 0..5 {     // 0, 1, 2, 3, 4
    print(i)
}

for i in 0..=5 {    // 0, 1, 2, 3, 4, 5
    print(i)
}

Operator Precedence #

From highest to lowest:

  1. Postfix / access: ., ?. (null-safe chain), !. (force-unwrap chain), ? (error propagate)
  2. Unary: -x, !x
  3. Exponentiation: **
  4. Multiplicative: *, /, %, ~/
  5. Additive: +, -
  6. Range: .., ..=
  7. Comparison: <, >, <=, >=
  8. Equality: ==, !=
  9. Logical AND: &&
  10. Logical OR: ||
  11. Null coalesce: ??
  12. Pipe: |> — Fallible pipe ?> (same precedence as |>)
  13. Tap: &.
  14. Assignment: =, +=, -=, *=, /=, %=, ~/=
enespt-br