Operators
Every operator, its precedence, and the rules it follows. Values and literals
are on Syntax; the functions are on Functions.
Every => line is executed by all five implementations.
- Precedence
- Arithmetic
- Concatenation
- Comparison: three families
- Logic
- Coalescing
- Bitwise
- Assignment
- Building lists and sequencing
- The pipeline operator
.> - Traps for readers of other languages
Precedence
Tightest binding first:
| # | Operators | Associativity | Notes |
|---|---|---|---|
| 1 | x[k] f(…) ( ) x .> f(…) |
left | indexing, call, grouping, forward pipeline |
| 2 | -x |
prefix | numeric negation |
| 3 | * / % |
left | |
| 4 | + - |
left | |
| 5 | & |
left | concatenation |
| 6 | BAND, then BXOR, then BOR |
left | bytes of equal length |
| 7 | ?? ??? |
right | coalescing, short-circuit |
| 8 | == != < <= > >= $== $!= $< $<= $> $>= EQL IN |
none | a comparison cannot chain |
| 9 | NOT x |
prefix | looser than comparison |
| 10 | AND, then XOR, then OR |
left | AND and OR short-circuit |
| 11 | = += -= *= /= %= &= |
right | |
| 12 | , |
left | list building, argument separator |
| 13 | ; |
left | sequence |
Arithmetic
+ - * / % and unary - take numbers — TEXT that reads as
-?digits[.digits] — and compute exactly. There is no floating point
anywhere in SEL.
0.10 + 0.20 == 0.30 => TRUE
2.50 + 2.50 => 5.00
1.5 * 1.5 => 2.25
-(2 - 5) => 3Scale. Addition and subtraction keep the larger scale of the two operands; multiplication adds them. Division gives the minimal scale when the result is exact, and ten fraction digits, rounded half away from zero, when it is not:
4 / 2 => 2
10 / 4 => 2.5
1 / 8 => 0.125
1 / 3 => 0.3333333333
2 / 3 => 0.6666666667
1 / 0 => !E_DIV_ZERO% is the remainder of truncated division; it takes the sign of the dividend:
5 % 3 => 2
-5 % 3 => -2
5.5 % 2 => 1.5An operand that is not a number is refused, and so is NULL:
"a" + 1 => !E_NOT_NUM
NULL * 2 => !E_NULLRounding is a function, not an operator — ROUND(x, n), FLOOR, CEIL,
TRUNC are on Functions. So is CANON, which gives
every number the one spelling all equal numbers share, for the places where the
spelling matters (Comparison).
Concatenation
& joins text, and numbers are text:
"A" & "B" => AB
2 & "A" => 2A
"a" & TRUE => !E_NOT_TEXTIf either side is BIN the result is BIN, with text encoded as UTF-8:
TO_HEX("A" & FROM_HEX("42")) => 4142Comparison: three families
The same two values can be equal as numbers and different as text. That is the point:
"5.00" == "5" => TRUE
"5.00" $== "5" => FALSE
"5.00" EQL "5" => FALSE==!=<<=>>=compare numerically; both sides must be numbers.$==$!=$<$<=$>$>=compare text, byte by byte in UTF-8 order — the same order on every host, whatever the host's own strings do.EQLcompares structure: the same kind, the same scalar, the same children under the same keys in the same order.x IN listis true when some element oflistisEQLtox.
"PL" IN ("DE", "PL") => TRUE
(1, 2) EQL (1, 2) => TRUE
"b" $> "a" => TRUE
"Z" $< "a" => TRUEEQL — and everything built on it: IN, DISTINCT, DEDUPE, BUCKET's keys,
index keys — tells 1, 1.0 and 1.00 apart, because scale is part of a
number. Where a rule means "the same number", it says so with CANON:
1.0 EQL 1 => FALSE
CANON(1.0) EQL CANON(1) => TRUE
LIST(0.5 + 0.5, 4 / 4) .> DEDUPE() .> COUNT => 2
LIST(0.5 + 0.5, 4 / 4) .> MAP(CANON(_)) .> DEDUPE() .> COUNT => 1Logic
AND, OR, XOR and NOT take booleans and nothing else — there is no
truthiness:
TRUE AND FALSE => FALSE
TRUE XOR TRUE => FALSE
NOT FALSE => TRUE
1 AND TRUE => !E_NOT_BOOLAND and OR short-circuit, so the right side is not evaluated when the left
decides:
FALSE AND (1 / 0 == 0) => FALSE
TRUE OR (1 / 0 == 0) => TRUECoalescing
a ?? b yields b when a is NULL — or is a variable or key that does not
exist. a ??? b also yields b when a is vacant: empty text, text that is
only whitespace, or an empty list. Both evaluate b only when they need it.
NULL ?? "default" => default
"value" ?? "fallback" => value
MISSING ?? "fallback" => fallback
R["x"] = 1; R["y"] ?? 0 => 0
"" ?? "fallback" =>
"" ??? "fallback" => fallback
" " ??? "fallback" => fallback
"value" ??? "fallback" => valueThey associate to the right, so A ?? B ?? "last" tries A, then B.
Bitwise
BAND, BOR and BXOR work on bytes, not on integers, and both sides must
be the same length:
TO_HEX(FROM_HEX("0f0f") BAND FROM_HEX("00ff")) => 000f
TO_HEX(FROM_HEX("0f") BXOR FROM_HEX("ff")) => f0
FROM_HEX("0f") BOR FROM_HEX("0f0f") => !E_LEN_MISMATCHAssignment
= stores a value and yields it; += -= *= /= %= &= read the target, apply the
operator and store the result, so they need a target that exists:
A = "x"; A &= "y"; A => xy
A += 1 => !E_UNDEF_VAR
3 = 4 => !E_BAD_ASSIGNThe target is a name, optionally indexed. Intermediate levels are created, index expressions are evaluated once, left to right, before the right-hand side, and the value lands at that path in the tree as it is after the right-hand side ran:
A[COUNT(A)] = 1; A => -{"0"=t"1"}
A = 1; A += (A = 5); A => 6
A[1] = (A = 2); A => t"2"{"1"=t"2"}The last line is worth a second look. A[1] = … stores under key 1 of A, but
the right-hand side replaced A with the number 2 first — so the store lands
in the new A, and the result is 2 carrying a child 1. Most languages would
have written into the old, now unreachable A and silently lost the assignment;
SEL prefers the visible answer to the vanished one, the same way it prefers an
error to a truthy guess. Nobody writes this on purpose, but rules grow in layers,
and the day two layers touch one variable, all five hosts still agree.
Assignment copies. , is the only other operator that copies; everything
else hands on the value itself:
A = (1, 2); B = A; B[1] = 9; A[1] => 1Building lists and sequencing
, builds a list keyed "1", "2", …; an operand that is itself a list
contributes its values, and the keys are always renumbered:
(1, 2), 3 => -{"1"=t"1", "2"=t"2", "3"=t"3"}
A = ("a", "b"); A = (A, "c"); COUNT(A) => 3; evaluates the left, then the right, and yields the right:
A = 2; B = 3; A * B => 6Inside a call, ; binds looser than the argument separator, so a sequence as one
argument needs its own parentheses:
IF(TRUE, (A = 1; A + 1), 0) => 2The pipeline operator .>
x .> F(…) is F(x, …): the value on the left becomes the first argument of the
call on the right. It rewrites the program at compile time, so it costs nothing,
and it lets a chain of steps read in the order they happen:
"hello" .> UPPER => HELLO
"hello world" .> LEFT(5) => hello
(1, 2, 3, 4) .> FILTER(_ % 2 == 0) .> COUNT => 2
(3, 1, 2) .> SORT() .> JOIN(",") => 1,2,3When the call already has at least as many arguments as the function needs and
one of them is a bare _, the value goes there instead of first:
"," .> JOIN((1, 2, 3), _) => 1,2,3.> binds as tightly as indexing, so a pipeline is an operand like any other:
(1, 2, 3) .> COUNT > 2 => TRUE
(5, 1) .> SORT() .> JOIN("") $== "15" => TRUEPipelines are how SEL writes data processing — FILTER, MAP, SORT_BY,
BUCKET, LINK and the rest are ordinary functions, and a chain of them over a
relation is what the SQL layer can turn into one SELECT
(SQL pipelines):
R = LIST(RECORD("c", "a", "v", 2), RECORD("c", "b", "v", 5), RECORD("c", "a", "v", 4)); R .> BUCKET(_["c"], RECORD("c", _K, "total", SUM(_, _["v"]))) .> SORT_BY(_["total"], "DESC") .> MAP(_["c"] & "=" & _["total"]) .> JOIN(" ") => a=6 b=5Traps for readers of other languages
NOT binds looser than a comparison, so this reads as NOT (1 == 2):
NOT 1 == 2 => TRUEA comparison does not chain — the error arrives when the rule is compiled, not as a puzzling type error later:
1 < 2 < 3 => !E_SYNTAXText from a form is not a number until it is trimmed, and a number compared as text is compared by its bytes:
" 2" + 1 => !E_NOT_NUM
"10" $< "9" => TRUE
10 < 9 => FALSE