Comparing implementations of the Monkey Language IX: A monkey and his Bun (TypeScript)
5 min read
This is my first post on Hashnode, but this series has been running for a while over on Medium. So, If everything goes well, I'll move all the posts in this series from Medium to Hashnode.
In this series, I implement an interpreter (and sometimes a compiler/VM) for the Monkey Language on a different programming language, tweak certain parts or implement feedback that I received from those programming language communities.
If you want to read the entire series, this is the list of the episodes:
II. Kotlin and Go part 2: Focused on performance tweaks for Kotlin and the introduction of GraalVM Native images
III. Kotlin Native: A failed experiment trying to run it using Kotlin Native
V: Crystal part 2: Focused on Performance tweaks and some implementation suggestions from the Crystal Community
VI: Crystal part 3: More Crystal tweaks
VII: Scala 3
VIII: Ruby, Python and Lua
In the last episode, we wrote three implementations of the Monkey language with Python, Ruby and Lua.
TSMonkey is my implementation of the Monkey Language interpreter.
My history with TypeScript
I started working with TypeScript while using Angular 2/4 when I was working for Disney (yes, I'm an Ex-Disney) around 2017. I'm mainly a backend developer, but trying to challenge myself, I decided to take a frontend role for a particular internal project. After that, I didn't touch the language anymore, but I found it fun while working with it.
What I like about TypeScript
TypeScript (2012) has a particular flavour, close to other languages created around that era, such as Scala (2003), Kotlin (2011) and Swift (2014). It is effortless to jump from Scala/Kotlin to TypeScript
TypeScripts has, as its name indicates, incredible support for all kinds of types: Union types, Structural types, Generics, Conditionals and so on. Admittedly, the TypeScript type system is slightly below Scala in features, but that is plenty to keep you entertained for hours.
Tooling and other resources
For TSMonkey, I choose Bun. Although with some modifications. you can run it with Deno, i.e., Imports in Deno require the file extension.
I ran the traditional Monkey benchmark. A recursive
fibonacci(35) that looks like this:
With Bun and Deno
So Bun runs 1.67 times faster than Deno. Very good. But if you pay attention to the absolute numbers... Bun and Deno are faster than Ruby, Python and Lua (the languages that I used in the last episode)
There is a caveat here regarding Lua. The feedback from the Lua community is that my implementation uses too much OOP emulation (Lua doesn't have OOP natively, but you can emulate it with metaprogramming), and if I use a proper Lua style, it can run a lot faster. It'll make for an interesting post, so maybe I'll do it.
But wait, there is more. The Bun version is so fast that it is faster than my Go version.
You read it well. Bun 0.3.0 runs faster (2% faster), an interpreted version of an interpreter than a Go 1.19 compiled native version of the same interpreter on an MBP early 2019, macOS Ventura, 2.3 GHz 8-Core Intel Core i9.
Now, you can say that because TSMonkey is my 8th implementation of the same interpreter, I nailed down all the possible optimisations. But, even if that is true, it still doesn't explain how an interpreted language runs faster than a compiled one.
Let's rerun it on a Linux VM with an AMD Ryzen 9
Basically the same, is still impressive for an interpreter running an interpreter.
Bun is a truly remarkable piece of software engineering.
What I don't like about TypeScript
this semantics still apply to TypeScript in all its glory.
And those fancy types? Forget about it. Once it gets transpilled, all of them disappear, which means no reified generics, no runtime Type information and so on.
And, unlike Scala/Kotlin, control structures are statements, not expressions; therefore, you cannot return or declare a variable from an
if or a
It lacks some features
There is no proper syntax for extension functions (i,e: Adding functions to existing types).
No operator overloading
No macros or other forms of metaprogramming.
Decorators are an experimental feature, and Bun doesn't support them.
You cannot return from an inner function.
Compare this Kotlin code to the following:
The TypeScript version is not as expressive and concise as the Kotlin one.
TypeScript is a good language, my complaints are minor, and Bun's performance is incredible for an interpreted language.
In the next episode, we'll revisit some old benchmarks.