Skip to content

The Alternative Implementation Problem

In this post, I want to talk about a dynamic that I’ve seen play itself over and over again in the software world. In fact, I would venture a guess that this kind of situation probably happens in the hardware world as well, but I’ll speak about software systems since this is where my experience lies. This discussion is going to touch a bit on human psychology, and outline a common trap so that you can hopefully avoid getting stuck in it.

Most of my career, both in academia and industry, has been spent trying to optimize dynamically-typed programming languages. During my master’s I worked on a simple optimizing JIT for MATLAB. For my PhD I worked on a JIT for JavaScript. Today I’m working on YJIT, an optimizing JIT for Ruby which has now been upstreamed into CRuby.

During my PhD, while working on my own JavaScript JIT, I read many papers and blog posts about JIT compilers for other dynamic languages. I read about the design of HotSpot, Self, LuaJIT, PyPy, TruffleJS, V8, SpiderMonkey, and JavaScriptCore among others. I also had the chance to interact with and meet face to face with many of the really smart people behind these projects.

One of the things that struck me is that the PyPy project was kind of stuck in a weird place. They had developed an advanced JIT compiler for Python which could produce great speedups over CPython. By all accounts many people could benefit from these performance gains, but PyPy was seeing very little use in the “real world”. One of the challenges that they faced is that Python is a moving target. New versions of CPython come out regularly, always adding many new features, and PyPy struggles to keep up, is always several Python versions behind. If you want you Python software to be PyPy-compatible, you’re much more limited in terms of which Python features you use, and most Python programmers don’t want to have to think about that.

Reading about LuaJIT, I found that it was and still is highly regarded. Many people regard its creator, Mike Pall, as an incredible programmer. LuaJIT offers great performance gains over the default, interpreted Lua implementation, and has seen some decent adoption in the wild. However, I again saw that there are a number of Lua users who do not want to use LuaJIT because the Lua language keeps adding new features and LuaJIT is several versions behind. This is a bit strange considering that Lua is a language that is known for its minimalism. It seems like they could have made an effort to slow down the addition of new features and/or coordinate with Mike Pall, but this wasn’t done.

Almost 4 years ago, I joined Shopify to work on Ruby. For some reason, the space of Ruby JITs has been particularly competitive, and there had been a number of projects to build Ruby JITs. The TruffleRuby JIT boasted the most impressive performance numbers, but again, had seen limited deployments. There were some practical reasons for this, the warm up time of TruffleRuby is much longer than that of CRuby, but I also saw a similar dynamic to that of PyPy and LuaJIT, where CRuby kept adding features, and TruffleRuby contributors had to work hard to try and keep up. It didn’t really matter if TruffleRuby could be quite a bit faster, because Ruby users would always view CRuby as the canonical implementation, and anything that wasn’t fully compatible wasn’t seen as worthy of consideration.

Hopefully, at this point, you see where I’m going with this. What I’ve concluded, based on experience, is that positioning your project as an alternative implementation of something is a losing proposition. It doesn’t matter how smart you are. It doesn’t matter how hard you work. The problem is, when you build an alternative implementation, you’ve made yourself subject to the whims of the canonical implementation. They have control over the direction of the project, and all you can do is try to keep up. In the case of JITted implementations of traditionally interpreted languages, there’s a bit of a weird dynamic, because it’s much faster to implement new features in an interpreter. The implementers of the canonical implementation may see you as competition they are trying to outrun. You may be stuck trying to ice skate uphill.

Almost 4 years ago, with support from Shopify, two dedicated colleagues and I started a project to build YJIT, yet another Ruby JIT. The difference is that we made the key choice to build YJIT not as an alternative implementation, but directly inside CRuby itself. This came with a number of design tradeoffs, but critically, YJIT could be 100% compatible with every CRuby feature from the start. YJIT is now the “official” Ruby JIT, and is deployed at Shopify, Discourse and GitHub among others. If you’ve visited github.com today, or any Shopify store, you’ve interacted with YJIT. We’ve had more success than any other Ruby JIT compiler so far, and compatibility has been key in achieving this.

You may read this and think that the key lesson of this post follows the old adage that “if you can’t beat them, join them”. In some ways, I suppose it does. What I want to say is that if you start a project to try and position yourself as an alternative but better implementation of something, you are likely to find yourself stuck in a spot where you’re always playing catch up and living in the shadow of the canonical implementation. The canonical project keeps evolving, and you have no choice but to follow along with limited decisional power over where your own project is headed. That’s no fun. You may have better luck trying to join up with the canonical implementation instead. However, that’s only part of the answer.

In the Ruby space, there is also Crystal, a Ruby-like language that is statically compiled with type inference. This language is intentionally not Ruby-compatible, it has chosen to diverge from Ruby, but has still seen limited success. I think this is interesting because it gives us a broader perspective. Rubyists don’t like Crystal because it’s almost-Ruby-but-not-quite. It looks like Ruby, syntactically, but has many subtle differences and is very much incompatible in practice. This just confuses people, it breaks their expectations. Crystal probably would have had better luck if it had never marketed itself as being similar to Ruby in the first place.

Peter Thiel has a saying that “competition is for losers”. His main point is that you shouldn’t put yourself in a position where you’re forced to compete if you don’t have to. My advice to younger programmers would be, if you’re thinking of creating your own programming language, for example, then don’t go trying to create a subset of Python, or something superficially very close to an existing language. Do your own thing. That way, you can evolve your system at your own pace and in your own direction, without being chained by expectations that your language should have to match the performance, feature set, or library ecosystem of another implementation.

I’ll finish with some caveats. What I said above applies when you have a situation where there is a canonical implementation of a language or system. It doesn’t apply in a space where you have open standards. For example, if you want to implement your own JSON parser, there is a clearly defined specification that is relatively small and doesn’t evolve very fast. This is very much something you can achieve. You also have a situation where there are multiple browser-based implementations of JavaScript. This is possible in part because there is an external standard body that governs the JS specification, and the people working on the JS standard understand that JIT-compiled implementations are critical for performance and guide the evolution of the language accordingly. They are not in the game of adding many new features as fast as possible.

The Race to Make Humans Obsolete

I got up today, had my morning coffee, and was surprised to find that AI music is pretty much solved. Okay, there are still some audible artifacts, and you can argue that the song I linked to is generic if it makes you feel better. Still, as someone who’s dabbled with music and composition and worked in the AI field for a while, this is incredibly impressive. We now have a software system that can crank out more catchy music than 99 out of 100 musicians, and do it about 100,000 faster than a human could. The end game of AI music seems pretty obvious. Soon, you’ll be able to have access to an app like Spotify that generates an endless stream of music tailored specifically to your unique and ever-changing personal tastes.

As with any technology, it will open many new possibilities. You may never have to be in a situation where you can’t find new music to listen to. It’s also now going to be within your reach to have an app generate multiple custom pieces of music for your friend’s birthday, incorporating all your favorite inside jokes. It also seems not too unrealistic that we could have an AI-powered Netfix eventually. What if you could have an endless TV series fully tailored to your preferences? What if that TV show could have you as a character? I know that personally, I’d love to watch 12 new seasons of Star Trek: The Next Generation. If I’m being honest, I’d probably watch 3 new episodes every day, until I get so sick of Star Trek that I need to take a multi-year break.

