Skip to main content

Operators

Arithmetic

OperatorNameExampleResult
+Addition3 + 47
-Subtraction10 - 37
*Multiplication3 * 412
/Division10 / 42
%Remainder10 % 31
-Unary negation-5-5

Two ints divide to an int, truncated toward zero: 10 / 4 is 2 and -7 / 2 is -3. If either side is a float the result is a float, so 10 / 4.0 is 2.5.

% takes its sign from the left operand, matching C: -7 % 2 is -1 and 7 % -2 is 1.

Integer arithmetic is checked. A result outside the 63-bit int range raises integer overflow rather than wrapping.

Bitwise

OperatorNameExampleResult
&Bitwise AND6 & 32
|Bitwise OR5 | 27
^Bitwise XOR7 ^ 34
~Bitwise NOT (unary)~0-1
<<Left shift1 << 416
>>Right shift64 >> 216

Logical

OperatorNameExampleResult
&& or andLogical AND (short-circuit)true && falsefalse
|| or orLogical OR (short-circuit)false || truetrue
! or notLogical NOT (unary)!truefalse

&& and || are short-circuit: the right operand is not evaluated if the left operand decides the result. Both operands must be bool; mixing types is a type error.

The word forms are exact aliases of the symbols, not separate operators, so a and b and a && b compile to the same thing and bind at the same level. Pick one style and stay with it.

Comparison

OperatorNameExampleResult
==Equal3 == 3true
!=Not equal3 != 4true
<Less than1 < 2true
>Greater than2 > 1true
<=Less than or equal2 <= 2true
>=Greater than or equal3 >= 2true

Equality (==, !=) requires both operands to be the same type — mixing int and float is a type error, caught by jade check. The one exception is char against str, which is explained in Types.

Ordering (<, >, <=, >=) works on two ints, two floats, an int against a float, two bools (false is below true), and two strings (character by character). Anything else is a type error.

Comparisons do not chain. 1 < 2 < 3 groups as (1 < 2) < 3, which then compares a bool to an int and fails. Write 1 < 2 && 2 < 3.

Membership

OperatorNameExampleResult
inContains2 in [1, 2]true
not inDoes not contain3 not in [1, 2]true

in works on an array (is any element this value), a string (is this a substring), and a dict (is this a key — never a value). It always produces a bool and never raises on a type mismatch: an element of another type simply answers false.

print(2 in [1, 2, 3]) // true
print("ell" in "hello") // true
print("name" in {"name": 1}) // true
print("x" not in {"name": 1}) // true

Pipe

OperatorNameDescription
|>PipePass left-hand value as the first argument to the right-hand function

The pipe operator threads a value through a chain of function calls left-to-right. x |> f is equivalent to f(x). When the right-hand side is a call expression with arguments, the piped value is inserted as the first argument: 5 |> add(3) is equivalent to add(5, 3).

fn double(x) { return x * 2 }
fn add(a, b) { return a + b }

// Simple pipe
let n = 5 |> double // 10

// Chained pipes — left-associative
let m = 3 |> double |> double // 12

// Pipe with extra arguments (value inserted as first arg)
let r = 5 |> add(3) // add(5, 3) = 8

// Pipe to print
"hello" |> print

// Pipe with arithmetic on the left
let x = (2 + 3) |> double // 10

What a stage can be

A stage is the thing to the right of a |>. There are three kinds, and which one applies depends on what the name refers to, not on where the |> appears:

StageMeaning
A functionApplied to the value, which becomes its first argument
A type nameOn a prompt dereference, constrains generation with a grammar and coerces the reply; elsewhere it is the ordinary type constructor, so x |> int is int(x)
A Grammar valueConstrains sampling on a prompt dereference

Two rules settle a name that could be more than one thing, and they only matter on a prompt dereference:

  • A builtin type keyword is always a type, never a function. int, float, bool, char and str are also callable constructors, so without this rule ?p |> int would stop constraining the model and merely try to convert whatever came back.
  • A struct you declared is always a type. A struct registers a constructor under its own name, so by callability alone every struct looks like a function; ?p |> City would have become City(?p).

Everything else prefers a function.

prompt p = "What is 21 + 21? Respond with only the number."

let n = ?p |> int |> double // constrain, coerce, then apply
let g = Grammar.new('"yes" | "no"')
let a = ?p |> g // constrain sampling with a grammar
note

Until v1.2.0 this was really two operators sharing a spelling. |> after a prompt dereference was read by a different parse rule that accepted exactly one constraint and could not chain, and a typed dereference was banned inside print(...). Both restrictions are gone, and a stage that is none of the three above is now an InvalidPipeStage type error naming what it found rather than a parse error talking about tokens.

Pipes are left-associative and have lower precedence than all other operators, so the entire expression to the left of |> is fully evaluated before being passed to the function on the right.

Precedence

Operators bind from tightest to loosest in this order:

  1. Unary: ~, !, -
  2. Multiplicative: *, /, %
  3. Additive: +, -
  4. Shifts: <<, >>
  5. Bitwise AND: &
  6. Bitwise XOR: ^
  7. Bitwise OR: |
  8. Comparison and membership: ==, !=, <, >, <=, >=, in, not in
  9. Logical AND: && / and
  10. Logical OR: || / or
  11. Pipe: |> (lowest — entire left expression is the piped value)

Note that comparison is looser than the bitwise operators, unlike C. 1 << 2 & 15 is (1 << 2) & 15, and a & b == c is (a & b) == c.

let x = 2 + 3 * 4
let y = 1 << 2 & 15
let z = 1 < 2 && 3 > 0

When an operator fails

Operator failures split cleanly by when they are found, and the split is worth knowing because only one half can be caught with try.

Caught by jade check, before anything runs. These are type errors. The program does not start, so no output appears at all.

  • Mismatched operands: 1 + true, 1.0 & 2
  • Cross-type equality: 1 == 1.0
  • Non-bool logic: 1 && true
  • A pipe stage that cannot be applied: 5 |> 3

Raised while the program runs. These depend on values, not types, so the checker cannot see them coming. Each is catchable with try / catch.

  • Division by zero: x / 0
  • Remainder by zero: x % 0
  • Invalid shift amount, negative or ≥ 64: 1 << 64
  • Integer overflow: a result outside the 63-bit int range
  • Index out of bounds: [1, 2][5]
  • A missing dict key: d["nope"]

An operator on a value whose type the checker could not work out — an element of a mixed array, a value from an imported package — also lands in the second group. The check is deferred to run time rather than skipped:

let mixed = [1, "two"]
print(mixed[0] + mixed[1]) // passes `jade check`, then raises