slate

Statements

Declarations

val name = "slate"
var n = 0

val cannot be assigned to again; var can. A val bound to an array or an object still allows that container to be changed — the binding is what is fixed, not the value.

Either may say what it holds, and an annotated var is TypeScript’s let: the declared type is what the name holds for its whole life, so every assignment is checked against it.

val name: string = "slate"
var n: integer = 0

n += 1

print(name, n)
slate 1

A definition is a statement too, and so are type, class and data, each of which belongs to the top level of a file.

using — a binding that releases what it held

using is a val whose value is released when the block around it is left. It is what slate has instead of a finally: the release is written at the line that acquired the thing, where it cannot be forgotten in a branch added later.

made(name) = { dispose: () -> print("released " + name) }

read() =
    using file = made("file")

    print("working")

read()
working
released file

The release runs on every way out of the block, and there are five: falling off the end, a return, a break, a continue, and a fault travelling past. An await and a yield are not ways out — the block has not been left, and the resource is still the running program’s.

made(name) = { dispose: () -> print("released " + name) }

first(xs) =
    for x in xs
        using turn = made("turn " + string(x))

        if x > 1 then return x

    null

print(first([1, 2, 3]))
released turn 1
released turn 2
2

Several in one block are released in reverse, which is the acquisition order run backwards: a later resource may have been made out of an earlier one.

made(name) = { dispose: () -> print("released " + name) }

run() =
    using a = made("a")
    using b = made("b")

    print("body")

run()
body
released b
released a

The protocol is a method named dispose — the way a source is anything with a next. An object with the field has one, a class that writes the method has one for every instance, and every built-in resource answers to it: a socket, a listening server, a database, a Redis client, a WebSocket connection. There is no interface to implement.

null is skipped, so an optional resource needs no branch around the block:

wanted(yes) = if yes then { dispose: () -> print("released") } else null

run(yes) =
    using maybe = wanted(yes)

    print("body")

run(false)
run(true)
body
body
released

A value that is neither null nor disposable is refused at the DECLARATION, before the block has run — the mistake is in the line that acquired the thing, and the reader is told before any work is done rather than after it.

anything(v) = v

run() =
    using a = anything(42)

    print("never reached")

run()
`using` needs a value with a `dispose` method to call when the block is left, and this is an integer

A using is a val, and the three shapes a val has are the three it has. An annotation is checked where the value arrives, a pattern takes the value apart — and a pattern releases the whole value, the names being a way of reading it rather than a list of separate resources.

run() =
    using { port } = { port: 8080, dispose: () -> print("released") }

    print(port)

run()
8080
released

A release that fails while a fault is travelling does not replace it. The original fault is what the program is told about, and the release’s own complaint rides along as its suppressed field. A release that fails with nothing travelling propagates on its own.

fails(why) =
    throw why

run() =
    using a = { dispose: () -> fails("could not release") }

    fails("the original")

val e = run() catch caught -> caught

print(e.message)
print(e.suppressed.message)
the original
could not release

await using is not in slate. A release has to be able to run while a fault is unwinding, and nothing can wait there, so a dispose that answers a promise is called and its answer is not awaited.

A using written at a file’s own top level is released when the file’s statements end, the top level being a block like any other. That is before the event loop drains, so a resource a later turn still needs belongs in a function rather than at the top of a file.

What is in scope where

A definition is HOISTED to the top of the block it is written in, which is JavaScript’s rule, so a name may be used above the line that defines it:

val sink = written

written(r) = "wrote " + string(r)

print(sink(1))
wrote 1

A val or var initialiser stays sequential — it may read what is above it and not what is below — and that is the half that matters: nothing about hoisting makes a program’s data flow depend on where a function happens to sit.

val a = b
val b = 1

print(a)
`b` is not defined

The definitions run where they are hoisted, so a statement standing between two of them is not reordered around anything: only the binding moves, and what it binds is a function that has not been called yet.

A definition may not take a name its own block has already declared, whether by another definition or by a val or a var. The hoisting is the reason: the two would not run in the order they are written, so what the second one means depends on a rule the reader cannot see.

f() = 1

print(f())

f() = 2
`f` is declared twice in this block

A nested block is its own, so shadowing an outer name is untouched — and a val written below a definition is not a collision but the ordinary way to name a generator and then run it:

counting()
    yield 1
    yield 2

val counting = counting()

print(counting.next().value, counting.next().value)
1 2

Assignment

Assignment is a statement, never an expression, so = cannot appear inside an expression and there is nothing for it to be confused with.

var n = 0
var xs = [1, 2]
var o = { f: 0 }
var a = 1
var b = 2

n = 3
xs[0] = 9
o.f = 9
a, b = b, a                 // several places at once

print(n, xs, o, a, b)
3 [9, 2] {f: 9} 2 1

Assignment binds nothing. g = 1 where g was never declared is refused before the program runs, naming the nearest name it does know; val and var are the only things that introduce a name.

The compound forms are += -= *= /= %= and the bitwise &= |= ^= <<= >>=. A compound form evaluates its place once, so xs[next()] += 1 calls next a single time.