Endless music and TV shows seem pretty exciting until you stop to think that Spotify pays artists less than half a cent per stream on average. More content seems better, but the online world already feels, in some ways, like some weird endless, borderline dystopian competition for attention. What’s going to happen if we throw content-generating machines into the mix? Surely, like me, you’ve already stumbled upon a number of AI-generated videos on YouTube. Remind yourself that this is just the beginning. Eventually, no matter how smart and educated you are, you’ll have genuine difficulty telling apart what’s AI-generated and what’s not. I’m sure this makes you feel warm and fuzzy inside (that was sarcasm).

I’ve worked in an academic AI research lab for a few years. I genuinely believe that AI, as a technology, has tremendous potential. I dream of one day having a humanoid robot at home to keep my place clean, do my laundry, do home repairs, and occasionally cook. I’d really love to be able to spend more time with friends and working on side-projects without ever having to worry about doing chores. What if my AI-assistant could file my taxes for me? A world without chores or paperwork, wouldn’t that be amazing? I’m a bit scared of having children because of how much work it represents, and the huge resulting loss in free time. What if I could have a kid without ever having to change diapers? Maybe the robot could even come up with clever games to teach my kid how to read while also being physically active?

The potential for AI is truly exciting. It’s truly the most transformative technology we’ve ever created. The thing is though, that fundamentally, the more AI and robotics technology advances, the more we’re devaluing human labor, and even human creativity. You can try to pretend that’s not true, but we all know that the end goal is to create machines that can do anything a human can. If we succeed, then eventually, there will be no task that a human can do that a machine can’t do better and more efficiently.

We’re trying to create a world where we have access to infinite labor. Economics dictate that as the supply for labor increases, its value must decrease. You can say that’s not true, because with infinite labor, the economy can also grow to infinite size. The reality is, neither you or I have a crystal ball. The socio-political ramifications are massive. AI is going to transform the world in ways we can’t predict. I understand why you might find that exciting, but that excitement should also be tempered by an appropriate amount of fear. We’re on the verge of technological breakthroughs that have the potential to necessitate a radical transformation of the worldwide economic system, and make your own skills and creativity basically irrelevant.

I’ve watched a documentary about the lives of migrant workers living in Shenzhen. A man, looking tired, was living in an illegal dwelling. It was a cube about 2m (6ft) on every side, with a bed and a tiny television. Long hours, gruelling work, no social life. What happens to these people when their work can be automated? Do they instantly have access to UBI and food to eat?

I’ve also watched a documentary about the lives of Hikikomori in Japan. Shut-ins who experience social anxiety, never leave home and often spend most of their time gaming. Thanks to technological progress, you can now get groceries, clothing and household items delivered, get Uber Eats with no social contact. You can take courses online. Work remotely. Game in virtual reality. Substitute relationships and intimacy for a porn addiction. What if we do get access to UBI? Is that going to turn our world into a utopia? Do you think we’ll all be sipping margaritas on the beach all day while giving each other back rubs? Are you going to be really motivated to learn to sing or play the guitar if nobody cares to listen?

I apologize if the tone of this post is dark. I generally consider myself an optimist, and I don’t think it’s all that useful to engage in doomerism. However, I also think we’re facing unprecedented change. The future is really hard to predict, this is a crazy experiment, and we don’t necessarily get to try it multiple times. So I’d like to ask a few open-ended questions: is it possible to be simultaneously excited but also cautious? Can we acknowledge that even without killer machines, there are real dangers here, if only because this is completely uncharted territory? How can we help humans remain both physically and mentally healthy in a world where machines can potentially do everything we do better than we can, the online world is saturated with content, and it’s possible to live without any social contact?

Human-Scale vs Asymmetric Social Media

Every once in a while, I see people mention “dark patterns” in UI design. Patterns that are actively trying to deceive users, either to maximize engagement, to click some ad, or to get them to perform an action they didn’t want to perform. An obvious example would be some huge pop-up ad which can be closed by the user, but makes it as difficult as possible for you to find the god damn button to close it.

I’ve been thinking that there are parallels to draw with the design of social media websites. There are many websites which could be categorized as forms of social media, and many ways to design such websites. Since the public internet exploded in mid-1990s, there’s always been both good and bad things to be found in online communities. Opportunities to connect and exchange ideas with people, but also misinformation and problematic behaviors.

Many people have been lamenting, with a sense of nostalgia, that the internet has sort of been going downhill over the past 20+ years. The original magic is mostly gone. Online discussions have become more toxic and politicized. No doubt, we live in a different world today than we did 20 or 30 years ago. Many people would point to the events of 2001 as a sort of turning point after which people started to become more cynical. Still, today, I want to raise a simple question: what if part of the problems we see in online communities today is structural? What if it’s not just the people that changed, but the design of social media websites itself that exacerbates toxic behavior?

Back in the mid to late 90s, when the internet still felt new and exciting, the main way that people would connect online was via chat rooms and smaller, usually topic-oriented online discussion forums. These still exist today, but by and large, they’ve been supplanted by large social media websites such as twitter/X, Instagram, and Facebook. The main argument I want to make is that we’ve seen a large shift from human-scale social media, to what I would refer to as influencer-follower (or asymmetric) social media, and this shift in the design of social media platforms exacerbates toxic social dynamics.

The earliest online communication tools, chat rooms and small forums, try to imitate the way humans communicate in the real world. On these platforms, you communicate with people one-to-one, or with small groups of people for focused discussions. Because of the similarity to real-world interactions, I would describe these kinds of communication styles as human-scale. A platform like twitter is fundamentally different, because it puts the emphasis on one-to-many dissemination of information. There are influencers, and there are followers. Users are more or less explicitly ranked by popularity, with a small minority of popular users having a much, much, much louder voice than everyone else, and the less popular essentially existing in a different class where influencers hardly hear their voice. I would argue that this pattern, which functions almost like an online caste system, is fundamentally toxic.

There have always been celebrities and politicians. There have always been people who are more popular than others and whose voice carries more influence. The difference is that in the information age, where everyone carries a smart device, information flows extremely quickly. It takes seconds to share your hot take on any topic with potentially hundreds of millions of people, and seconds to react. There’s also a question of scale, where the massive scale amplifies pre-existing toxic social dynamics. Humans are tribal creatures, instinctively driven to follow thought leaders, and to exclude those who aren’t part of their in-group, or those that thought leaders deem as bad people. The obvious example here being twitter mobs, where an influencer leverages their followers to engage in personal attacks and cyber-bullying of someone with less social status.

As a counter-point to all this, many will disagree with me here, but I would argue that websites like Reddit and Hacker News are fundamentally healthier social media platforms than twitter and Instagram, purely because of their design, which is more human-scale and community-driven. On Reddit and HN, the content is first. Anyone can submit a link or write a post, and discussions are centered around that topic, and if the content is found to be interesting, it rises to the top. It’s not about which content some famous person might be looking at, or some half-baked hot take they posted from the bathroom.

Yes, there are problematic people and behaviors on Reddit/HN as well. Humans gonna human, but I think that the design of a platform like twitter or Instagram, where people follow influencers, is fundamentally more unhealthy. Beyond the problems introduced by the fact that you’re having asymmetric interactions with people on twitter, where influencers automatically have more of a voice than you, there’s also the problem that many have already outlined on platforms like Instagram, where you’re looking at curated, filtered glamor shots of people’s lives. The fact that these platforms encourage you to post short-form content and penalize you for doing the opposite is also definitely not helpful for any attempt at having a balanced and nuanced discussion about anything.

