Skip to content
Saule
Docs

CLI

Terminal window
saule [OPTIONS] [COMMAND]

Running bare saule prints help and exits successfully.

Option Description
-v, --version Print the version and exit

Run a project or a single source file.

Terminal window
saule run [TARGET] [-- ARGS...]
Argument Description
TARGET A project directory or a .sau file. Defaults to the current directory.
ARGS Everything after --, forwarded verbatim to the script’s Os.args()
Option Description
--profile-bytecode Count what the bytecode VM executes and print an opcode and opcode-pair histogram to stderr when the program finishes

Whether TARGET is a directory is the only thing that picks the mode:

Invocation What runs
saule run The project in the current directory
saule run <dir> The project rooted at <dir>
saule run <file.sau> That file, on its own
saule run -- a b The current project, with Os.args() == ["a", "b"]
saule run <file> -- a b That file, with Os.args() == ["a", "b"]

Project mode requires the file named by entry: in saule.config to declare a Main class with a static fn main(). Top-level statements in that file execute first, then Main.main() is called.

Single-file mode executes the file top-to-bottom like a Lua script. No Main class is required, and any surrounding saule.config is ignored. If the script does happen to define Main.main(), it is invoked after the top-level body finishes.

Script arguments go after -- and are never interpreted by the CLI — which is what lets a program take a filename or a flag of its own:

Terminal window
saule run -- input.bf # project in the cwd, Os.args() = ["input.bf"]
saule run tool.sau -- -v file # single file; script args may start with `-`

Type-check a project or a source file without running it.

Terminal window
saule check [OPTIONS] [TARGET]
Argument Description
TARGET A project directory or a .sau file. Defaults to the current directory.
Option Description
--json Write the diagnostics to stdout as JSON instead of rendering them
--dump-type-coverage Report how much of each file the type checker proved a type for

TARGET follows exactly the same rule as run: a directory (or no argument at all) means project mode, a file means that file alone. Two differences from run are deliberate:

  • Every diagnostic, not just the first. run stops at the first because it is about to execute and the first error already settled that. check reports the whole list so it can be fixed in one pass.
  • Every file, not just what the entry point reaches. A module nothing imports yet is precisely the one whose errors are worth hearing about before it is imported.

Libraries (kind: "library") have no entry point and check normally. Exits with status 1 when anything is reported, so it can gate CI.

For anything that has to act on the error list rather than read it: a build script, an editor not using the language server, a CI job, or a harness running the compiler over a great many programs. The rendered form is for a person, and screen-scraping it fails quietly the moment the layout changes.

Terminal window
saule check src/main.sau --json
{
"saule_version": "26.6-dev+9ce2dbb",
"commit": "9ce2dbb",
"ok": false,
"files": [
{
"path": "src/main.sau",
"diagnostics": [
{
"severity": "error",
"phase": "type",
"message": "cannot assign value of type `any` to variable of type `integer`",
"span": { "start": 226, "end": 227 },
"help": "the value's type isn't compatible with the declared type"
}
]
}
],
"summary": { "files": 1, "errors": 1, "files_with_errors": 1 }
}
Field Meaning
saule_version, commit Which toolchain issued these verdicts. A binary on PATH can lag the tree it was built from, so a stored result can always be attributed.
ok True when nothing was reported; mirrors the exit status
files Every file examined, including the clean ones
path The file as given — a bare basename is ambiguous once two src_dirs each hold a main.sau
span Byte offsets into the source, half-open. Absent when the error carries no label.
help, code Absent rather than null when the error has none

phase says which pass produced the diagnostic — one of io, lex, parse, import, semantic, type, compile or runtime. It is the classification available today; code, a stable per-error identifier, is wired into the shape but not yet declared by any error variant.

The document is the same contract the web playground consumes, so the two cannot drift. --json replaces the rendered diagnostics rather than adding to them, and stdout carries nothing else — warnings and the progress summary stay on stderr. The exit status is unchanged, so this stays usable as a CI gate.

Format one or more source files.

Terminal window
saule fmt [OPTIONS] <FILE>...
Option Description
-w, --write Overwrite the files in place instead of printing to stdout
--indent <N> Columns per indent level, 1–16
--tabs Indent with hard tabs
--spaces Indent with spaces

At least one FILE is required. --tabs and --spaces override each other, so the last one on the command line wins and a wrapper script can safely append one.

