For most of my life, it seemed like there were only a few real programming languages worth learning, and they each had very specific constraints that you just had to live with. My dad put together an Ohio Scientific kit computer when I was really young, but I first learned how to poke around in programming on a Commodore 64, which booted into a BASIC interpreter. I stuck around with that until Junior High.
We were lucky enough to have a BASIC programming class in middle school, and I ended up doing independent study there in 8th grade. The teacher had a Pascal environment for Apple ][ and so I started learning about procedures. I didn't realize, nor would I have understood, at the time, but Pascal actually came with its own Virtual Machine, much as Java does today. This is actually how the original Wizardry was built.
Once I got to college, I learned Scheme, C, C++, MIPS Assembly, Fortran (!), Make, BASH, JavaScript, and Java. Of those, Java came bundled with its own bespoke VM. Scheme, Make, JavaScript and BASH were interpreted. The rest compiled to native code. When I started working, I did a lot of JavaScript and Java, and then C++, but also learned Perl (interpreted) and did more Make and BASH.
Now, there seems to be an explosion of new (and newish) languages worth learning: C#, Dart, ECMAScript 6, Go, Kotlin, Lua, Python, Rust, R, Scala, TypeScript, etc... And there are more languages being invented every day, any one of which might be really useful in general, or for very specific purposes.
Once you've learned a bunch of programming languages, picking up other languages isn't too hard. Often, the basic assumptions of a language can greatly increase productivity for certain tasks, and greatly reduce productivity for other certain tasks. So, it greatly behooves the modern programmer to use the right tool for the job. A major barrier to doing that is that these languages have exacting deployment requirements, severely limiting what language you might be able to use for a given purpose.
Broadly, these are the targets of software that have meaningfully different deployment constraints:
- Cloud - AWS, et al
- Desktop - Windows, macOS, Linux
- Mobile - iOS and Android
- Web - Chrome, Safari, Firefox
- Embedded Linux - set top boxes, streaming sticks, Smart TVs, etc...
- Microcontroller - all sorts of tiny things
People are finding clever ways around the inherent limitations of various languages (e.g. Transcrypt, Pyodide) but these workarounds have limitations. If nothing else, they generally come with a significant performance cost from nested levels of virtualization.
Other Problems
There's also not a great story for language interoperability. Some languages work well together, like C++ calling into C. Most do not. It's hard to use the right tool for the job when choosing one tool locks you into only using that tool. When you pick up your screwdriver, you had better hope there aren't going to be any nails in your near future.
Look at Python vs JavaScript. They are both very dynamic, and started off with extremely simple interpreters. Loads of resources have been put into making JavaScript fast, and... it's surprisingly fast for a fully interpreted language. Python, sadly, does not have the same reputation. But why can't Python leverage the advances in making JavaScript fast? They aren't all that different when it comes down to the fundamental operations occurring. The answer is just that Python has a different VM that hasn't had that kind of attention.
Transpiling works very well in some cases, like TypeScript, but very not in others. For example, Python and JavaScript falsiness are pretty different. So, for every Python conditional evaluation, the transpiler would need to run through a series of JavaScript conditionals to achieve the exact same behavior. So, now either that CPU effort is sacrificed to the Virtualization Gods, or your Python code breaks when you try to transpile it with JavaScript falsiness.
JavaScript also has some known performance problems. Case in point: serializing binary data, which is a thing that people want to do a lot, it turns out. This is why there's a whole incompatible JavaScript format for protocol buffers that leverages the built-in JSON parser. If you transpile to JavaScript, you layer those performance problems on top of any other overhead in the transpilation, and any inherent performance issues in the language you are using.
Proposal: A Common VM Target for All Languages
Instead of each language coming with its own interpreter (like Python's CPython), or its own VM (like Java's JVM), or only compiling to native code (like, I dunno, Fortran), or only transpiling to another language where it needs to be debugged (like Haxe), I propose that we curate a VM that is available nearly everywhere, focused on portability, deployability, security, and efficiency, and target all languages to it.
Of the above deployment scenarios, Microcontrollers are so constrained that it doesn't make sense to try to deploy VMs there. The only options there are going to be low-level, explicit memory management languages, like C/C++. Maybe Rust? So, let's acknowledge that scenario and set it aside. We should be able to target the rest of the scenarios with pretty much any language we want.
Writing VMs is hard. It takes specialized knowledge and a hell of a lot of work to make a VM that is performant, secure, portable, and ported. If one decides to undertake such an endeavor, I would imagine that one would want that work to apply as broadly as possible. No?
Some languages have already chosen a model where they avoid trying to own their own VM. Scala, Kotlin, and Clojure, all target the JVM. I think this is a great step in this direction, but it still has a couple problems. The most obvious is that the JVM isn't that easy to deploy everywhere, iOS for example. But the second problem is that the JVM is not in any way beholden to these languages. The mission of JVM development is only to support Java. As such, the JVM developers are likely to make decisions that subtly deteriorate support for these other languages.
The closest thing we have now Microsoft's Common Language Runtime. It has been embraced by Unity and Godot as a language solution that can be deployed on Desktop and Mobile. It does have Web support, too, but only by building the entire CLR on top of Web Assembly. With Mono, the CLR has an open-source implementation that can be ported whenever you might need, though it probably isn't going to be all that easy. The biggest problem here is that it is still essentially controlled by Microsoft, and I don't think we should go back to the 90's if we can help it.
And there's another problem with both the CLR and JVM - they don't support dynamically shaped objects the way that, say, V8 does. Most of what is happening in any program is reading and writing values. If accessing a property always includes a hashtable lookup, that's going to be a severe waste of resources for languages that rely on that. All the solutions I've seen for languages like JavaScript and Python have been to implement a VM on top of a VM. I think that's great for prototyping, but can't be considered a deployable solution for commercial software. It's just frittering away much of the computing power we have developed so far, because we couldn't get organized to provide a real solution.
Solution Requirements
We need a solution that minimizes the limitations evidenced by the current contenders, which I would say are JVM, CLR, and Web Assembly.
First rule would be that we don't want to support languages by just compiling a full interpreter to the platform. The solution should be able to flatten any nested VM hierarchy, and run the result performantly.
In order to support all languages natively, the solution would need to performantly support both GC'd languages and manually memory managed languages. JVM and CLR support only GC'd languages. Web Assembly only directly supports manual memory management.
Allocations fall into a few categories that need to be supported:
- Arrays of structures
- Compile-time shaped structures (C, Java)
- Run-time shaped structures (JavaScript, Python)
- Unshaped hashtables
If a VM can directly support all these use cases, GC'd and not, it should be able to efficiently support any language I'm aware of. But, no VM currently natively handles both Compile-time and Run-time shaping.
Journey Onward
Nothing currently fits the bill. So where do we go from here?
There is a VM that has been deployed almost everywhere, and that's Web Assembly via the major browsers. It lacks native support for GC, but that can currently be built inside the Web Assembly to provide the functionality. Because of its broad deployment, and being based on consortium-defined standards, I think it is the best candidate to get us there. The biggest thing it's is missing is native support for GC languages, and native support for runtime shaping.
The GC support in Web Assembly is being worked on, though it seems to be coming along slowly. The amount of information that has to be passed down to the VM for it to be able to mark and sweep memory is not trivial. But, JVM and CLR both do it, so it seems a matter of time.
I don't know if there are plans to support hashtables and shaped objects in Web Assembly, but it runs inside V8, which does support object shaping. You would need to add high level instructions to allocate and manipulate objects and hashtables. But, this would accelerate a ubiquitous language feature.
Because Web Assembly is based on open W3C standards that go through a multi-entity approval process, the actual VM implementation is potentially a source of competition, as with browsers. You could choose your favorite implementation that supports your platform to deploy. More controlled platforms will come with their own implementation that will be suitable for many purposes out-of-the-box. By default, the stewards will be the browser makers, but this could become more diversified over time, if the technology becomes a nexus of usage.
How Should Languages Get on Board?
I'm in no way saying that interpreted languages shouldn't go on being interpreted most of the time. It's very useful for rapid development. But, when it comes to deployment, I think every language should provide a compiled solution. With typed Python and TypeScript, even these historically dynamic languages could really benefit from ahead of time compiling. Every serious language should provide a compiler as a first class deployment option.
The Clang compiler collection uses an Intermediate code Representation (IR) called LLVM, which can then be used to generate code for a variety of targets. It already targets Web Assembly.
The way to get started now is for all languages to actively pursue targeting Web Assembly as a first class target, either directly or through LLVM. This may mean providing a Web Assembly runtime to handle GC, but hopefully that would only be a partial virtualization, and a temporary one, waiting for the official GC extensions to come to fruition.
Addendum
Wouter van Oortmerssen has a talk on how to actually add WASM support to your language!
Edit: Added a paragraph on VM stewardship.
Edit: Added addendum with Wouter's talk.