slate

Lexical structure

What the compiler reads before it reads anything else: files, whitespace, names and literals.

Files

A slate program is a .sl file. A file that writes elements is conventionally .slx, after .tsx — but the extension says nothing to the compiler, which parses both the same way, and elements are read in every file. It is a name for a reader and an editor.

A file with a #! line on its first byte is a command. slate skips that line and everything after the program’s name on the command line belongs to the program:

#!/usr/bin/env slate

import { args, exit } from slate:process

if args.length == 0
    print("usage: greet <name>...")

for name in args
    print("Hello, " + name + "!")
usage: greet <name>...

#! is read at the very first byte and nowhere else. # is not a comment in slate.

Comments

// to the end of the line. There is no block comment.

Indentation is structure

slate is off-side ruled, as Python and sysl are. A block is opened by indenting under the line that introduces it and closed by returning to the outer column.

A newline inside brackets means nothing, which is what lets an argument list or an array literal be split across lines. Two tokens suspend that rule, and only where they end a line: -> and match. Either one at the end of a line opens a block even inside brackets, which is what lets a callback with a real body be written where it is passed:

forEach([1, 2, 3], x ->
    val doubled = x * 2
    print(x, doubled))
1 2
2 4
3 6

An arrow written mid-line would hand the block to whatever line came next, so it does not open one.

A , that ends its line, at the block’s own bracket depth, ends the block — so a block lambda need not be the last argument: the comma closes the block and the arguments after it are read as arguments. A comma written mid-line belongs to the statement it is in, which is what keeps a, b = b, a whole inside such a body; and one written deeper in brackets, inside a call or an array literal, belongs to those brackets and closes nothing.

A binary operator that ends its line continues the statement onto the next one, at any indentation. Every arithmetic, comparison and bitwise operator does this, and so do the two operators that read a field, . and ?., and the compound assignment operators — nothing but blanks or a trailing comment may stand between the operator and the newline. -> is left out, having already opened a block under the rule above. Two more are left out for the same reason as ->, without a mechanism of their own to say so: a bare =, since name() = followed by a deeper line is how a definition’s body opens by the ordinary rule below; and ../..<, since for i in 1.. followed by a deeper line is how a for loop’s own body opens, and a range left open-ended on purpose is exactly the shape a for refuses by name. A line that instead starts with an operator is not this rule and is a statement of its own, exactly as it always was:

isShort(n) = n.length < 4
isKnown(n) = n == "ada"

val name = "ada"
val ok = isShort(name) ||
    isKnown(name)

print(ok)
true

Brackets still suspend the off-side rule on their own terms too, which is what lets a longer expression be broken wherever it reads best rather than only right after an operator.

A block that closes ends the expression it is part of, as it ends the statement. So the line under a block is a statement of its own whatever it begins with — a -, a [, a ( or a { there is never read as an operator on the block’s value:

sign(x)
    if x > 0
        return 1

    -1

count()
    var i = 0
    while i < 2
        i = i + 1
    [i, 1]

print(sign(0), count())
-1 [2, 1]

end, and the closing words

A block that has grown long enough to want a closing marker takes one: end if, end while, end for, end loop, end <name> for a definition or a class. end is a soft word — a program may still call something end — and so are class, data and type.

Names

An identifier is a letter or _ followed by letters, digits and _. Names are case-sensitive.

The type words are not keywords. null, boolean, integer, real, number, string, array, object, function, date, time, zone and the rest of the twenty-one listed in Patterns are ordinary names in expression position — val int = 3 and val date = readIt() both work — and are read as type tests only in pattern position.

There are no abbreviations. It is boolean and function, never bool and fn; a short form is refused with the long one named.

A keyword may name a FIELD, as it may in JavaScript — after a ., in an object literal, in a with, and as a pattern’s key. A word the grammar happens to use is not a word a program controls: somebody else’s JSON has an if in it.

val o = { with: 1, if: 2, match: 3 }

print(o.with, o.if, (o with { if: 9 }).if)

val { if: n } = o

print(n)
1 2 9
2

A keyword still may not name a BINDING, so the { name } shorthand has nothing to introduce in a pattern and nothing to read in a literal, and both say so:

val { with } = { with: 1 }
is a word the grammar has taken
print({ with })
is a word the grammar has taken

Literals

val text = "it"

print(null, true, false)
print(42, -7, 1_000_000)
print(0xff, 0b1011, 0o17)
print(3.14, 2e10, 1e-3)
print("text", s"interpolated ${text}")
print([1, 2, 3], [0; 5])
print({ a: 1, b: 2 })
print(0..<3, 1..10)
null true false
42 -7 1000000
255 11 15
3.14 20000000000 0.001
text interpolated it
[1, 2, 3] [0, 0, 0, 0, 0]
{a: 1, b: 2}
0..<3 1..10

An integer may be written in hexadecimal, binary or octal, and _ may be written between digits of any of them.

An integer literal has no width, so a run of digits is read as the number it spells however many of them there are — in any of those bases, separators and all.

Integers and reals are separate values, not one numeric type. An integer never wraps: it grows. / between two of them answers a real; \ is the whole-number division, and it divides towards zero, with % taking the sign of the left operand.

\ is an operator only outside a string literal. The string lexer reads its own escapes, so "a\\b" and "\t" mean exactly what they always meant, and a backslash begins nothing else anywhere in the grammar. \= is the compound form.

Every comma list takes a trailing comma: an array, an object, an argument list, a parameter list.

[v; n] is an array of n copies of v.

Strings

A string literal is double-quoted with the usual escapes (\n, \t, \\, \", \u{...}). There is no single-quoted form and no character type — a single character is a string of one.

An s-string interpolates:

val w = 3
val h = 4
val point = { x: 7 }

print(s"${w} by ${h}")
print(s"$w by $h")           // the same thing
print(s"$point.x")           // interpolates `point`, then `.x` is text
print(s"${point.x}")         // says the other thing
print(s"costs 5$")           // a `$` that begins no name is just itself
print(s"$$")                 // a literal `$`
3 by 4
3 by 4
{x: 7}.x
7
costs 5$
$

A hole that is a single name needs no braces. The short form is identifier-only and stops at the end of the name, which is why $point.x and ${point.x} differ.

There is no raw string. A backslash in a string is an escape wherever it appears, so a regular expression written as a literal doubles its backslashes: regex("\\d+").

What a program never writes

There is no undefined, no NaN that spreads, and no Invalid Date. Each of those is a value that travels through a program silently changing what everything downstream computes, and slate has none of them: the equivalent situation is either a null the program can test or a fault it can catch.

Search

Esc
↑↓ to navigate ↵ to open Esc to close