The REPL#

tempest repl reads Temper a line at a time and runs each complete input on the interpreter, against what earlier inputs declared, printing each result as interactive#N: value. It's temper repl's counterpart; the tutorial uses it throughout.

$ let greet(name: String): String { "Hello, ${name}!" }
interactive#0: void
$ greet("REPL")
interactive#1: "Hello, REPL!"
$ [1, 2, 3].map { x => x * 10 }
interactive#2: [10,20,30]
$ class Point(public x: Int, public y: Int) {}
interactive#3: Point
$ { x: 1, y: 2 }
interactive#4: {class: Point, x: 1, y: 2}

Ctrl-D (the end of input) ends the session, with exit code 0 whatever failed. On a terminal, lines are edited as in a shell, with history, completion, and color (below). Piped input works the same, without prompts or editing:

printf '1 + 2\nconsole.log("hi")\n' | tempest repl

Inputs#

A line ends an input unless something is still open: a bracket ((, [, {, or ${), a multi-quoted string (""", ended by a line that doesn't go on with it, such as a blank one), or a block comment. A line of only comments waits too, so a doc comment joins what it documents. Meanwhile the next line, on a terminal, starts indented, two spaces and two more for each bracket open; piped in, the prompt is instead of $ . A " string never spans lines: one left open is an error at the end of its line, rather than swallowing the lines after it.

So else on a line of its own starts a new input, as it would be an error in a file too; keep } else { together. To type several statements as one input, put them on one line, separated by ;, or use --separator semisemi, which ends inputs at a ;; at the end of a line, as OCaml's REPL does; --separator eof makes all of the input one.

Results#

Each input prints what it reports, in order: its errors and warnings, what it logs as it runs, why it failed if no error already says, and then its result, as temper repl prints it:

Input Result
An expression Its value: 3, 2.0, "text" (with Temper's escapes), true, null, [1,2,3] for a list, [(1,"a")] for a map, {class: Point, x: 1, y: 2} for an object (its fields), and ƒ for a function
A trailing ;, or a declaration void
A class or interface Its name, as Point
Anything that failed fail

A value that contains itself prints ↻ where it recurs, and a very deep or long one ends with …, a very long string too ("yyy…"…), so printing never ends the session. A very long line in an error's snippet is cut to the part around the error, with … where it's cut.

Names#

Each input sees the names earlier ones declared. Declaring a name again makes a new one for later inputs, while code that already used the old one keeps it, so let s = "${s}!" reads the earlier s:

$ let s = "first"
interactive#0: void
$ let shout(): String { "${s}!" }
interactive#1: void
$ let s = "${s} and second"
interactive#2: void
$ s
interactive#3: "first and second"
$ shout()
interactive#4: "first!"

A class declared again works the same way. Its old instances print their class with the input that declared it, as interactive#5.Point, when a later Point hides it.

A top-level var can be assigned from later inputs, checked against its type. A function can't use a name a later input declares, since an input's names are settled once; mutually recursive functions go in one input (on one line, or with ;;).

Errors#

Errors are reported with the input's number, line, and column, and the line with the span marked:

$ let total: Int = "many"
interactive#0:1:18: error: Cannot assign to Int32 from String
    let total: Int = "many"
                     ^^^^^^
interactive#0: void
$ total + nope
interactive#1:1:9: error: nope has not been declared
    total + nope
            ^^^^
interactive#1: fail

As in a library, an input with errors still runs as far as it can, and the session goes on. A declaration whose input failed stays declared, and a later read of it fails, saying why:

$ let half = 1 / 0
interactive#2: fail
$ half + 1
Failed: half has no value: interactive#2 failed
interactive#3: fail

Tests#

The first input's tests run once, after it, as tempest test runs a library's: each failure, then the summary. What it declares works either way. So tempest repl --separator eof < file.temper, or -e, runs a file's tests, then goes on with what it declares:

$ let twice(n: Int): Int { n * 2 }; test("doubles") { assert(twice(2) == 5) }
Test failed: doubles - expected twice(2) == (5) not (4)
Tests passed: 0 of 1
interactive#0: void
$ twice(21)
interactive#1: 42

A later input's tests don't run: each is an error.

Async#

Promises and async blocks last across inputs, and an input may await at its top level. An input that awaits a promise nothing has resolved yet prints 1 promise is unresolved. and fail, and goes on when a later input resolves it:

$ let reply = new PromiseBuilder<String>();
interactive#0: void
$ let answer = await reply.promise
1 promise is unresolved.
interactive#1: fail
$ reply.complete("yes");
interactive#2: void
$ answer
interactive#3: "yes"

The REPL's own functions#

Four functions are declared before the first input. They're Temper functions, so they complete and check as any other, and an input that declares its own help, describe, translate, or runTests hides the REPL's for the inputs after it, as any redefinition does; there's no other name to reach them by then.

What describe(n, "module") and translate(n) show is one module: input n after the earlier inputs it uses, through the names they declare, transitively, and the earlier inputs that change what those declare, as v = 2; or list.add(x); do, each input whole. Where two of those inputs declare the same name, the later one's is renamed, x__2, as are the uses that meant it, so the module means what the inputs did; an import of what an earlier one imported, under the same name, is dropped instead. A module with errors isn't translated: its errors are shown, located in the module as describe(n, "module") shows it. A top-level await is one, as it runs only in the REPL; to translate it, put it in an async block.

$ let x = 1
interactive#0: void
$ let x = x + 1
interactive#1: void
$ let unused = 0
interactive#2: void
$ x * 10
interactive#3: 20
$ describe(1, "types")
x: Int32
interactive#4: void

Input 3 used inputs 0 and 1, and its module renames the second x:

$ describe(3, "module")
// interactive#0
let x = 1;
// interactive#1
let x__2 = x + 1;
// interactive#3
x__2 * 10;
interactive#5: void
$ translate(3, "py")
# interactive/interactive/interactive.py
from temper_core import int_add as _int_add, int_mul as _int_mul

__all__ = ()

_x: int = 1
_x2: int = _int_add(_x, 1)
_int_mul(_x2, 10)
interactive#6: void

What they show is printed when they run, so an input's other output comes before or after it as the calls are. A failed call, as describe(99), fails the input, saying why. Ctrl-C stops them, as it stops any input.

Running input 0's tests again#

Tests declared in input 0, as test("name") { … }, run once, after it, printing each failure and tempest test's summary. runTests() runs them again: their source is settled afresh as a hidden input after the latest one, so the names they use mean the newest declarations, and they print the same lines.

$ let twice(n: Int): Int { n * 2 }; test("three") { assert(twice(3) == 6) }
Tests passed: 1 of 1
interactive#0: void
$ let twice(n: Int): Int { n + 1 }
interactive#1: void
$ runTests()
Test failed: three - expected twice(3) == (6) not (4)
Tests passed: 0 of 1
interactive#2: void

On a terminal#

When standard input and output are both a terminal, lines are edited with rustyline, Emacs-style:

Key Does
Enter Runs the input once it's whole; else starts a new line in it, indented
Up, Down Move between an input's lines, then through history
Ctrl-R Searches history
Tab Completes the name being typed
Ctrl-C Drops the input being typed; stops one that's running
Ctrl-D On an empty line, ends the session

Color#

--color auto, the default, colors only output to a terminal, and not when NO_COLOR is set (to anything but empty), TERM is dumb, or CI is set; --color always and --color never decide outright. Color applies to the prompt, results (fail in red), diagnostics (errors red, warnings yellow), and input as it's typed, by the classes the playground uses: keywords, true/false/null/void, numbers, strings (multi-quoted ones too) and their ${…}, comments, types' names, decorators, and tokens the lexer rejects. The bracket at the cursor and its match are marked.

History#

Each input run is kept in a history file, one entry however many lines it has:

Platform File
Linux and other Unix $XDG_DATA_HOME/tempest/repl-history.txt, or ~/.local/share/tempest/repl-history.txt
macOS ~/Library/Application Support/tempest/repl-history.txt
Windows %APPDATA%\tempest\repl-history.txt

What's typed may be private, a token pasted into a string say, so the file is opened once, checked, and only read and written through what was opened, never again by its path, so it can't be swapped for a link to another file as the session goes on:

If any of that fails, the session goes on without history, saying why. A line starting with a space isn't kept, and --no-history keeps none and reads none. -e inputs and piped input are never kept. help() says which file the session uses. The file is in rustyline's format: #V2, then an entry a line, its line breaks written \n.

Security#

Imports#

Inputs import:

Importing a module runs its top-level code, once, when the input that first imports it runs.

Stopping#

Ctrl-C stops the input running, and the session goes on. At the prompt, it drops the input begun so far: at once on a terminal, or for piped input, once the line is entered. If tempest itself is ever stuck, pressing Ctrl-C again ends it. On Windows, Ctrl-Break does as Ctrl-C does.

Options#

Option Meaning
-w DIR, --workroot DIR The work root whose libraries inputs import
--no-workroot No work root, not even the one around the current directory
-ign FILE, --ignorefile FILE An ignore file for the work root
-e CODE, --exec-before CODE An input to run first; may be given more than once
--separator newline|semisemi|eof What ends an input (above)
--prompt normal|hide Whether to prompt; by default, only when standard input is a terminal
--color auto|always|never Whether to color output (above); auto by default
--no-history Neither read nor keep history (above)
--fuel N|none The steps each input may take; none by default
-v, --verbose Report hints too
--std DIR Read std from DIR instead of the copy built in

temper repl -b BACKEND, which opens a backend language's own REPL with the work root's libraries loaded, isn't in tempest yet, and says so (exit code 3).

Compared with temper repl#

The same inputs, results, and interactive#N: lines, with these differences: