Mojo🔥 roadmap & sharp edges

A summary of our Mojo plans, including upcoming features and things we need to fix.

This document captures the broad plan about how we plan to implement things in Mojo, and some early thoughts about key design decisions. This is not a full design spec for any of these features, but it can provide a “big picture” view of what to expect over time. It is also an acknowledgement of major missing components that we plan to add.

Overall priorities

Mojo is still in early development and many language features will arrive in the coming months. We are highly focused on building Mojo the right way (for the long-term), so we want to fully build-out the core Mojo language features before we work on other dependent features and enhancements.

Currently, that means we are focused on the core system programming features that are essential to Mojo’s mission, and as outlined in the following sections of this roadmap.

In the near-term, we will not prioritize “general goodness” work such as:

  • Adding syntactic sugar and short-hands for Python.
  • Adding features from other languages that are missing from Python (such as public/private declarations).
  • Tackling broad Python ecosystem challenges like packaging.

If you have encountered any bugs with current Mojo behavior, please submit an issue on GitHub.

If you have ideas about how to improve the core Mojo features, we prefer that you first look for similar topics or start a new conversation about it in our GitHub Discussions.

We also consider Mojo to be a new member of the Python family, so if you have suggestions to improve the experience with Python, we encourage you to propose these “general goodness” enhancements through the formal PEP process.

Small independent features

There are a number of features that are missing that are important to round out the language fully, but which don’t depend strongly on other features. These include things like:

  • Tuple support (partially implemented today)
  • Keyword arguments in functions: foo(x=42)
  • Improved package management support.
  • Many standard library features, including canonical arrays and dictionary types, copy-on-write data structures, etc.
  • Support for “top level code” at file scope.
  • Algebraic data types like enum in Swift/Rust, and pattern matching.
  • Many standard library types, including Optional[T] and Result[T, Error] types when we have algebraic datatypes and basic traits.

Ownership and Lifetimes

The ownership system is partially implemented, and is expected to get built out in the next couple of months. The basic support for ownership includes features like:

  • Capture declarations in closures.
  • Borrow checker: complain about invalid mutable references.

The next step in this is to bring proper lifetime support in. This will add the ability to return references and store references in structures safely. In the immediate future, one can use the unsafe Pointer struct to do this like in C++.

Protocols / Traits

Unlike C++, Mojo does not “instantiate templates” in its parser. Instead, it has a separate phase that works later in the compilation pipeline (the “Elaborator”) that instantiates parametric code, which is aware of autotuning and caching. This means that the parser has to perform full type checking and IR generation without instantiating algorithms.

The planned solution is to implement language support for Protocols - variants of this feature exist in many languages (e.g. Swift protocols, Rust traits, Haskell typeclasses, C++ concepts) all with different details. This feature allows defining requirements for types that conform to them, and dovetails into static and dynamic metaprogramming features.

Classes

Mojo still doesn’t support classes, the primary thing Python programmers use pervasively! This isn’t because we hate dynamism - quite the opposite. It is because we need to get the core language semantics nailed down before adding them. We expect to provide full support for all the dynamic features in Python classes, and want the right framework to hang that off of.

When we get here, we will discuss what the right default is: for example, is full Python hash-table dynamism the default? Or do we use a more efficient model by default (e.g. vtable-based dispatch and explicitly declared stored properties) and allow opt’ing into dynamism with a @dynamic decorator on the class. The latter approach worked well for Swift (its @objc attribute), but we’ll have to prototype to better understand the tradeoffs.

C/C++ Interop

Integration to transparently import Clang C/C++ modules. Mojo’s type system and C++’s are pretty compatible, so we should be able to have something pretty nice here. Mojo can leverage Clang to transparently generate a foreign function interface between C/C++ and Mojo, with the ability to directly import functions:

from "math.h" import cos

print(cos(0))

Full MLIR decorator reflection

All decorators in Mojo have hard-coded behavior in the parser. In time, we will move these decorators to being compile-time metaprograms that use MLIR integration. This may depend on C++ interop for talking to MLIR. This completely opens up the compiler to programmers. Static decorators are functions executed at compile-time with the capability to inspect and modify the IR of functions and types.

fn value(t: TypeSpec):
    t.__copyinit__ = # synthesize dunder copyinit automatically

@value
struct TrivialType: pass

fn full_unroll(loop: mlir.Operation):
    # unrolling of structured loop

fn main():
    @full_unroll
    for i in range(10):
        print(i)

Sharp Edges

