Skip to content

Expressions

do main() {
{1, 2, 3}
{"a", "b", "c"}
{} // Empty array (not to be confused with {:} which is an empty map)
}

All elements in an array literal must be the same type. Mixed-type literals are rejected at compile time:

do main() {
// {1, "two", 3} // error: mixed types in array literal
mut numbers [i64] = {1, 2, 3}
println(numbers)
}

See Maps for map literal syntax, including the {:} empty map literal.

const Point struct {
x i64
y i64
}
const Person struct {
name string
age i64
active bool
}
do main() {
mut a Point = Point{x: 10, y: 20}
mut b Person = Person{name: "Alice", age: 30, active: true}
mut c Point = Point{} // Zero-initialized
println(a)
println(b)
println(c)
}
Operator Description Operand Types Result Type
+ Addition i64, i64 i64
+ Addition f64, f64 f64
+ Concatenation string, string string
- Subtraction i64, i64 i64
- Subtraction f64, f64 f64
* Multiplication i64, i64 i64
* Multiplication f64, f64 f64
/ Division i64, i64 i64 (truncated)
/ Division f64, f64 f64
% Modulo i64, i64 i64

Division by zero produces a runtime error.

+ also concatenates two string operands, producing a new string. Both operands must be strings; mixing a string with any other type is an error (use string interpolation or fmt.format() to build strings from other types). += appends to a mutable string.

Operator Description
== Equal
!= Not equal
< Less than
> Greater than
<= Less than or equal
>= Greater than or equal

Comparison operators return bool.

Comparison operators only work on primitive types (numeric kinds, bool, char, string for equality, enums) and pointer equality (== / != against nil or another pointer of the same pointee type). They are not defined on aggregate types:

  • Arrays — use arrays.is_equal(a, b) for equality.
  • Maps — use maps.is_equal(a, b) for equality; ordering is not defined on maps.
  • Structs — compare individual fields (a.x == b.x).
  • Pointer arithmetic and ordering are not supported; equality (== / !=) on pointers is allowed.
Operator Description
&& Logical AND (short-circuit)
|| Logical OR (short-circuit)
! Logical NOT

Logical AND and OR use short-circuit evaluation: the right operand is not evaluated if the result can be determined from the left operand alone.

Operator Description
in Membership test
not_in Non-membership test
!in Non-membership test (shorthand for not_in)
import @arrays
do main() {
mut numbers [i64] = {1, 2, 3}
mut lookup map[string:i64] = {"a": 1}
mut x i64 = 5
if 3 in numbers { println("3 in numbers") }
if "key" not_in lookup { println("key not in lookup") }
if x in range(0, 10) { println("x in range") }
// if 10 !in range(0, 10) { } // '!in' is shorthand for not_in (don't mix them in one file)
}
Operator Description
= Assignment
+= Addition assignment (string append when the target is a string)
-= Subtraction assignment
*= Multiplication assignment
/= Division assignment
%= Modulo assignment
Operator Description
++ Post-increment
-- Post-decrement
do main() {
mut x i64 = 5
x++ // x is now 6
x-- // x is now 5
}

Grayscale uses keyword operators for bitwise operations. Symbol alternatives (&, ^, |) are unavailable because ^ is the pointer type and dereference sigil and & is used for mutable parameters in function signatures.

Operator Syntax Description Operand Types
bit_and a bit_and b Bitwise AND integer types and char
bit_or a bit_or b Bitwise OR integer types and char
bit_xor a bit_xor b Bitwise XOR integer types and char
bit_not bit_not a Bitwise NOT (complement) integer types and char
bit_shift_left a bit_shift_left n Left shift by n bits integer types and char
bit_shift_right a bit_shift_right n Right shift by n bits integer types and char

bit_not is a prefix operator. All others are infix operators. Results have the same type as the operands.

// Basic operations
do main() {
mut a i64 = 0b1010
mut b i64 = 0b1100
println(a bit_and b) // 8 (0b1000)
println(a bit_or b) // 14 (0b1110)
println(a bit_xor b) // 6 (0b0110)
println(bit_not a) // -11 (bitwise complement)
println(1 bit_shift_left 3) // 8
println(16 bit_shift_right 1) // 8
}

A common use is flag manipulation with named constants:

do main() {
const READ i64 = 0b001
const WRITE i64 = 0b010
const EXEC i64 = 0b100
mut perms i64 = READ bit_or WRITE // set READ and WRITE flags
if perms bit_and READ == READ {
println("readable")
}
if perms bit_and EXEC == 0 {
println("not executable")
}
perms = perms bit_xor WRITE // clear WRITE flag
}

From highest to lowest precedence:

  1. Parentheses: ()
  2. Prefix/Unary: !, - (negation), bit_not
  3. Multiplicative: *, /, %
  4. Additive: +, -
  5. Shift: bit_shift_left, bit_shift_right
  6. Membership: in, not_in
  7. Comparison: <, >, <=, >=
  8. Bitwise: bit_and, bit_or, bit_xor
  9. Equality: ==, !=
  10. Logical AND: &&
  11. Logical OR: ||
do main() {
mut arr [i64] = {10, 20, 30}
mut val i64 = arr[1] // 20
arr[0] = 100 // Modification
mut str string = "hello"
mut c char = str[0] // 'h'
mut m map[string:i64] = {"a": 1}
mut v i64 = m["a"] // 1
}
const Direction enum {
NORTH
EAST
SOUTH
WEST
}
const Point struct {
x i64
y i64
}
do main() {
mut p Point = Point{x: 10, y: 20}
mut x i64 = p.x // 10
p.y = 30 // Modification
mut status i64 = Direction.NORTH // Enum access
println(x)
println(status)
}
do add(a i64, b i64) -> i64 { return a + b }
do greet(name string) -> string { return "Hello, ${name}" }
do main() {
mut sum i64 = add(1, 2)
mut greeting string = greet("World")
println("Hello!")
println(sum)
println(greeting)
}
do main() {
range(0, 10) // 0, 1, 2, ..., 9 (increment)
range(0, 10, 2) // 0, 2, 4, 6, 8 (increment)
range(10, 0, -1) // 10, 9, 8, ..., 1 (decrement)
range(10, 0, -2) // 10, 8, 6, 4, 2 (decrement)
}

Ranges are inclusive of the start value and exclusive of the end value.

A for loop over a range gives its variable the type i64. When any bound is a wide integer (i128, u128, i256, u256), the range runs in the widest of them, every bound and the step are checked as that type (see Sized Types), and the loop variable has that type. A u64 bound of an i64 range, or a u128 bound of an i128 range, crosses signedness and is E3019.

Step validation rules:

  • Positive step (or omitted) expects start ≤ end; negative step expects start ≥ end. A range that violates this (its step points away from end, e.g. range(0, 10, -1)) yields no elements.
  • When such a range is written entirely with integer literals and drives a for loop (for _ in range(0, 10, -1)), the compiler rejects it up front with E9005. If any operand is a variable, there is no diagnostic — the loop body just never runs.
  • start == end is always a valid empty range.
  • Zero step always panics at runtime with P0090, for literal and variable operands alike.