Rill v0.13 Reference

Chapter 3

Expressions

Everything is an expression; there are no statements that don't produce a value (except bindings).

1 + 2 * 3            # arithmetic: + - * / %  (% is Int only)
a == b               # equality on any value type, structural for data
a < b                # ordering: Int, Float, Str, or a type with Ord
x && y || !z         # logical operators, short-circuiting
"a" + "b"            # string concatenation
f(x, y)              # call
\x, y -> x + y       # lambda
xs |> map(f) |> sum  # pipeline: x |> f(a) means f(x, a)
if c then a else b   # conditional (an expression, not a statement)
f(x)?                # the value inside an `Ok`, or the function's `Err`
p.x                  # one field of a one-constructor type

Bit operations are functions rather than operators: bit_and, bit_or, bit_xor, bit_not, shl and shr (shr is logical, since protocol code works with bit patterns).

Precedence, loosest to tightest: |>, ||, &&, comparisons, + -, * / %, unary - !, then the postfix [], ? and ., calls and literals.

Tuples

(a, b) hands back more than one thing without declaring a type for it:

fn divmod(a, b) = (a / b, a % b)

fn main() =
  (q, r) = divmod(17, 5)      # a binding that takes the value apart
  println(q)

A tuple holds two to eight values, and (A, B) is its type. It is an ordinary data type underneath β€” Tuple2 to Tuple8 in the prelude β€” so it compares, shows and pattern-matches like any other, and (n, s) -> ... is a pattern anywhere a pattern goes.

A binding may take apart any type with exactly one constructor: Parsed(v, rest) = parse(s) reads as a binding because it cannot fail. On a type with more than one, the compiler says the match is not exhaustive, which is the truth: use match.

== and != on floats follow IEEE 754, as C and Python do: a NaN is equal to nothing, itself included, so x == x is false for one and x != x is true β€” which is how a program asks whether a number is a number.

Lanes: Float2

Float2 is two Floats that travel together in one 128-bit register β€” what a kernel written with the machine's vector lanes in mind is written in:

p = float2(1.5, -2.0)
q = p * splat2(4.0) + float2(0.25, 0.25)   # both lanes at once: (6.25, -7.75)
lane0(q) + lane1(q)
v = f2_load(buf, i)                        # buf[i] and buf[i + 1], as one
f2_store(buf, i, fma2(a, b, v))            # a fused multiply-add on both lanes
fmin2(a, b)                                # the smaller of each lane; fmax2 too

+ - * / work lane by lane with the same one loosening as a Float (a multiply and the add that consumes it may fuse), == is of both lanes, fmin2 and fmax2 are the smaller and the larger of each lane, and a Float2 prints as a pair, goes in a map or down a channel, and sits in a flat record like any number. It is not a general vector type β€” no Float4, no lane-wise < β€” but it is what the matrix product in lib/num/grid.rill keeps its block of answers in, and it is why that product runs at the speed of a BLAS. The rest of a numeric loop needs none of this: a plain for over a buffer is vectorized by the compiler on its own.

The one loop that is not is a reduction over Float, and the reason is the language keeping its word rather than the loop being poor. Vectorizing a sum means splitting it into independent partial sums and adding those at the end β€” which is reassociation, and reassociation changes the answer, so a compiler may only do it once told the program will accept a different one. Rill never tells it that. So the additions stay in the order they were written, each one waiting on the last, and a sum that should stream at memory speed runs at the latency of fadd β€” about six times slower, measured over a range that fits in L1. Clang with strict floating point does exactly the same thing, for exactly the same reason.

Writing the accumulators out is what recovers it: several of them so the adds are independent, in Float2 so each holds two lanes. lib/num/col.rill has the column aggregates already written that way β€” sum, sum_sq, dot, mean, variance, min_of, max_of β€” and they run five to eight times faster than the plain loop, at better than 100 GB/s, matching a C loop built with -ffast-math while leaving + on a Float meaning what it says everywhere else. fmin2 and fmax2 need none of that trade, since a minimum does not care what order it sees its candidates in.