I’m sure that many will disagree with me, and will try to make the cynical argument that Reddit is just as toxic as every other social media platform. You’re entitled to your own opinion. I think that there are definitely a number of issues with Reddit. Many of these issues stem, I believe, from mismanagement and poor decision making by the parent company. Still, I would argue that Reddit and HN are some of the few places where the original magic of the early internet still lives on, and part of this is because of their design and structure, which encourages human-scale and community-based social exchanges.

Some may be tempted to disagree with me out of an emotional attachment to twitter/X. I’ve been casually using twitter for a few years. I use it pretty much exclusively to talk about my tech-related interests, and I follow pretty much only people who also post about tech and that I would consider to be friendly and kind. For the sake of own mental health, I try to stay way from hot button political discussions. The problem is, the platform is actively trying to hinder my efforts. It recommends tweets from people I don’t follow, influencers who post low-effort hot takes. This is less of a problem since the recent algorithm changes, but I’ve also gotten angry at twitter when I noticed that the platform was showing me inflammatory political tweets, uninformed hostile political opinions, seemingly trying to drag me into a flame war to maximize engagement. Why is this crud in my feed?

If you do agree with me that a big part of the toxicity of modern online platforms comes from issues with the nature of asymmetric, individualistic, follower-influencer design, there’s another question that arises: what can we do about it? We live in an individualistic world, and many people are drawn to influencers and the kind of ego-driven content they tend to produce. Building platforms that cater to influencers can also clearly net you a lot of money. People have gotten rich by enabling personality cults, so there’s a financial incentive to continue doing so.

I think that the first step would be to develop an awareness of the nature of the problem. Being aware and willing to acknowledge that a problem exists, and having an understanding of its cause, can help us inform design decisions so that we can build and promote better platforms. At the very least, if we choose to use social media websites, we, as individual users, have the freedom choose platforms which are better for our own mental health, and it helps to have some understanding of why.

Preparing UVM for 3D Graphics

Since the beginning of this year, I’ve been casually working on UVM, a project to a minimalistic virtual machine that is portable and easy to target. As part of this project, I’ve also been working on a toy C compiler to make creating software for this VM easier. I first wrote about UVM on February 24th, and got a fairly mixed reaction on Hacker News. Many people couldn’t see the appeal/interest in creating yet another VM or bytecode format when there’s already technology such as the JVM and WASM out there.

I’m doing this in part for the learning experience, and also because I think it’s fun. The end goal is to end up with a VM that has a very simple architecture that’s easy to understand and a stable foundation to build software on, without having to worry about issues of portability. Part of my inspiration comes from retrocomputing. Computers used to be simple. You used to be able to power on a Commodore 64 and immediately write BASIC code. Programming today can be a lot more complex. It’s hard to understand the entire machine, and just setting up a new development project can sometimes feels tedious. I’d like UVM to be simple to use, but also capable of effectively using the capabilities of modern computers, without arbitrary restrictions.

Since I last wrote about UVM, I’ve spent a fair bit of time improving NCC, the toy C compiler, adding features like typedefs, structs, and a custom C preprocessor with macros. Writing a C compiler has made me realize that there’s more complexity to C than meets the eye. The compiler is still missing several features, but it’s already quite capable, fun to use and fairly well tested. I wrote several example C programs that can run on UVM, including a a 2.5D raycaster, and a pentatonic step sequencer. I had fun writing these programs. It’s a different experience being able to do graphics and audio in C without having to think about portability and without any boilerplate. I’ve also somehow convinced Oscar Toledo to port nanochess to UVM, and Abdul Bahajaj to write a simple BASIC interpreter with graphics capabilities. Oscar reported 3 bugs in the C compiler, which I quickly fixed.

The way graphics work in UVM is that there is a system call (an API call into the host VM), to create a window, which has an associated RGBA32 frame buffer. An entire frame of pixels can be copied and displayed into the window with another system call. Because each pixel is 32 bits, individual pixels can be manipulated with a single 32-bit store instruction. There’s also a memset32 instruction which makes it possible to fill rows of pixels really fast as this can use SIMD instructions internally. That’s all there is to UVM the graphics API. Everything else is done in software for now. I may eventually add another system call that does fast RGBA32 blitting with alpha blending (loosely inspired by the Amiga), so that UVM can render GUIs really fast, but I haven’t decided yet.

I thought it would be extra cool if I were able to do 3D graphics on UVM. It currently runs on an interpreter, but in release builds, the interpreter is able to hit about 500 MIPS (million instructions per second) on my MacBook Air M1, which should be comparable to the speed of a Pentium II, and sufficient for some basic rasterization or even just wireframe 3D graphics. I started by adding floating-point support to the VM and compiler so that I could do vector and matrix math. Then I used ChatGPT to implement some OpenGL-like functions to generate rotation and perspective matrices, and transform 3D points. I don’t think I saved any time doing that, because it took me a while to verify and fix up the output that came out of ChatGPT, but I did end up with working code.

To represent 3D vectors and 4×4 matrices, I created typedefs for floating-point arrays. In order to be able to use these comfortably in my C code, I also needed to add support in NCC for stack-allocating array variables. Without that, every array variable would need to be a global. This was a bit tricky to do because UVM doesn’t allow you to directly take the address of something on the stack (like a stack-allocated array) for performance reasons, and so if you want to take the address of things on the stack, the compiler needs to manage a separate stack for the things you take the address of. Once I got that working, I was able to put all the pieces together, and render a glorious 3D rotating wireframe cube.

In terms of future plans, I’d like to add a simple asynchronous TCP networking API to UVM. That will make it possible to write something like a simple web server running on top of UVM, or maybe something like a custom bulletin board or a chat that can run without a web browser. I’m also thinking that it might be fun to create something like a little 64KB demoscene competition. In that spirit, I added some demos implementing demoscene fire and plasma effects based on Lode Vandevenne’s excellent tutorials.

If you think that UVM sounds like a cool project and you’d like to contribute, there are some open issues on GitHub. I can use feedback on various design issues. I also really welcome bug reports, and new example programs to showcase what’s possible with UVM. All of the examples are licensed as CC0 (public domain) so that anyone can freely reuse the code and remix them. I would love to see new cool demoscene effects, games and audio/music programs, or anything fun, creative or useful you can come up with. If you find any of this interesting, feel free to reach out via GitHub issues and discussions or via twitter/X.

Software Bugs That Cause Real-World Harm

Years ago, when I was an undergraduate student at McGill, I took a software engineering class, and as part of that class, I heard the infamous story of the Therac-25 computer-controlled radiotherapy machine. Long story short: a software bug caused the machine to occasionally give radiation doses that were sometimes hundreds of times greater than normal, which could result in grave injury or death. This story gets told in class to make an important point: don’t be a cowboy, if you’re a software engineer and you’re working on safety-critical systems, you absolutely must do due diligence and implement proper validation and testing, otherwise you could be putting human lives at risk. Unfortunately, I think the real point kind of gets lost on many people. You might hear that story and think that the lesson is that you should never ever work on safety-critical systems where such due diligence is required, and that you’re really lucky to be pocketing hundreds of thousands of dollars a year working on web apps, where the outcome of your work, and all the bugs that may still remain dormant somewhere in your code, will never harm anyone. Some people work on safety-critical code, and these people bear the weight of tremendous responsibility, but not you, you’re using blockchain technology to build AirBnB for dogs, which couldn’t possibly harm anyone even if it tried. I’d like to share three stories with you. I’ve saved the best story for last.

