Compiler Reference¶
Complete reference for the Sushi compiler: CLI options, optimization levels, and error codes.
Table of Contents¶
Command Line Interface¶
Basic Usage¶
./sushic [options] <source-file> [-o output]
Examples¶
# Compile single file (output: hello)
./sushic hello.sushi
# Specify output name
./sushic program.sushi -o myprogram
# Multi-unit project (units are pulled in via `use` imports, not extra CLI args)
./sushic main.sushi -o app
# With optimization
./sushic --opt O3 program.sushi -o optimized
Options¶
| Option | Description |
|---|---|
-o NAME |
Specify output executable name |
--opt LEVEL |
Set optimization level (none, mem2reg, O1, O2, O3) |
--lib |
Compile to a library (.slib) instead of an executable |
--lib-kind KIND |
What the library ships: source (default), binary, hybrid |
--lib-version X.Y.Z |
Version of the library being built |
--lib-info FILE |
Print the metadata report of a .slib file |
--docs |
With --lib-info: print each symbol's documentation block |
--color WHEN |
always, never or auto (the default) |
--ignore-compiler-version |
Load libraries this compiler does not satisfy (CE3503) |
--warn-missing-docs |
Warn about anything with no documentation block (CW7002-CW7006) |
--traceback |
Show full Python traceback on errors |
--dump-ast |
Print abstract syntax tree |
--dump-ll |
Print LLVM IR to terminal |
--write-ll |
Write LLVM IR to <output>.ll file |
--no-incremental |
Force full rebuild, ignoring cached object files |
--clean-cache |
Remove __sushi_cache__/ directory and exit |
--cache-dir PATH |
Custom cache directory location |
Documentation Completeness¶
./sushic --warn-missing-docs main.sushi
The compiler always checks what a doc block CLAIMS against the declaration beside it. This flag adds the other half: it reports what is missing.
mymodule.sushi: warning [CW7006]: this unit has no documentation block.
mymodule.sushi:3:8: warning [CW7002]: this struct has no documentation block: 'Ship'.
| struct Ship:
` --+-
mymodule.sushi:9:4: warning [CW7004]: 'hull_of' returns a value, and no '- Returns:' tag says what it is.
| fn hull_of(Ship ship) i32:
` ---+---
Every declaration is asked, public and private. fn main() and the unsafe external seam
are the only exemptions, and a library's units are never linted. The five codes and the
rules behind them are in
Documentation Blocks.
Library Compilation¶
# Compile source to a reusable library
./sushic --lib --lib-version 1.0.0 mylib.sushi -o mylib.slib
# Inspect library metadata
./sushic --lib-info mylib.slib
# The same report, with every documentation block in it
./sushic --lib-info mylib.slib --docs
A .slib ships source by default: the consumer compiles its units and caches the
objects, so one file works on every platform. --lib-kind binary ships LLVM bitcode
instead, which binds the library to the platform that built it, and --lib-kind hybrid
ships both.
Every library states its own version. --lib-version supplies it, unless a nori.toml
beside the sources does; neither is CE3505. A build also stamps requires_compiler --
~<major>.<minor> of the building compiler -- because a source library is compiled by the
consumer's compiler and a later one may reject it. A consumer outside that range is
CE3503, and --ignore-compiler-version overrides the check for the whole build.
The plain report is the API surface: one line per symbol, plus a parameter's nom mode
beside its type, which is the one mode a type cannot spell for itself. A perk prints with
each method's signature, receiver mode included (fn read(poke self, u8[] buf) i32 |
IoError), and Perk Implementations lists every type that implements it, a generic-target
template (extend Box@(T) with Show) beside the concrete ones.
--docs prints each symbol's documentation block under its signature. It is opt-in
because prose is what makes a report long -- a library of forty documented functions runs
to ten screens with the blocks in and one and a half without. A .slib carries the doc
text of every symbol it exports, so this answers what a library holds without its
sources -- see
Documentation Blocks.
In a repository checkout, --lib-info runs the Sushi-written toolchain/bin/slib-info
binary when it exists (build it with ./toolchain/build.py) and returns its exit code;
without the binary the built-in Python reader prints the same report. --docs and
--color reach the tool as themselves, and the tool answers --help on its own. Set
SUSHI_TOOLCHAIN=off to force the Python path, or SUSHI_TOOLCHAIN_BIN=DIR to point at
a different tool directory.
Colour¶
Everything the compiler prints to a terminal -- a diagnostic, the version banner and the
--lib-info report -- makes one colour decision. Highest precedence first:
| # | Rung | Answer |
|---|---|---|
| 1 | --color=always / --color=never |
as asked |
| 2 | NO_COLOR set to anything, the empty string included |
off |
| 3 | CLICOLOR_FORCE set to anything but 0 |
on, terminal or not |
| 4 | TERM=dumb |
off |
| 5 | the stream is a terminal | on, else off |
Rung 2 is no-color.org's rule: the variable's presence is the signal, never its value. Rung 3 is what lets a script capture a coloured report, and what lets this project's own gates compare one.
Colour changes no text. Strip the escapes from a coloured report and the plain report comes back, byte for byte.
Libraries are used via use <lib/...> statements in source code:
use <lib/mylib>
fn main() i32:
# Library functions/types are now available
return Result.Ok(0)
See Libraries for complete documentation.
Incremental Compilation¶
Multi-unit projects (those with use statements to other .sushi files) automatically use incremental compilation. Each unit is compiled to its own .o file and cached in __sushi_cache__/. Only units whose content or dependencies change are recompiled.
# First build: compiles all units
./sushic main.sushi
# Second build: reuses cached .o files (near-instant codegen)
./sushic main.sushi
# Force full rebuild
./sushic --no-incremental main.sushi
# Clean cache and exit
./sushic --clean-cache
# Custom cache directory
./sushic --cache-dir /tmp/my_cache main.sushi
How it works:
- Semantic analysis always runs whole-program (fast, pure Python)
- After analysis, a content-based fingerprint is computed per unit
- A unit's fingerprint covers its own source and declarations, and the interface of
every unit in its dependency closure: each declaration's shape (a struct's field order
and field types, an enum's variant order and payloads, a signature's parameter modes
and error channel) and each public constant's value, because a dependent bakes all
of those into its own object. The closure is transitive, so a type reached through a
public use counts, and it includes a source library's injected units
- Each cached object is content-addressed: its filename is
{name}.{global_key}.{fingerprint}.o, where global_key digests the compiler
version, target triple, opt level, and a content digest of the compiler's own
sources, and fingerprint digests the unit itself
- A hit therefore means exactly "an object built from this input, by this compiler,
with these settings" — there is no separate staleness check to get wrong; a
compiler upgrade, a compiler source edit, or a settings change simply names a
different file and is a cache miss by construction
- Stdlib and library imports are cached as .o files the same way
- Publishing an object is atomic (write to a temp file, then os.replace), so a
concurrent build sharing the same cache directory never links a truncated object
- All .o files are linked together at the end
Because invalidation is structural, --clean-cache is never needed for
correctness — it only prunes entries for settings/versions you no longer use.
Output example:
Code generation:
main [cached]
helpers/math [rebuilt]
helpers/strings [cached]
Codegen: 3 units (2 cached, 1 rebuilt) in 0.05s
Linking: 3 units + 1 stdlib in 0.03s
Success! Wrote native binary: main
Notes:
- Single-file programs skip incremental compilation entirely
- --dump-ll forces the monolithic (non-incremental) path
- --write-ll is not supported in incremental mode
- The cache directory (__sushi_cache__/) is already in .gitignore
Optimization Levels¶
Sushi provides a complete LLVM optimization pipeline with multiple levels.
Overview¶
| Level | Description | Use Case | Compile Time |
|---|---|---|---|
none / O0 |
No optimization | Debugging, development | Fastest |
mem2reg |
SROA only (default) | Quick builds with SSA | Very fast |
O1 |
Basic optimizations | Fast compilation + improvements | Fast |
O2 |
Moderate optimizations | Recommended for production | Moderate |
O3 |
Aggressive optimizations | Maximum performance | Slower |
Default Behavior¶
If no --opt flag is specified, mem2reg is used (basic SROA for SSA form).
mem2reg (Default)¶
What it does: - SROA (Scalar Replacement of Aggregates) - promotes stack allocations to registers - Converts code to SSA (Static Single Assignment) form - Minimal overhead, fast compilation
Use when: - Quick development builds - Testing and iteration - SSA form needed for debugging
./sushic program.sushi
# Equivalent to:
./sushic --opt mem2reg program.sushi
O1 - Basic Optimizations¶
What it does: - All of mem2reg, plus: - CFG simplification - removes redundant branches - Instruction combining - peephole optimizations - Dead code elimination - removes unused code and variables - Basic constant folding
Use when: - Development with some performance - Testing with realistic speed - Fast CI/CD builds
./sushic --opt O1 program.sushi
Performance impact: 10-30% faster than none, minimal compile time increase.
O2 - Moderate Optimizations (Recommended)¶
What it does: - All of O1, plus: - SCCP (Sparse Conditional Constant Propagation) - Loop optimizations (rotation, simplification, deletion) - GVN (Global Value Numbering) - eliminates redundant computations - Memory optimizations (memcpy optimization, dead store elimination) - Jump threading - optimizes conditional branches - Tail call elimination - Interprocedural optimizations (IPSCCP, dead argument elimination)
Use when: - Production builds - Benchmarking - Deployment
./sushic --opt O2 program.sushi -o production
Performance impact: 50-200% faster than none, moderate compile time.
O3 - Aggressive Optimizations¶
What it does: - All of O2, plus: - Aggressive loop unrolling - unrolls loops for better performance - Loop strength reduction - optimizes loop induction variables - Aggressive instruction combining - Instruction sinking - moves instructions for better register allocation - Argument promotion - converts by-reference to by-value where beneficial - Function merging - combines identical functions
Use when: - Maximum performance required - Benchmarking - Performance-critical production code
./sushic --opt O3 program.sushi -o maximum_performance
Performance impact: 100-300% faster than none, longest compile time.
Optimization Examples¶
Example program impact:
Level IR Lines Binary Size Relative Speed
none (O0) 278 34,016 bytes 1.0x (baseline)
mem2reg 245 34,000 bytes 1.1x
O1 220 33,980 bytes 1.3x
O2 195 33,968 bytes 1.8x
O3 181 33,960 bytes 2.2x
Real optimizations observed:
# Source code
let i32 x = 10
let i32 y = (x + 5) * 2
# After O2/O3 constant folding
let i32 y = 30 # Computed at compile time
Viewing Optimized Code¶
# Save LLVM IR to file
./sushic --opt O3 --write-ll program.sushi
cat program.ll
# Print IR to terminal
./sushic --opt O3 --dump-ll program.sushi
Recommendations¶
Development:
# Fastest iteration
./sushic --opt none program.sushi
# Or default (mem2reg)
./sushic program.sushi
Testing:
# Balance of speed and compile time
./sushic --opt O1 program.sushi
Production:
# Recommended for most deployments
./sushic --opt O2 program.sushi -o app
# Maximum performance
./sushic --opt O3 program.sushi -o app
Benchmarking:
# Always use O3 for true performance measurement
./sushic --opt O3 benchmark.sushi -o bench
./bench
Debugging Options¶
Full Traceback¶
Show complete Python stack trace on compiler errors:
./sushic --traceback program.sushi
When to use: - Reporting compiler bugs - Understanding internal errors - Debugging compiler itself
Dump AST¶
Print the abstract syntax tree:
./sushic --dump-ast program.sushi
Output includes: - Parsed AST structure - Type annotations - Scope information
When to use: - Understanding parsing - Debugging grammar issues - Compiler development
Print LLVM IR¶
Display generated LLVM IR:
./sushic --dump-ll program.sushi
When to use: - Understanding code generation - Verifying optimizations - Learning LLVM IR - Performance debugging
Save LLVM IR to File¶
Save IR to <output>.ll:
./sushic --write-ll program.sushi
cat program.ll
# With custom output name
./sushic --write-ll program.sushi -o myapp
cat myapp.ll
When to use: - Analyzing optimized code - Sharing IR for debugging - Comparing optimization levels
Combining Debug Options¶
# Full debug output
./sushic --traceback --dump-ast --dump-ll program.sushi
# Save IR with optimization
./sushic --opt O3 --write-ll program.sushi
Error Codes¶
Sushi uses structured error codes for diagnosing issues.
Error Code Format¶
- CE0xxx: Internal/function errors
- CE1xxx: Scope/variable errors
- CE2xxx: Type/array/struct errors
- CE3xxx: Unit management errors
- CWxxxx: Warnings
- RExxxx: Runtime errors
Common Errors¶
CE1001: Undeclared Identifier¶
fn main() i32:
# ERROR CE1001: use of undeclared identifier 'x'
println(x)
return Result.Ok(0)
Fix: Declare variable with let before use.
CE1002: Rebind to Undeclared Variable¶
fn main() i32:
# ERROR CE1002: rebind to undeclared variable 'count'
count := 5
return Result.Ok(0)
Fix: Declare with let first (let i32 count = 0) before rebinding with :=.
CE2024: Use of Destroyed Dynamic Array¶
fn main() i32:
let i32[] arr = from([1, 2, 3])
arr.destroy()
# ERROR CE2024: use of destroyed dynamic array 'arr'
println(arr.len())
return Result.Ok(0)
Fix: Don't use a variable after .destroy(), or use .free() instead.
CE2502: .realise() Wrong Argument Count¶
fn get_value() i32:
return Result.Ok(42)
fn main() i32:
let Result@(i32, StdError) r = get_value()
# ERROR CE2502: realise() requires exactly 1 argument, got 0
let i32 x = r.realise()
return Result.Ok(0)
Fix: Provide default value: r.realise(0).
CE2503: .realise() Type Mismatch¶
fn get_value() i32:
return Result.Ok(42)
fn main() i32:
let Result@(i32, StdError) r = get_value()
# ERROR CE2503: realise() default type mismatch: expected 'i32', got 'string'
let i32 x = r.realise("wrong")
return Result.Ok(0)
Fix: Use correct type: r.realise(0).
CE2505: Assigning Result Without Handling¶
Assigning a Result-returning call directly to a non-Result variable is refused, and
the code names the fix:
fn get_value() i32:
return Result.Ok(42)
fn main() i32:
# ERROR CE2505: cannot assign Result@(T) to non-Result variable without handling
let i32 x = get_value()
return Result.Ok(0)
Fix: Use .realise(): let i32 x = get_value().realise(0).
CE2507: Using ?? on Non-Result Type¶
fn main() i32:
let i32 x = 5
# ERROR CE2507: ?? can only be used with Result@(T) or Maybe@(T)
let i32 y = x??
return Result.Ok(0)
Fix: Only use ?? with Result@(T) or Maybe@(T).
CE2508: Using ?? Outside Result Function¶
extend i32 squared() i32:
# ERROR CE2508: ?? only works in Result-returning functions
let i32 x = might_fail()??
return self * self
Fix: Don't use ?? in extension methods (limitation). Extension methods return
their value directly (bare return, no Result.Ok(...) wrapper).
CE3007: No main() Function¶
# ERROR CE3007: no main() function: an executable needs an entry point
fn helper(i32 a) i32:
return Result.Ok(a + 1)
Fix: Add fn main() i32:, or compile the unit as a library with --lib. A library must
not carry a main() -- that is the mirror error, CE3501.
CE3008: Linking Failed¶
Reported when the C compiler used as the linker exits non-zero -- most often an
unsafe external declaration naming a symbol that no linked library provides. The linker's
own output is attached to the diagnostic as notes.
Fix: Read the attached notes. An undefined symbol means the link name is wrong or the library it lives in was not linked.
Constant Expression Errors¶
CE0108: Expression Not Compile-Time Constant¶
fn get_value() i32:
return Result.Ok(42)
# ERROR CE0108: Expression is not a compile-time constant
const i32 X = get_value()
Fix: Only use compile-time evaluable expressions (literals, arithmetic, bitwise, etc.).
CE0109: Circular Constant Dependency¶
# ERROR CE0109: Circular constant dependency detected: A -> B -> A
const i32 A = B + 1
const i32 B = A + 1
Fix: Remove circular dependencies between constants.
CE0110: Unsupported Operation in Constant¶
# ERROR CE0110: Unsupported operation '&' in constant expression
const f64 INVALID = 3.14 & 2.0 # Bitwise AND on float
Fix: Use only supported operations for the type (bitwise only on integers).
CE0111: Invalid Type Cast in Constant¶
# ERROR CE0111: Invalid type cast in constant expression from string to i32
const i32 INVALID = "hello" as i32
Fix: Only cast between compatible numeric types.
CE0112: Division by Zero in Constant¶
# ERROR CE0112: Division by zero in constant expression
const i32 INVALID = 100 / 0
Fix: Ensure divisor is non-zero.
Warnings¶
CW2001: Unused Result Value¶
fn get_value() i32:
return Result.Ok(42)
fn main() i32:
# WARNING CW2001: Unused Result@(i32) value
get_value()
return Result.Ok(0)
Fix: Handle result or explicitly discard:
let Result@(i32) r = get_value() # Store for later
let i32 x = get_value().realise(0) # Use immediately
Runtime Errors¶
RE2020: Array Bounds Check Failed¶
fn main() i32:
let i32[3] arr = [1, 2, 3]
let i32 i = 10
# Runtime error: direct indexing out of bounds
let i32 x = arr[i]
return Result.Ok(0)
Direct indexing (arr[i]) is checked at runtime and aborts on an out-of-bounds
access. (Use arr.get(i), which returns Maybe@(i32), for safe access instead.)
Runtime output:
Runtime Error RE2020: array index 10 out of bounds for array of size 3
RE2021: Memory Allocation Failed¶
Runtime error RE2021: Memory allocation failed (malloc returned null)
Occurs when system runs out of memory during dynamic allocation.
Testing¶
Test Runner¶
# Run all tests (compilation only)
python tests/run_tests.py
# Run with runtime validation
python tests/run_tests.py --enhanced
# Filter tests by pattern
python tests/run_tests.py --filter hashmap
python tests/run_tests.py --filter test_result
Test Types¶
Positive tests (test_*.sushi):
- Must compile successfully (exit code 0)
- Executable may or may not run
Warning tests (test_warn_*.sushi):
- Must compile with warnings (exit code 1)
Error tests (test_err_*.sushi):
- Must fail compilation (exit code 2)
- Used to verify error detection
Writing Tests¶
# tests/test_my_feature.sushi
fn main() i32:
let i32 x = 42
println("Test passed")
return Result.Ok(0)
# Run your test
./sushic tests/test_my_feature.sushi
./test_my_feature
Test Naming Conventions¶
test_@(feature).sushi- Positive testtest_warn_@(feature).sushi- Expected warningtest_err_@(feature).sushi- Expected errortest_@(category)_@(specific).sushi- Organized by category
See also: - Getting Started - Installation and first program - Language Reference - Complete syntax - Compiler Internals - How the compiler works