April 2023
Week of 2023-04-24β
-
π’ The default behavior of nested functions has been changed. Mojo nested functions that capture are by default are non-parametric, runtime closures, meaning that:
def foo(x): # This: def bar(y): return x * y # Is the same as: let bar = lambda y: x * yThese closures cannot have input or result parameters, because they are always materialized as runtime values. Values captured in the closure (
xin the above example), are captured by copy: values with copy constructors cannot be copied and captures are immutable in the closure.Nested functions that don't capture anything are by default "parametric" closures: they can have parameters and they can be used as parameter values. To restore the previous behavior for capturing closures, "parametric, capture-by-unsafe-reference closures", tag the nested function with the
@parameterdecorator. -
π’ Mojo now has full support for "runtime" closures: nested functions that capture state materialized as runtime values. This includes taking the address of functions, indirect calls, and passing closures around through function arguments. Note that capture-by-reference is still unsafe!
You can also take references to member functions with instances of that class using
foo.member_function, which creates a closure withfoobound to theselfargument. -
π’ Mojo now supports Python style
withstatements and context managers.These things are very helpful for implementing things like our trace region support and things like Runtime support.
A context manager in Mojo implements three methods:
fn __enter__(self) -> T: fn __exit__(self): fn __exit__(self, err: Error) -> Bool:The first is invoked when the context is entered, and returns a value that may optionally be bound to a target for use in the with body. If the with block exits normally, the second method is invoked to clean it up. If an error is raised, the third method is invoked with the Error value. If that method returns true, the error is considered handled, if it returns false, the error is re-thrown so propagation continues out of the 'with' block.
-
π’ Mojo functions now support variable scopes! Explicit
varandletdeclarations inside functions can shadow declarations from higher "scopes", where a scope is defined as any new indentation block. In addition, theforloop iteration variable is now scoped to the loop body, so it is finally possible to writefor i in range(1): pass for i in range(2): pass -
π’ Mojo now supports an
@valuedecorator on structs to reduce boilerplate and encourage best practices in value semantics. The@valuedecorator looks to see the struct has a fieldwise initializer (which has arguments for each field of the struct), a__copyinit__method, and a__moveinit__method, and synthesizes the missing ones if possible. For example, if you write:@value struct MyPet: var name: String var age: IntThe
@valuedecorator will synthesize the following members for you:fn __init__(inout self, owned name: String, age: Int): self.name = name^ self.age = age fn __copyinit__(inout self, existing: Self): self.name = existing.name self.age = existing.age fn __moveinit__(inout self, owned existing: Self): self.name = existing.name^ self.age = existing.ageThis decorator can greatly reduce the boilerplate needed to define common aggregates, and gives you best practices in ownership management automatically. The
@valuedecorator can be used with types that need custom copy constructors (your definition wins). We can explore having the decorator take arguments to further customize its behavior in the future. -
π Memcpy and memcmp now consistently use count as the byte count.
-
π Add a variadic string join on strings.
-
π Introduce a
reduce_bit_countmethod to count the number of 1 across all elements in a SIMD vector. -
π Optimize the
powfunction if the exponent is integral. -
π Add a
lenfunction which dispatches to__len__across the different structs that support it.
Week of 2023-04-17β
-
π’ Error messages have been significantly improved, thanks to prettier printing for Mojo types in diagnostics.
-
π’ Variadic values can now be indexed directly without wrapping them in a
VariadicList! -
π’
letdeclarations in a function can now be lazily initialized, andvardeclarations that are never mutated get a warning suggesting they be converted to aletdeclaration. Lazy initialization allows more flexible patterns of initialization than requiring the initializer be inline, e.g.:let x: Int if cond: x = foo() else: x = bar() use(x) -
π’ Functions defined with
defnow returnobjectby default, instead ofNone. This means you can return values (convertible toobject) insidedeffunctions without specifying a return type. -
π’ The
@raisesdecorator has been removed. Raisingfnshould be declared by specifyingraisesafter the function argument list. The rationale is thatraisesis part of the type system, instead of a function modifier. -
π’ The
BoolLiteraltype has been removed. Mojo now emitsTrueandFalsedirectly asBool. -
π’ Syntax for function types has been added. You can now write function types with
fn(Int) -> Stringorasync def(&String, *Int) -> None. No more writing!kgen.signaturetypes by hand! -
π’ Float literals are not emitted as
FloatLiteralinstead of an MLIRf64type! -
π’ Automatic destructors are now supported by Mojo types, currently spelled
fn __del___(owned self):(the extra underscore will be dropped shortly). These destructors work like Python object destructors and similar to C++ destructors, with the major difference being that they run "as soon as possible" after the last use of a value. This means they are not suitable for use in C++-style RAII patterns (use thewithstatement for that, which is currently unsupported).These should be generally reliable for both memory-only and register-passable types, with the caveat that closures are known to not capture values correctly. Be very careful with interesting types in the vicinity of a closure!
-
A new (extremely dangerous!) builtin function is available for low-level ownership muckery. The
__get_address_as_owned_value(x)builtin takes a low-level address value (of!kgen.pointertype) and returns anownedvalue for the memory that is pointed to. This value is assumed live at the invocation of the builtin, but is "owned" so it needs to be consumed by the caller, otherwise it will be automatically destroyed. This is an effective way to do a "placement delete" on a pointer.# "Placement delete": destroy the initialized object begin pointed to. _ = __get_address_as_owned_value(somePointer.value) # Result value can be consumed by anything that takes it as an 'owned' # argument as well. consume(__get_address_as_owned_value(somePointer.value)) -
Another magic operator, named
__get_address_as_uninit_lvalue(x)joins the magic LValue operator family. This operator projects a pointer to an LValue like__get_address_as_lvalue(x). The difference is that__get_address_as_uninit_lvalue(x)tells the compiler that the pointee is uninitialized on entry and initialized on exit, which means that you can use it as a "placement new" in C++ sense.__get_address_as_lvalue(x)tells the compiler that the pointee is initialized already, so reassigning over it will run the destructor.# "*Re*placement new": destroy the existing SomeHeavy value in the memory, # then initialize a new value into the slot. __get_address_as_lvalue(somePointer.value) = SomeHeavy(4, 5) # Ok to use an lvalue, convert to borrow etc. use(__get_address_as_lvalue(somePointer.value)) # "Placement new": Initialize a new value into uninitialied memory. __get_address_as_uninit_lvalue(somePointer.value) = SomeHeavy(4, 5) # Error, cannot read from uninitialized memory. use(__get_address_as_uninit_lvalue(somePointer.value))Note that
__get_address_as_lvalueassumes that there is already a value at the specified address, so the assignment above will run theSomeHeavydestructor (if any) before reassigning over the value. -
π’ Implement full support for
__moveinit__(aka move constructors)This implements the ability for memory-only types to define two different types of move ctors if they'd like:
fn __moveinit__(inout self, owned existing: Self): Traditional Rust style moving constructors that shuffles data around while taking ownership of the source binding.fn __moveinit__(inout self, inout existing: Self):: C++ style "stealing" move constructors that can be used to take from an arbitrary LValue.
This gives us great expressive capability (better than Rust/C++/Swift) and composes naturally into our lifetime tracking and value categorization system.
-
The
__call__method of a callable type has been relaxed to takeselfby borrow, allow non-copyable callees to be called. -
Implicit conversions are now invoked in
raisestatements properly, allowing converting strings toErrortype. -
Automatic destructors are turned on for
__del__instead of__del___. -
π Add the builtin FloatLiteral type.
-
π Add integral
floordivandmodfor the SIMD type that handle negative values. -
π Add an F64 to String converter.
-
π Make the
printfunction take variadic inputs.
Week of 2023-04-10β
-
π’ Introduce consume operator
x^This introduces the postfix consume operator, which produces an RValue given a lifetime tracked object (and, someday, a movable LValue).
-
Mojo now automatically synthesizes empty destructor methods for certain types when needed.
-
The
objecttype has been built out into a fully-dynamic type, with dynamic function dispatch, with full error handling support.def foo(a) -> object: return (a + 3.45) < [1, 2, 3] # raises a TypeError -
π’ The
@always_inlinedecorator is no longer required for passing capturing closures as parameters, for both the functions themselves as functions with capturing closures in their parameters. These functions are still inlined but it is an implementation detail of capturing parameter closures. Mojo now distinguishes between capturing and non-capturing closures. Nested functions are capturing by default and can be made non-capturing with the@noncapturingdecorator. A top-level function can be passed as a capturing closure by marking it with the@closuredecorator. -
π’ Support for list literals has been added. List literals
[1, 2, 3]generate a variadic heterogeneous list type. -
Variadics have been extended to work with memory-primary types.
-
Slice syntax is now fully-supported with a new builtin
sliceobject, added to the compiler builtins. Slice indexing witha[1:2:3]now emits calls to__setitem__and__getitem__with a slice object. -
Call syntax has been wired up to
__call__. You can nowf()on custom types! -
Closures are now explicitly typed as capturing or non-capturing. If a function intends to accept a capturing closure, it must specify the
capturingfunction effect. -
π Add a
Tile2Dfunction to enable generic2Dtiling optimizations. -
π Add the
slicestruct to enable getting/setting spans of elements viagetitem/setitem. -
π Add syntax sugar to autotuning for both specifying the autotuned values, searching, and declaring the evaluation function.
Week of 2023-04-03β
-
The
AnyTypeandNoneTypealiases were added and auto-imported in all files. -
π’ The Mojo VS Code extension has been improved with docstring validation. It will now warn when a function's docstring has a wrong argument name, for example.
-
π’ A new built-in literal type
TupleLiteralwas added in_CompilerBuiltin. It represents literal tuple values such as(1, 2.0)or(). -
π’ The
Inttype has been moved to a newBuiltinmodule and is auto-imported in all code. The type of integer literals has been changed from the MLIRindextype to theInttype. -
Mojo now has a powerful flow-sensitive uninitialized variable checker. This means that you need to initialize values before using them, even if you overwrite all subcomponents. This enables the compiler to reason about the true lifetime of values, which is an important stepping stone to getting automatic value destruction in place.
-
π’ Call syntax support has been added. Now you can directly call an object that implements the
__call__method, likefoo(5). -
π’ The name for copy constructors got renamed from
__copy__to__copyinit__. Furthermore, non-@register_passabletypes now implement it like they do an init method where you fill in a by-reference self, for example:fn __copyinit__(inout self, existing: Self): self.first = existing.first self.second = existing.secondThis makes copy construction work more similarly to initialization, and still keeps copies
x = ydistinct from initializationx = T(y). -
π’ Initializers for memory-primary types are now required to be in the form
__init__(inout self, ...):with a None result type, but for register primary types, it remains in the form__init__(...) -> Self:. TheT{}initializer syntax has been removed for memory-primary types. -
Mojo String literals now emit a builtin
StringLiteraltype! One less MLIR type to worry about. -
New
__getattr__and__setattr__dunder methods were added. Mojo calls these methods on a type when attempting member lookup of a non-static member. This allows writing dynamic objects likex.foo()wherefoois not a member ofx. -
Early destructor support has been added. Types can now define a special destructor method
__del___(note three underscores). This is an early feature and it is still being built out. There are many caveats, bugs, and missing pieces. Stay tuned! -
π Integer division and mod have been corrected for rounding in the presence of negative numbers.
-
π Add scalar types (UI8, SI32, F32, F64, etc.) which are aliases to
SIMD[1, type].
Was this page helpful?
Thank you! We'll create more content like this.
Thank you for helping us improve!