Back in 2016, I completed my PhD, and took my first “real” job, working at Apple in California. I was joining a team that was working on the GPU compiler for the iPhone and other iDevices. While getting set up in California prior to starting the job, it occurred to me that showing up to work with an Android phone, while being part of a team that was working on the iPhone, might not look so great, and so I decided to make a stop at the Apple store and bought the best iPhone that was available at the time, an iPhone 6S Plus with 128GB of storage. Overall, I was very pleased with the phone: it was lightweight, snappy and beautiful, with great battery life, and the fingerprint sensor meant I didn’t have to constantly type my pin code like on my previous Android phone, a clear upgrade.

Fast forward a few months and I had to catch an early morning flight for a work-related conference. I set an early alarm on my phone and went to sleep. The next day, I woke up and instantly felt like something was wrong, because I could see that it was really sunny outside. I went to check the time on my iPhone. I flipped the phone over and was instantly filled with an awful sinking sense of dread: it was already past my flight’s takeoff time! The screen on my phone showed that the alarm I had set was in the process of ringing, but for some reason, the phone wasn’t vibrating or making any sound. It was “ringing” completely silently, but the animation associated with a ringing alarm was active.

I did manage to get another flight, but I needed my manager’s approval, and so I had to call him and explain the situation, feeling ashamed the whole time (I swear it’s not my fault, I swear I’m not just lazy this bug is real, I swear). Thankfully, he was a very understanding man, and I did make it to the conference, but I missed most of the first day and opening activities. It wasn’t the first or the last time that I experienced this bug, it happened sporadically, seemingly randomly, over the span of several months. I couldn’t help but feel angry. Someone’s incompetence had caused me to experience anxiety and shame, but it had also caused several people to waste time, and the company to waste money on a missed flight. Why hadn’t this bug been fixed after several months? How many other people were impacted? I had a cushy tech job where if I show up to work late, people ask if I’m doing alright, but some people have jobs where being late can cause them to be fired on the spot, and some of these people might have a family to support, and be living paycheque to paycheque. A malfunctioning alarm clock probably isn’t going to directly cause a person’s death, but it definitely has the potential to cause real-world harm.

The point of this blog post isn’t to throw Apple under the bus, and so I’ll share another story (or maybe more of a rant) about poor software design in Android OS and how it’s impacted my life. About 3 years after working at Apple, when the replacement battery in my iPhone 6S Plus started to wear out, I decided to try Android again, and so I got myself a Google Pixel 3A XL. This phone also had a nice fingerprint scanner, but the best differentiating feature was of course the headphone jack. Unfortunately, Android suffers from poor user interface design in a few areas, and one of the most annoying flaws in its user interface is simply that the stock Android OS doesn’t have flexible enough options when it comes to controlling when the phone rings, which is one of the most important aspects of a phone.

Being a millenial, I don’t particularly like phone calls. I would much prefer to be able to make appointments and file support tickets using an online system. Unfortunately, our society is still structured in such a way that sometimes, I have to receive “important” phone calls. For instance, my doctor recently placed a referral for me to see a specialist. I’ve been told that the hospital is going to call me some time in the next few weeks. I don’t want to miss that phone call, and so I have to disable “do not disturb”. However, because the stock Android OS has only one slider for “Ring & notification volume”, disabling do not disturb means that my phone will constantly “ding” and produce annoying sounds every time I get a text message or any app produces a notification, which is very disruptive. The fact is, while I occasionally do want my phone to ring so I can receive important phone calls, I basically never want app notifications to produce sound. I’ve been told that I should go and individually disable notifications for every single app on my phone, but you tell me, why in the fuck can’t there simply be two separate fucking sliders for “Ring volume” and “Notification volume”? In my opinion, the fact that there isn’t simply highlights continued gross incompetence and disregard for user experience. Surely, this design flaw has caused millions of people to experience unnecessary anxiety, and should have been fixed years ago.

This is turning out to be a long-ish blog post, but as I said, I’ve kept the best story for last. I’m in the process of buying a new place, and I’ll be moving in two weeks from now. As part of this, I’ve decided to do some renovations, and so I needed to get some construction materials, including sheets of drywall. This is a bit awkward, because I’m a woman living in the city. I don’t have a car or a driver’s license. Sheets of drywall are also quite heavy, and too big to fit in the building’s elevator, meaning they have to be carried in the stairs up to the third floor. Yikes.

In Montreal, where I live, there are 3 main companies selling renovation supplies: Home Depot, Rona and Reno-Depot. Home Depot is the only one that had all the things I needed to order, so I went to their website and added all the items to my cart. It took me about 45 minutes to select everything and fill the order form, but when I got to the point where I could place the order, the website gave me a message saying “An unknown error has occurred”. That’s it, no more details than that, no description of the cause of the error, just, sorry lol, nope, you can’t place this order, and you don’t get an explanation. I was really frustrated that I had wasted almost an hour trying to place that order. A friend of mine suggested that maybe she could try placing the order and it would work. I printed the page with the contents of my cart to a PDF document and sent them over. It worked for her, she was able to place the order, and so I sent her an electronic payment to cover the costs.

Since my new place is on the third floor, we had some time pressure to get things done, and heavy items would have to be carried up the stairs, we paid extra specifically to have the items delivered inside the condo unit and within a fixed time period between noon and 3PM. The total cost for delivery was 90 Canadian dollars, which seems fairly outrageous, but sometimes, you just have no choice. I was expecting my delivery before 3PM, and the Home Depot website had said that I would get a text 30 minutes before delivery. At 2:59PM, I received two text messages at the same time. The first said “Your order has just been picked up”. The second said “Your order has just been delivered, click here to rate your delivery experience”. Again, I was filled with a sense of dread. Had they tried to reach me and failed? Had they just dumped the construction materials outside? I rushed downstairs. There was no sign of a delivery truck or any of the materials. I figured there must be another software bug, despite what the second text message said, the delivery clearly hadn’t happened yet.

Sure enough, at 3:27PM, 27 minutes after the end of my delivery window, I received a phone call from a delivery driver. He was downstairs, and he was about to dump the construction materials on the sidewalk. NO! I explained that I had paid extra to have the materials delivered inside the unit. I could show him the email that proved that I had paid specifically for this service. He argued back, according to his system, he was supposed to dump the materials at the curb. Furthermore, they had only sent one guy. There was no way he alone could carry 8 foot long, 56-pound sheets of drywall up to the third floor. I raised my voice, he raised his. After a few minutes, he said he would call his manager. He called back. The delivery company would send a second truck with another guy to help him carry the materials upstairs. I felt angry, but also glad that I had stood my ground in that argument.

The first guy waited, sitting on the side of the curb in the heat, looking angry, doing nothing, for about 30 minutes until the second guy showed up to help. When the second delivery guy showed up, he asked to see the email. I showed him proof that I had paid to have things delivered upstairs. He also stated that their system said they only had to drop things in front of the building, but that he believed me. The delivery company was a subcontractor, and this was a software bug they had encountered before. This bug had caused multiple other customers to be extremely upset. So upset, in fact, that one customer, he said, had literally taken him hostage once, and another one had assaulted him. Gross, almost criminal incompetence on the part of one or more developers somewhere had again caused many people to waste time and to experience stress, anger, and even violence. The most infuriating part of this though, of course, is that bugs like this are known to exist, but they often go unfixed for months, sometimes even years. The people responsible have to know that their incompetence, and their inaction is causing continued real-world harm.

