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.
Arithmetic
Section titled “Arithmetic”+ - * / % ** and their compound-assign forms += -= *= /= %= **=.
| Operator | Trait | Method | Compound | Built-in dispatch |
|---|---|---|---|---|
+ | Add | add(other) -> Self | += | AddInt, AddNumber, StringConcatTyped, … |
- | Sub | sub(other) -> Self | -= | SubInt, SubNumber, … |
* | Mul | mul(other) -> Self | *= | MulInt, MulNumber, … |
/ | Div | div(other) -> Self | /= | DivInt, DivNumber, … |
% | Mod | mod(other) -> Self | %= | ModInt, ModNumber |
** | (no trait) | (built-in only) | **= | PowInt, PowNumber |
let a = 10let b = 3
print(a + b) // 13print(a - b) // 7print(a * b) // 30print(a / b) // 3 — integer division (truncates)print(a % b) // 1print(a ** b) // 1000 — exponentiationExponentiation works on both int and number:
print(2 ** 10) // 1024print(2.0 ** 0.5) // 1.4142135623730951Compound 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 = 1x = 5x += 2x -= 1x *= 3x /= 2x %= 4x **= 2print(x) // 4Operator overloading: Add on a user type
Section titled “Operator overloading: Add on a user type”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 + bprint(c.x) // 11print(c.y) // 22Sub, Mul, Div, Mod follow the same shape (method names sub, mul,
div, mod).
** is built-in only
Section titled “** is built-in only”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.
Comparison
Section titled “Comparison”== != < <= > >= and the cmp integer-ordering convention.
| Operator | Trait | Method | Built-in dispatch |
|---|---|---|---|
==, != | Eq | eq(other) -> bool | EqInt, EqNumber, EqString, … |
<, <=, >, >= | Ord | cmp(other) -> int | LtInt, 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) // trueprint(a == b) // falseprint(a < b) // trueprint(b > a) // trueEq and Ord are independent — a type that participates in both == and
< must implement both traits.
instanceof type test
Section titled “instanceof type test”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")}Fuzzy comparison (~=, ~<, ~>)
Section titled “Fuzzy comparison (~=, ~<, ~>)”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) // trueBitwise
Section titled “Bitwise”& | ^ ~ << >> and the compound assigns &= |= ^= <<= >>=.
| Operator | Trait | Method | Compound | Built-in dispatch |
|---|---|---|---|---|
& | BitAnd | bitand(other) -> Self | &= | BitAndInt |
| | BitOr | bitor(other) -> Self | |= | BitOrInt |
^ | BitXor | bitxor(other) -> Self | ^= | BitXorInt |
<< | Shl | shl(other) -> Self | <<= | BitShlInt |
>> | Shr | shr(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 = 0b1100let b = 0b1010
print(a & b) // 8 — bitwise ANDprint(a | b) // 14 — bitwise ORprint(a ^ b) // 6 — bitwise XORprint(~a) // -13 — bitwise NOT (two's complement)print(a << 2) // 48 — left shiftprint(a >> 1) // 6 — right shiftCombined with compound-assigns:
let mut x = 0xF0x &= 0xFFx |= 0x01x ^= 0x10x <<= 1x >>= 1print(x) // 225Operator 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 & bprint(c.bits) // 8BitOr, BitXor, Shl, Shr follow the same shape (method names
bitor, bitxor, shl, shr).
Logical
Section titled “Logical”&& || ! operate on bool values.
| Operator | Trait | Method | Built-in dispatch |
|---|---|---|---|
&& | (built-in only) | — | typed boolean conjunction |
|| | (built-in only) | — | typed boolean disjunction |
! | Not | not() -> Self | typed Not opcode for bool |
let ok = truelet ready = false
print(ok && !ready) // trueprint(ok || ready) // trueand / 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.
| Operator | Trait | Method | Built-in dispatch |
|---|---|---|---|
-x | Neg | neg() -> Self | NegInt, NegNumber, … |
!x | Not | not() -> Self | typed boolean negation |
~x | (no trait) | (built-in only) | BitNotInt |
print(-10)print(!true)print(~0b1010) // -11 — two's complement of 10Unary negation on an int variable:
let a = 10print(-a) // -10Operator overloading: Neg on a user type
Section titled “Operator overloading: Neg on a user type”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 = -vprint(n.x) // -3print(n.y) // 4Not 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.
Indexing
Section titled “Indexing”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>.
| Operator | Trait | Method | Built-in dispatch |
|---|---|---|---|
c[k] | Index<K,V> | index(key: K) -> V | GetElemI64, GetElemF64, TypedArrayGet*, hashmap get |
c[k] = v | IndexMut<K,V> | index_set(key: K, value: V) | SetElemI64, hashmap set |
Built-in array indexing:
let arr = [10, 20, 30]print(arr[0]) // 10print(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] }}Pipe (|>)
Section titled “Pipe (|>)”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)) → 11print(result)Assignment
Section titled “Assignment”Simple assignment and compound assignments cover every binary operator that
has an associated trait, plus = itself:
let mut x = 1x = 5 // simple assignmentx += 2 // Addx -= 1 // Subx *= 3 // Mulx /= 2 // Divx %= 4 // Modx **= 2 // Pow (built-in only)x <<= 1 // Shlx >>= 1 // Shrx &= 0xFF // BitAndx |= 0x01 // BitOrx ^= 0x10 // BitXorprint(x)Ternary (? :)
Section titled “Ternary (? :)”let n = 7let kind = n % 2 == 0 ? "even" : "odd"print(kind)Ranges (.., ..=)
Section titled “Ranges (.., ..=)”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.
Error / Option Operators
Section titled “Error / Option Operators”Try (?)
Section titled “Try (?)”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)}Context (!!)
Section titled “Context (!!)”Attach a context message to an error before propagating.
let cfg = (load_config("shape.toml") !! "failed to load configuration")?Coalesce (??)
Section titled “Coalesce (??)”Provide a default when a value is None / null.
let maybe_name: Option<string> = Nonelet name = maybe_name ?? "anonymous"print(name)Optional Property Access (?.)
Section titled “Optional Property Access (?.)”Short-circuits to None when the receiver is absent:
type Server { port: int }type Config { server: Server }
let cfg: Option<Config> = Nonelet port = cfg?.server?.port ?? 8080print(port)Object Merge (+)
Section titled “Object Merge (+)”+ 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) // 1print(c.y) // 20 — overridden by right operandprint(c.z) // 30Type Assertion (as)
Section titled “Type Assertion (as)”Convert one type to another. as is infallible (uses Into); as Type?
returns Option<Type> (uses TryInto).
let n = 42 as number // int → numberprint(n)Type assertion supports comptime field overrides on target types:
let display = value as Percent { decimals: 4 }See Traits for Into / TryInto / From / TryFrom.
Character Literals
Section titled “Character Literals”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)Percent Literal
Section titled “Percent Literal”A numeric literal followed by % desugars to the literal divided by 100:
let half = 50% // 0.5 (number)let tax = 7.5% // 0.075print(half)print(tax)Precedence
Section titled “Precedence”From tightest to loosest binding (selected highlights):
- Calls, indexing, property access (
.,?.,[],(...)) - Postfix
?,using,as - Unary
!,~,- **(exponent, right-associative)*,/,%+,-<<,>>&(bitwise)^(bitwise)|(bitwise)instanceof, comparisons (<,<=,>,>=,==,!=,~=,~<,~>)&&/and||/or..,..=(ranges)!!(context) — binds before final?inlhs !! rhs?, parsed as(lhs !! rhs)???(coalesce)? :(ternary)|>(pipe)=,+=,-=,*=,/=,%=,**=,<<=,>>=,&=,|=,^=
Use parentheses when clarity matters.