Comparing implementations of the Monkey Language IX: A monkey and his Bun (TypeScript)

Featured on Hashnode

Disclaimer

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:

I. Kotlin and Go

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

IV: Crystal

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

Previously

In the last episode, we wrote three implementations of the Monkey language with Python, Ruby and Lua.

TSMonkey

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

Types

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

TypeScript tooling is excellent. JetBrains IDE's had outstanding support for TypeScript, and I hear good things about VS Code and even NeoVim. There are plenty of books, websites and others. And that is without counting the JavaScript resources that can be used immediately with TypeScript.

Performance

Because JavaScript is used everywhere (frontend, backend, mobile and desktop), a lot of time and resources are expended yearly on running JavaScript as fast and efficiently as possible. TypeScrpit benefits directly from those efforts as well.

Currently, there are two mainstream JavaScript engines, V8 and JavaScriptCore and three popular runtimes Node (V8), Deno (V8) and the newest one, Bun (JavaScriptCore).

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)

For interpreted languages, TypeScript (JavaScript) is faster than any other language by a huge margin.

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

Is still JavaScript

As fancy as TypeScript can be, you're still in JavaScript land. E.g. The weird JavaScript 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 try.

It lacks some features

Let's go with some controversial takes (Some of these are still related to the JavaScript runtime).

  • 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.

Conclusion

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.