The point of this blog post is that, although most of us don’t work on software that would directly be considered safety-critical, we live in a world that’s becoming increasingly automated and computerized, and sometimes, bugs in seemingly mundane pieces of code, even web apps, can cause real-world suffering and harm, particularly when they go unfixed for weeks, months or even years. Part of the problem may be that many industry players lack respect for software engineering as a craft. Programmers are seen as replaceable cogs and as “code monkeys”, and not always given enough time to do due diligence. Some industry players also love the idea that you can take a random person, put them through a 3-month bootcamp, and get a useful, replaceable code monkey at the other end of that process. I want to tell you that no matter how you got to where you are today, if you do your job seriously, and you care about user experience, you could be making a real difference in the quality of life of many people. Skilled software engineers don’t wear masks or capes, but they can still have cool aliases, and they truly have the power to make the world better or worse.

Memory, Pages, mmap, and Linear Address Spaces

We don’t always think of it this way, but on modern machines, memory and pointers are an abstraction. Today’s machines have virtual memory, divided in blocks called “pages”, such that the addresses represented by pointers don’t necessarily map to the same address in physical RAM. In fact, mmap even makes it possible to map files to memory, so some of these addresses aren’t even mapped to RAM addresses at all.

Two weeks ago, I wrote about UVM, the small virtual machine I’ve been building in my spare time. This VM has a relatively low-level design where it has untyped instructions and pointers for instance. Generally speaking, I’ve done my best to design the VM to be fairly “conventional”, in the sense that most design elements are aligned with common practice and unsurprising. In my opinion, this is important because it keeps the design approachable to newcomers. Having a design that’s unsurprising means that new users don’t need to read a 100-page manual and familiarize themselves with new terminology to get something done.

Even though I’ve done what I could to keep the design of UVM unsurprising, there is one aspect that’s unconventional. At the moment, UVM uses what’s known as a Harvard Architecture, where code and data live in two separate linear address spaces. Essentially, code and data live in two separate arrays of bytes. There’s actually a third address space too: the stack is distinct from the address space used for the heap. That means you can’t directly get a pointer to something that’s stored on the stack.

It’s maybe not that unconventional when you think about it, because WASM works the same way. You can’t directly get a pointer to a stack variable in WASM either, and you also can’t directly read/write to code memory. Same goes for the JVM. It just seems unconventional because UVM presents itself as a fairly low-level virtual machine that gives you pointers, and yet there are restrictions on what you can do with those pointers.

There’s a few reasons why the stack, the heap and executable memory are separate in UVM. The main reason is performance. By creating distinct address spaces, we make accesses to these different address spaces explicit. At the moment, UVM is interpreted, but my goal is to eventually build a simple JIT compiler for it. That brings in performance considerations. If everything lived in a single address space, then potentially, every write to memory could write anywhere. Every single time that you write to memory, UVM would have to validate, somehow, that you didn’t just overwrite the code that is about to be executed.

There are also performance considerations for stack variable accesses. In a JIT, it can be useful for some performance optimizations to be able to assume, for instance, that you know the type of things that are stored on the stack. It can also be useful to be able to store stack elements in registers in a way that’s not visible to the running program. If you can directly get pointers to stack elements, then it becomes much harder for the JIT to be able to make any kind of assumptions about what may or may not be on the stack. As an aside, to some degree, you can guard against that by using a register machine. Registers, when you think about it, are a kind of private address space accessible only to the currently running thread.

I ended up having a discussion with Justine Tunney on twitter about whether UVM should use a more traditional architecture with pages and mmap instead of a Harvard Architecture:

Justine’s Blink VM uses a SIGSEGV trick in order to catch writes to executable memory without having to explicitly check for them. It leverages the hardware’s page protection to do the checking. I’m not sure if this trick would be portable to a platform such as Windows, for example, but it does solve that problem on Linux/Mac and most POSIX platforms.

However, I don’t think that having code live in the same memory space as data is all that useful, and Justine seemed to agree. Writes to executable memory are relatively rare. They have to be, because it’s an expensive operation. On modern-day systems, whenever you write to executable memory, you need to perform a system call to change memory protection twice, and you may also need to flush the instruction cache. As such, needing a system call to write to executable memory instead of being able to directly do it though a pointer is hardly an inconvenience.

A more important question with regards to the design of UVM is whether it should or shouldn’t have a concept of pages and a primitive like mmap, instead of a flat linear address space for code. At the moment, there are no pages in UVM’s heap, just a long array of bytes, which you can extend and shrink with a primitive similar to sbrk. I thought that this could be problematic when it comes to freeing memory, because it means UVM can only release memory to the OS by shrinking the heap. It can’t “punch holes” to release the memory of individual pages to the OS. However, Justine mentioned that the dlmalloc allocator has been designed to favor sbrk, by ensuring that the topmost memory is more likely to be empty. The conclusion of that discussion is that implementing mmap in UVM would add complexity, and probably isn’t necessary.

At the moment, I think that the design of UVM’s memory address space seems to satisfy most of the needs I can anticipate, except for one. In the future, it seems like it would be useful to be able to share memory between different processes. The question is: how do you do that in a VM that doesn’t have mmap? If you can’t directly map a chunk in your memory space to be shared memory, then how do you share memory at all? There are at least three possible answers to that question. The first would be to not support shared memory, the second would be to add support for an mmap-like primitive later on (not impossible), and the last would be to share memory using a different mechanism.

Ideally, if UVM supports shared memory in the future, I’d like to be able to provide strong guarantees. For example, I’d like to be able to guarantee that all reads/writes to shared memory are atomic. I think it might also be useful to be able to get a lock on a chunk of shared memory, or to implement something like transactional memory (but only within that specific shared chunk). I don’t know how well that works with mmap, because we’re talking about much stronger guarantees than what is normally provided by hardware. It seems it could be possible, however, to allocate shared memory blocks with a unique ID, and to provide special primitives to access these shared memory blocks. Here we’re essentially talking about a 4th memory space that would have to be accessed trough a special kind of “fat pointer”.

In conclusion, at the moment, UVM doesn’t have pages, mmap or shared memory. The design is pretty simple and it works well. I can see a path towards adding mmap support in the future if it turns out to be necessary, but I don’t think that it will be. There are still some quirks that I think I may need to fix. One odd property in UVM is that address zero in the heap is a valid address, and accessing it doesn’t fault. However, C, Rust and other languages tend to rely on address zero being invalid. As such, it might make sense to add that restriction to UVM as well, to be more in line with established practice. Another quirk is that at the moment, the heap can be resized to have any requested size. It might make sense to only allow the heap to have a size that is a multiple of some arbitrary “page size”, such as 4KiB or 16KiB, if only to allow future optimizations.

Building a Minimalistic Virtual Machine

Just over a year ago, I wrote a blog post about a topic that’s very important to me, which is the problem of code rot, of software constantly breaking because of shifting foundations, and the toll it takes on programmers, and on society at large. We’re no doubt collectively wasting billions of dollars and millions of human hours every year because of broken software that should never have been broken in the first place My own belief is that stability is undervalued. In order to build robust, reliable software, it’s important to be able to build such software on stable foundations.

One of the potential solutions I outlined last year is that if we could build a simple Virtual Machine (VM) with no dynamic linking and a small set of minimalistic APIs that remain stable over time, it would make it a lot easier to build software without worrying about becoming randomly broken by changing APIs or dependencies. Such a VM wouldn’t necessarily meet everyone’s needs for every possible use case, but it could help a lot of us build software for the long term.

