sysl

Installation

Install the compiler from the tap, or build it from source.

Install

brew tap sysl-lang/tap
brew trust sysl-lang/tap
brew install sysl-lang/tap/sysl

The brew trust line is not optional and its absence does not look like a missing step. Homebrew requires per-tap trust before it will load a formula, and it checks after fetching and verifying the download — so an install without it fails at the end of a successful download, in words that read like a defect in the formula:

Error: sysl-lang/tap/sysl: Refusing to load formula sysl-lang/tap/sysl from untrusted tap
sysl-lang/tap. Run `brew trust --formula sysl-lang/tap/sysl` or `brew trust sysl-lang/tap`

Trusting the tap once covers every formula in it, and every upgrade after it.

That is a native binary — there is no JVM under it and nothing to start up. It brings LLVM with it, which sysl needs at runtime: the compiler emits textual LLVM IR and hands it to clang to assemble and link, and llvm-ar is what archives the standard module’s compiled half into the compiler’s cache.

It also brings pkgconf. A package that binds an installed C library can name it — requires { pkg_config { sdl3 = "…" } } — and sysl asks pkg-config where that library’s headers and link line are, so building against SDL3 or cairo needs no flags. macOS ships no pkg-config and the libraries do not bring one, so the formula does.

Check it, and see what it offers:

sysl --version
sysl --help

The formula is macOS on Apple silicon only. sysl 0.3.1 is cut by the compiler itself on a Mac and ships one binary, darwin-arm64; Linux binaries return when a Linux build is part of the release. Anything else builds from source, below.

Your first compile

echo 'main()
    print("Hello, sysl!")' > hello.sysl
sysl run hello.sysl

The first run prints a line on stderr about building the standard module. That is expected and it happens once — see below.

sysl build compiles without running, leaving an executable you can ship:

sysl build hello.sysl -o hello
./hello

Build from source

The compiler is written in sysl and builds itself, so the path in is a clone and a sysl build by a compiler you already have. You want this if you are working on sysl, or if you are on a platform the tap has no binary for.

why
an installed syslthe compiler is a sysl program, so building it takes a sysl — the seed
clangsysl emits textual LLVM IR; clang assembles and links it
llvm-arthe standard module’s compiled half is an ar archive of objects
pkg-configonly for a package that names an installed C library; without it, say where the library is with --include-path and --link-path

clang is the only one most systems already have — pkg-config is common on Linux and absent from a stock macOS. On macOS the Xcode command-line tools supply one; on Debian and Ubuntu it is the clang package.

llvm-ar has to be the LLVM one: the standard module’s archive holds objects for the machine it was built for, and a platform archiver indexes only its own format and silently drops the rest. On a Mac, Homebrew keeps its LLVM deliberately off the PATH, so sysl looks in /opt/homebrew/opt/llvm/bin as well.

The repository holds two things side by side: compiler/, the compiler’s own project, and library/, the standard library it ships. Every build of the compiler runs from compiler/ — the repository root is not a project, and a build there reads the library’s files as though they were the program’s.

The build is the one a release makes, in two stages. The seed builds the tree against its own library, since library/ may use forms the seed’s library has not got; what that produces then builds the tree again against library/, and the second is the compiler:

git clone https://github.com/sysl-lang/sysl.git
cd sysl/compiler
sysl build . -o stage1
SYSL_LIB=$(cd ../library; pwd) ./stage1 build . -o sysl

Each stage takes several minutes. Debug or release is the optimization level and nothing else: the compiler’s default is -O1, and a release passes the same -O to both stages.

Check it

./sysl --version
./sysl run hello.sysl

A compiler built from a checkout answers sysl 0.0.1 — it is a development build, not a release, and it checks no version floor. Run from compiler/ it finds the standard library as ../library with nothing set; anywhere else, set SYSL_LIB to the checkout’s library/. An install is the same two things laid out as a prefix — bin/sysl and share/sysl/library beside it — which is where the compiler looks first.

The standard library

