SCUA

Manual

Operators and precedence

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 != 0 reads as (flags & Mask) != 0, the grouping you want.
  • Arithmetic binds tighter than the shifts. So 1 << 2 + 1 is 1 << (2 + 1), which is 8.
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