Skip to content

Operators

Shape’s operators fall into six families: arithmetic, comparison, bitwise, logical, unary, and indexing. For built-in scalar types (int, number, decimal, bigint, string, bool, Array<T>) the compiler emits typed opcodes directly (AddInt, MulNumber, EqString, …) and never routes through trait dispatch. For user-defined types, the operator desugars to a call to the corresponding operator trait — implement the trait once and the operator becomes available on values of that type.

The operator-trait declarations live in std::core::{add, sub, mul, div, modulo, neg, not, eq, ord, bitwise, shift, index}; they are imported automatically via the prelude.

+ - * / % ** and their compound-assign forms += -= *= /= %= **=.

OperatorTraitMethodCompoundBuilt-in dispatch
+Addadd(other) -> Self+=AddInt, AddNumber, StringConcatTyped, …
-Subsub(other) -> Self-=SubInt, SubNumber, …
*Mulmul(other) -> Self*=MulInt, MulNumber, …
/Divdiv(other) -> Self/=DivInt, DivNumber, …
%Modmod(other) -> Self%=ModInt, ModNumber
**(no trait)(built-in only)**=PowInt, PowNumber
let a = 10
let b = 3
print(a + b) // 13
print(a - b) // 7
print(a * b) // 30
print(a / b) // 3 — integer division (truncates)
print(a % b) // 1
print(a ** b) // 1000 — exponentiation

Exponentiation works on both int and number:

print(2 ** 10) // 1024
print(2.0 ** 0.5) // 1.4142135623730951

Compound assigns desugar at parse time (x += y becomes x = x + y), so a single impl Add for X covers both + and += on user-defined types.

let mut x = 1
x = 5
x += 2
x -= 1
x *= 3
x /= 2
x %= 4
x **= 2
print(x) // 4

Implementing Add makes + work on values of that type:

type Vec2 { x: int, y: int }
impl Add for Vec2 {
method add(other: Vec2) -> Vec2 {
Vec2 { x: self.x + other.x, y: self.y + other.y }
}
}
let a = Vec2 { x: 1, y: 2 }
let b = Vec2 { x: 10, y: 20 }
let c = a + b
print(c.x) // 11
print(c.y) // 22

Sub, Mul, Div, Mod follow the same shape (method names sub, mul, div, mod).

Unlike the other arithmetic operators, ** has no operator trait — user types cannot overload exponentiation. The operator is restricted to the built-in int and number types and lowers directly to PowInt / PowNumber opcodes.

== != < <= > >= and the cmp integer-ordering convention.

OperatorTraitMethodBuilt-in dispatch
==, !=Eqeq(other) -> boolEqInt, EqNumber, EqString, …
<, <=, >, >=Ordcmp(other) -> intLtInt, GtNumber, …
print(5 > 3)
print(5 >= 5)
print(2 < 1)
print(2 == 2)
print(2 != 3)

String comparison uses lexicographic order:

print("apple" < "banana")
print("apple" == "apple")
print("z" > "a")

Operator overloading: Eq and Ord on a user type

Section titled “Operator overloading: Eq and Ord on a user type”

Eq::eq returns bool; != is desugared to !(a.eq(b)) so only one method is needed. Ord::cmp returns a signed int (< 0, 0, > 0) and the compiler lowers <, <=, >, >= to a cmp call followed by an integer comparison against 0.

type Money { cents: int }
impl Eq for Money {
method eq(other: Money) -> bool {
self.cents == other.cents
}
}
impl Ord for Money {
method cmp(other: Money) -> int {
self.cents - other.cents
}
}
let a = Money { cents: 100 }
let b = Money { cents: 250 }
let c = Money { cents: 100 }
print(a == c) // true
print(a == b) // false
print(a < b) // true
print(b > a) // true

Eq and Ord are independent — a type that participates in both == and < must implement both traits.

instanceof narrows a union type to one of its members. In an if instanceof branch the variable is narrowed to the tested type:

let value: int | string = 42
if value instanceof int {
print("is an int")
}

