Skip to content

bpr/mojito

Repository files navigation

mojito

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.

Project goals

  • 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.

Status

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__.mojo re-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 IntLiteral and exact finite FloatLiteral evaluation, 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 generic Optional, List, Set, Dict, and keyword-owning StringDict implementations
  • basic collection/protocol traits such as Iterable, Iterator, Sized, Equatable, and Comparable where 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.

Not Mojo Proper

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 @implicit constructors participate in that ranking, while the full Mojo specialization lattice remains open
  • no complete effect system; direct, method, overloaded, and indirect callable raises propagation is checked, including typed and parametric errors and Never, 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
  • Tuple remains 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.

Pipeline

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 parsing
  • src/module.rs: filesystem-backed module loading/linking
  • src/comptime.rs: compile-time elaboration and MIR/VM-backed CTFE support
  • src/checker.rs: type checking, trait checks, overload resolution, call matching, value-parameter checks, and borrow checks
  • src/hir/mod.rs: control-flow graph lowering
  • src/mir/mod.rs: flattened register/place MIR
  • src/analysis/mod.rs: move analysis, liveness, and drop insertion
  • src/backend/vm.rs: register VM execution
  • src/runtime/mod.rs: shared runtime values and builtin operations
  • stdlib/: self-hosted mojito library types
  • assets/: executable and negative test fixtures
  • tests/: 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.

Build And Test

cargo build
cargo test
cargo clippy --all-targets

If 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-targets

Differential Conformance

conformance/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.toml

The 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.

CLI Usage

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.mojo

check, 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.mojo

Stage errors are written to standard error with a non-zero exit code, so the CLI is usable in scripts.

Writing Programs

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.mojo

Fixture Workflow

The 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.mojo

Negative 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.

Ownership, Borrowing, And Destruction

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 from a to b
  • 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
  • var parameters consume their argument; removed owned syntax is rejected
  • mut and ref parameters 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

comptime is implemented as an elaboration phase before runtime checking and MIR lowering.

Supported pieces include:

  • comptime NAME = expr constants
  • comptime if branch selection
  • comptime for over range and 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.

Overloading And Dispatch

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.

Self-Hosted Standard Library

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

Development Direction

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.

Library API

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());

About

An implementation of a Mojo-like language in Rust

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages