mojito is a small Rust implementation of an evolving subset of Mojo. It is not Mojo, and it is not trying to compete with Mojo's production compiler. It is a compact compiler playground for studying the shape of a modern systems programming language compiler. Mojo was chosen as a target because it is a rich language, with value semantics, ownership/borrowing, ASAP destruction, generics, overloading, and compile time execution. It has interesting features associated with C++, Rust, and Zig.
- Parse all of current Mojo and report syntax errors
- Approach semantic parity with a single-threaded, CPU-only subset of current Mojo. All mojito programs should be runnable by Mojo, though platform-specific Mojo programs will remain outside the target.
- Keep the register VM as the executable semantic oracle while adding a stable, human-readable MIR/VM assembly format and, later, native Cranelift and LLVM backends.
See the authoritative feature matrix for the distinction between executable, checked-only, parse-only, and unsupported features. The list below is only a high-level snapshot.
mojito currently has:
- a lexer and Pratt parser for a useful slice of Mojo-like syntax
- a type checker with structs, functions, methods, overload sets, traits, generics, value parameters, builtin scalar types, lists, tuples, strings, and SIMD-like values
- source modules and packages with lexical, dotted, relative, qualified, and
aliased imports; dotted-prefix namespaces; ordinary namespace directories;
source-package precedence; explicit
__init__.mojore-exports; isolated linked identities; configurable roots; and bundled standard-library lookup - compile-time elaboration for
comptime if,comptime for, richer compile-time values, and fuel-bounded pure-function CTFE through the MIR/VM path - arbitrary-precision
IntLiteraland exact finiteFloatLiteralevaluation, with checked, explicit MIR materialization into wrapping integer and rounded floating runtime scalars - a HIR control-flow graph lowering pass
- a MIR/A-normal lowering pass with explicit registers, variables, places, moves, drops, calls, method calls, exceptions, and loop control
- ownership analysis for
^moves, including use-after-move, conditional moves, double moves, partial field moves, and reinitialization - borrow checking for ordinary call arguments, including mutable/shared aliasing checks and place-sensitive field borrowing
- liveness-driven ASAP destruction via
__del__(deinit self) - a register VM backend used as the runtime implementation
- self-hosted standard-library proofs in
stdlib/, including genericOptional,List,Set,Dict, and keyword-owningStringDictimplementations - basic collection/protocol traits such as
Iterable,Iterator,Sized,Equatable, andComparablewhere the current self-hosted library needs them - fixture-based tests for accepted programs, parse errors, type errors, runtime errors, ownership errors, and ownership-ok cases
This project is intentionally small and direct. The compiler is not wrapped in a large framework.
mojito is much smaller than real Mojo. Its gaps fall into two different categories: language subset work that belongs on the near-term roadmap, and larger infrastructure work that may or may not ever be part of this project.
Language deficiencies:
- no complete Mojo standard library; a small self-hosted
stdlib/exists, but it is a proof of direction, not a compatible replacement - no full trait system; nominal conformance, common bounds, receiver conventions, associated compile-time facts, refinement, and statically materialized default methods run, but the complete Mojo protocol and constraint libraries remain incomplete
- no full parametric polymorphism story comparable to Mojo; generics, value parameters, bound-checked heterogeneous packs with per-index concrete types, dependent parameters, and contextual generic callable specialization cover the current library, but not the full language
- overload resolution ranks conversion count, variadic use, signature length,
and generic ties across functions, methods, and constructors; checked
user-defined
@implicitconstructors participate in that ranking, while the full Mojo specialization lattice remains open - no complete effect system; direct, method, overloaded, and indirect callable
raisespropagation is checked, including typed and parametric errors andNever, but trait-requirement effects and full unwind semantics remain open - no full exception/unwind model beyond the VM-supported subset
- no complete model of Mojo's ownership, origins, and lifetime semantics
- non-escaping nested functions use explicit unified capture lists and support sibling calls, recursion, generics, mutable/reference environments, and nominal callable structs; escaping closures remain intentionally rejected to match Mojo
- no self-hosted
String; string literals and runtime strings still rely on VM support while storage, Unicode, slicing, and literal interop are designed Tupleremains mostly compiler/runtime-shaped; fully self-hosting arbitrary heterogeneous tuples would need deeper type-level/variadic machinery- no complete support for every Mojo expression form; some advanced forms still parse before they are fully checked or executed
Infrastructure and backend boundaries:
- GPU, concurrency/parallelism, distributed execution, Python interoperability, and MLIR are intentionally outside first-pass parity
- no production optimizer
- no Cranelift or LLVM native code generation yet
- no real SIMD lowering to machine vector instructions; SIMD values are modeled at the VM value level
- no performance claim beyond "useful as a reference implementation"
The language deficiencies are the ones most likely to shrink as mojito grows. The infrastructure deficiencies are larger bets: interesting, but not necessary for mojito to be useful as a model implementation of ownership, borrowing, ASAP destruction, and a register-VM compiler.
The goal is honest subset semantics. A feature is usually parsed before it is fully supported, and unsupported semantics should fail cleanly instead of producing a wrong answer.
The compiler pipeline is:
source
-> lex
-> parse
-> module link
-> comptime elaboration
-> check
-> HIR CFG
-> MIR
-> ownership / borrow / liveness analysis
-> drop elaboration
-> register VM
The Compiler driver owns this ordering for normal whole-program use and
returns a CompiledProgram only after all compile stages succeed. Individual
stage APIs remain available for syntax tools and compiler development.
The major source directories are:
src/lexer.rs,src/parser.rs,src/ast.rs: tokens, AST, Pratt parser, and statement parsingsrc/module.rs: filesystem-backed module loading/linkingsrc/comptime.rs: compile-time elaboration and MIR/VM-backed CTFE supportsrc/checker.rs: type checking, trait checks, overload resolution, call matching, value-parameter checks, and borrow checkssrc/hir/mod.rs: control-flow graph loweringsrc/mir/mod.rs: flattened register/place MIRsrc/analysis/mod.rs: move analysis, liveness, and drop insertionsrc/backend/vm.rs: register VM executionsrc/runtime/mod.rs: shared runtime values and builtin operationsstdlib/: self-hosted mojito library typesassets/: executable and negative test fixturestests/: parser, checker, HIR, MIR, VM, ownership, and drop tests
For code navigation and responsibility ownership, see the symbol-level architecture map. For the complete pipeline, see the architecture guide.
The surface syntax is documented in grammar.md. The VM transition
and instruction model are documented in
docs/vm-instruction-set.md.
cargo build
cargo test
cargo clippy --all-targetsIf your local environment sets RUSTC_WRAPPER to a tool that cannot write its
cache, this form is useful:
env RUSTC_WRAPPER= cargo test
env RUSTC_WRAPPER= cargo clippy --all-targetsconformance/parity.tsv is the authoritative Mojo comparison ledger. It pins the
reference build and records, for every inventoried manual feature family, the
status, scope, relationship to Mojo, each implementation's behavior, and
evidence. Relations distinguish matching semantics, strict-subset rejection,
true divergence, representation differences, explicit exclusions, and stretch
goals. conformance/manual-sections.tsv fixes the official manual inventory
boundary.
The current language target is recorded in
docs/mojo-nightly.md. Mojito presently tracks
Mojo 1.0.0b3.dev2026072406 (2026-07-24); the audit records nightly language
drift separately from claims of implemented parity.
Shared cases in conformance/cases.tsv run under both implementations. They can
assert matching output, matching rejection, a documented strict-subset gap, or a
documented acceptance or output divergence. scripts/check-parity-manifest
validates the schema, IDs, classifications, evidence links, fixtures, manual
coverage, the rule that every divergence has an executable differential case,
and the rule that every implemented first-pass match cites differential evidence.
Point the runner at a Pixi project containing the pinned Mojo compiler:
scripts/conformance --mojo-pixi-manifest /path/to/pixi.tomlThe same path can be supplied through MOJO_PIXI_MANIFEST. The ordinary
scripts/check gate does not require Mojo or network access; differential
conformance is an explicit additional gate.
Run a compiler stage over a file:
cargo run -- <command> [FILE]Commands:
| Command | What it does |
|---|---|
lex |
print the token stream, one token per line |
parse |
print the parsed AST |
check |
parse and type-check |
own |
parse, type-check, and run ownership analysis |
run |
compile and execute on the register VM |
FILE is optional. Use a path, -, or omit it to read from standard input:
cargo run -- parse conformance/fixtures/integer_arithmetic.mojo
cargo run -- check -
echo 'def main(): print(1)' | cargo run -- lex
cargo run -- run assets/ok/list_and_struct.mojocheck, own, and run link imports. Add repeatable module roots with
--module-path PATH (or -I PATH) and explicit standard-library roots with
--stdlib PATH; either spelling also accepts --option=PATH. Resolution checks
the importing file's directory first, then CLI roots in their command-line order,
then mojito's bundled stdlib/ fallback:
cargo run -- run -I vendor --module-path ../shared --stdlib ~/mojo/stdlib main.mojoStage errors are written to standard error with a non-zero exit code, so the CLI is usable in scripts.
Like Mojo, Mojito permits declarations, imports, and compile-time constants at
file scope but rejects executable statements there. Put runtime work in a
function; a zero-argument main() is called as the program entry point.
Example:
@fieldwise_init
struct Counter:
var n: Int
def bump(mut self, by: Int):
self.n += by
def main():
var c: Counter = Counter(10)
c.bump(5)
print(c.n)Run it:
cargo run -- run path/to/file.mojoThe easiest way to add coverage is to place .mojo files under assets/.
The test harness walks these folders:
| Folder | Meaning |
|---|---|
assets/ok/ |
program should lex, parse, check, pass ownership analysis, and run |
assets/parse_error/ |
lexer or parser should reject it |
assets/type_error/ |
parser accepts it, checker rejects it |
assets/runtime_error/ |
checker accepts it, VM reports a runtime error |
assets/ownership_ok/ |
ownership analysis should accept it |
assets/ownership_error/ |
ownership analysis should reject it |
So adding an accepted language example is usually just:
$EDITOR assets/ok/my_feature.mojo
cargo test
cargo run -- run assets/ok/my_feature.mojoNegative fixtures can pin part of the expected error with a top comment:
# expect: use after move
@fieldwise_init
struct Box:
var n: Int
def main():
var a: Box = Box(1)
var b: Box = a^
print(a.n)See assets/README.md for the fixture rules.
mojito treats ^ as an ownership transfer. Moving a value leaves the source
uninitialized. Later use is rejected by analysis before the VM runs.
Examples of modeled behavior:
var b = a^transfers ownership fromatob- moving the same value twice is rejected
- moving a value on one branch and using it after the merge is rejected
- partial field moves are tracked separately from sibling fields
- assigning back to a moved field reinitializes it
varparameters consume their argument; removedownedsyntax is rejectedmutandrefparameters borrow and can write back through caller places- conflicting borrows in the same call are rejected
- values with
__del__(deinit self)are destroyed at last use, not scope end - moved values are dropped once, at their new owner
- structs drop their own destructor first, then fields in reverse declaration order
This is the part of the project that makes it useful as a model implementation: it shows how systems-language semantics can be enforced as compiler analyses over a small MIR instead of being scattered through an interpreter.
comptime is implemented as an elaboration phase before runtime checking and
MIR lowering.
Supported pieces include:
comptime NAME = exprconstantscomptime ifbranch selectioncomptime foroverrangeand compile-time tuple/list values- richer compile-time values such as integers, booleans, strings, tuples, and lists, plus compile-time-only type facts
- small pure-function CTFE, implemented by cloning/restricting helper bodies, folding compile-time-only facts, and executing the result through MIR/VM with fuel
- materialization of compile-time values into ordinary runtime code where the subset supports it
This is intentionally still a small model of Mojo's comptime system. It is powerful enough to support the self-hosted library experiments, but not a full replacement for Mojo's compile-time evaluation and specialization machinery.
mojito supports same-name top-level functions, methods, and constructors as overload sets. The checker chooses a single best candidate at each call site:
- distinct arities work directly
- same-arity overloads work when argument types make one candidate uniquely best
- exact type matches beat candidates that require coercion
- ambiguous coercion cases are rejected at type-check time
The selected callee is recorded as a checker fact and preserved through MIR
lowering. Overloaded definitions lower to stable signature-based names such as
choose$ov$Int or Box.__init__$ov$String; the source still says
choose(x) or Box(x).
This same mechanism underpins ordinary function calls, method calls, dunder operator dispatch, subscript dispatch, and constructor selection. It is not yet a complete implementation of Mojo's overload ranking, but it is enough for the numeric and container patterns that need ordinary type-directed overloads.
mojito includes a small stdlib/ written in mojito itself. These are
ordinary .mojo modules imported by programs and executed by the VM.
Current self-hosted proof types include:
Optional[T]List[T]Set[T]Dict[K, V]
The point is not that these are production collections. The point is that user structs now have enough language hooks to behave like real value types:
- dunder operator and builtin dispatch
- subscript read/write
- type-directed function, method, and constructor overloading
__len__- user iteration
__init__(out self)__copyinit____moveinit____del__(deinit self)UnsafePointer[T]- modules
- comptime helpers
Near-term work:
- continue tightening Mojo compatibility within the chosen subset
- deepen self-hosted
stdlib/coverage while deleting or shrinking Rust intrinsics where practical - design self-hosted
String - clarify what remains compiler-primitive about
Tuple - improve diagnostics and source spans
- expand trait and generic support
- document the architecture in
docs/architecture.md - add more VM disassembly/introspection tools
- keep growing fixture coverage from real Mojo examples
Longer-term possible directions:
- a versioned assembler/disassembler for flattened MIR/VM programs, including textual serialization, parsing, verification, round-tripping, and execution
- optional compact bytecode serialization derived from the same schema
- Cranelift as the first native backend, followed by LLVM
- richer borrow checking and lifetime diagnostics
- better specialization of value-parameterized code
- deeper comptime specialization and generated declarations
- an explicitly documented unsafe/unsupported boundary
eBPF and MLIR are stretch backends. GPU, Python interoperability, and parallel/distributed execution are not goals for the first parity pass.
The frontend stages are also available as library functions:
let tokens = mojito::lex(source)?;
let ast = mojito::parse(source)?;
mojito::check(&ast)?;
mojito::check_ownership(&ast)?;For execution, use the backend trait:
use mojito::{BackendKind, Backend};
let program = mojito::parse(source)?;
let checked = mojito::check_program(&program)?;
mojito::check_ownership_checked(&checked)?;
let mut backend = BackendKind::Vm.make();
backend.run(&checked)?;
println!("{}", backend.output());