LEVIATHAN v962456e · 962456eee1

Compiler

Native backends: LLVM and C++

The two ways to turn a program into a native executable, what each one covers, and the runtime limits of natively built programs.

since 0.1.0-alpha.1linux

Description

The compiler can produce a native executable in two ways. Both start from the same checked and lowered program that the interpreters run (see lang.engines).

The LLVM backend

The LLVM backend is the primary native backend. It covers the whole language: classes, collections, closures, exceptions, and the standard library's system services such as the event loop, timers, async and await, files and threads. It is available when the compiler was built with LLVM; a compiler built without it reports this build has no LLVM backend.

Option Result
--emit-llvm Prints the program as LLVM IR text.
--native-obj <out.o> Writes an object file directly, without calling any external tool.
--build-native <out> Writes the object file, links it with the runtime library and the system C++ compiler, and leaves the executable <out>.
--opt-level <0|2> 2 (the default) optimises. 0 generates code faster and is easier to follow in a debugger. Any value other than 0 optimises.

--build-native links against liblvrt.a, the runtime library, which is installed beside the leviathan program; --runtime <path> names a different one. The linker driver is the first of clang++, g++ and cc found on the PATH. A file named lvrt.link beside the runtime library lists any extra linker flags the runtime needs, such as the TLS libraries, and --build-native reads it for you. If you link an object file from --native-obj yourself, you have to supply those flags yourself.

A program for either native backend

bool isPrime(int n) {
    if (n < 2) {
        return false;
    }
    int d = 2;
    while (d * d <= n) {
        if (n % d == 0) {
            return false;
        }
        d = d + 1;
    }
    return true;
}

int found = 0;
int last = 0;
for (int n in 1..50) {
    if (isPrime(n)) {
        found = found + 1;
        last = n;
    }
}
console.writeln("${found} primes up to 50, the largest is ${last}");
15 primes up to 50, the largest is 47

Saved as primes.lev:

leviathan --build-native primes primes.lev
./primes
leviathan --opt-level 0 --build-native primes_debug primes.lev
leviathan --native-obj primes.o primes.lev

Memory limits of a natively built program

The escaping heap, which holds objects, arrays, maps, closures and strings that live beyond the function that made them, starts as one 256 MiB region per thread and grows by mapping further regions when it runs out, up to a ceiling of 16 GiB of address space per thread. A program that fits in the first region never pays for the growth. The environment variable LANG_HEAP_MAX_MB changes the ceiling, in whole mebibytes, when it is set for the running program:

  • A positive value below 256 is raised to 256, so an override can never make the limit smaller than the first region.
  • A value that is not a whole positive number, such as 0, -1 or abc, means "use the default 16 GiB".
  • Reaching the ceiling, or being refused memory by the operating system, prints lvrt: heap exhausted and ends the program with exit status 1.
  • A single object larger than 2 GiB, less a 16-byte header, can never be allocated however much heap is left. The program prints lvrt: object too large and exits with status 1.
  • The arena that holds short-lived values of a single function is a fixed 64 MiB per thread. Running out of it prints lvrt: arena exhausted and exits with status 1.
LANG_HEAP_MAX_MB=512 ./primes

The C++ backend

The C++ backend is the older native path, kept for environments without LLVM. --emit-cpp prints a self-contained C++ translation unit, with its runtime included, on standard output. --build <out> pipes that unit to the first of clang++, g++ and cc found on the PATH, at optimisation level -O2, and leaves the executable <out>. It is always available.

It compiles the language itself, including classes, multiple inheritance, accessors and operators, collections, closures and exceptions, and the standard library's pure code. It does not compile programs that use the system services of the event loop. Files, timers, sockets and async/await are rejected at compile time with a diagnostic such as

error: native backend: native 'sysOpen'

and no executable is produced. Reading standard input is supported. Use the LLVM backend, or run the program with --run, for programs that need the rejected features.

Rules

  • --build-native and --native-obj need a compiler built with the LLVM backend. --build and --emit-cpp do not.
  • --build needs a C++ compiler on the PATH. --build-native needs one as the linker driver.
  • --target and --opt-level apply to --native-obj and --build-native.
  • A program that the C++ backend cannot compile fails with a diagnostic naming the feature. It never compiles into a program that behaves differently.
  • LANG_HEAP_MAX_MB is read by the running program, not by the compiler.

Examples

Reading the generated code, for debugging or learning:

leviathan --emit-llvm primes.lev > primes.ll
leviathan --emit-cpp primes.lev > primes.cpp

Building with the C++ backend and checking that it agrees with the LLVM backend:

leviathan --build primes_cpp primes.lev
leviathan --build-native primes_llvm primes.lev
./primes_cpp > a.txt
./primes_llvm > b.txt
cmp a.txt b.txt

Notes

  • The standard library ships as files that the compiler reads at start-up: the directory is found as described in lang.compiler-cli, with a copy built into the compiler as the last resort. The part of it that exposes the browser's DOM is only read when the target is WebAssembly.

See also

  • The leviathan command line — Every option of the leviathan compiler, grouped by what it does, with the exit statuses and how arguments reach your program.
  • The execution engines — The four ways the compiler runs a program, why they always agree, and the few places where one of them has to refuse a program.
  • Cross-compilation: --target and per-target runtimes — Building executables for another operating system or processor, what the build needs for each target, and what a target can reject.
  • The WebAssembly (browser) target — Building a program as a WebAssembly module for the browser, what it can use, what it cannot, and how it reaches the page.