Consider compiling down to machine code instead of VM bytecode
Summary
Inko currently compiles down to bytecode for its own VM. This means you write your application code once, then run it on multiple platforms, without the need for recompiling your program. Instead of doing this we should investigate compiling Inko source code straight to Rust, removing the need for an interpreter.
Note that this is just an idea/shower thought, it's not a serious plan at this stage.
Motivation
Interpreters inevitably end up being a bottleneck. The usual way around this is to either implement a JIT, or write your code as a native extension of some sort. Inko doesn't have (nor will have) support for native extensions, and writing a JIT is an enormous undertaking. Even with a JIT present, chances are you still won't get the performance you need. Having a VM also means having to write your own debugger and related tooling.
Compiling to native code somehow would let you work around this. And unlike a VM+JIT combination, you only need to write one compiler (= your AOT compiler). Using LLVM isn't an option, as it's API is highly unstable and different platforms either ship different LLVM versions (= every Linux distribution). The result is that many users of LLVM end up vendoring it, only updating every now and then. This in turn makes distribution of the code more difficult, as you may end up vendoring a version not supported by a platform you wish to support.
A way around that is to compile to an existing language, such as C, Zig, or Rust. C is the usual suspect in this regard, and it makes sense: the language is stable, supports pretty much every platform out there, and is lightweight. But C is also full of footguns, with even integer addition introducing undefined behaviour. I don't feel comfortable compiling to such a language.
Compiling to Rust would be an interesting approach. First, I'm much more familiar and comfortable with Rust. There are also fewer footguns, we can still take advantage of a low/no overhead FFI (Inko's FFI would translate to Rust's FFI), get debugger support, etc. Porting the VM to this might also be easier: we'd basically introduce a "libinko" crate that just provides the runtime, and the generated code would link to said library. If we compile to C, we'd first have to write all that in C, and I'm pretty sure we'd introduce 2000 CVEs in the process.
The obvious downside is the compile times: Rust's compiler is slooooow, and adding Inko's compiler on top only makes that worse. There may be ways around that though: first, we could disable the standard library, depending on how much we need it in "libinko". Second, we can further reduce the number of dependencies, and maybe also reduce the amount of generic code to compile (especially in combination with disabling the standard library).
Compiling to Rust offers another benefit: we can let rustc take care of the heavy lifting of optimisations, monomorphisation (assuming we don't continue boxing values, which we probably will for a while at least), etc. For example, we wouldn't need to inline code, instead rustc would do that for us. Of course we can (and probably should) write certain optimisations ourselves, as we may be able to do them faster than rustc can, but it's not strictly necessary.
Implementation
At this stage, I have no idea. Here are just some ideas that come to mind:
Reducing dependencies
Probably a first step is to reduce the number of dependencies we have (fortunately we don't have that many). For example, we probably can get rid of the crossbeam crates if we just use regular synchronised data structures. That alone saves us three direct dependencies.
The libffi crate would also no longer be needed, as we'd use Rust's FFI instead (requiring special syntax in Inko for this). Initially we probably should just keep it though, as I'm not sure what the FFI syntax would look like (especially since I don't want to introduce C pointer/int types to Inko). libloading can also be removed along with this.
dirs-next we could probably just replace with some custom code, or we keep it; I don't think it really contributes to compile times anyway.
Suspending processes and stacks
Processes currently use their own stack, which is basically just a Vec per function call. This isn't great for performance, but it makes suspending/resuming processes trivial as we don't need to fiddle with assembly. Initially we should just keep this setup, but over time we'd need a way such that variables in Inko translate directly to variables in Rust. That requires a way of saving/restoring them.
Types
Inko classes would translate to structs and methods as in Rust. Traits wouldn't exist in the Rust layer, instead methods are just duplicated whenever traits are implemented. Generics would be erased initially, but at some point we could keep them if we decide we want to monomorphise them. This will contribute to compile times, so I prefer investigating different strategies.
Drawbacks
It's a really big undertaking, and will slow down compile times one way or another. While working on this, probably other work needs to wait. It's also not clear if this would actually matter much in the coming years, as Inko is currently not used for anything that would benefit from this change.