Chapter 4: Do the Math (and Then Some): Operators & Expressions

In Chapter 3, we focused on names: let, const, scope, and how bindings differ from the values they refer to.

Now we finally get to the part where sleepy raccoons become productive: expressions.

Expressions are how ZuzuScript computes values.

  • 1 + 2 is an expression.
  • name _ "!" is an expression.
  • score > 10 and not tired is an expression.
  • left union right is an expression.

If Chapter 3 gave us labeled boxes, Chapter 4 teaches us how to do useful work with what is inside the boxes.

As in the previous chapters, this material is written against the Perl implementation, and examples follow syntax validated by the language ztests and POD-backed runtime docs.

4.1 What counts as an expression?

An expression is anything that evaluates to a value.

let naps := 2 + 1;
let mascot := "Zia" _ " the sleepy";
let ready := naps < 3 and true;

Each right-hand side computes a value:

  • 2 + 1 -> number,
  • "Zia" _ " the sleepy" -> string,
  • naps < 3 and true -> boolean-like truth result.

You can nest expressions as deeply as needed:

let label := `status=${ ( 8 ÷ 2 ) > 3 ? "ok": "hmm" }`;

The parser evaluates this using precedence rules we will cover later.

4.2 Arithmetic operators (with Unicode aliases)

Let’s start with the daily coffee math.

Core numeric operators

let a := 7 + 5;      # 12
let b := 7 - 5;      # 2
let c := 3 * 4;      # 12
let d := 3 × 4;      # 12 (unicode alias)
let e := 9 / 4;      # 2.25
let f := 9 ÷ 4;      # 2.25 (unicode alias)
let g := 2 ** 5;     # 32
let h := 17 mod 5;   # 2

A practical note: this guide prefers Unicode forms (×, ÷, , , etc.) when available, but ASCII forms are fully supported and are used in many real scripts.

Numeric coercion in the Perl runtime

In the Perl implementation, numeric operators coerce operands to numbers using runtime rules. That means values like booleans and numeric-looking strings can still participate in arithmetic.

say "12" + 5;     # 17
say null + 3;     # 3

This is powerful, but you should still keep your intent explicit. When a value should be numeric, keep it numeric on purpose.

4.3 Comparison operators: numeric, string, and type-aware

One of ZuzuScript’s friendliest design choices is splitting comparison families by intent.

Numeric comparisons

Use numeric operators for numeric meaning:

say 2 = 2;
say 2 != 3;
say 2 ≠ 3;
say 2 < 3;
say 2 <= 2;
say 2 ≤ 3;
say 3 >= 3;
say 3 ≥ 2;
say 1 <=> 9;      # -1
say 9 ≶ 1;        # 1
say 4 ≷ 4;        # 0

Yes, in ZuzuScript = is numeric equality inside expressions. Declaration and assignment still use :=, so there is no ambiguity.

String comparisons

Use lexical string operators when comparing text:

say "abc" eq "abc";
say "abc" ne "abd";
say "b" gt "a";
say "a" lt "b";
say "a" cmp "b";     # -1

Case-insensitive variants are also available:

say "A" eqi "a";
say "B" gti "a";
say "a" cmpi "B";    # -1

Type-aware equality

== checks type-aware equality, not loose coercive equality.

say 1 == 1;       # true
say 1 ≡ 1;        # true
say "1" == 1;    # false (different types)
say 1 ≢ "1";     # true

This catches whole classes of bugs early, especially in scripts that mix user input, config text, and numeric data.

4.4 Boolean logic and truthiness

Boolean expressions are where conditions get assembled.

Logical operators

say true and true;
say false or true;
say true xor false;
say not false;

Unicode aliases also exist:

say true ⋀ true;
say false ⋁ true;
say true ⊻ false;
say ¬false;

nand and are available too:

say true nand true;   # false
say true ⊼ false;     # true

Truthiness (important in conditionals)

In the Perl runtime, condition evaluation uses a truthiness model:

  • falsy scalars include null, "", "0", "0.0", and numeric 0,
  • non-empty collections are truthy,
  • empty collections are falsy,
  • objects are truthy unless they provide custom boolean behaviour.

That means this is valid and common:

let queue := [ ];
if ( not queue ) {
  say "No tasks yet.";
}

And this is also common:

let queue := [ "lint" ];
if ( queue ) {
  say "Work exists (unfortunately).";
}

4.5 String expressions: concatenation and text helpers

ZuzuScript uses _ for string concatenation.

let name := "Zia";
let msg := "Hello, " _ name _ "!";
say msg;

Assignment form _= is useful for builders:

let line := "TODO:";
line _= " drink coffee";
line _= " and write tests";

Template interpolation

Template strings let you embed expressions directly.

let sleepy := true;
say `Zia mood=${ sleepy ? "sleepy": "awake" }`;

Interpolation evaluates full expressions, not just variable names.

to_String hooks for objects

In the Perl implementation, non-string objects can participate in string contexts by implementing to_String.

class Label {
  let value;

  method to_String () {
    return "L:" _ value;
  }
}

let label := new Label( value: 7 );
say "x=" _ label;      # "x=L:7"

This makes object output feel natural in logs, templates, and messages.

4.6 Set and bag operators (the fun part)

Chapter 2 introduced sets and bags. Now we operate on them.

Membership

Use in, , and to test membership:

say 2 in[ 1, 2, 3 ];
say 2 ∈ << 1, 2, 3 >>;
say 4 ∉ << 1, 2, 3 >>;

Union, intersection, and difference (sets)

let left := << 1, 2 >>;
let right := << 2, 3 >>;

let u1 := left union right;
let u2 := left ⋃ right;

let i1 := left intersection right;
let i2 := left ⋂ right;

let d1 := << 1, 2, 3 >> \ << 2 >>;
let d2 := << 1, 2, 3 >> ∖ << 2 >>;

Subset, superset, and set-equivalence

say << 1, 2 >> subsetof << 1, 2, 3 >>;
say << 1, 2 >> ⊂ << 1, 2, 3 >>;

say << 1, 2, 3 >> supersetof << 1, 2 >>;
say << 1, 2, 3 >> ⊃ << 1, 2 >>;

say << 1, 2 >> equivalentof << 2, 1 >>;
say << 1, 2 >> ⊂⊃ << 2, 1 >>;

Bags and multiplicity

Bags keep duplicate counts, so operations should be read with multiplicity in mind.

let snacks := <<< "cookie", "cookie", "tea" >>>;
say snacks.count("cookie");

As we go deeper in Chapter 8, we will map out exactly how each bag operation treats counts.

4.7 Ranges are expressions too

Range syntax is start ... end and can be used inside arrays, sets, and bags.

say [ 1 ... 5 ];
say [ 5 ... 1 ];

say << 1 ... 4, 3 ... 1 >>;
say <<< 1 ... 2, 2 ... 1 >>>;

Both ascending and descending forms are supported.

For beginners, ranges are a great way to avoid noisy index-building loops when you just need a short sequence.

4.8 Assignment operators as expressions-in-action

You already saw := in Chapter 3. Here are the operator forms that bundle a computation with assignment.

let x := 1;
x += 4;      # 5
x -= 1;      # 4
x *= 3;      # 12
x ×= 2;      # 24
x /= 6;      # 4
x ÷= 2;      # 2
x **= 3;     # 8

String assignment:

let s := "ab";
s _= "cd";

Null-coalescing assignment:

let preferred;
preferred ?:= "coffee";   # assigned
preferred ?:= "tea";      # ignored (already defined)

?:= is excellent for defaults coming from environment, CLI options, or config layers.

In-place regexp replacement assignment with ~=:

let label := "10x20 30x40";
label ~= /([0-9]+)x([0-9]+)/g -> `${m[1]}y${m[2]}`;
label ~= /foo/i → ( m[0] _ "!" );

Inside the replacement expression, m is the regexp match array for that replacement pass only.

These assignment forms also work on path lvalues:

report @ "/meta/count" += 1;
report @@ "/users/*/role" _= "-active";
report @? "/meta/title" ?:= "Untitled";

4.9 Prefix/postfix operators and reference expressions

Increment/decrement

let x := 1;
x++;

let y := 1;
say ++y;

Both prefix and postfix forms are supported for ++ and --.

