Skip to content

Operators and casts

Prismio 0.1 arithmetic, comparison, logical, bitwise, shift, unary, assignment, and cast operators.

Last verified

Operators form expressions from typed operands. Prismio provides arithmetic + - * / %, comparison == != < <= > >=, short-circuit logic and or, none-coalescing ??, bitwise & | ^, shifts << >>, and unary ! - ~. Explicit casts use expression as Type. String has its own set — see String operators below.

Operators are statically checked. Prismio generally requires matching operand types and does not insert C-style integer promotions or integer-to-float conversions.

Arithmetic

+, -, *, /, and % operate on compatible numeric values. On integers % takes the dividend's sign (-7 % 2 is -1; floorMod takes the divisor's). On Float it is C's fmod: -7.5 % 2.0 is -1.5, and x % 0.0 is NaN. Unary - negates a signed integer or floating-point value, and on a Float it flips only the sign, so -(0.0) is -0.0.

Float comparisons follow IEEE 754: NaN != NaN is true, and ==, <, <=, >, >= with a NaN operand are false.

prismio
fn main() -> Int {
    let width: Int = 7
    let height: Int = 6
    let area = width * height
    let remainder = area % 5
    return area + remainder - 44
}

Division or remainder by zero, signed overflow, and the most negative signed value divided by -1 are not recoverable Prismio exceptions. Avoid those inputs; the exact backend outcome is not a portable program contract.

Comparisons

== and != compare compatible scalar values, fieldless enums, and supported optional/none cases. <, <=, >, and >= order compatible numeric values. Comparisons return Bool.

prismio
fn main() -> Int {
    let small: U8 = 4
    let normal: Int = 10
    if (small < normal) { return 0 }
    return 1
}

The two typed operands do not gain an implicit common width. Give the comparison one exact type, for example by widening small as Int.

Logical operators

and, or, and ! operate on Bool. and evaluates its right operand only when the left operand is true. or evaluates its right operand only when the left operand is false.

prismio
if (index >= 0 and index < limit) {
    println(index)
}

Prismio uses the words and and or, not && and ||. not is a second spelling of !, and is and is not of == and !=: if x is not none and not done { }. They are read into the same nodes as the symbols, is ranks with == and not binds as ! does, to the next operand only (not x == y negates x; write not (x == y) for the comparison). is sits between its operands as in Python, so write x is 3, never x is(3): a ( touching the word is an error, and a grouped operand takes a space (x is (y + 2)).

None-coalescing

a ?? b is a when the optional a has a value, and b when it is none. b is evaluated only in that case.

prismio
import std.io
import std.option

fn main() {
    let found: Int? = none
    let other: Int? = 4
    println(found ?? 10)           // 10
    println(found ?? other ?? 10)  // 4
    println(other ?? 10 + 1)       // 4: `+` binds tighter than `??`
}

The result is a T when b is a T, and a T? when b is one. ?? needs import std.option, because it is a call to optionCoalesce in that module, and its left side must be an optional: 5 ?? 6 is refused.

Bitwise operators and shifts

&, |, ^, and unary ~ operate on compatible integers. << and >> shift an integer value. Right shift is arithmetic for signed types and logical for unsigned types.

Keep shift counts within the bit width of the left operand. Negative or oversized counts do not have a stable source-level safety guarantee.

prismio
let permissions: U8 = 5
let readable = (permissions & (1 as U8)) != (0 as U8)

Assignment

Assignment forms are =, +=, -=, *=, /=, %=, &=, |=, and ^=. Direct and compound assignment to a binding require mut.

prismio
fn main() -> Int {
    let mut bits: U8 = 1
    bits |= 4 as U8
    bits ^= 1 as U8
    return (bits as Int) - 4
}

Compound assignment is supported for plain variable bindings. It is not currently generalized to struct fields or indexed collection places. Assignment is a statement operation, not a value expression to embed inside a condition.

Precedence

From lowest to highest, binary precedence is:

  1. or
  2. and
  3. == !=
  4. < <= > >=
  5. ??
  6. |
  7. ^
  8. &
  9. << >>
  10. + -
  11. * / %

Unlike C, Prismio deliberately binds bitwise operators more tightly than equality, so flags & mask != 0 groups as (flags & mask) != 0. Right shift is arithmetic for signed values and logical for unsigned values.

Unary operators and casts bind more tightly than the binary levels above, and a unary operator binds inside a cast: -x as T is (-x) as T. Parentheses are recommended whenever a mixed bitwise, comparison, and logical expression would otherwise require the reader to recall the complete table.

Operators at the same precedence normally group left-to-right; ?? is the exception and groups right-to-left, so a ?? b ?? c tries a, then b, then c. Do not use associativity to hide numerically significant grouping, especially for subtraction, division, shifts, or floating-point expressions.

Casts

Operands generally require exact compatible types. There are no implicit integer-width or integer/float promotions; cast explicitly.

prismio
let source: I16 = -12
let wide: I64 = source as I64
let magnitude: Float = (wide as Float) * -1.0

The documented conversions are:

  • widening a signed integer sign-extends;
  • widening an unsigned integer, Bool, or Char zero-extends;
  • narrowing an integer keeps the low-order bits;
  • integer-to-float converts numerically but may lose precision;
  • float-to-integer truncates toward zero and saturates: out-of-range values clamp to the destination's MAX/MIN, and NaN becomes 0;
  • x as String is the text x.toString() gives, for every number, Bool, Char and String;
  • an enum converts to its ordinal with as Int, and nothing converts to Bool.

as T states intent; it does not validate a range. When the value may not fit, write as T?: it is none rather than a truncated or saturated number -- 300 as U8?, -1 as U64?, 3.5 as Int?, and from text "42" as Int?. See conversions for the full rules.

A prefix operator binds inside a cast: -x as T is (-x) as T, so -1 as U8 is 255.

String operators

String supports five operators. Each is rewritten during semantic analysis into the std.string call it means, so all of them need import std.string, and two of them allocate exactly as the call they stand for does.

OperatorMeansAllocates
a == b, a != bcontent equality, same answer as a.equals(b)no
a < b, a <= b, a > b, a >= bsign of a.compare(b) — byte orderno
a + ba.concat(b)yes
s[i]s.charAt(i) — the byte at i, NUL out of rangeno
s[start..<end]s.slice(start, end) — stops before end, clampedyes
s[start..end]s.slice(start, end + 1) — includes end, clampedyes
prismio
import std.io
import std.string

fn main() -> Int {
    let name = "prismio"

    if (name == "prismio") { println("matched") }
    if (name < "rust") { println("sorts first") }

    let head = name[0..<4]
    let label = "lang: " + head
    println(label)
    return 0
}

A String is a pointer, so a raw address comparison would quietly answer "not equal" for two equal strings. == is the content comparison, and there is no way to spell the address one.

+ and [a..b] produce an owned value, released like any call's result: println(a + b) frees the joined string once println returns.

A chain of + is one call, not a nest of them: a + b + c becomes a.concat(b, c), one allocation rather than one per +. Up to six parts are supported; past that, build a Vec<String> and call join on it.

Both sides of + must be Strings. Nothing is promoted implicitly, so write "n = " + count.toString().

No user-defined operator overloading

Prismio 0.1 has traits and impl blocks, but a trait cannot give a type an operator: the String operators above are compiler desugarings for one built-in type, not an overloadable protocol. Structs and lists do not acquire arithmetic or comparison behavior merely because their fields or elements support it. Write a named function for domain-specific operations.