This is something I’ve been thinking about for at least two years, but in all honesty, I didn’t really dare even get started on this project, because I felt scared of the amount of work it represented. I was also scared of potentially getting a fairly negative reception with lots of criticism and cynicism. Last December though, I decided that, well, fuck it, I’m really interested in working on this project, I keep thinking about it, so I’m doing it. I’m well aware that the concept of a VM is not a new idea, and I’m sure that some people are going to tell me that I’m basically reinventing WASM, or that I should base my system on an existing processor architecture and work on something like Justine Tunney’s blink instead. I’ll elaborate a bit on why I’m taking a different approach.

WASM is trying to be a universal binary format and satisfy many different use cases in very different areas. Because it has so many stakeholders, it evolves very slowly. For instance, we were promised that WASM would have support for JIT compilers and include a garbage collectors 5 years ago, but this support still isn’t here today. At the same time, even though WASM is evolving relatively slowly, there are a ton of new features in the works, and it will surely become a very complex system. With so many stakeholders, the risk of massive feature creep is real.

In my opinion, the focus on minimalism is crucial for guaranteeing the longevity of both the VM itself and the software running on it. Exposing large, complex APIs to software running on the VM can become a liability. One of the biggest issue with modern web browsers, in my opinion, is that they’ve become so complex, it’s basically impossible to guarantee that your web app will behave the same on different browsers.

Working on my own web-based music app, I discovered that there are major differences in the way the pointer events API behaves on Chrome, Firefox and Safari. That’s kind of insane to even think about, because handling mouse clicks is something basic and fundamental that so many web apps rely on, and this API has been around at least as far back as 2016. Why can’t it behave the same in every browser? In my opinion, it’s in part because browsers are such hugely complex beasts that are growing in complexity so fast, that even tech giants can’t manage to have enough developers to implement all the APIs properly. If you don’t believe me, just take a look at this audio API bug that I ran into two years ago, that was reported 4 years ago, and still isn’t fixed at the time of this writing.

UVM has a fairly strict focus on minimalism, which will help keep the VM portable and maintainable. It’s also a lot smaller than other systems. It’s a simple untyped stack-machine. I made that choice because I want the VM to be both intuitive and easy to target. It’s not based on emulating an existing Instruction Set Architecture (ISA), because in my opinion, existing ISAs have a lot of quirks and a broad surface area (the ISA itself is a large, complex API). There’s also a phenomenon where if I said, for example, that UVM emulates an x86 CPU and Linux system calls, then people would expect UVM to support more and more x86 instructions, as well as more and more system calls. In order for the VM to maintain its strict focus on minimalism and simplicity, it has to be its own thing.

At this stage, UVM is just an interpreter with a tiny set of APIs. It’s still very immature and should be considered hobbyist-grade at best. That being said, some of its strengths is that it’s a small system that’s designed to be easy to understand. It’s important to me that software be approachable. It can run programs written in its own assembler syntax, but unlike other systems, the assembler doesn’t use some weird esoteric syntax, it uses a simple syntax based on NASM/YASM. If you’ve programmed in assembler before, and you understand how a stack machine works, then the syntax of UVM’s assembler should seem fairly intuitive to you.

I’m also in the process of building a toy C compiler that targets UVM. It’s currently lacking several features, but it already supports macros and enough C to be able to write something like a simple paint program, a snake game, and a few other simple graphics programs. UVM provides a frame buffer API that’s really easy to use. Just two function calls and you can plot pixels into a window, which makes the system fun to develop for as you can write a simple 2D game in something like 200 lines of C code without any boilerplate.

So here it is, UVM is currently in very early stages, and I don’t expect everyone to understand the purpose of this project, but I would really like to connect with people who share the vision, and find potential collaborators. If you’d like to know more, I recorded a short video demo and wrote a decent amount of documentation on GitHub, including some notes to explain the various technical decisions made in the design of UVM. There’s also automatically-generated documentation for the system calls that UVM exposes to software running on the VM. I’m also happy to answer any questions and to accept pull requests to improve the documentation.

In terms of what’s coming next, I want to improve the C compiler and I’d like to add an audio API. I could use some input on how to best design a simple file IO and networking API. Longer-term, I would also like to design a simple and safe parallelism model for UVM. Probably something based on actors, with or without the possibility of sharing memory, but parallel computation is not my area of expertise. I could honestly use some input. If UVM is something that’s interesting to you, feel free to reach out via GitHub issues and discussions or via twitter.

Typed vs Untyped Virtual Machines

One of the things that’s been on my mind recently is the idea of building a virtual machine for code archival purposes. Something that’s optimized for long-term stability and longevity, with the goal of helping prevent code rot. This is a fundamentally hard problem to solve, because the world changes, and so software changes with it, but at the same time, there’s no fundamental reason why software written 5, 10, 20 or even 50 years ago couldn’t run anymore.

To some extent, you can still run 50 year old software if you have the right emulator, but I think that this is going to become harder and harder to do, because modern software stacks are becoming increasingly complex and typically have a lot of external dependencies. Then there’s the added problem that you also have to worry about the emulator itself suffering from code rot. A friend of mine who’s learning about programming was asking me the other day why it is that iPhone apps need to be updated so often when the functionality of the apps isn’t changing. It’s because the foundation that these apps are sitting on keeps changing, and if said apps aren’t routinely updated, they’ll quickly stop working. Often, in the software world, dependencies are liabilities.

Many kinds of softwares could be designed to work just fine with very limited kinds of inputs and outputs. If you think about something like a spreadsheet program, a text editor, or many kinds of 2D and 3D games, most of these programs fundamentally only need access to a keyboard, a mouse, the filesystem and a display. All of these things have been available for over 30 years, and although the fundamentals of what they do haven’t really changed, the APIs to access them are changing constantly. Many software programs could be much better protected from code rot if they were written to run on a VM with stable APIs/ABIs and fully statically linked.

In my opinion, to protect software from code rot, we need an approach to software design that’s more intentional, minimalistic and disciplined. That approach to design should start with the VM itself. The VM should provide a stable set of API. It’s easier to keep the set of APIs stable if the VM itself is minimalistic and small. Keeping the VM small makes it easier to port to new architectures, which makes keeping software running easier. A small VM is also easier to implement correctly and consistently across platforms.

There are many different ways to design a VM. The design I have in mind is something like a single-threaded bytecode VM. The VM could be register-based or stack-based. I have a certain fondness for stack-based VMs because they’re very simple, easy to target, and stack-based bytecode has the neat side-effect that it provides implicit liveness information (values popped off the stack are dead). At the end of the day, whether a VM is register-based or stack-based isn’t very important because it’s entirely possible to convert between the two.

Another important design axis which is not as trivial and can have much broader implications is whether the VM is typed or untyped. The WASM and Java VMs are typed in the sense that they have a built-in notion of the type of values, functions, modules and objects. In contrast, something like a Commodore 64 emulator would emulate the instruction set of its 6510 CPU, but does so without assigning types to values in registers or in memory, treating values as bits and bytes without really tracking what they represent.

There are advantages to building a type system into a VM. There’s the potential for optimizations, potentially increased safety, and also the ability to more easily decompile and repair broken programs. However, on the flip side, a VM design that incorporates a type system seems inherently more complex and more constrained. Simply put, if you want to enforce rules around typing in your VM, then you have to build typing rules into your design, and that forces you to try to precisely categorize the kinds of computations your VM can perform with a lot more detail. This in turn forces the typed VM design to take on a lot more responsibilities.

