slate

Getting started

Install it, write a program, run its tests, and compile the same program to JavaScript.

Installing

Homebrew, and the tap is a step of its own:

brew tap slate-language/tap
brew install slate
slate --version

slate-language/tap is the repository the formula lives in, and tapping it is what tells Homebrew where to look; after that slate is an ordinary formula, so brew upgrade slate is how a new version arrives.

Homebrew asks you to trust a tap the first time it loads a formula from one. If the install is refused with “Refusing to load formula … from untrusted tap”, the answer is one command:

brew trust slate-language/tap

macOS on Apple silicon is the only build there is today, and that is worth saying plainly rather than finding out. sysl — the language slate is written in — does not cross-compile, so a Linux binary has to be built on Linux and nothing does that yet; the formula names the one platform it has rather than offering an install that cannot run.

The tarball attached to each GitHub release is the fallback where brew is not wanted — it is bin/slate and nothing beside it, the standard modules being compiled into the executable. Anywhere else, build it from source, which is a clone and one command given sysl installed:

git clone https://github.com/slate-language/slate
cd slate
sysl build .

A first program

A slate program is a .sl file. Put this in hello.sl:

val name = "world"

print("Hello, " + name + "!")
Hello, world!

and run it:

slate hello.sl

There is no main function and no ceremony. The file is the program, its statements run top to bottom, and print is in scope with nothing imported.

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

import { args, exit } from slate:process

if args.len() == 0
    print("usage: greet <name>...")
    exit(2)

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

With a #!/usr/bin/env slate on the first line and the execute bit set, that file is greet:

$ chmod +x greet.sl
$ ./greet.sl world slate
Hello, world!
Hello, slate!

Enough of the language to read the rest

Indentation is structure, as it is in Python. A block is opened by indenting under the line that introduces it and closed by returning to the outer column.

A definition is a name, a parameter list and a body, and the body may be an expression after = or an indented block:

double(x) = x * 2

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

print(double(21), grade(95), grade(83))
42 A B

A block’s value is its last expression, so grade needs no return — though return exists for leaving early.

val binds and var re-binds. A closure captures the variable rather than a copy of it, which is what makes a counter a counter:

counter()
    var count = 0

    bump()
        count = count + 1
        count

    bump

val c = counter()

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

match is postfix — a transformation of the thing to its left. Patterns test literals, shapes and alternatives, and a guard runs after the pattern has bound:

classify(v) = v match
    { kind: "point", at: [0, 0] } -> "origin"
    { kind: "point", at: [x, y] } if x == y -> "diagonal"
    [first, ...rest] -> "a list starting " + string(first)
    "sat" | "sun" -> "a weekend"
    n @ number if n < 0 -> "a negative number"
    _ -> "something else"

print(classify({ kind: "point", at: [0, 0] }))
print(classify([9, 8, 7]))
print(classify("sun"))
print(classify(-4))
origin
a list starting 9
a weekend
a negative number

A type is a shape with a name, and it is not erased. One declaration serves both the pattern and the check at a boundary:

type Note = { title: string, pinned?: boolean }

print({ title: "Milk" } is Note)
print(Note.test({ title: 7 }))
print(Note.mismatch({ title: 7 }))
true
false
[{path: "title", wanted: "string", got: "integer"}]

An async function answers a promise, and await waits for one. There is a real event loop underneath — libuv, the one node uses:

import { readFile } from slate:fs

async main()
    val r = await readFile("/no/such/file")

    print(if r.ok then "read it" else "no such file")

main()
no such file

That last program is worth reading twice. A file that is not there is not a fault — it is an answer, { ok: false, error: … }, which the caller has to look at. See Faults for the rule that decides which failures work this way.

Tests

A test is a function marked @test, and it lives beside what it tests. There is no test framework to install and no file naming convention to follow:

add(a, b) = a + b

@test
addsTwoNumbers()
    assertEq(add(2, 3), 5)

print("compiled")
compiled

Run every test in a file or a directory:

slate test .

assert, assertEq, assertNe and assertFails are the assertions, and Tests has the whole list. A test that is async is awaited.

The JavaScript back end

slate js reads the same tree a second time and writes JavaScript, so the program you just wrote runs under node, under quickjs, or in a browser:

slate js hello.sl -o hello.js
node hello.js

One self-contained file — the runtime and the program — so there is no bundler, no node_modules, and nothing to install beside it.

The same suite runs on the other host with one flag:

slate test --js .

That is one suite and two engines, not two suites. Where the two back ends genuinely differ — a browser has no server socket, no host has a brotli encoder — the difference is written down in JavaScript and the name refuses with a sentence saying which host lacks what.

Packages

slate add github.com/slate-language/lath

writes the dependency into package.sl, fetches it, and records the hash of the extracted tree in slate.sum. A package is imported by a bare word, where a file is imported by a quoted path and one of slate’s own modules by slate:name — three syntaxes rather than two readings of one string, so nothing already written changes meaning:

import { createElement, mount, useState } from lath

The packages lists what is written in slate today — a UI framework, an API server, a PostgreSQL client, a component library and a logger. Packages is the reference: the manifest, the cache, and what slate.sum guarantees.

Where to go next

Every program on these pages is run by slate’s own test suite, and every refusal is quoted from the diagnostic the compiler really prints. A page that drifts from the compiler fails the build.

Search

Esc
to navigate to open Esc to close