Indentation is resolved in layers, each overriding the one before it:

  1. The formatter’s defaults — spaces, width 2.
  2. indent_style and indent_width from the project’s saule.config.
  3. The --indent / --tabs / --spaces flags.

Because the language server reads the same saule.config keys, a project’s declared style survives both a Reformat in the IDE and a saule fmt -w in a terminal.

Exits with status 1 if any file failed to format.

Compile to bytecode and print the disassembly.

Terminal window
saule disasm [OPTIONS] [TARGET]
Argument Description
TARGET A project directory or a .sau file. Defaults to the current directory.
Option Description
--deps Also disassemble the modules the target imports. Ignored in project mode, which emits every module either way.

TARGET follows the same rule as run and check: a directory, or no argument at all, means project mode; a file means that file.

Invocation What is disassembled
saule disasm Every module in the project in the current directory
saule disasm <dir> Every module in the project rooted at <dir>
saule disasm <file.sau> That file’s chunk
saule disasm <file.sau> --deps That file, plus the modules it imports

A debugging aid for the bytecode compiler, not a user-facing format: the listing shows one line per instruction with registers, constants and resolved jump targets. Each chunk names its own source on its first line, so several chunks in one listing stay distinguishable:

; chunk from probe/src/base.sau
; 2 proto(s), 0 constant(s), 1 module slot(s)
...

Imports are resolved at compile time, so that a class declared in one module and extended in another has a single layout. A module that belongs to an import graph is therefore compiled together with that graph rather than on its own — which is what lets a file using extends, an imported name or an import be disassembled at all.

Modules are emitted in post-order: every module appears after the ones it imports, which is the order they were compiled in.

Project mode roots at every source file in turn rather than only at the entry, for the same reason check walks every file — a module nothing imports yet is precisely the one worth looking at. A consequence worth knowing: a library has no entry point, and disassembles like anything else.

Inside a project, chunks are labelled with their project-relative path (probe/src/base.sau), so the listing is the same on any machine. A file outside any project is labelled by absolute path, as diagnostics label it.

The compiler is under construction. Anything it cannot compile yet is reported by name rather than silently omitted, so the output is either a complete chunk or an explicit gap. In project mode one unusable file does not stop the rest: the chunks that compiled are printed, the failures go to stderr, and the exit status is 1.

Run a project’s tests.

Terminal window
saule test [OPTIONS] [TARGET]
Option Description
--filter <TEXT> Only run tests whose name contains this text

A test is a top-level function named test_... with no parameters. In a project it is looked for in tests/ and in files ending _test.sau in the source directories; given a file, only that file is run. The program’s Main.main is not called.

A test fails if an assert fails, a throw goes uncaught, or a runtime error occurs, and the rest still run. The exit status is 1 if any test failed or a test file does not compile, and 0 otherwise, including when there are no tests.

Scaffold a new project in ./<name>.

Terminal window
saule init [--lib] <NAME>
Option Description
--lib Scaffold a library — importable by other projects, with no entry point

NAME is both the directory that gets created and the project’s name: in saule.config, which is the prefix dependents use to import from it.

Install a package, or everything this project needs.

Terminal window
saule install lauriszz123@saule-shine # the newest tagged version
saule install lauriszz123@saule-shine:v0.1.0 # a specific tag or branch
saule install ../uikit # a project on this machine
saule install # everything in saule.config

A package is <owner>@<repo>, pinned with :<version>. Without one, the newest tag is installed and recorded, so a fresh clone gets that same version; a repository with no tags falls back to its default branch. Packages are installed once, globally, and shared by every project, which records only the ones it uses in dependencies:. A path is used where it lies — nothing is copied, so edits to it take effect immediately.

Move this project’s packages to their newest tagged version.

Terminal window
saule update [--check] [PACKAGE]
Option Description
--check Report what is out of date and install nothing; exit 1 when something is behind, 2 when a package could not be reached

A package pinned to a tag moves to the newest tag; one tracking a branch is compared by commit and reinstalled from that branch. Path dependencies are left alone.

Remove a package from this project’s dependencies:.

Terminal window
saule remove [--purge] <PACKAGE>
Option Description
--purge Also delete the globally installed copy, not just this project’s reference to it

The installed copy is otherwise kept, because other projects may use it. The package may be named as owner@repo:v1, owner@repo, just repo, or the name you import it under.

List this project’s dependencies and what is installed for each.

Terminal window
saule list [--global]
Option Description
--global Every package installed on this machine, whichever project asked for it