Appendix C · Operator table and precedence
Every operator in the language, with its precedence and
associativity. It’s a quick reference: when you’re not sure
how a | f |> g parses, you look it up and move on.
Precedence is numbered from highest (1, binds first) to lowest (8, binds last). Each level resolves against the level immediately above.
C.1 Main table
| Level | Operators | Associativity |
|---|---|---|
| 1 | call f(args), field ., index [i] | Postfix |
| 2 | unary -, not | Prefix |
| 3 | *, /, //, % | Left |
| 4 | +, - (binary), ++ (string concat) | Left |
| 5 | ?? (Option default) | Right |
| 6 | ==, !=, <, >, <=, >= | Non-associative |
| 7 | and | Left (short) |
| 8 | or | Left (short) |
| 9 | |>, |, ||, |?, |! | Left |
Per-level details
- Level 1 (postfix). Left-associative:
a.b.cmeans(a.b).c;f(x)(y)means(f(x))(y). - Level 2 (prefix). Unary
-xandnot x. Apply before any binary operator. - Level 3 (multiplicative).
*and/onIntandReal,//is integer division,%is modulo. Left- associative:a / b / cis(a / b) / c. - Level 4 (additive). Binary
+and-on numbers,++to concatenate strings. - Level 5 (
??). The default for anOption:o ?? dis whatocarries when it isSome, anddwhen it isNone. Right-associative, soa ?? b ?? cisa ?? (b ?? c), and the right side runs only in theNonecase. - Level 6 (comparisons). Don’t chain.
a < b < cis a syntax error; writea < b and b < c. That eliminates the bug class where1 == 1 == trueparses unexpectedly. - Level 7 (
and) and 8 (or). Short-circuit logical.andbinds tighter thanor, soa or b and cisa or (b and c). - Level 9 (pipes). Five operators at the same level:
|>(apply),|(map),||(flat-map),|?(filter) and|!(each). All left-associative:xs | f |> gis(xs | f) |> g.|!returnsUnit, so it ends the pipeline and no pipe can follow it.
C.2 Forms that aren’t operators
Some forms that look like operators in other languages are separate syntactic constructs in kaikai:
=(letbinding): part oflet’s syntax, not an operator.let x = expris a declaration.:=(write to mutable cell): part ofvar’s syntax.name := expris the sugared form ofState.set(name, expr). It doesn’t participate in the precedence table.&exprand@r(Ref[T]cells):&vbuilds a cell holdingvand@rreads it. Both are prefixes and do not participate in the table. Avar’s cell is a different thing: it reads naked, by its name.;(expression separator): separates two expressions written on the same line. A newline does the same, so you rarely need it; at the end of a line it’s harmless and does not discard the expression’s value (§3.7).->(inmatch, in lambdas, inhandle): syntactic separator, not an operator./(in effect signature): separates the return type from the effect row:fn f() : Int / Log. Only appears in signature position.!(propagation ofOption/Result): postfix on expressions returningOption[T]orResult[E, T], performs early propagation of the error orNone. Rough equivalent of Rust’s?. It isn’t a binary operator.?and?name(holes): hole markers, not operators....(spread): appears in record and list literals ([h, ...t],Point { ...p, x: 10 }); also in patterns. Not an operator.<>(unit-of-measure annotation):1.50<USD>,Real<EUR>. The unit sits inside the angle brackets; these aren’t the operators<and>.
C.3 Useful equivalences
Some operators are sugar for calls to stdlib functions. The equivalences help when the operator doesn’t seem to work, or when you want to see the mechanism underneath.
| Operator | Equivalent to |
|---|---|
| `x | > f` |
| `x | > f(_, b)` |
| `xs | f` |
| `xs | |
| `xs | ? p` |
| `xs | ! f` |
a ++ b | string_concat(a, b) (when they’re String) |
a[i] | Mutable.array_get(a, i) |
a[i] := v | Mutable.array_set(a, i, v) |
&v / @r | Ref[T] cell: make and read |
name := v | State.set(name, v) (with var) |
C.4 Reminder: comparison is non-associative
The level 5 rule deserves its own callout: it’s the one most likely to catch you if you come from another language.
In most languages:
1 == 1 == 1 # Python: false (1 == 1 → true, true == 1 → false)
1 < 2 < 3 # C: 1 (1 < 2 → 1, 1 < 3 → 1; not what it looks like)
In kaikai, neither one compiles. The compiler requires you to spell out the intent:
1 == 1 and 1 == 1 # clearly: two comparisons
1 < 2 and 2 < 3 # range: each side compared explicitly
That eliminates a small but irritating class of bugs. If you come from Python and the extra typing annoys you, the rule is the same in Pascal, Eiffel, and SQL. It isn’t a kaikai oddity.