The entire Modular kernel library is written in Mojo, and its development has been prioritized based on the internal needs of those users. Given that Mojo is still a young language, there are a litany of missing small features that many Python and systems programmers may expect from their language, as well as features that don’t quite work the way we want to yet, and in ways that can be surprising or unexpected. This section of the document describes a variety of “sharp edges” in Mojo, and potentially how to work around them if needed. We expect all of these to be resolved in time, but in the meantime, they are documented here.

No list or dict comprehensions

Mojo does not yet support Python list or dictionary comprehension expressions, like [x for x in range(10)], because Mojo’s standard library has not yet grown a standard list or dictionary type.

No lambda syntax

Mojo does not yet support defining anonymous functions with the lambda keyword. This is just because we couldn’t decide where to put the type annotations, so stay tuned!

No global variables

Mojo has no support for global variables. You cannot declare a dynamic value outside of a top-level function. Alias declarations, however, are allowed at the top-level scope.

var value = 1 # error!
alias constant = 2 # ok
alias ssize_t = SIMD[DType.index, 1] # ok

fn foo():
    value += constant

No parametric aliases

Mojo aliases can refer to parametric values but cannot themselves be a parameter. We would like this example to work, however:

alias Scalar[dt: DType] = SIMD[dt, 1]
alias mul2[x: Int] = x * 2

Exception is actually called Error

In Python, programmers expect that exceptions all subclass the Exception builtin class. The only available type for Mojo “exceptions” is Error:

fn raise_an_error() raises:
    raise Error("I'm an error!")

The reason we call this type Error instead of Exception is because it’s not really an exception. It’s not an exception, because raising an error does not cause stack unwinding, but most importantly it does not have a stack trace. And without polymorphism, the Error type is the only kind of error that can be raised in Mojo right now.

No Python-style generator functions

Mojo does not yet support Python-style generator functions (yield syntax). These are “synchronous co-routines” – functions with multiple suspend points.

No async for or async with

Although Mojo has support for async functions with async fn and async def, Mojo does not yet support the async for and async with statements.

The rebind builtin

One of the consequences of Mojo not performing function instantiation in the parser like C++ is that Mojo cannot always figure out whether some parametric types are equal and complain about an invalid conversion. This typically occurs in static dispatch patterns, like:

fn take_simd8(x: SIMD[DType.f32, 8]): pass

fn generic_simd[nelts: Int](x: SIMD[DType.f32, nelts]):
    @parameter
    if nelts == 8:
        take_simd8(x)

The parser will complain,

error: invalid call to 'take_simd8': argument #0 cannot be converted from
'SIMD[f32, nelts]' to 'SIMD[f32, 8]'
        take_simd8(x)
        ~~~~~~~~~~^~~

This is because the parser fully type-checks the function without instantiation, and the type of x is still SIMD[f32, nelts], and not SIMD[f32, 8], despite the static conditional. The remedy is to manually “rebind” the type of x, using the rebind builtin that can be found in the TypeUtilities module.

from TypeUtilities import rebind
fn generic_simd[nelts: Int](x: SIMD[DType.f32, nelts]):
    @parameter
    if nelts == 8:
        take_simd8(rebind[SIMD[DType.f32, 8]](x))

Scoping and mutability of statement variables

Python programmers understand that local variables are implicitly declared and scoped at the function level. As the programming manual explains, this feature is supported in Mojo only inside def functions. However, there are some nuances to Python’s implicit declaration rules that Mojo does not match 1-to-1.

For example, the scope of for loop iteration variables and caught exceptions in except statements is limited to the next indentation block, for both def and fn functions. Python programmers will expect the following program to print “2”:

for i in range(3): pass
print(i)

However, Mojo will complain that print(i) is a use of an unknown declaration. This is because whether i is defined at this line is dynamic in Python. For instance the following Python program will fail:

for i range(0): pass
print(i)

With NameError: name 'i' is not defined, because the definition of i is a dynamic characteristic of the function. Mojo’s lifetime tracker is intentionally simple (so lifetimes are easy to use!), and cannot reason that i would be defined even when the loop bounds are constant.

Also stated in the programming manual: in def functions, the function arguments are mutable and re-assignable, whereas in fn, function arguments are rvalues and cannot be re-assigned. The same logic extends to statement variables, like for loop iteration variables or caught exceptions:

def foo():
    try:
        bad_function():
    except e:
        e = Error() # ok: we can overwrite 'e'

fn bar():
    try:
        bad_function():
    except e:
        e = Error() # error: 'e' is not mutable

Name scoping of nested function declarations

In Python, nested function declarations produce dynamic values. They are essentially syntax sugar for bar = lambda ....