In order to track the types of object fields an array elements, the typed VM design has to have a notion of what is an object (or struct), of what is an array, etc. It has to have a pre-established notion of what is a function so that it can assign types to function calls. It also has to have a notion of every kind of control flow structure that is supported so that it can assign types to them. In contrast, an untyped VM design can easily represent any control flow mechanism, be it function calls, exceptions, continuations, and coroutines using simple jump instructions. The untyped VM also doesn’t care about the way objects and arrays are implemented, because it treats memory as a linear array of bytes.

Beyond having the ability to assign types to everything, it seems to me that a typed VM design must inherently take on more responsibilities than an untyped VM design. In order to maintain typing constraints while offering decent performance, the typed VM is forced to take the responsibility of implementing a sophisticated JIT compiler that will understand and enforce all these constraints. This is because the running program can’t be trusted to implement its own JIT compiler as this can’t be proven safe. Furthermore, if the typed VM wants to support garbage collection, it also has to take on the responsibility of implementing its own GC, because once again, the running program can’t be trusted to manage its own memory while respecting typing constraints.

An untyped VM design can be much more minimalistic. It has to enforce a small set of hard constraints, such as making sure that pointer dereferences respect valid address bounds so that the running program can’t crash the host VM, but it doesn’t really need to care about the types of values or enforcing typing constraints. For performance, it can implement a very barebones JIT compiler based on dynamic binary translation, but it doesn’t have to care about optimizing type checks, and it can even allow the running program to manage its own memory and implement its own garbage collector.

In summary, I think there’s a strong case to be made that an untyped VM design can easily be much smaller and more minimalistic than a typed VM design. A simpler untyped VM has two major strengths. The first is that it doesn’t place as many restrictions on running programs. Programs can implement their own control flow structures, their own GC, or even their own JIT. The second is that a smaller, simpler VM is much easier to port, reimplement and maintain. If you think about the amount of effort that would be required to build a new Java VM from scratch and make it perform well, you quickly realize that such an undertaking is only possible for a massive corporation. There is still no official RISC-V support by the JVM, and it’s easy to understand why.

Minimalism in Programming Language Design

Four years ago, I wrote a blog post titled Minimalism in Programming, in which I tried to formulate an argument as to why it’s usually a good idea to try to minimize complexity in your programming projects. Today, I want to write about something I’ve been thinking about for a long time, which is the idea that we also ought to take a more intentionally minimalistic philosophy when designing programming languages.

Designing a programming language to be intentionally minimalistic is an idea that’s highly underrated in my opinion. Most modern programming languages adopt much more of a maximalist design approach. Rapidly adding new features is seen as a competitive edge over other programming languages. The general thinking seems to be that if your language doesn’t have feature X, then people will choose to use another language, or that adding more features is an easy way to show progress. This line of thinking is simplistic, and disregards many other key aspects that are necessary for a programming language to succeed and thrive, such as learnability, stability, tool support and performance.

Change and Churn

I’d like to make the argument that intentionally designing a programming languages to have fewer features, and to change less rapidly over time, is in itself a powerful feature. When a programming language changes often, it necessarily causes breakage and churn. Tools become out of date, codebases need to be updated, libraries become broken, but it causes churn on the human side too.

I first started programming in C++ around 1998. I haven’t really touched the language in a few years, and I have to say, I feel kind of lost. So many new features have been added that it’s a different language now. Last year, I wanted to use C++20 modules in a new project, only to find that support in G++ and Clang was so incomplete that modules were just not a viable feature. My general impression at the time was that there aren’t enough people working on C++ compilers to keep said compilers up to date. The language has become so complex, and so many new features have been added, that compiler developers are kind of burned out. It seems to me that slowly but surely, C++ is crumbling under its own weight.

Something that many people forget, is that for a language to succeed, there has to be good tool support. If the language and its feature set keeps changing, then tools need to be updated constantly. One of the many problems with C++ is that its grammar is very hard to parse. That was already the case back in 1998. If you add on top of that the problem that the grammar changes to become even more complex every year or two, what do you think the impact of that will be? The people maintaining C++ tools are going to want to go do something else with their lives, and so will the users of those tools.

Learnability and the Human Element

More recently, colleagues and I have decided to port a C codebase to Rust. I’m generally pleased with the core feature set of Rust and I feel that in many ways it’s a great improvement over C and C++. However, one of the main weaknesses of Rust, in my opinion, is its high complexity. Both at the syntactic and semantic level, Rust is a very complex language. The syntax can get very verbose, and there’s a lot to know, a lot of rules and unintuitive subtleties about what you can and can’t do where. The learning curve is steep and the cognitive load is high.

Last week, I was pair programming with a colleague when he said “I feel like the Rust compiler is always telling me that I’m too stupid”. That remark surprised me, because I’d had the same thought. Somehow Rust feels unergonomic, and the high complexity of the language surely contributes to that feeling that the language is a bit user-hostile. It breaks your intuition, and it constantly feels like the compiler is telling you that you’re writing code wrong. Two days after my colleague made that remark, I saw a post appear on Hacker News titled Rust: A Critical Retrospective which echoed similar feelings about Rust’s complexity.

In a lot of ways, I feel like designing a language to be minimalistic, to have fewer concepts, and to choose primitives that combine well together, is a good way to make the language easier to learn. If the programming language has fewer concepts, there’s less to learn, and your level of proficiency will increase faster. Code written in a more minimalistic language may also be easier to read. If we think about C++ code, we have a situation where the language has so many redundant features that a typical workplace will mandate that code be written in a subset of C++, with some language features being explicitly banned. That can mean that people writing C++ code at different workplaces will have a hard time reading each other’s code because foreign C++ code will be written in a different dialect.

In some ways, I feel like intentionally minimizing complexity and keeping the feature set small is a way of better respecting programmers. It means we respect that programmers are people with potentially busy lives and many things to do, and that they probably don’t have time to read hundreds of pages of documentation to learn our language. Programming languages are user interfaces, and as such, they should obey the principle of least surprise. Minimizing complexity is also a way to reduce cognitive load and respect human limitations. Human beings are amazingly capable creatures, but we’re also basically just clever monkeys that can talk. We can only keep a few items in our working memory, we can only account for so many design constraints, and we can only focus for so long. A well-designed programming language ought to help us succeed despite our human limitations.

At the end of the day, I think that a language’s complexity and how intuitive it feels is going to affect its ability to attract and retain new users. In my opinion, the focus on reducing friction contributed greatly to Python’s initial success and rapid increase in popularity. I think it’s also fair to say that many people were frustrated when the complexity of the Python ecosystem increased, for example, during the switch from Python 2 to 3, or when the redundant walrus operator was introduced.

Minimalism

So far, I’ve made multiple references to minimalism and I’ve also briefly mentioned the principle of least surprise. I’ve hinted that minimalism also means having a smaller feature set and less concepts to learn. Minimalism doesn’t just mean a smaller feature set though. It also means carefully choosing features that combine together seamlessly. If we design a language with a large feature set, there’s a combinatorial explosion in how these different features could interact, which means we’re more likely to end up with situations where some language features interact together poorly.

Imperative programming languages typically make a grammatical distinction between statements and expression. Functional languages instead tend to be structured in a way that everything inside a function body is an expression. The latter is more minimalistic, and also imposes less constraints on the programmer. Some languages impose a distinction between code that can be run at compile time vs code that can be run at program execution time. This distinction often increases the complexity of the language as there tends to be a duplication of language features and fairly arbitrary restrictions as to what code the compiler is able to run at compilation time.

