Documentation pages
Getting Started
This guide takes you from a source checkout to a running program. It covers building the compiler, writing a first program, compiling, interpreting and checking it, cross-compiling, the LLVM backend, and the language and format of diagnostics. It assumes you are comfortable with a command line and a statically typed language such as Rust, Go or C.
Building the compiler
SultanC is self-hosted: the compiler is written in SultanC and builds itself in stages. A build starts
from a small seed (Stage0), uses it to build Stage1, and uses Stage1 to build the final compiler, Stage2.
build.sh drives all stages through CMake.
git clone https://github.com/sultan-language/sultanc.git
cd sultanc
./build.sh
The finished compiler is build/sultanc. The stage products stay under build/bootstrap/cmake/bin
(cmake-wasm/bin for the WebAssembly seed). Copy build/sultanc anywhere on your PATH, or run it in place.
The seed can come from three places, chosen with --bootstrap:
| Route | Command | Needs |
|---|---|---|
| C seed (default) | ./build.sh |
a C compiler and LLVM |
| WebAssembly seed | ./build.sh --bootstrap=wasm |
only a C compiler |
| An existing compiler | ./build.sh --bootstrap=existing |
a built build/sultanc, or one named with --with=PATH |
The existing-compiler route performs a single self-hosting step and takes about a minute, which makes it the fast loop during development. Use the C or WebAssembly route on a new system, when the existing compiler is too old for the sources, and when verifying a release.
By default the finished compiler contains only the Native backend and has no dependency on libLLVM. Add
--enable-llvm to build the LLVM backend into it (see The LLVM backend). The C seed
always uses LLVM, so LLVM 19 to 23 must be installed for the default route. On macOS with Homebrew, point the build at
it first:
export LLVM_CONFIG="$(brew --prefix llvm)/bin/llvm-config"
Select another LLVM with LLVM_CONFIG=/path/to/llvm-config, or with -DLLVM_DIR for a direct CMake build.
The remaining build.sh arguments name a stage or a check:
| Argument | Effect |
|---|---|
stage0, stage1, stage2 |
Build up to that stage. |
bootstrap |
Build Stage0 and Stage1 only. |
verify |
Build Stage2, then check that Stage3 and Stage4 are byte-for-byte identical. With --enable-llvm it also checks LLVM convergence. |
--target=TRIPLE |
Build for another target. |
--backend=native|llvm |
Backend Stage1 uses to generate Stage2 (default native). |
OUTPUT |
Where to copy the finished compiler. |
Windows has no Native backend. Build there with ./build.sh --bootstrap=c --enable-llvm --backend=llvm.
Where the compiler looks for std
The compiler knows the standard library (the package std, Arabic المكتبة) and finds it by its layout, as
rustc finds its sysroot: it takes the first of these library directories that holds a std/ folder, whose root
file std/std.sn declares the spellings of std's modules (see
Module spellings):
$SULTANC_STD, the library directory itself (when set, it is used even if it is wrong, so the error names it);library/in the current directory (the repository root);- next to the compiler:
<bin>/library,<bin>/../library(asbuild/sultancin a checkout) and<bin>/../lib/sultanc/library(an install prefix,<prefix>/bin/sultanc).
A project's own library/ directory is not mistaken for std. Without one, the error is SULTANC-STD-NOT-FOUND.
Projects with dependencies: use Supax
sultanc compiles the files it is given, their relative imports and std. It reads no Supax.toml. Inside a
Supax project, run supax check, supax build or supax run: Supax resolves the dependencies and passes the
compiler a package graph (--package-graph). A plain sultanc check src/main.sn in such a project does not see
its dependencies or the package's own name.
A source file may be named by a relative path
(.. included) or an absolute one; relative sources under the working directory are rooted there, others at
their common directory. A root reached from relative sources stays relative (.), so modules are read by
relative paths and the result does not depend on how the host reports the working directory. On Windows, \ and / both separate path components and drive letters are roots.
A first program
SultanC reads source files with the extension .sn. The entry function is main in English and الرئيسية
in Arabic. It returns an int, which becomes the process exit status. The two spellings are
interchangeable at every point, and a file may mix them.
import std.io.println;
import std.text.number;
def main(): int {
println("Hello from SultanC");
let answer: int = 6 * 7;
println(int_to_text(answer));
return 0;
}
The first two lines import std modules. std.io.println provides println and std.text.number
provides int_to_text. Statements end with a semicolon, and a block that is used as a statement,
such as an if, ends with one too. The program is in the repository as examples/en/basics/hello.sn, and its Arabic version as
examples/en/basics/hello.sn.
Compiling and running
Pass the source file and, optionally, an output path:
sultanc examples/en/basics/hello.sn -o hello
./hello
Hello from SultanC
42
Without -o the executable takes the base name of the source file. The default output is a native executable
for the host. Other products are selected with --emit:
| Value | Product |
|---|---|
exe |
executable (default) |
obj |
relocatable object; -c is the short form |
asm |
assembly text |
llvm-ir |
optimized LLVM IR text (LLVM backend) |
shared, reloc and module are also accepted. Whether a target and backend can produce them varies; the
compiler reports SULTANC-LINK-OUTPUT-UNSUPPORTED when they cannot.
To link a C library, pass -l name for libname and -L dir to add a directory to the search before the
standard locations. A build links only the libraries named, the C library included: an extern C function
such as labs needs -l c.
Running with the interpreter
sultanc run executes a program without producing a file:
sultanc run examples/en/basics/hello.sn
The interpreter runs the same verified intermediate representation (MSIR) and the same std sources that the
compiler uses, so a program behaves the same under run and as an executable. Arguments after the source file
are passed to the program unchanged. It is the quickest way to try a snippet, and it runs on Linux, macOS,
Windows and FreeBSD. See The interpreter for how it works.
-l and -L mean for run (and interpret) what they mean for a build. They are written before the source
file: after it they are the program's own arguments (sultanc run app.sn -l x passes -l x to app.sn).
sultanc run -l sqlite3 app.sn
sultanc run -L vendor/lib -l mylib app.sn
Each -l name is searched as a build searches it (the -L directories, then the target's), and the shared
library found is loaded into the running program; a program whose C functions run only from an executable
links them instead. -l c and -l m are always satisfied: the interpreter process already provides the C
and math libraries. A library found only as a static archive (libname.a) is linked when the program runs as
an executable (a compiler without a dynamic loader, or C the interpreter cannot call); when the interpreter
runs the program, which loads shared libraries only, it is reported as SULTANC-RUN-LIBRARY-STATIC-ONLY. The
archive never changes how the program runs. On macOS a libname.tbd stub loads the
dylib it names (SULTANC-RUN-LIBRARY-NO-INSTALL-NAME when it names none); on Windows -l name loads
name.dll from an -L directory, or through the system's DLL search.
run always provides the C and math libraries to the program, so an extern C function that works under
run may still need -l c (or -l m) when the program is built.
Checking without building
sultanc check parses, resolves names, type-checks and runs the ownership and borrow analysis, and stops
before code generation:
sultanc check src/main.sn
sultanc check --target wasm32-wasi src/main.sn
It exits with status 0 when the program is accepted and 1 when diagnostics report an error. With --target
it checks for that target, which matters for code that depends on the platform. Without it the host is assumed.
Testing
A function marked @@test (@@اختبار) is a test. sultanc test builds the program's tests and runs each in its
own process, then prints a summary:
sultanc test src/main.sn
sultanc test --filter parse src/main.sn
sultanc test --list src/main.sn
It exits with status 0 when every test passed, 1 when one failed, and 2 when the tests could not be built. An ordinary build never runs a test. Testing has the details.
Cross-compiling
--target (-t) selects the target triple, and the compiler produces an executable for it with no further
setup:
sultanc --target=aarch64-linux-gnu examples/en/basics/hello.sn -o hello-arm
sultanc --target=x86_64-windows-msvc examples/en/basics/hello.sn -o hello.exe
sultanc --target=wasm32-wasi examples/en/basics/hello.sn -o hello.wasm
sultanc targets lists every registered target with its architecture, operating system, object format and
level of support. The compiler has its own linkers for ELF on x86_64 and aarch64 Linux, Mach-O, COFF and
WebAssembly. Other Linux ELF targets and FreeBSD are linked with ld.lld, which you can override with the
SULTANC_LINKER environment variable. Executables on Linux and FreeBSD are static and use no libc. The
Targets reference says what each target supports today.
The LLVM backend
Two code generators exist. The Native backend generates x86_64, arm64 and wasm32 code directly. The LLVM
backend covers every target and optimizes at O2. sultanc targets prints the backends your compiler contains.
Select one with -b or --backend:
sultanc --backend=llvm examples/en/basics/hello.sn -o hello
sultanc --backend=llvm --target=riscv64-linux-gnu examples/en/basics/hello.sn -o hello-rv
sultanc --backend=llvm --emit=llvm-ir examples/en/basics/hello.sn -o hello.ll
A compiler built without --enable-llvm has only the Native backend. See
MSIR and code generation for how both backends consume the same IR.
Diagnostics
Every diagnostic has a stable identity of the form SULTANC-<AREA>-<NAME>, a message, and the source
locations involved. Diagnostics are written in Arabic by default. --lang chooses the language:
ar, en, or auto, which follows the host locale.
sultanc --lang=en check bad.sn
error[SULTANC-TYPE-MISMATCH]: expected `int`, found `string` — The value does not match the declared type of its destination.
--> bad.sn:2:18..2:24
|
2 | let x: int = "text";
| ^^^^^^
|
Quoted names are wrapped in Unicode bidirectional isolates so that a Latin identifier inside Arabic text keeps its direction.
--diagnostic-format selects how diagnostics are written:
| Format | Output |
|---|---|
human |
the annotated source excerpt shown above (default) |
short |
one line per diagnostic: file:line:column: severity[ID]: message [reason] |
json |
one JSON object per diagnostic, for editors and tools |
A JSON diagnostic carries the schema version, the language, the identity, the severity, the message, the primary
span with file, byte offsets, line and column, and the arguments of the message in both languages. --color
accepts auto, always and never.
sultanc explain describes a diagnostic identity, in the language chosen by --lang:
sultanc --lang=en explain SULTANC-OWNERSHIP-USE-AFTER-MOVE
SULTANC-OWNERSHIP-USE-AFTER-MOVE
Value used after move. Find the move in the related spans.
A program that ends on an error, a panic or a failed assertion prints its own frame on standard error, in
the language of the program's environment (SULTANC_LANG, then LC_ALL, LC_MESSAGES and LANG, then the
--lang it was compiled with). Errors describes that path.
Where to go next
- Language tour walks through the language with short examples.
- The
examples/directory holds runnable programs in both languages, grouped by topic. - Type system, Ownership, Errors, Compile-time evaluation and Modules and imports specify the language.
- Arabic terminology lists the canonical Arabic vocabulary.
- Compiler architecture explains how the compiler is organized.