def foo():
    def bar(): # creates a function bound to the dynamic value 'bar'
        pass
    bar() # indirect call

In Mojo, nested function declarations are static, so calls to them are direct unless made otherwise.

fn foo():
    fn bar(): # static function definition bound to 'bar'
        pass
    bar() # direct call
    let f = bar # materialize 'bar' as a dynamic value
    f() # indirect call

Currently, this means you cannot declare two nested functions with the same name. For instance, the following example does not work in Mojo:

def pick_func(cond):
    if cond:
        def bar(): return 42
    else:
        def bar(): return 3 # error: redeclaration of 'bar'
    return bar

The functions in each conditional must be explicitly materialized as dynamic values.

def pick_func(cond):
    let result: def() capturing # Mojo function type
    if cond:
        def bar0(): return 42
        result = bar0
    else:
        def bar1(): return 3 # error: redeclaration of 'bar'
        result = bar1
    return result

We hope to sort out these oddities with nested function naming as our model of closures in Mojo develops further.

Non-lexical alias declarations

Alias declarations are non-lexical, meaning that they can be referenced before they are defined. This is true for both forward alias declarations and alias initializer declarations. For instance:

fn use_before_def():
    alias y = x
    alias x = 10
    return y

This feature is intentional, and it can be used to powerful effect. For example, non-lexical forward alias declarations allow a called function to return a required buffer size as a compile-time value:

fn get_target_info_blob() -> String:
    alias req_size: Int
    let buf = DynamicVector[SI8](req_size) # no-reallocation needed!
    _get_target_info_impl[() -> req_size](buf)
    return buf

Alias declarations currently follow different name scoping rules than dynamic value declarations, like let and var. Only @parameter if statements create new scopes for alias declarations.

Module imports don’t work

Module imports are not currently supported. E.g.,

import Vector # import the whole 'Vector' module

fn foo() -> Vector.DynamicVector[Int]: # access the 'DynamicVector' type
    pass

Please use direct imports from other modules,

from String import String
from Vector import DynamicVector as DVector

No polymorphism

Mojo will implement static polymorphism through traits/protocols in the near future and dynamic polymorphism through classes and MLIR reflection. None of those things exist today, which presents several limitations to the language.

Python programmers are used to implementing special dunder methods on their classes to interface with generic methods like print and len. For instance, one expects that implementing __repr__ or __str__ on a class will enable that class to be printed via print.

class One:
    def __init__(self): pass
    def __repr__(self): return '1'

print(One()) # prints '1'

This is not currently possible in Mojo. Overloads of print are provided for common types, like Int and SIMD and String, but otherwise the builtin is not extensible. Overloads also have the limitation that they must be defined within the same module, so you cannot add a new overload of print for your struct types.

The same extends to range, len, and other builtins.

Tuple limitations

Tuples syntactically require explicit parentheses.

let x = 1, 2 # error :-(
let y = (1, 2) # ok :-)

Tuple destructuring and multiple return values are also not implemented:

fn multiple_returns() -> (Int, Int):
    return 0, 1

a, b = multiple_returns() # one day this will work :<

No lifetime tracking inside collections

Due to the aforementioned lack of polymorphism, collections like lists, maps, and sets are unable to invoke element destructors. For collections of trivial types, like DynamicVector[Int], this is no problem, but for collections of types with lifetimes, like DynamicVector[String], the elements have to be manually destructed. Doing so requires quite an ugly pattern, shown in the next section.

No safe value references

Mojo does not have proper lifetime marker support yet, and that means it cannot reason about returned references, so Mojo doesn’t support them. You can return or keep unsafe references by passing explicit pointers around.

struct StringRef:
    var ref: Pointer[SI8]
    var size: Int
    # ...

fn bar(x: StringRef): pass

fn foo():
    let s: String = "1234"
    let ref: StringRef = s # unsafe reference
    bar(ref)
    _ = s # keep the backing memory alive!

Mojo will destruct objects as soon as it thinks it can. That means the lifetime of objects to which there are unsafe references must be manually extended. See the lifetime document for more details. This disables the RAII pattern in Mojo. Context managers and with statements are your friends in Mojo.

No lvalue returns also mean that implementing certain patterns require magic keywords until proper lifetime support is built. One such pattern is retrieving an unsafe reference from an object.

struct UnsafeIntRef:
    var ptr: Pointer[Int]

fn printIntRef(x: UnsafeIntRef):
    # "deference" operator
    print(__get_address_as_lvalue(x.ptr)) # Pointer[Int] -> &Int

