How long until mojo is production ready(guesstimate)?
I understand that this is a hard question to answer, but I am interested in getting into speculations. For example, Mozilla started working on Rust on 2009 and the language was released (1.0) on 2015 that means 6 years of work. Do you expect a similar timeline (2022 -> 2028)?
64 Replies
Just speculation on my side but I highly doubt they will take that long. I would expect 1.0 release towards end of 2024 or early 2025
A year maybe, to reach beta. 2 years to reach stability. The only major language core we've not seen any release for is C/C++ interop.
I would guess other higher-level details would plug into these core functionalities, and I would estimate a year to have them - namely: Classes, generators, comprehension, and extensions (this would possibly come earlier as part of improving traits). Then another year to run through beta - fix bugs, fix API, improve the docs, generally polish the language.
This is of course assuming that reaching stdlib parity with Python is not necessary to reach 1.0
Makes sense, and it makes sense to live the stdlib for latter when you have a fully stable language to build it
Pinging this question. Any new info or updates re the Beta release or production worthiness?
What about C & Python interops?
C interop is one-way and still rough. Python interop is also one-way but better. Mojo doesn't have networking in the standard library yet, you can't print a list and we added the ability to read from a terminal a month or so ago. The language itself is also very much WIP, with a long list of features needed before we can even finish the standard library.
The GPU beta will be out when it's ready. The language can be used right now. We're still considering changes which would force rewriting almost all code that uses collections, so I wouldn't use it in production.
The GPU beta will be out when it's ready. The language can be used right now. We're still considering changes which would force rewriting almost all code that uses collections, so I wouldn't use it in production.
Which changes are you talking about? The derefencing operator?
Making allocations that can fail raise.
What does that mean? That all functions that do allocations need
raises
?
ah, no, I think I got it.More or less. There's some narrow cases where they wouldn't, but
List.append
would raise since it can fail.What would be the side effects besides adding
raises
to a bunch of places in your code?All of your functions that transitively allocate would either raise or handle allocation failures.
Where is this discussion being had? I would be surprised if Mojo can actually get away with having functions like
List.append
raise.
That's always seem like a pipe dream to meNot having it means locking mojo out of a bunch of domains, and for GPUs and other accelerators being able to handle allocation failure without crashing is important.
Yes, but conversely, having it means making Mojo harder to use for application-level programming 🙂
All it means is sticking “raises” on your functions.
And even web servers can blow through memory on smaller VMs if they do caching.
Which is a huge syntactic burden
I suppose if we offer sugar for saying "abort immediately if this fails", we can make this viable, e.g.
items.append!(x)
.Or you use def if you don’t want to be explicit about things.
Chris was talking about having ways to move towards gradually making dynamic features more static for performance, so the last levels could be raises and checked exceptions.
I'm not sure how you'd alleviate the syntactic overhead of pushing exceptions up the function call hierarchy
Resorting to
def
just so that you can ignore the fact that List.append
raises seems like a bad ideaOr if we can make it per-feature, you can decided what to not care about on a per scope basis.
If you can get def to where fn is now, is it really that bad?
I suppose it depends on what
def
ends up entailingIf we want stack traces from allocation failures (if we abort), that machinery already needs to be in place .
If
def
enables a lot of footguns, nobody is going to want to use itI think the goal is that fn is the “show me everything” preset, and def is the “I just want to code, stop bothering me” preset, and you can toggle on/off static features as you require.
I'll be interested to see how that pans out
I think it will be helpful, especially if we start to add on things like being able to detect blocking syscalls (which is every syscall on Windows).
I think the "how long until Mojo is production ready" question is hard to answer because it depends on the specific needs of the company or person who wants to use Mojo. For example:
* Can you write a non-gpu production-ready isolated Mojo program today? Yes. It may be harder as the stdlib is not complete and the Mojo ecosystem is still pretty small, so you will probably end up writing more code than you expected or importing third-party Python modules.
* Will you need to rewrite the production-ready code in the future because of backward incompatible changes? Yes, 100%. The difference with e.g. Python2 -> Python3 migration is that you will have the help of the compiler and most of the work would be making the compiler happy and you will be 95% ready to go.
* Can you compile Mojo modules to be called from Python? Not yet.
Could the more extreme checks such as that be put behind a compiler option maybe? Or is that opening a can of worms itself?
If you do that then you split the ecosystem.
I get the spirit of what you're saying, but I can't help but feel like there needs to be a middle ground between "do what you want with def" and "I have a try block here in case I run out of memory"
Congrats @bgreni, you just advanced to level 6!
Is there a reason we couln't just implement two functions.
.append()
which will just fail in the case of an allocation failure and .append_raises() raise
which will allow the programmer to gracefully handle the error?I don't know what the official stance is but I'm not a huge fan of having two versions of everything
I agree that having two versions of the same function is not ideal, but I think it's better than sticking
raise
onto every function, bc if we do this, it may become difficult to know when we should handle a functions exception and when it isn't necessary to (for example, webservers typically don't care about handling allocation failures).
It introduces a "boy who cried wolf" scenario where it becomes unclear if a raises
function actually has a moderate change of failing (like a network request), or if it only has raises
because it appends to a list somewhereChecked exceptions/error unions should help with that.
I agree two versions is bad.
As Chris was discussing in #Mojo as a Python Superset, I think we can make it so that
fn
is the "I can handle the truth" mode which holds nothing back, and def
can be ratcheted up in terms of guarantees as needed up to the level of having to care about blocking syscalls, allocation failures, process safety, signal safety, etc.
But fn
forces you to see all of it as a way of helping people who do want to see everything see everything, and then you can ratchet in the other direction to turn off bits you don't care about.Reading though the rest of the convo now. I think I like this method the most.
Even rust (which i would consider an "I can handle the truth" language) provides
.unwrap()
, which would be similar to this, allowing devs to ignore errors they know are impossible/extremely unlikely for their use caseRust is not "I can handle the truth" for this, see matklad's (author of rust analyzer) blog post about it: https://matklad.github.io/2022/10/06/hard-mode-rust.html
Hard Mode Rust
This post is a case study of writing a Rust application using only minimal, artificially constrained API (eg, no dynamic memory allocation).
It assumes a fair bit of familiarity with the language.
But, an unwrap-like feature would be nice, so long as we encourage library authors not to use it unless it's for an invariant violation.
Why not just go the C route and have
.alloc()
return the number of items allocated (or -1 on failure). If people want to ignore it, put a _ = UnsafePointer[SomeType].alloc(some_amnt)
. Then you can use conditional conformance on the function overloads for your type to make them raising or whatever (for example having a raise_on_alloc_fail
parameter and corresponding function signatures). But this way we wouldn't force all functions upstream to be raising
đź’Ż , it will make a lot of things very annoying to handleI imagine Owen can enlighten me but I also can't imagine a scenario where failing to allocate memory is a recoverable situation
Lets say you're a webserver, and you've cached some images or other static content, you can free that. If you're an AI app, you can use that to see that the target GPU doesn't have enough vram and stream the weights in to make it work, if slowly, or use a lower precision format and try again.
I see, makes sense
We could have .alloc() return
Optional[NonNullUnsafePointer[]]
, but then how do we expose that to users?Oops, yeah my logic kind of failed forgot the function returns a pointer hehehe. But still, you just make your struct have a parameter option for function overloads that raise on failed allocations or return an error or whatever API
Which means we need to be generic over
raise
.
Which brings me back to the "flags" system idea, not a full effect system, to help manage being generic over things like this.Idk about that. I would also for example like to be able to signal that I'm in a memory constrained environment like a microcontroller. But making it global would lead to C style hell
I'm thinking of scopes to turn it on.
Why not just have a
MemoryConfig
struct as a paremeter?We can probably do that, but the idea is to ask for one feature that handles a bunch of stuff. Flags also handle being generic over async, whether functions can block, etc.
Yeah this is definitely going to require some very mindful consideration or the result will be very unpleasant for some portion of the user population
I agree, but I lean towards people needing to handle allocation failures anyway, and it should mean that most people start to write code with their allocations up front to avoid viral raises, which should help with performance quite a bit.
I can generally get on board with that. I think we might want to consider how much we want people to be using
fn
. I think needing to think about this when using simple heap allocated collections will drive lots of people towards def
.
Originally I had always gotten the impression def
was mostly to make python programmers feel at home, but if we're instead going to say "fn
is for Owen and all the casuals should just use def
" (assuming the appropriate improvements to def
) I can convince myself that fn
should be imposing all these responsibilities on the programmerWhat I'm imagining is that we can do project or file-scoped annotations that say "turn off this thing". This makes
fn
the "as much information as we can get by default" option, and then you add @ignore_allocation_failures
, @allow_blocking_in_async
, @ignore_32bit_compat
, etc. We can probably make a few things, like signal safety, only opt in for the 5 functions in most programs that need them. This way, an fn
user is explicitly choosing what help from the compiler to give up. A def
user gets the most minimal guard rails from the compiler at the start, then asks for the features they want like @static_member_functions
, @static_members
, @typed_members
, @disable_gil
, etc.
This lets you figure out what the right level of compiler assistance and decisions around runtime failures you care about. If you want to be able to assume that Int
is 64 bit, go ahead, if you don't want to deal with Send + Sync aside from thread locals, we'll put an invisible ArcPointer[ReentrantRwLock[T]]
on all of your stuff.
We could even try to allow for opt-in GC.
(although given what happened to D that is probably a bad idea)I think that makes sense. GC is gross but I get what you're saying haha
Every once in a while it's useful as a way to handle large amounts of particularly horrible lifetimes.
You could use refcounting but it ends up being more expensive past a point (which most people never reach).
In the case of
append
, and other functions where allocation can fail, another potential solution would be for the function to take a callback function as a parameter, which is invoked whenever allocation fails. By default, this function either aborts, or raises an exception. But you can override the default by providing your own callback, e.g. items.append[abort](x)
or items.append[raise_oom](x)
.
One benefit of this approach is that the function can be specialized to the callback, and therefore the call to append[abort]
doesn't need to reserve a register to store an exception value.
List
could also define aliases for these methods, e.g. try_append
could be an alias for append[raise_oom]
.This makes proper handling of errors much harder, since you don’t get control flow back inside of the calling function (which has the context on what you’re doing). Also, this means that every single type or function that can allocate will now need to take some number of handlers (since you may want to mix them depending on which allocation fails). This gets very messy very quickly and could easily lead to dozens of parameters.
you don’t get control flow back inside of the calling functionThe point of
append[raise_oom]
that that the caller does get control back, because when the callee invokes the callback it raises an exception, and the callee is meant to let this bubble up back to the caller.
Using something similar to Swift's rethrows
clause, append
can be specialized to return an exception value only if the callback returns an exception value.Or, we add the ability to be generic over
raises
.That's what
rethrows
isFair enough
However, this might mean some kind of messy resumption characteristics if you are doing anything aside from crashing. For instance, if you have an iterator which does batches of 2 items, it may grab the first one, then the second one may run into being out of memory. It needs the have the capability to resume itself after having run part of the process of getting the next item.
But that was always going to be an issue.
the callback can free memory and then return an integer indicating how much memory was freed
and the callee can decide whether to continue or abort
We probably will need iterators to do
raises
for impossible to recover from failures and have a Result
for things where failure is not fatal to continuing.or conversely, the callback can take an integer specifying how much memory it needs to free
it's like a reverse malloc
We need to support this for things which will never fit as well. For instance, allocating space for a too-large LLM on a GPU.