Three more write the place only under a condition, and they ask the question the operator they are named after asks:

formwrites when the place isso it leaves
x ??= vabsent — null, or a read that found nothing0, "" and false alone
x \|\|= vfalse by truthinesseverything truthy alone
x &&= vtrueeverything falsy alone

The value is not worked out at all where the place is left alone, which is what these are for and what x = x ?? build() could not promise:

var cache = { one: null, two: 2 }
var built = 0

build()
    built += 1
    "made"

cache.one ??= build()
cache.two ??= build()

var count = 0
count ||= 10                    // zero is there, so `??=` would have left it

print(cache, built, count)
{one: "made", two: 2} 1 10

The place is still worked out exactly once whichever way the test goes, so xs[next()] ??= 1 calls next a single time. They write one place: a, b ??= 1, 2 is refused, a multi-assignment working out every value before it writes any of them.

++ and -- step a name, a field or an element, prefix or postfix.

if

grade(mark)
    if mark >= 90
        "A"
    elif mark >= 80
        "B"
    else
        "C"

print(grade(95), grade(85), grade(20))
A B C

The inline form takes then, and its body is a statement, not an expression — which is the rule that makes the short forms worth having:

first_big(xs)
    for x in xs
        if x <= 2 then continue
        if x > 2 then return x

    null

print(first_big([1, 2, 7, 9]), first_big([1, 2]))
7 null

An if is an expression when every branch answers one: val g = if c then 1 else 2.

Loops

Three of them, and every one is an expression:

var c = 3

while c > 0
    c -= 1

for x in [1, 2]
    print(x)

var n = 0

loop
    n += 1

    if n == 2 then break

print(c, n)
1
2
0 2

do introduces a one-line body — while c do …, for x in xs do …, loop do ….

A for over a range walks every number it covers, and by is how that range takes a step: for i in 0..<10 by 2 counts the evens, and for i in 10..0 by -1 counts down.

What a for walks is an array, bytes, a string, a range, a generator, a set or a map, and anything else is refused naming all seven. A string walks its characters, which is what every other name on a string counts in and what JavaScript’s for...of yields; a map walks its pairs; a buffer walks the number at each position.

for c in "a👋"
    print(c)
a
👋

for await x in source is the fourth, and it walks something that answers next() a value at a time — see Asynchrony.

A for head may take its element apart with a pattern:

for [k, v] in entries({ a: 1, b: 2 })
    print(k, v)
a 1
b 2

An array answers entries() too, with the position for a key, which is how the indexed walk is written — there is no for i, x in xs, a head binding one element:

for [i, x] in ["a", "b"].entries()
    print(i, x)
0 a
1 b

What a loop answers

break is what gives a loop a value. A loop that finishes on its own answers null, or whatever its else clause left:

find_first(xs, wanted)
    for i in 0..<xs.length do
        if xs[i] == wanted then break i
    else
        -1
end find_first

print(find_first([4, 5, 6], 5), find_first([4, 5, 6], 9))
1 -1

The else clause runs when the loop ended without a break, and its value is the loop’s.

Labels

A label says which loop a break leaves, which is the only way out of a nested one:

val found = 'search for a in [1, 2, 3]
    for b in [4, 5]
        if a * b == 8 then break 'search [a, b]

print(found)
[2, 4]

continue starts the next turn, and takes a label the same way.

Leaving early closes what the loop walked

A loop that stops before its iterator is done calls the iterator’s return(), which is JavaScript’s rule. A break, a return, a break or continue to an outer loop and a fault on its way past all close it; a loop that runs out closes nothing, the iterator having ended itself. A generator’s return() leaves its body from the yield it stood at, so what the generator holds in a using is released there — using is what slate has where JavaScript writes finally:

lines() =
    using f = { dispose: () -> print("closed") }

    yield "one"
    yield "two"
    yield "three"

for line in lines()
    print(line)
    if line == "two" then break

print("after")
one
two
closed
after
  • A using inside the loop’s body is released first, then the iterator is closed.
  • for await closes the same way, asking an object with a return field to close and awaiting what it answers. A next() that faults closes nothing: an iterator that could not step is not one to tell it was abandoned. On a fault the answer return() gives is not awaited, a fault’s unwind being somewhere nothing can wait — the one place this differs from JavaScript.
  • If return() faults, that is the fault a break or a return raises; where a fault was already leaving, the first one wins.
  • A generator’s return(v) can be called directly, and answers { value: v, done: true }. A catch around the yield does not catch it.

Blocks

A block’s value is its trailing expression. return is for leaving a function early and nothing else, so a function whose last statement is its answer does not write one.

counter()
    var count = 0

    bump()
        count = count + 1
        count

    bump

val c = counter()

print(c(), c(), c())
1 2 3

throw

throw v is a statement, like return and break — nothing after it runs, and the value is not optional. See Faults.

Closing words

end if, end while, end for, end loop, and end <name> for a definition, a class or a data type. All are optional and all are for a block long enough to want one.

Search

Esc
↑↓ to navigate ↵ to open Esc to close