Approximate comparison with an optional within tolerance. Useful for floating-point comparisons where exact equality is unsafe.

print(0.1 + 0.2 ~= 0.3 within 0.0001) // true

& | ^ ~ << >> and the compound assigns &= |= ^= <<= >>=.

OperatorTraitMethodCompoundBuilt-in dispatch
&BitAndbitand(other) -> Self&=BitAndInt
|BitOrbitor(other) -> Self|=BitOrInt
^BitXorbitxor(other) -> Self^=BitXorInt
<<Shlshl(other) -> Self<<=BitShlInt
>>Shrshr(other) -> Self>>=BitShrInt
~(no trait)(built-in only)BitNotInt

Bitwise operators apply only to integer types — int, i8..i64, u8..u64. Attempting 1.0 & 2.0 is a compile error.

let a = 0b1100
let b = 0b1010
print(a & b) // 8 — bitwise AND
print(a | b) // 14 — bitwise OR
print(a ^ b) // 6 — bitwise XOR
print(~a) // -13 — bitwise NOT (two's complement)
print(a << 2) // 48 — left shift
print(a >> 1) // 6 — right shift

Combined with compound-assigns:

let mut x = 0xF0
x &= 0xFF
x |= 0x01
x ^= 0x10
x <<= 1
x >>= 1
print(x) // 225

Operator overloading: BitAnd on a user type

Section titled “Operator overloading: BitAnd on a user type”
type Flags { bits: int }
impl BitAnd for Flags {
method bitand(other: Flags) -> Flags {
Flags { bits: self.bits & other.bits }
}
}
let a = Flags { bits: 0b1100 }
let b = Flags { bits: 0b1010 }
let c = a & b
print(c.bits) // 8

BitOr, BitXor, Shl, Shr follow the same shape (method names bitor, bitxor, shl, shr).

&& || ! operate on bool values.

OperatorTraitMethodBuilt-in dispatch
&&(built-in only)typed boolean conjunction
||(built-in only)typed boolean disjunction
!Notnot() -> Selftyped Not opcode for bool
let ok = true
let ready = false
print(ok && !ready) // true
print(ok || ready) // true

and / or are accepted as keyword spellings of && / ||.

Both && and || short-circuit: the right-hand operand is evaluated only when the left-hand operand does not already decide the result (false for &&, true for ||). Short-circuit evaluation is identical under the VM and the JIT, so a side-effecting call guarded by && / || runs the same way in both execution modes.

- ! ~ apply to a single operand.

OperatorTraitMethodBuilt-in dispatch
-xNegneg() -> SelfNegInt, NegNumber, …
!xNotnot() -> Selftyped boolean negation
~x(no trait)(built-in only)BitNotInt
print(-10)
print(!true)
print(~0b1010) // -11 — two's complement of 10

Unary negation on an int variable:

let a = 10
print(-a) // -10
type Vec2 { x: int, y: int }
impl Neg for Vec2 {
method neg() -> Vec2 {
Vec2 { x: -self.x, y: -self.y }
}
}
let v = Vec2 { x: 3, y: -4 }
let n = -v
print(n.x) // -3
print(n.y) // 4

Not follows the same single-method shape (method not() -> Self).

There is no compound-assign form for unary operators (no NegAssign / NotAssign trait), and no ~ operator trait — ~ is built-in for integer types only.

c[k] (read) and c[k] = v (write) work on built-in indexable types (Array<T>, HashMap<K,V>, string) and can be overloaded for user-defined types via Index<Key, Value> and IndexMut<Key, Value>.

OperatorTraitMethodBuilt-in dispatch
c[k]Index<K,V>index(key: K) -> VGetElemI64, GetElemF64, TypedArrayGet*, hashmap get
c[k] = vIndexMut<K,V>index_set(key: K, value: V)SetElemI64, hashmap set

Built-in array indexing:

let arr = [10, 20, 30]
print(arr[0]) // 10
print(arr[2]) // 30
// User-defined Index trait — declaration shape (see std::core::index)
type Counter { values: Array<int> }
impl Index<int, int> for Counter {
method index(key: int) -> int {
self.values[key]
}
}