Path lvalues can also be updated this way, but they should be wrapped in parentheses so the target is unambiguous:

( report @ "/meta/count" )++;
++ ( report @@ "/users/*/age" );
--( report @? "/meta/maybe_count" );

Lvalue references with unary \

The Perl implementation supports creating references to assignable locations (lvalues), including indexes, dict entries, and slices.

let row := [ 10, 20 ];
let slot := \ row[1];

say slot();      # getter
slot(42);        # setter
say row[1];

This is advanced, but very useful for APIs that need a reusable read/write handle to part of a larger structure.

Path lvalues can also produce references:

let title_ref := \( report @ "/meta/title" );
let age_refs := \( report @@ "/users/*/age" );
let maybe_ref := \( report @? "/meta/title" );

As with ++ and --, keep the path expression in parentheses when using unary \ so it is parsed as the intended lvalue target.

4.10 Operator precedence: who binds first?

When you write a mixed expression, precedence rules determine grouping unless you add parentheses.

A practical high-to-low sketch for common day-to-day use:

  1. **
  2. *, ×, /, ÷, mod
  3. +, -
  4. _
  5. set operators (union, intersection, \, )
  6. comparisons (=, <, eq, in, ~, path operators)
  7. type-aware equality (==, !=, , )
  8. and, xor, or (low precedence family)
  9. ternary ? : and short ternary ?:
  10. assignment forms (:=, ~=, +=, _=, ?:=)

** is right-associative. Most other binary operators are left- associative.

Parentheses are a kindness

Even if you know precedence, add parentheses when it aids readability:

let ok := ( retries < 3 ) and ( status eq "ready" );
let msg := "n=" _ ( count + 1 );

Readable code wins over clever code every time.

4.11 Expressions with custom object coercion

Objects can hook into expression behaviour by providing methods such as to_Number, to_String, and to_Boolean.

class Numeric {
  method to_Number () {
    return 7;
  }
}

class Flag {
  method to_Boolean () {
    return 0;
  }
}

let n := new Numeric();
let f := new Flag();

say n + 5;        # 12
say f ? 1: 2;     # 2

This is a powerful extension point, but use it with restraint: people reading your code should still be able to predict behaviour quickly.

4.12 Common expression pitfalls (and easy fixes)

Pitfall 1: using numeric operators for strings

# Not ideal when you mean lexical compare:
# if ( user_input > "m" ) { ... }

Use string operators for text intent:

if ( user_input gt "m" ) {
  say "late alphabet";
}

Pitfall 2: forgetting type-aware ==

If you expect coercion, == may surprise you.

say "1" == 1;    # false
say "1" = 1;     # true after numeric coercion

Choose explicitly: strict type-aware comparison, or numeric compare.

Pitfall 3: relying on implicit precedence in dense expressions

If you need to pause and mentally parse it, future-you will too. Use grouping parentheses.

Pitfall 4: treating set and bag operations as interchangeable

Sets answer membership. Bags answer multiplicity. Pick based on what the program is trying to preserve.

4.13 Mini walkthrough: Zia’s coffee task score

Let’s combine several operator families in one small expression pipeline.

let tasks := [ "lint", "test", "docs" ];
let done := << "lint", "docs" >>;
let coffees := 2;

let remaining := << "lint", "test", "docs" >> \ done;
let score := ( done.length() * 10 ) + ( coffees * 3 );
let mood := score >= 20 and not remaining.contains("docs");

say `remaining=${remaining.length()} score=${score}`;
say mood ? "Zia cartwheel-ready": "Zia nap-ready";

This reads close to plain language:

  • compute remaining tasks with set difference,
  • compute a numeric score,
  • compute boolean mood from threshold logic.

That is the essence of expression-driven code.

4.14 What’s next

You now have the operator toolbox needed for meaningful computation:

  • arithmetic and comparison,
  • boolean logic,
  • string composition,
  • collection algebra,
  • precedence and grouping habits.

In Chapter 5, we move from expressions to execution direction: if, loops, switch/match patterns, and the control-flow tools that turn single expressions into full program behaviour.

Next Chapter Chapter 5: Choose Your Own Adventure: Control Flow