Vitte is an experimental systems language and toolchain.
It is designed to make low‑level code readable, explicit, and predictable.
Vitte is not trying to be clever. It prefers clarity over implicit magic, and it favors simple, structured syntax that tools can parse and analyze reliably. The goal is to make it easier to build serious system software without sacrificing the ability to reason about what the code does.
This section helps you understand the philosophy of the project before you start.
Vitte aims to be:
- A language that is easy to read and reason about. That means fewer hidden rules and fewer surprises.
- A structured syntax designed for robust tooling. Editors and linters work better when the grammar is consistent.
- A clean compiler pipeline (frontend → IR → backend). You can inspect or improve each stage without guessing where bugs are.
- A practical base for kernel, embedded, and toolchain projects. This is the world Vitte targets.
Vitte is not:
- A stable language. The syntax can still change as the compiler evolves.
- A clone of Rust/C/C++. The goal is not to copy, but to explore a different design.
- A web‑framework‑first language. This is a systems language, not a frontend stack.
Many systems languages are powerful but difficult to parse mentally. They often include implicit behavior, complex rules, or decades of legacy. Vitte tries to keep the surface syntax small and explicit so you can read code quickly and understand it without context.
That makes Vitte a good fit for:
- hobby OS development,
- bare‑metal runtimes,
- system tooling,
- embedded platforms (Arduino/STM32/ESP32),
- and experimental toolchains.
If your priority is strong safety guarantees, Vitte may not be the right choice. If your priority is clarity and control, Vitte might be a good fit.
This is the minimal path to compile and run something.
brew install vitteFrom the repository root:
make buildThis builds the compiler. The binary will be located here:
bin/vitte
./bin/vitte build examples/syntax_features.vitThis takes a .vit file and compiles it. If the compiler reports an error, it will show a location in the source file.
vitte check examples/syntax_features.vitThis is useful while editing because it validates syntax without producing a build.
Vitte can emit deterministic .o files for strict byte-for-byte comparison with C/C++ outputs. This mode is designed for macOS and clang, and focuses on object files rather than final binaries.
Build the compiler, then run:
make reproOr manually:
vitte build --repro --emit-obj -o build/repro/vitte.o tests/repro/min.vitNotes:
--reproforces flags that reduce non-determinism (debug off, fixed prefix maps, and linker UUID disabled where applicable).--emit-objproduces an object file and skips linking.--reproalso enables strict IR lowering order to reduce codegen drift.--repro-strictexists for explicit control, but--reproalready implies it.- See
tools/repro_compare.shfor the comparison logic and current fixtures undertests/repro. - To regenerate C++ fixtures from the current lowering, run
make repro-generate. - To enable binary comparison, run
COMPARE_BIN=1 make repro. - To generate binary mismatch diagnostics (otool/nm), run
COMPARE_BIN=1 DIAG_BIN=1 make repro. - To treat code signatures as ignorable for binary compare, run
COMPARE_BIN=1 STRIP_CODESIG=1 make repro. - To compare against hand-written C++ fixtures (best-effort), run
COMPARE_MANUAL_CPP=1 make repro.
Use this section if you want a clean, repeatable setup.
Requirements: clang, openssl, curl.
brew install llvm openssl@3 curlBuild:
OPENSSL_DIR=/opt/homebrew/opt/openssl@3 make buildIf you get OpenSSL errors, make sure OPENSSL_DIR points to your Homebrew install.
sudo apt-get update
sudo apt-get install -y clang libssl-dev libcurl4-openssl-dev
make buildSupport is experimental. The recommended path is WSL2 (Ubuntu). Install dependencies inside WSL and follow the Linux steps.
The examples below are simple and meant for beginners. Each snippet shows one core idea.
A procedure is a function. It can take parameters and return a value.
proc add(a: int, b: int) -> int {
give a + b
}
procdefines a function.givereturns a value (likereturn).
An entry block defines the program start. It is similar to main in C.
entry main at core/app {
let x: int = 42
give x
}
A form is like a struct. It groups fields together.
form Point {
x: int
y: int
}
A pick is like an enum with variants. It is useful for result types.
pick Result {
case Ok(value: int)
case Err(code: int)
}
match is pattern matching. It selects a branch based on a value.
match res {
case Ok(v) { give v }
case Err(e) { give e }
otherwise { give 0 }
}
Low‑level code is explicit. You must enter unsafe before doing raw operations.
unsafe {
asm("hlt")
}
This is a minimal tutorial you can follow without prior knowledge.
Create a file named hello.vit in the repo root:
entry main at core/app {
let msg: string = "Hello, Vitte!"
give msg
}
./bin/vitte check hello.vitIf there is an error, the compiler will show the file, line, and column.
./bin/vitte build hello.vitThis produces output using the default backend. If something fails, re‑run with check to get clearer errors.
Try modifying the file:
- Add a
procand call it. - Add a
formand pass it around. - Add a
pickand match on it.
This is the fastest way to learn the syntax by doing.
This section adds practical examples of common things you will write in Vitte.
Use let for immutable values and make for mutable values.
let x: int = 10
make counter as int = 0
set updates a mutable value.
make counter as int = 0
set counter = counter + 1
if x > 10 {
give 1
} otherwise {
give 0
}
make i as int = 0
loop {
if i >= 3 { break }
set i = i + 1
}
for item in list {
// use item
}
match value {
case Ok(v) { give v }
case Err(e) { give e }
otherwise { give 0 }
}
select value
when Ok(v) { give v }
when Err(e) { give e }
otherwise { give 0 }
When Vitte fails, it reports file, line, and column, plus a small code snippet. This makes it easier to see the exact place where the error happened.
Vitte keeps its core types simple. These are the most common ones you will see:
intfor integersstringfor textboolfor true/false
Example:
let age: int = 30
let name: string = "Ada"
let ok: bool = true
You can use these types in forms, procedure parameters, and return values.
Procedures are functions. They can take parameters and return a value.
proc greet(name: string) -> string {
give "Hello, " + name
}
If a procedure does not return a value, you can omit the return type.
proc log(msg: string) {
// do something
}
Vitte has explicit module syntax, designed to keep code organized.
useimports a module or specific namespullbrings a module into scope (typically by path)sharere‑exports names for other modules
Examples:
use std/core/types.{int, string}
use std/kernel.{console, time}
pull my/project/module as mod
share all
This makes dependencies explicit and keeps large projects readable.
Vitte uses form to define structured data (similar to a struct) and pick to define variants (similar to an enum).
Use form when you always have the same fields:
form User {
id: int
name: string
}
Use pick when you have multiple possible shapes:
pick Result {
case Ok(value: int)
case Err(code: int)
}
unsafe is required for low‑level operations that the compiler cannot guarantee are safe. asm allows embedding raw assembly.
Example:
#[extern("C")]
proc outb(port: u16, val: u8)
entry main at core/app {
unsafe {
asm("hlt")
}
return
}
These conventions keep code consistent and easy to read:
- Use
snake_casefor variable and function names - Use
PascalCaseforformandpicktypes - Keep one module per file when possible
- Prefer explicit
useimports over global assumptions
These are not enforced yet, but following them makes collaboration easier.
Vitte often uses Result for operations that can fail, and Option for values that may or may not exist.
Example with Option:
proc maybe_find(id: int) -> Option[int] {
if id == 0 {
give Option.None
}
give Option.Some(id)
}
Example with Result:
proc parse_int(s: string) -> Result[int, string] {
if s.len == 0 {
give Result.Err("empty input")
}
give Result.Ok(1)
}
Tests live in the tests/ directory. When you add or modify syntax, you should add a minimal test case that exercises the change.
- Prefer small, focused tests
- Use descriptive filenames
- Keep inputs short so failures are easy to debug
Example:
tests/my_feature.vit
This small module shows imports, a form, a pick, and a simple procedure.
space my/demo
use std/core/types.{int, string}
form User {
id: int
name: string
}
pick FindResult {
case Found(user: User)
case NotFound
}
proc find_user(id: int) -> FindResult {
if id == 1 {
let u: User = User(id = 1, name = "Ada")
give FindResult.Found(u)
}
give FindResult.NotFound
}
This is the general style you can follow for small modules.
This example shows how a small project can be split into modules. You can copy this into a folder and build it.
File structure:
my_project/
main.vit
math.vit
math.vit
space my_project
proc add(a: int, b: int) -> int {
give a + b
}
main.vit
space my_project
use my_project.math
entry main at core/app {
let v: int = math.add(2, 3)
give v
}
This example keeps the module path consistent (space my_project) and imports it from the main file.
When Vitte fails, it shows the exact file, line, and column. You should always read the line above and below the highlighted snippet.
Steps you can follow:
- Read the error message and the shown snippet.
- Check syntax for missing braces, commas, or parentheses.
- Re‑run with
checkto get faster feedback:./bin/vitte check path/to/file.vit - Reduce the file by commenting out sections to isolate the error.
Most beginner errors are missing braces, wrong keywords, or incorrect module paths.
Use this list before you open a PR:
- Code builds locally (
make build) - Tests run if relevant (
make test,make parse,make hir-validate) - New syntax or behavior has a minimal test
- Documentation updated if behavior changed
This keeps reviews fast and helps others understand your changes.
The repository is split into clear areas. This helps beginners navigate:
src/: the compiler implementation.src/vitte/std/: the standard library.examples/: example.vitprograms.tests/: language and regression tests.tools/: scripts and utilities.target/: runtimes and target‑specific toolchains.
These are the most common commands you will use while experimenting:
vitte parse path/to/file.vit
vitte check path/to/file.vit
vitte build path/to/file.vitparseonly checks parsing.checkvalidates more rules without building.buildproduces compiled output.
Vitte is designed for code that is close to the metal, but still readable:
#[extern("C")]
proc outb(port: u16, val: u8)
entry main at core/app {
unsafe {
asm("hlt")
}
return
}
This style is intentional: explicit, minimal, and clear.
Vitte is guided by a small set of principles. These ideas shape both syntax and tooling.
- Explicitness over inference: if something matters, it should be spelled out.
- Readable structure: blocks, forms, and control flow are visually clear.
- Tooling‑friendly syntax: predictable grammar makes IDEs and tooling reliable.
- Deterministic builds: the toolchain aims for stable, reproducible outputs.
- Low‑level control: you should be able to reach the metal when needed.
The compiler is intentionally simple and layered. This makes it easier to debug and extend.
- Frontend: parses the source into an AST and checks syntax and structure.
- HIR (High‑level IR): lowers surface syntax into a more uniform representation.
- MIR (Mid‑level IR): normalizes control flow, patterns, and low‑level operations.
- Backend: emits C++ for now, then relies on the native toolchain.
If you understand this pipeline, you can reason about where changes belong.
This is a simple comparison to help newcomers:
- C: maximum control, minimal safety. Vitte keeps control but adds structure.
- C++: very powerful, but complex. Vitte aims for clarity and smaller rules.
- Rust: strong safety guarantees. Vitte focuses on readability and explicitness.
If you want absolute safety, Vitte is not your choice. If you want explicit, readable systems code, it might be.
Is Vitte stable?
No. Vitte is experimental and syntax may change.
Why a new syntax?
The goal is to make system‑level code more readable and tooling‑friendly.
Can I build an OS with it?
That’s a primary goal; VitteOS development is ongoing.
Is C compatibility guaranteed?
Not directly. The approach is explicit runtime/bindings rather than drop‑in C.
Where do I get help?
See SUPPORT.md or open a GitHub issue with a minimal example.
These files provide more detail:
docs/cli.md: commands and optionsdocs/errors.md: diagnostic codesdocs/stdlib.md: standard library modulesCONTRIBUTING.md: contribution guidelinesSECURITY.md: security policySUPPORT.md: support and helpROADMAP.md: project roadmapCHANGELOG.md: notable changes
Vitte is experimental.
Stability is not guaranteed and breaking changes are expected.
See LICENSE.