var c: Int = 10
# "reference" operator
let ref = UnsafeIntRef(__get_lvalue_as_address(c)) # &Int -> Pointer[Int]

Parameter closure captures are unsafe references

You may have seen nested functions, or “closures”, annotated with the @parameter decorator. This creates a “parameter closure”, which behaves differently than a normal “stateful” closure. A parameter closure declares a compile-time value, similar to an alias declaration. That means parameter closures can be passed as parameters:

fn take_func[f: fn() capturing -> Int]():
    pass

fn call_it(a: Int):
    @parameter
    fn inner() -> Int:
        return a # capture 'a'

    take_func[inner]() # pass 'inner' as a parameter

Parameter closures can even be parametric and capturing:

fn take_func[f: fn[a: Int]() capturing -> Int]():
    pass

fn call_it(a: Int):
    @parameter
    fn inner[b: Int]() -> Int:
        return a + b # capture 'a'

    take_func[inner]() # pass 'inner' as a parameter

However, note that parameter closures are always capture by unsafe reference. Mojo’s lifetime tracking is not yet sophisticated enough to form safe references to objects (see above section). This means that variable lifetimes need to be manually extended according to the lifetime of the parametric closure:

fn print_it[f: fn() capturing -> String]():
    print(f())

fn call_it():
    let s: String = "hello world"
    @parameter
    fn inner() -> String:
        return s # 's' captured by reference, so a copy is made here
    # lifetime tracker destroys 's' here

    print_it[inner]() # crash! 's' has been destroyed

The lifetime of the variable can be manually extended by discard it explicitly.

fn call_it():
    let s: String = "hello world"
    @parameter
    fn inner() -> String:
        return s

    print_it[inner]()
    _ = s^ # discard 's' explicitly

A quick note on the behaviour of “stateful” closures. One sharp edge here is that stateful closures are always capture-by-copy; Mojo lacks syntax for move-captures and the lifetime tracking necessary for capture-by-reference. Stateful closures are runtime values – they cannot be passed as parameters, and they cannot be parametric. However, a nested function is promoted to a parametric closure if it does not capture anything. That is:

fn foo0[f: fn() capturing -> String](): pass
fn foo1[f: fn[a: Int]() capturing -> None](): pass

fn main():
    let s: String = "hello world"
    fn stateful_captures() -> String:
        return s # 's' is captured by copy

    foo0[stateful_captures]() # not ok: 'stateful_captures' is not a parameter

    fn stateful_nocapture[a: Int](): # ok: can be parametric, since no captures
        print(a)

    foo[stateful_nocapture]() # ok: 'stateful_nocapture' is a parameter

The standard library has limited exceptions use

For historic and performance reasons, core standard library types typically do not use exceptions. For instance, DynamicVector will not raise an out-of-bounds access (it will crash), and Int does now throw on divide by zero. In other words, most standard library types are considered “unsafe”.

let v = DynamicVector[Int](0)
print(v[1]) # crash 💥

print(1/0) # does not raise and could print just about anything

This is clearly unacceptable given the strong memory safety goals of Mojo. We will circle back to this when more language features and language-level optimizations are avaiable.

Recursive structs don’t work

Structs that recursively refer to themselves, even through a level of indirection do not currently work. I.e.

struct Node:
    var next: Pointer[Node] # boom 💥 :-(

A workaround for this limitation is to manually perform type erasure:

struct Node:
    var next: Pointer[NoneType]

    fn get_next() -> Pointer[Node]:
        return next.bitcast[Node]()

Nested functions cannot be recursive

Nested functions (any function that is not a top-level function) cannot be recursive in any way. Nested functions are considered “parameters”, and although parameter values do not have to obey lexical order, their uses and definitions cannot form a cycle. Current limitations in Mojo mean that nested functions, which are considered parameter values, cannot be cyclic.

fn try_recursion():
    fn bar(x: Int): # error: circular reference :<
        if x < 10:
            bar(x + 1)

Only @register_passable types may be used as parameter values

As of now, only @register_passable types may be used in parameter expressions and as parameter values. E.g., you cannot declare an alias of String type. Trivial types like Int and DType are register-passable, and can be used freely in parameter expressions.

Because function argument default values are parameter values, that means only register-passable function arguments are currently allowed to have default values.

Only certain loaded MLIR dialects can be accessed

Although Mojo provides features to access the full power of MLIR, in reality only a certain number of loaded MLIR dialects can be accessed in the Playground at the moment.

The upstream dialects available in the Playground are the index dialect and the LLVM dialect.