The pipe operator threads a value through a function call. The left-hand value becomes the first argument of the right-hand call.

fn double(x: int) -> int { x * 2 }
fn inc(x: int) -> int { x + 1 }
let result = 5 |> double |> inc // inc(double(5)) → 11
print(result)

Simple assignment and compound assignments cover every binary operator that has an associated trait, plus = itself:

let mut x = 1
x = 5 // simple assignment
x += 2 // Add
x -= 1 // Sub
x *= 3 // Mul
x /= 2 // Div
x %= 4 // Mod
x **= 2 // Pow (built-in only)
x <<= 1 // Shl
x >>= 1 // Shr
x &= 0xFF // BitAnd
x |= 0x01 // BitOr
x ^= 0x10 // BitXor
print(x)
let n = 7
let kind = n % 2 == 0 ? "even" : "odd"
print(kind)
for i in 0..5 {
print(i)
}
for j in 0..=3 {
print(j)
}

a..b is exclusive at b; a..=b is inclusive. Ranges are used in for loops and in slice indexing (arr[1..5]). Open forms (..n, n.., ..) are recognized in syntax but not yet wired to runtime iteration — use closed start..end / start..=end today.

Propagates Err/None from the enclosing function. Inside a function that returns Result<T, E> or Option<T>, expr? unwraps the success value or returns the failure unchanged.

fn try_parse(s: string) -> Result<int, string> {
if s == "ok" { Ok(42) } else { Err("bad") }
}
fn run(s: string) -> Result<int, string> {
let n: int = try_parse(s)?
Ok(n + 1)
}
match run("ok") {
Ok(n) => print(n),
Err(e) => print(e)
}

Attach a context message to an error before propagating.

let cfg = (load_config("shape.toml") !! "failed to load configuration")?

Provide a default when a value is None / null.

let maybe_name: Option<string> = None
let name = maybe_name ?? "anonymous"
print(name)

Short-circuits to None when the receiver is absent:

type Server { port: int }
type Config { server: Server }
let cfg: Option<Config> = None
let port = cfg?.server?.port ?? 8080
print(port)

+ on two object literals produces a merged object. The right operand wins on conflicting keys.

let a = { x: 1, y: 2 }
let b = { y: 20, z: 30 }
let c = a + b
print(c.x) // 1
print(c.y) // 20 — overridden by right operand
print(c.z) // 30

Convert one type to another. as is infallible (uses Into); as Type? returns Option<Type> (uses TryInto).

let n = 42 as number // int → number
print(n)

Type assertion supports comptime field overrides on target types:

let display = value as Percent { decimals: 4 }

See Traits for Into / TryInto / From / TryFrom.

Single-character literals use single quotes; they evaluate to integer code points (useful in interop and parsing).

let newline = '\n'
print(newline)
let smiley = '\u{1F600}'
print(smiley)

A numeric literal followed by % desugars to the literal divided by 100:

let half = 50% // 0.5 (number)
let tax = 7.5% // 0.075
print(half)
print(tax)

From tightest to loosest binding (selected highlights):

  1. Calls, indexing, property access (., ?., [], (...))
  2. Postfix ?, using, as
  3. Unary !, ~, -
  4. ** (exponent, right-associative)
  5. *, /, %
  6. +, -
  7. <<, >>
  8. & (bitwise)
  9. ^ (bitwise)
  10. | (bitwise)
  11. instanceof, comparisons (<, <=, >, >=, ==, !=, ~=, ~<, ~>)
  12. && / and
  13. || / or
  14. .., ..= (ranges)
  15. !! (context) — binds before final ? in lhs !! rhs?, parsed as (lhs !! rhs)?
  16. ?? (coalesce)
  17. ? : (ternary)
  18. |> (pipe)
  19. =, +=, -=, *=, /=, %=, **=, <<=, >>=, &=, |=, ^=

Use parentheses when clarity matters.