A compiled programming language with a Lark parser frontend and LLVM backend that produces native binaries.
PARENTAL ADVISORY
This project is a result of a thought process when I asked myself "how hard is it to create a programming language?" Then I went and wrote:
fn main():
return(0)
I spent a couple of hours reading about grammar parsers, AST, LLVM, and similar stuff, and then after some time I had a compiled executable file that did nothing.
Then I started working on print() and then on variables, soon
after this I realized that this is going way too slow and I will
need to ask peoplerobots to help me. Then things really took
off.
Long story short, now I am vibe coding a programming language, losing my mind trying to convince LLMs to output the code that I want, and I generally know much more about compilers that I used to know. It's been fun.
If you are triggered by mediocre and badly written code, you might want to stay away. Or you might find this funny.
Apart from this section, most of the things in the documentation were just briefly checked for sanity, and revised by a human. Same goes for code. A rough estimate is that 80% of the code was iteratively generated by LLMs. Then LLMs were used to refactor this code. Obvious mistakes spotted by a human were then corrected mostly by LLMs. This code base is in some parts quite ridiculous and can bring even seasoned developers and code reviewers to tears. You have been warned.
Sushi is actively tested on macOS (Apple Silicon) and Linux (via CI). No testing has been done on Windows yet, so the current status is unknown.
What is the goal with Sushi? Mostly me teaching myself about compilers and related subjects, and playing around with LLMs. At one point I will decide that I added enough features, then I will try and turn it into a self-hosted compiler.
Sushi Lang is a statically-typed compiled language designed with safety, simplicity, and performance in mind. The compiler follows a clean multi-pass architecture with comprehensive semantic analysis and LLVM-powered code generation.
Key Features:
- Static type system with explicit casting
- Generic types with compile-time monomorphization (
Result@(T, E),Maybe@(T),List@(T),HashMap@(K, V), user-defined structs, enums, and functions) - Generic functions with automatic type inference and perk constraints
- Perks (traits/interfaces) for polymorphic behavior with static dispatch
- Error types declared with
error, the error propagation operator (??), and declared conversions between error types (extend FileError as AppError:) - Parameter modes: a parameter borrows by default,
nomhands the value over, andpeek/pokeborrow by pointer, all with compile-time borrow checking Own@(T)heap allocation for recursive types (linked lists, trees)- Extension methods for zero-cost method chaining, with an optional error channel (
| E) and static methods (extend Vec static at(...), called asVec.at(...)) - Visibility: every declaration is private to its unit unless it says
public - Units as namespaces (
use "geometry" as geo) and re-exports (public use) - Unit-level storage (
var) beside compile-time constants (const) - Reference bindings (
let peek T x = ...,let poke T x = ...) - The predefined perks
Drop(a type that owns a resource) andHashable - Documentation blocks (
##: ... :##) that the compiler checks and a library carries - I/O contracts (
Reader,Writer,Seek) and buffered I/O (BufReader,BufWriter) - Closures / lambdas (
|x| expr) with RAII-managed, move-semantics captures - First-class functions (
fn(i32) -> i32values, passable and callable) - Foreign Function Interface (
unsafe external "C") for calling C libraries - Variadic functions (memory-safe native
...Tarray sugar; C...for FFI bindings) - Variadic generics / parameter packs (
...Ts: Perk+expand(x in args), compile-time unrolled) - Rust-style enums with exhaustive pattern matching
- Automatic memory management (RAII) for structs, arrays, and collections
- Full UTF-8 Unicode support
- Native code generation via LLVM
- Incremental compilation with per-unit object file caching
# Compile a program
./sushic hello.sushi
# Run the compiled binary
./hello
# Optimization levels
./sushic --opt O2 program.sushi # Recommended: balanced performance
./sushic --opt O3 program.sushi # Maximum performance
# Create and use libraries
./sushic --lib --lib-version 1.0.0 mylib.sushi -o mylib.slib # Compile to library
export SUSHI_LIB_PATH=. # Set library path
./sushic main.sushi # use <lib/mylib> in sourcefn main() i32:
println("Mostly Harmless")
return 0
- Installation and Setup - Get Sushi running on your machine
- Language Guide - Friendly tour of Sushi's features
- Examples - Learn by example (29 hands-on programs)
- Language Reference - Complete syntax and semantics
- Standard Library - Built-in types and functions
- Error Handling -
Result@(T, E),Maybe@(T), error types, the??operator and error conversion - Memory Management - RAII, references, and ownership
- Generics - Generic types and compile-time monomorphization
- Perks - Traits/interfaces for polymorphic behavior
- Compiler Reference - CLI options and optimization levels
- Libraries - Creating and using reusable libraries
- Architecture - Compiler design and structure
- Semantic Passes - Pass-by- pass analysis
- Backend - LLVM code generation
A function that can fail writes its error channel | E, and its call returns a
Result@(T, E). A function with no | E is bare: it returns the value itself. A bare
function is the exception: use it only for a function that is total over its inputs and
will stay so. A public function keeps a channel when there is any doubt, because a
channel added later breaks every caller. See
the error channel design.
fn divide(i32 a, i32 b) i32 | StdError:
if (b == 0):
return Result.Err(StdError.Error)
return Result.Ok(a / b)
fn main() i32:
let i32 result = divide(10, 2).realise(0)
println("Result: {result}")
return 0
The ?? operator unwraps a Result or propagates its error. When the two error types
differ, it calls a conversion that the program declares:
use <io/fs>
fn read_config() string | IoError:
let File f = open("config.txt", FileMode.Read())??
let string content = f.read_all()??
f.close()??
return Result.Ok(content)
error AppError:
Io(IoError)
extend IoError as AppError:
return AppError.Io(self)
fn load() string | AppError:
return Result.Ok(read_config()??) # IoError as AppError
Exhaustive pattern matching with enums:
enum Status:
Idle()
Working(i32)
Done()
fn check(Status s) ~:
match s:
Status.Idle() -> println("Idle")
Status.Working(progress) -> println("Progress: {progress}%")
Status.Done() -> println("Completed")
Type-safe generics with zero runtime overhead:
struct Pair@(T, U):
T first
U second
fn main() i32:
let Pair@(i32, string) p = Pair(first: 42, second: "answer")
println("{p.second}: {p.first}")
return 0
Compile-time borrow checking and RAII. A parameter borrows unless it says nom, and a
marked mode is written at both ends:
fn increment(poke i32 counter) ~:
counter := counter + 1
fn eat(nom i32[] items) i32:
return items.len() # items is freed here
fn main() i32:
let i32 count = 0
increment(poke count)
println("Count: {count}") # 1
let i32[] data = from([1, 2, 3])
println(eat(nom data))
# println(data.len()) # CE2405: data was handed over
return 0
Static polymorphism through perks with zero runtime overhead:
perk Shape:
fn area() i32
struct Rect:
i32 w
i32 h
extend Rect with Shape:
fn area() i32:
return self.w * self.h
# Generic function with perk constraint
fn total_area@(T: Shape)(T value) i32:
return value.area()
fn main() i32:
let Rect r = Rect(4, 5)
let i32 a = total_area(r) # T is inferred as Rect
println("Area: {a}")
return 0
Heterogeneous, perk-constrained parameter packs, fully monomorphized at compile time:
perk Display:
fn display() string
extend i32 with Display:
fn display() string:
return "int:42"
extend string with Display:
fn display() string:
return self.clone()
fn print_all@(...Ts: Display)(...Ts args) ~:
expand(a in args): # compile-time unrolled, not a runtime loop
println(a.display())
fn main() i32:
print_all(42, "hi") # monomorphizes per (arity, type-tuple)
print_all() # arity-0: expand body runs 0 times
return 0
See the variadics design doc for the full design and its limits.
| Level | Description | Use Case |
|---|---|---|
none |
No optimization | Debugging |
mem2reg |
Basic SROA (default) | Quick builds |
O1 |
Basic optimizations | Fast compilation |
O2 |
Moderate optimizations | Recommended for production |
O3 |
Aggressive optimizations | Maximum performance |
# Development
./sushic program.sushi
# Production
./sushic --opt O2 program.sushi -o app# Run test suite
python tests/run_tests.py
# Only the fixtures that do not run a binary (the fast diagnostics gate)
python tests/run_tests.py --compile-only
# Run only the leak-annotated subset (the same check, a faster gate)
python tests/run_tests.py --leaks-only
# Filter specific tests
python tests/run_tests.py --filter hashmapEvery run executes each compiled binary and enforces the # EXPECT_* directives it
declares, including EXPECT_NO_LEAKS: the binary is run again under a malloc interposer
(tests/leakcheck) and an allocation that is not freed fails the test. A flag only selects
WHICH fixtures run, never how hard they are checked. --leaks-only selects the tests that
carry EXPECT_NO_LEAKS.
CI (GitHub Actions) additionally runs ruff and mypy (blocking over a growing set of
type-checked packages, informational over the full tree) as a lint gate, and the
cross-platform leak gate (tests/run_tests.py --leaks-only) on both Linux and macOS before
the full suites run.
Check out the examples directory for hands-on learning:
- 01-hello.sushi - Basic program structure
- 04-strings.sushi - String operations
- 07-result.sushi - Error handling
- 15-lists.sushi - Generic lists
- 16-hashmaps.sushi - Hash tables
- 28-ffi.sushi - Foreign function interface
sushi/
├── sushi_lang/ # Main package (installed to site-packages)
│ ├── compiler/ # CLI entry point, pipeline & caching
│ ├── grammar.lark # Lark grammar specification
│ ├── internals/ # Diagnostics, parser, error registry
│ ├── semantics/ # Semantic analysis passes
│ │ ├── passes/ # Multi-pass type checking
│ │ └── generics/ # Generic type system
│ ├── backend/ # LLVM code generation
│ │ ├── expressions/ # Expression emission
│ │ ├── statements/ # Statement emission
│ │ └── types/ # Type-specific codegen
│ ├── sushi_stdlib/ # Standard library
│ │ ├── src/ # Python IR generators
│ │ ├── src_sushi/ # Stdlib modules written in Sushi
│ │ └── dist/ # Precompiled .bc files
│ └── packager/ # Nori package manager CLI
├── toolchain/ # Sushi programs that compile Sushi (repository only)
├── sushic # Development wrapper script
├── tests/ # Test suite
└── docs/ # Documentation
Sushi combines:
- Rust's safety - Ownership, borrowing, explicit error handling
- Python's simplicity - Clean syntax, readable code
- C's performance - Zero-cost abstractions, native binaries
- Python 3.13+ (managed by uv)
- LLVM 20 (llvmlite 0.45 requirement)
- cmake (required for building llvmlite)
# Install uv (if not already installed)
curl -LsSf https://astral.sh/uv/install.sh | sh
# Install cmake (macOS)
brew install cmake
# Install LLVM 20
brew install llvm@20
# Install Python dependencies
uv sync --extra dev
# Build standard library
uv run python sushi_lang/sushi_stdlib/build.py
# Test installation
./sushic --helpImportant: LLVM 20 is keg-only on macOS. The build process will automatically use the correct version through the LLVM_CONFIG environment variable if needed.
# Compile with debug output
./sushic --traceback --dump-ll program.sushi
# View AST
./sushic --dump-ast program.sushi
# Save LLVM IR
./sushic --write-ll program.sushi
cat program.ll
# Run test suite
uv run python tests/run_tests.py
# Run the Python unit layer
uv run python -m pytest -qSushi can be packaged as a Python wheel for easy installation without requiring users to set up LLVM or clone the repository.
# Build the wheel (requires hatchling)
uv build --wheel
# The wheel will be created in dist/
ls dist/
# sushi_lang-0.15.0-py3-none-any.whl# Install in a virtual environment
pip install sushi_lang-0.15.0-py3-none-any.whl
# The sushic command is now available
sushic program.sushiThe wheel contains:
- The
sushi_langpackage with all compiler modules - Grammar file (sushi_lang/grammar.lark)
- Standard library sources, both the Python IR generators (sushi_lang/sushi_stdlib/src/) and the Sushi-source modules (sushi_lang/sushi_stdlib/src_sushi/)
- Precompiled stdlib bitcode for macOS and Linux (sushi_lang/sushi_stdlib/dist/)
Users don't need to install LLVM - the llvmlite dependency bundles
LLVM binaries in its wheels.
GitHub releases automatically build wheels with fresh stdlib for both
platforms via .github/workflows/release.yml. The workflow:
- Builds stdlib .bc files on Linux (Debian Trixie) and macOS
- Merges artifacts and builds the wheel
- Attaches the wheel to the GitHub release
To create a release:
- Tag a commit:
git tag v0.15.0 - Push the tag:
git push upstream v0.15.0 - Create a release on GitHub from the tag
- The workflow will automatically build and attach the wheel
Contributions welcome! See documentation for compiler internals and architecture.
Sushi (すし) — The programming language and its compiler.
In Japanese, sushi refers to a dish of vinegared rice accompanied by various ingredients such as raw fish, seafood, and vegetables. It is one of the most recognized symbols of Japanese cuisine worldwide. Just as sushi combines simple ingredients into something refined, the Sushi language aims to blend safety, simplicity, and performance into a cohesive whole.
Nori (海苔) — The package manager for Sushi Lang.
Nori is edible seaweed, most commonly used as the dark green wrapper that holds sushi rolls together. In the Sushi ecosystem, Nori is the tool that wraps everything up — it packages compiled libraries into distributable
.noriarchives, manages project manifests (nori.toml), and handles installing and publishing packages.
Bento (弁当) — The package storage directory structure.
A bento is a Japanese meal served in a compartmentalized box, with each section neatly holding a different dish. Similarly, the bento directories (
~/.sushi/bento/for global packages,.sushi_bento/for project-local dependencies) serve as organized containers where installed packages are stored, each in its own compartment.
Omakase (お任せ) — The central package repository.
Omakase literally means "I'll leave it up to you" and refers to a style of dining where the chef selects and serves the best available dishes. The Omakase repository at omakase.lubica.net is the curated central hub where Nori packages are published, discovered, and shared — the chef's choice of the Sushi ecosystem.
📚 Read the full documentation | 🚀 Get started | 💡 See examples