Every program is compiled against the standard module, and its source ships with the compiler. An install puts it at share/sysl/library under the install prefix — on a Homebrew Mac that is $(brew --prefix)/share/sysl/library — and the compiler finds it from its own location, the way rustc finds its sysroot. Running out of a checkout, it is the library/ directory in the tree. Nothing has to be configured, and there is no variable to set.

The directory was called lib/ until it was renamed, and both spellings are still looked for with the new one first — so a compiler installed before the change, and a checkout that has not been updated, both go on working.

It is meant to be read. The library is ordinary sysl, laid out as an ordinary sysl library, and every function in it is one a program could have written; sysl build-lib <root> --std is the same command that builds anybody else’s.

You do not have to build the artifact, either: when nothing usable is at the default path, the compiler builds it out of that source, says so on stderr, and gets on with the compilation. A fresh install just works.

It goes in your cache directory — ~/Library/Caches/sysl/ on macOS, ~/.cache/sysl/ on Linux, or wherever XDG_CACHE_HOME points — under a fingerprint of the library it was built from. So it is built once per machine rather than once per project, nothing is written into your source tree, and installing a compiler with a different library gets its own entry instead of a stale hit. Entries are held to 4 GB together: past that the oldest go, whole, though never one written in the last hour or the one the running compiler uses. Using an entry refreshes its age, so what goes first is what was used least recently.

The entry also names the compiler that built it — its version and a digest of its own executable — so a different build of the same version never reads back another build’s archive. The same key names the binaries sysl run and sysl test keep, so neither replays what an earlier compiler made for an unchanged program. A compiler before 0.0.151 keyed both on the version alone.

A kept binary is also keyed on where the program’s files are, not only on what they say, because the binary names its sources by their full path — a test report heads each file with it, and a trap reports it. So moving a project to another directory costs one rebuild of it, and two identical trees in two directories are two programs. Before 0.0.152 the second was handed the first one’s binary and reported the other directory’s paths as its own.

The fingerprint also carries the optimization level and, where they are asked for, LTO and a profile — the archive is compiled the way the rest of the build is, so -O3, --lto thin and a --profile-use file each earn their own entry rather than sharing one built at the default level. The key reads the levers in order, for example -O3-lto-thin, so a build that changes only one of them gets an archive that matches it instead of linking a standard module compiled differently from everything beside it. A --profile-use archive is keyed by what the profile says, not by its path, since the ordinary way of training is to merge a new profile over the old file in place.

Nothing there is ever evicted, and everything in it is derived: deleting the directory costs one rebuild.

Nothing names the cached artifact. To compile against another standard module, set SYSL_LIB to the root its source is in; --std-lib is refused, saying exactly that. --no-std-lib compiles the standard module from its source together with the program instead of linking the cached one.

Optimization

-O names the level handed to clang, spelled the way clang spells one — -O2, -Os, -O0 — and it reaches every object a build produces rather than only the final link. It can also be written --optimize 2.

The default is -O1, not off. That is worth knowing because it is unusual: -O0 is a different instruction selector rather than merely a slower one, it is the mode a back end’s own test suite covers least, and a real miscompile was found living there. If you drop to -O0 to make something easier to debug and the behaviour changes, suspect that before your program.

A project that wants a level of its own states it once, in its manifest, rather than on every command line — optimization. The flag still wins where it is given, so a project built at 2 is profiled at 0 by typing it.

If something goes wrong

clang: command not found — sysl got as far as emitting IR and had nothing to hand it to. Install clang and try again. Installing from the tap brings LLVM with it, so this is a built-from-source problem.

cannot find llvm-ar — the platform’s own ar is not a substitute, so sysl looks for LLVM’s: llvm-ar on the PATH, then /opt/homebrew/opt/llvm/bin and /usr/local/opt/llvm/bin. Install LLVM, or put its bin on the PATH.

cannot find the standard module's source — the compiler could not find the library it ships with, and the message lists every path it tried. From a package install that means the install is incomplete: reinstall it. From a checkout it usually means the working directory is not in the tree. Either way SYSL_LIB=/path/to/library names the library root outright, where the root is the directory holding sysl — that is, the library above library/sysl, not library/sysl itself.

Search

Esc
↑↓ to navigate ↵ to open Esc to close