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 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 cosprint(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@valuestruct TrivialType: passfn full_unroll(loop: mlir.Operation):# unrolling of structured loopfn main():@full_unrollfor i inrange(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# okalias ssize_t = SIMD[DType.index, 1] # okfn 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:
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.
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 inrange(3): passprint(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): passprint(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 calllet 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(): return42else:def bar(): return3# 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 typeif cond:def bar0(): return42 result = bar0else:def bar1(): return3# error: redeclaration of 'bar' result = bar1return 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 = xalias x =10return 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:
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' modulefn foo() -> Vector.DynamicVector[Int]: # access the 'DynamicVector' typepass
Please use direct imports from other modules,
from String import Stringfrom 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): passdef__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 destructuring and multiple return values are also not implemented:
fn multiple_returns() -> (Int, Int):return0, 1a, 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.
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.
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]():passfn call_it(a: Int):@parameterfn 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]():passfn call_it(a: Int):@parameterfn 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"@parameterfn 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.
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](): passfn foo1[f: fn[a: Int]() capturing ->None](): passfn 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 parameterfn stateful_nocapture[a: Int](): # ok: can be parametric, since no capturesprint(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.
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.
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.