In terms of minimizing surprise, we want to avoid introducing strange corner cases that only show up in some circumstances. Another important pitfall to avoid is introducing hidden behaviors that the programmer may not expect. An example of this would be the equality (==) operator in JavaScript, which actually includes an implicit conversion to the string type, meaning that 1 == “1” evaluates to true. Because of this undesirable hidden behavior, JS actually has a separate strict equality operator (===) which doesn’t perform the hidden string conversion. This suggests to me that JS should only ever have had a strict equality operator, and that if you want to convert the values you’re comparing to strings before performing the equality comparison, you should just have to explicitly spell that out.

Implementation Complexity

Language design is hard because the space of possible programming languages is infinite, and so compromises have to be made. It’s hard to provide hard numbers to quantify what makes one design better than another. Some of the things that can be quantified to some degree are the complexity of the implementation of a language and also the way that a particular language implementation performs.

My PhD thesis involved the implementation of a JIT compiler for JavaScript ES5. As such, I got to become intimately familiar with the semantics of the language and everything that has to go on behind the scenes to make JavaScript code run fast. At times, that was a frustrating experience. I’ve become convinced that a lot of the complexity and the hidden behaviors in JS and in many other languages are essentially bad for everyone.

Unnecessary complexity in a language is bad for those learning the language, because it makes the language less intuitive and harder to learn. It’s bad for the programmers working with the language everyday, because it increases their cognitive load and makes it harder to communicate about code. It’s bad for language implementers and tool maintainers, because it makes their job harder, but at the end of the day, it’s also bad for end users, because it leads to software with more bugs and poorer performance.

To give you an example of unnecessary implementation complexity, many object-oriented languages have this idea, borrowed from Smalltalk, that everything should be an object, including booleans and integer values. At the same time, languages implementation for these languages have to do a lot of work behind the scenes to try and represent integers efficiently (as machine integers) while presenting an interface to the user that resembles that of an object. However, the abstraction presented to the user for an integer object is typically not really the same as that of a normal OOP object, it’s a leaky abstraction, because being able to redefine integer values makes no sense, because integer values have to be singletons, and because being able to store properties/attributes on integers is both dumb and terrible for performance and so typically isn’t allowed.

Ultimately, integers are not objects in the object oriented sense. They’re a distinct type of atomic value with a special meaning, and that’s okay. The mistaken idea that “everything should be an object” doesn’t actually simplify anything in practice. We’re lying to ourselves, and in doing so, we actually makes the life of both language implementers and programmers more complicated.

Actionable Advice

This blog post has turned into more of a rant than I expected it to be. It’s easy to critique the status quo, but I’ll also try to conclude with some actionable advice. My first piece of advice for aspiring language designers is that you should start small. Your language is a user interface, and an API which people use to interface with machines. The smaller the API surface, the less you risk introducing accidental complexity and subtle design mistakes.

My second piece of advice is that if you can, you should try to keep your language small. Limiting yourself to a smaller feature set likely means you will want to choose features that don’t overlap and that provide the most expressiveness, the most value to programmers. If you do want to grow your language, do it slowly. Take some time to write code in your language and work through the potential implications of the design changes that you are making.

It’s easy to add new features later on, but if you add new features and people begin using them, it’s going to be hard or even impossible to take these features back, so choose wisely. Remember that you don’t have to please everyone and say yes to every feature request. No language or tool can possibly satisfy every use case, and in my opinion, trying to do so is a mistake.

Lastly, remember that language design is an art. It’s a delicate balance of many different constraints, just like user interface design. Brainfuck is a language that is very small and has very few concepts, but nobody would call it expressive or elegant. Lisp is regarded by many as one of the most beautiful and elegant languages in existence, but my PhD advisor, a Scheme fanatic, had the habit of writing code with single-letter variable names and very few comments. An elegant language doesn’t automatically make for elegant code, but you can encourage good coding practices if you lead by example.

Democracy, Dictatorships and Access to Information

The last two weeks have been a painful reminder that, as comfortable as we may have it in the west, democracy is not the default state of the world. Historically speaking, democracy has been a relatively recent and scarce concept. Even today, two of the largest countries in the world are ruled by authoritarian dictatorships. It’s both scary and sad to be reminded that one person’s ego, when left unchecked, can cause an incalculable amount of suffering, and the deaths of tens of thousands, or even millions.

One clear similarity between authoritarian regimes is that in order to maintain their power, they limit and suppress access to information. They can’t tolerate criticism, debate or dissent. They control the narrative by supplying one message and one storyline, and silencing all other voices by any means necessary. Suppressing the free flow of information has the added effect that even if people disagree with the sanctioned narrative, they may feel completely isolated, and so they are understandably too afraid to act. In a way, it’s almost tautological: centralized control relies on preventing the free flow of information, because allowing multiple voices to be heard automatically takes power away from centralized control.

Recently, I’ve been pleased to see that Signal, the end-to-end encrypted messaging system, has gained almost mainstream popularity. People are beginning to care a little bit more about privacy, and to look for alternatives to services like Facebook. However, I don’t think Signal is enough. I’ve heard that this is going to change, but the reliance on a phone number to identify users is anti-privacy by definition. Secondly, while Signal provides an alternative to Facebook Messenger and groups, it doesn’t do much to replace Facebook’s events. Lastly, signal still seems to rely on centralized servers, which makes the service inherently vulnerable to disruption.

In my humble opinion, we need something more decentralized and privacy-conscious. Something like a cross between Signal and BitTorrent, with a little bit of Tor sprinkled in there. End-to-end encryption is great, but it would be nice if there was a way to implement a messaging service without relying on one centralized server to identify and connect people. BitTorrent solves this problem by having a long list of “trackers”. Maybe this concept can be adapted by having many servers which act as dead drops, used by friends to exchange messages while still remaining anonymous. Possibly, this could also incorporate the peer-to-peer component of BitTorrent.

I’m not a cryptography expert and I’m not the best person to solve this problem, but I tend to believe that something like this should be possible, and that it’s a problem worth working on. It should be possible for each user to create a public and private key pair that lives only on a local device. Then, without needing a centralized account, each user can use their public key as their online identity, without even providing a username. Possibly, two users could become “friends” in a decentralized system by sharing a public/private key pair that only they are aware of. This shared key could be created and shared completely offline if desired, over a USB stick, over bluetooth, by tapping cellphones together in a coffee shop, or even using a QR code.

A shared key pair can be used to drop messages on a server or on a peer-to-peer basis without having the users identify themselves or the intended audience when sending the messages. Alice can encrypt a message for Bob, but the encrypted message contains no visible information saying it comes from Alice, or that it’s destined for Bob. Alice and Bob can download many messages from a server without letting the server know which messages they are looking for. If you think of this in a peer-to-peer context like that of BitTorrent, I think it paints an interesting picture. Alice can download many messages from peers and also upload many messages to other peers. Most of these messages are encrypted and she has no access to them. At some point, she writes a message for Bob and encrypts it using their secret key. She injects her own message in the stream and it starts to propagate among the network. Alice doesn’t tell anyone that this new message is her own, but Bob will be able to identify it when he sees it.

In terms of community organization, it may be possible to share details about an event, or to create something equivalent to a twitter hash tag, by broadly sharing a decryption key using a QR code. This would be the equivalent of telling many users on the network to follow your channel/feed.

The system I’ve loosely described above has flaws and could certainly be iterated on, but the key point is, something like that should be technologically possible, and it’s a problem worth solving. Access to information, freedom of expression and safe channels of communication are essential if we want to protect our democracies and the well-being of our species in general.