String builders

A string is a value, so building one a piece at a time with + copies what is already there every time. A StrBuf is storage for the bytes on their way to becoming one β€” a Buf for text:

b = strbuf()
b += "hello, "        # a string goes in as it is
b += "world"
b += 33               # a number goes in as one byte: `!`
println(strbuf_str(b))
println(#b)           # how many bytes it holds

b += s is the one op= whose target is a name, and it changes no binding: what it adds to is the storage the name points at, exactly as v[i] = x writes a buffer. strbuf_add(b, s) and strbuf_byte(b, n) are the same two operations spelled out. A builder is not a value type β€” it does not show, compare or cross a channel; strbuf_str takes the string out of it, and the builder stays usable. strbuf_clear(b) empties one without giving back the room it took, which is what a page rendered over and over into the same builder wants.

None of the three is a call. An append is written where it stands: the room is one compare against a capacity the compiler is holding in a register, a literal goes in as whole words because its length is known, and b += int_to_str(n) writes the digits themselves rather than a string that exists only to be copied. The runtime is asked for one thing only, a bigger block, and it is handed the block rather than the builder β€” so the length and the capacity survive the call in registers, and a loop of appends reads the header once rather than after every write. benchmarks/template is what that adds up to on a page.

Slices

s[i:j] is the bytes of a string from i up to but not j β€” the same half-open range for counts over. Either end may be left out for the string's own:

s[1:#s - 1]      # everything but the first and last byte
s[3:]            # from the fourth byte to the end
s[:3]            # the first three

An end outside the string is clamped rather than an error, as str_sub has always been, and each part is evaluated once. Slicing is for strings; a Buf is storage and is not copied by notation this quiet.

Characters

#s, s[i] and s[i:j] are bytes, and stay bytes: a lexer wants bytes, a wire protocol wants bytes, and the compiler that compiles the prelude is one of them. What bytes are not is characters. "gΓΌnaydΔ±n" is ten bytes and eight letters, and s[0:2] cuts the ΓΌ in half. The characters live on top, in the prelude, and say so in their names:

char_count(s) how many characters
char_at(s, i) the ith character, as a codepoint
char_sub(s, i, n) n characters from the ith
char_take(s, n) / char_drop(s, n) the first n, or all but the first n
chars(s) every character, a List(Int) of codepoints
from_char(c) / from_chars(cs) back to a string, one to four bytes each
char_decode(s, at) / char_wide(b) the character at a byte offset, and how many bytes the one beginning with byte b takes β€” for walking a long string without counting from the start each time

All of it walks, since UTF-8 gives no other way to the nth character. Malformed input never hangs it: a stray continuation byte counts as one character, and a character cut off by the end of the string comes back as its first byte.

chr(n) is a byte, not a character: it writes the low byte of n and nothing else, which is right for building a frame or a hash and wrong for every character above 127. A codepoint becomes text with from_char.

Blocks and bindings

An indented block runs its lines in order and evaluates to the last one:

fn area_of_two() =
  a = area(Circle(1.0))     # binding, visible for the rest of the block
  b = area(Rect(2.0, 3.0))
  a + b                     # the block's value

Bindings are immutable. There is no assignment operator β€” but a name may be bound again: x = x + 1 reads the x there was to bind a new one, and from then on x means the new one, while anything that captured the old one keeps it. What a block binds is the block's: a name bound in one branch of an if is not there after it, and a rebinding inside a branch shadows only inside it.

Pattern matching

fn classify(l) = match l
  Nil -> "empty"
  Cons(Some(0), _) -> "starts with zero"
  Cons(Some(n), Cons(Some(m), _)) -> "two: " + int_to_str(n + m)
  Cons(_, _) -> "something else"

Patterns nest freely and may be: _ (wildcard), a name (binds the value), an integer literal, or a constructor with sub-patterns. The compiler checks that a match covers every case β€” if not, it names one that is missing β€” and rejects arms that earlier arms already cover.

An arm may carry a guard: what a pattern cannot say, written after it.

fn size(l) = match l
  Cons(x, _) if x > 100 -> "starts big"
  Cons(x, _) if x > 0 -> "starts small"
  Cons(_, _) -> "starts at or below zero"
  Nil -> "empty"

The arm is taken when the pattern matches and the condition holds; a false condition falls through to the arms below, which the value is then matched against as though the guarded arm were not there. So a guarded arm covers nothing: the first two arms above leave Cons(_, _) to be answered, and without the third the match is not exhaustive β€” the checker says so, naming Cons(_, _). For the same reason a guarded arm makes nothing below it unreachable, while an unguarded one above it still does.

A guard sees the pattern's bindings and everything else in scope, and is an ordinary expression of type Bool. It costs what it says: the arms below a guarded one are tested again when its condition fails, rather than every value being tested once as an unguarded match arranges.

A match written across lines uses an indented block. Inside parentheses, where indentation carries no meaning, the arms simply follow one another on the line β€” nothing else in the grammar can continue an expression with pattern ->, so the two forms are unambiguous:

println(match o None -> 0 Some(n) -> n * 10)

select follows the same rule.

An arm's body is an inline expression, or β€” like a function's body β€” an indented block of statements. The arrow ends its line and the block sits one level deeper:

fn area(s) = match s
  Circle(r) ->
    pi = 3.14159
    pi * r * r
  Rect(w, h) -> w * h

if branches take the same block form, with else standing level with its if, and else if chains staying flat:

fn describe(n) =
  if n < 0 then
    m = -n
    "negative " + int_to_str(m)
  else if n == 0 then "zero"
  else
    "positive " + int_to_str(n)

The value of a block is its last statement's value, exactly as in a function body; every arm and branch must still agree on one type.

Carrying an error out: ?

Errors are Result values, which is the honest way round β€” a function that can fail says so in its type and a caller cannot forget. What that used to cost was a match per call, two of whose three lines were the same two lines every time. ? is those two lines: the value inside the Ok, or else the whole function's Err.

fn parse(text) =
  host = field(text, "host")?
  port = number(field(text, "port")?)?
  Ok(Config(host, port))

It is a match and nothing else. The rest of the block becomes the Ok arm, so the two lines above mean:

match field(text, "host")
  Ok(host) -> ...the rest of the block...
  Err(e) -> Err(e)

Which is where the one rule comes from: a ? belongs where the block it is in is what the function returns, because that is the door its Err leaves by. A ? in the middle of an expression is fine β€” number(field(text, "port")?)? names the inner result first, in the order the expression evaluates β€” and so is one inside an if branch or a match arm, as long as that branch is the function's answer. One written where the block is worth something else says so, and says what that something else is.

Nothing is added at run time: the match is the one that would have been written, an error travels no further than it did, and a tail call after a ? is still a tail call.

Layers have their own kinds of error, and the function's error type says how the lower ones are carried: a sum type with a constructor per kind.

type AppErr
  Io(IoErr)
  Parse(ParseErr)

fn load(path) -> Result(Config, AppErr) =
  text = read(path)?         # Err(e) -> Err(Io(e))
  Ok(parse(text)?)           # Err(e) -> Err(Parse(e))

Where the error a ? meets is not the function's, it goes out inside the one constructor of the function's error type that takes it β€” still the match that would have been written, only with the wrapping in it. Nothing converts that the type did not declare: two constructors that take it, or none, and the ? says so and asks for map_err(r, f)?, which rewrites the error before the ? carries it. The function's error type has to be known for this: written as its return type, or settled already by the block. ok_or(m[k], "no such key")? gives an absence the error it should be.

? works on an Option the same way, in a function that answers one: the value inside the Some, or else None. It does not cross between the two β€” an Option in a function that answers a Result is ok_or(o, e)?, the error being something only the program can say, and the error says so.