Every operator SCUA understands, what it does, and how tightly it binds. A few have semantics worth knowing exactly, so this page shows verified output for those.
Throughout, remember the truthiness rule: only nil and
false are falsey. 0 and the empty string are
truthy.
#Arithmetic
| Operator | Meaning |
|---|---|
+ |
Add. |
- |
Subtract (and unary negate). |
* |
Multiply. |
/ |
Divide. Always produces a float. |
// |
Integer divide. Floors toward negative infinity. |
% |
Remainder. |
** |
Power. Right-associative. An int to a non-negative
int power stays an exact int; otherwise a
float. |
/ always gives a float. ** keeps it exact
when it can: int ** non-negative-int is an int
(2 ** 10 is 1024), while a float operand or a
negative exponent gives a float (2 ** -1 is
0.5). Floats are inexact, so don't use them for money or
anything that can't tolerate rounding error — reach for the exact decimal
type (12.34d) instead. On decimals, + - * are
exact and / rounds (half-to-even) to a chosen scale; a
decimal and a float never mix.
print(7 / 2) -- 3.5
print(7 // 2) -- 3
print(-7 // 2) -- -4 (floors toward -inf)
print(7 % 3) -- 1
print(2 ** 10) -- 1024 (an int)
print(2 ** 0.5) -- 1.4142135623730951 (a float operand)
$ scua arith.scua
3.5
3
-4
1
1024
1.4142135623730951
** is right-associative, and unary minus binds tighter
than it. That second part is a sharp edge: -2 ** 2 is
(-2) ** 2, which is 4, not
-(2 ** 2).
print(2 ** 3 ** 2) -- 512 = 2 ** (3 ** 2)
print(-2 ** 2) -- 4 = (-2) ** 2
$ scua pow.scua
512
4
#Comparison
==, !=, <,
<=, >, >=. Ints and
floats compare by value, so 1 == 1.0 is true.
== is value equality for every value kind
— including arrays and records: [1, 2] == [1, 2] is true,
and two records with the same fields are equal (so a record stays equal
to its copy after it crosses a tell). It compares deeply,
with quick rejects (a shared reference, or a different length/shape, is
decided without walking the contents).
print(1 == 1.0) -- true
print([1, 2] == [1, 2]) -- true (structural, not reference identity)
print(1 != 2) -- true
print(3 <= 3) -- true
$ scua cmp.scua
true
true
true
true
#Logical
and, or, not. and
and or short-circuit, and they return one of their operands
rather than a forced bool. and returns its left operand if
that operand is falsey, otherwise its right. or returns its
left operand if that operand is truthy, otherwise its right.
not always returns a bool.
print(1 and 2) -- 2 (left is truthy, so the right)
print(nil and 2) -- nil (left is falsey, so the left)
print(nil or "fallback")-- fallback
print(0 or "unused") -- 0 (0 is truthy)
print(not nil) -- true
print(not 0) -- false
$ scua logic.scua
2
nil
fallback
0
true
false
Because or returns the operand, it is a common way to
supply a default. Note the difference from ?? below:
or falls through on any falsey value, while ??
falls through only on nil.
#Bitwise
&, |, ^, and
~ (unary complement) work on integers, along with the
shifts << and >>.
print(6 & 3) -- 2
print(6 | 1) -- 7
print(6 ^ 3) -- 5 (xor)
print(~0) -- -1
print(1 << 4) -- 16
print(256 >> 2) -- 64
$ scua bits.scua
2
7
5
-1
16
64
^ is bitwise xor here, not power. Power is
**.
#Nil-coalescing
a ?? b
Evaluates to a unless a is
nil, in which case it evaluates to b. Unlike
or, it only treats nil as missing, so a
false or 0 on the left passes through.
print(nil ?? "def") -- def
print(false ?? "def") -- false (not nil, so it stays)
print(5 ?? 9) -- 5
$ scua coalesce.scua
def
false
5
#Result propagation
expr?
A postfix operator for results. If
expr is Ok(v) it evaluates to v.
If expr is Error(e) it returns that
Error from the enclosing function immediately. It is how
you chain fallible calls without a pile of matches. See Errors and faults.
fn parse_amount(s)
if s < 0 then return Error("amount cannot be negative") end
return Ok(s)
end
fn charge(balance, amount)
let a = parse_amount(amount)? -- unwrap, or return the Error
if a > balance then return Error("insufficient funds") end
return Ok(balance - a)
end
print(charge(100, 30))
print(charge(100, -5))
$ scua propagate.scua
Ok(70)
Error(amount cannot be negative)
#Path read
root @ "a/b/c"
Read into nested data along a /-separated path. A
missing segment reads as nil, so it pairs naturally with
?? for a default. The right side can also be a
path"..." value. As an assignment target,
root @ "a/b" = v writes, creating intermediate tables as
needed. See get/set.
let player = { stats = { gold = 100 } }
print(player @ "stats/gold") -- 100
print(player @ "stats/guild" ?? "none") -- none
$ scua path.scua
100
none
@ binds more tightly than arithmetic, so
player @ "stats/gold" + 50 reads the field first, then
adds.
#Membership
value in array
True when array contains value. (The word
in also separates the loop variables from the iterable in a
for loop; that is a different use.)
print(2 in [1, 2, 3]) -- true
print(9 in [1, 2, 3]) -- false
$ scua membership.scua
true
false
#Contract test
value matches Contract
True when value satisfies a contract. It is a plain boolean check
and allocates nothing.
contract Positive(n)
pos: n > 0 else "must be positive"
end
print(5 matches Positive) -- true
print(-3 matches Positive) -- false
$ scua matches.scua
true
false
#Concatenation
a .. b
Join two values into a string, converting non-strings as
tostring would. Backtick interpolation,
`hp {x}`, is usually nicer; .. is there when
you want it.
print("a" .. "b" .. 3) -- ab3
$ scua concat.scua
ab3
On bytes, .. is binary
concatenation, not text. bytes .. bytes gives you
bytes, which is how you build up a payload:
let payload = b"" .. b"ab" .. b"cd" -- bytes, 4 long
And you cannot mix the two.
bytes .. string is an error, never a silent conversion:
cannot concatenate `bytes` with a string — `..` never coerces between bytes and text.
Convert the right side: `bytes.from_string(s)` to build bytes, or `bytes.to_string(b)` to build text
That's deliberate. Bytes and text are different things, and quietly turning one into the other is how a binary payload ends up on the wire as the text of its own hex dump. Say which you meant.
.. has a second, unrelated use inside patterns, where it
is the rest element. See the next section.
#Rest in patterns
Inside an array or table pattern, .. matches the
remaining elements. ..name binds them to a name; a bare
.. matches them without binding. See Pattern matching.
match [1, 2, 3, 4]
[first, ..rest] -> print(`first {first}, rest {rest}`)
_ -> print("no match")
end
$ scua rest.scua
first 1, rest [2, 3, 4]
#Ranges
lo:hi
lo:hi:step
A half-open range [lo, hi), optionally with a step. It
is most often used in a for loop, where it allocates
nothing. Call .reversed() on it to count down over the same
half-open set, so the low bound is included. See Control flow.
for i in 0:5 do print(i) end -- 0 1 2 3 4
for i in 0:10:2 do print(i) end -- 0 2 4 6 8
for t in (1:6).reversed() do print(t) end -- 5 4 3 2 1
The same : syntax slices an array or string, with either
bound optional: row[1:3], row[:2],
row[3:]. See Collections.
#Precedence
Binary operators bind in this order, loosest at the top. Operators on
the same row share a level. ** is right-associative; the
rest are left-associative.
| Level | Operators |
|---|---|
| loosest | ?? |
or |
|
and |
|
== != < <=
> >= in
matches |
|
| |
|
^ |
|
& |
|
<< >> |
|
.. |
|
+ - |
|
* / // % |
|
** |
|
| tightest | @ |
The unary operators -, not, and
~, and the postfix forms (a call f(), an index
a[i], a field a.x, a slice
a[i:j], and ?) bind more tightly than any
binary operator. The one surprise, shown above, is that this puts unary
minus ahead of **, so -2 ** 2 is
4.
Two consequences worth noting, both different from C:
- The bitwise operators bind tighter than the comparisons. So
flags & Mask != 0reads as(flags & Mask) != 0, the grouping you want. - Arithmetic binds tighter than the shifts. So
1 << 2 + 1is1 << (2 + 1), which is8.
print(6 & 3 == 2) -- true = (6 & 3) == 2
print(1 << 2 + 1) -- 8 = 1 << (2 + 1)
print(1 + 2 * 3) -- 7 = 1 + (2 * 3)
$ scua prec.scua
true
8
7