7. The tempest command#

The tempest command is a thin layer, argument parsing and output, over the temper-driver crate, which holds the registry of backends, the one way from a settled library to each backend's files, and running tests with each language's tools; the playground's wasm uses it too. Temper's two main commands are test, which runs a project's tests, and build, which writes every library in every language to temper.out. Temper's std library is built into the binary, so it needs nothing beside it.

tempest test [-b interp|BACKEND]... [--library NAME] [-v] [-w DIR | DIR]
tempest build [-w DIR] [-b BACKEND]... [-o DIR] [-v]
tempest run --library NAME -b interp|BACKEND [-w DIR]
tempest init [-o DIR]
tempest serve | lsp
tempest version | license | licenses | help [COMMAND]

Its commands and flags are Kotlin Temper's temper's, by the same names and short forms; every one, and how each differs, is in the command line. build, test, and run also take --ignorefile, --no-connected, and --logformat json, which writes a JSON record per line, as Kotlin's does (log). run builds the work root, then runs a library's top level with the backend's tools, or on the interpreter; init writes Kotlin's project skeleton (init). watch, repl, and doc say they aren't in tempest yet, and exit with 3.

serve, or lsp, is a language server for editors, over stdin and stdout, on the same library finding and Sources as test and build; it's the temper-lsp crate, with a page of its own.

Finding libraries#

Both commands start the same way. From the directory given, they find the work root: the nearest directory holding temper.out, or else the highest one holding a config.temper.md. Under it, each directory with a config.temper.md is a library, and each directory in a library is a module. Directories that .gitignore and .temperignore files list are skipped, as is what --ignorefile lists, which has the last word.

All reading goes through one Sources value, so the compiler never touches the file system itself. It stacks layers: the disk, std built in, or files in memory. The playground uses memory, and the language server puts unsaved editor buffers in front of the disk. Why, and how the search differs from Kotlin's (ignore files, configs read rather than run), is in the detailed page.

One compile path#

Settled librarychecked: to_tmpl, thenvalidatewith_std: std's names, ifusedtranslate: one BACKENDFiles
Settled librarychecked: to_tmpl, thenvalidatewith_std: std's names, ifusedtranslate: one BACKENDFiles
Diagram source
flowchart LR
  settled["Settled library"] --> checked["checked: to_tmpl, then validate"]
  checked --> withstd["with_std: std's names, if used"]
  withstd --> translate["translate: one BACKEND"]
  translate --> files["Files"]

Callers take these three steps, from temper-driver: checked turns a settled library into tmpl and validates it, with_std gives it std's names as the backend spells them, and translate runs one backend from the Registry, which holds each built-in backend crate's BACKEND, one per cargo feature, all on by default. (build sets std's names itself rather than calling with_std.) Which backends build writes without -b, and which names this build lacks, come from the registry and the list of KNOWN names, not lists of their own. The trade-offs are in the registry, a backend as plain data, and one compile path, in detail.

test and build#

-b interp, the default-b BACKENDFind librariesSettle each libraryRun its tests on theinterpreterThe compile pathWritetemper.out/BACKENDRun the tests with thelanguage's own toolsOne summary of testspassed
-b interp, the default-b BACKENDFind librariesSettle each libraryRun its tests on theinterpreterThe compile pathWritetemper.out/BACKENDRun the tests with thelanguage's own toolsOne summary of testspassed
Diagram source
flowchart TD
  find["Find libraries"] --> settle["Settle each library"]
  settle -->|"-b interp, the default"| interp["Run its tests on the interpreter"]
  settle -->|"-b BACKEND"| compile["The compile path"]
  compile --> written["Write temper.out/BACKEND"]
  written --> tools["Run the tests with the language's own tools"]
  interp --> tally["One summary of tests passed"]
  tools --> tally

Each command step by step, the tools each backend runs, and why imported libraries are settled again in each importer: the commands, in detail, each language's own tools, and libraries settled on their own.

Worked example#

A library with one file and one test:

triple/config.temper.md     export let name = "triple";
triple/triple.temper
export let triple(n: Int): Int { n * 3 }

test("triple of 14") {
  assert(triple(14) == 42);
}
$ tempest test -v .
Testing triple
  ok      triple of 14
Tests passed: 1 of 1

$ tempest test -b py .
Tests passed: 1 of 1

tempest build -b py writes the library, std, and the runtime under temper.out/py/. The test becomes a unittest case, with the assert macro's message built in:

class TestTriple(TestCase):
    def test_triple_of_14(self, /) -> None:
        'triple of 14'
        test: Test = Test()
        try:
            actual = triple(14)
            expected = 42
            test.assert_(actual == expected, lambda: f'expected triple(14) == ({expected}) not ({actual})')
        finally:
            test.soft_fail_to_hard()

Add a second test that misspells triple as tripel, and only that test fails, on the interpreter and in Python alike:

$ tempest test -b py .
triple/triple.temper:8:10: error: tripel has not been declared
triple: 1 error
Testing triple on py
  failed  misspelled - Traceback (most recent call last):
Tests passed: 1 of 2

Where in the code#

Dynamic backends (future work)#

The registry is a list built at startup, not a table fixed at compile time, so backends that aren't built in can join it later. The plan: first as external processes, each given a library as tmpl in a versioned, serialized form and answering with its files (and running its tests), so a backend can live in its own repository and language; then as DLLs, only if processes prove too slow. The wasm build keeps a fixed list of built-in backends. What has to change first: dynamic backends, in detail.

watch, repl, and doc come later (parity with Kotlin's temper); the next page says how watch will reuse all this, and page 9 how lsp does.

In detail: The tempest command in detail: each decision with its alternatives and Kotlin Temper's way, the main types, the tests, and known weaknesses.