After a ton of work, a lot of which was unexpected, I am ecstatic to announce that EndBASIC is now a reality on the web! The whole language interpreter can now run as a fully client-side web app on a computer, on a tablet… and even on a phone. Yes: the whole thing, which is written in Rust (94%), works in a modern browser with just a tiny bit of JavaScript glue (1%).
Witness for yourself in the dedicated website or… you know, right here in an iframe!
I have to confess that this launch is way more exciting than getting the command-line version of EndBASIC up and running. After all, I’ve written countless command-line tools over the years (or is it decades now?), but have done very little work on the web. Exploring new technologies—WASM, some HTML5 stuff, push on green—and seeing how my kids were wowed when they realized that this could reach anyone in the planet, instantaneously… is priceless.
Motivation
My plan after the 0.2.0 release was to add some “visual” features like COLOR
and LOCATE
, which happened relatively quickly. But after doing that, I felt that having to deploy new versions of the code to my kids’ computer (which they rarely use) felt clunky. What if this was all accessible via the browser so that they access it from, you know, the iPad they are now using every day for home schooling? After all, I’m looking for immediacy in getting to the coding experience with this project, so that felt fitting.
With that in mind, I had a rudimentary proof of concept up and running in less than two hours with zero past experience with WASM and very limited experience with JavaScript.
But getting to the final version has taken much longer… and a lot of sweat. In particular, I was stuck for almost five days trying to make the simple INPUT
statement work, and then I had to put a lot of grunge work to port the uninteresting bits to the browser (like SAVE
and LOAD
using HTML5’s local storage).
Why? Let’s peek through the journey.
Step 1: Trimming dependencies
The first roadblock that appeared when cross-compiling the EndBASIC interpreter to WASM were some dependencies that are not ready for this target. I had just finished adding features to change colors via the crossterm crate, and that crate does not have support for targeting WASM.
So the first step that had to happen was the thing I didn’t do originally out of fear of making things too complex: keep the language core separate from the REPL. Doing this wasn’t too complicated though: introduce a Cargo workspace, pull things apart, and do a couple of code changes to move the rustyline and crossterm dependencies to the REPL crate. The core is now lighter-weight and makes no assumptions on how it is connected to the user console.
… which means EndBASIC can now be trivially embedded into other programs if you so wish, with a very small cost (and with the potential of reducing that even further). Stay tuned on this.
Step 2: Cross-building to WASM
The second step was hooking up the interpreter to a web page. This was pretty easy thanks to the tutorial in the Rust and WebAssembly book, and as said above, it took 2 hours tops.
Integrating xterm.js wasn’t too hard either by following the trivial demo code, though this exercise highlighted that command editing on the web would be much crappier than on the console for a while. Simply put, rustyline doesn’t work on the web, and I haven’t yet implemented any of its fancy features.
Step 3: Dealing with blocking I/O
The third step is where the real pain came: making 99% of the EndBASIC interpreter work in a web context was trivial. But getting the remaining 1% took nearly five days—or, really, early mornings / late nights because, after all, this is happening in my free time!—of experimentation. So what was this 1%? INPUT
.
You see, the way I designed EndBASIC was: the REPL is a loop that executes statements. When one of these statements is a call to INPUT
, the interpreter issues a callback to a function supplied at construction time, and that callback blocks until it reads a line of text to feed back to the INPUT
processor. But… you can’t block in JavaScript.
I spent quite a bit of time trying crazy things like adding buffers, condition variables, futures, etc. especially after seeing that there seems to be some support for atomics in WASM. But I couldn’t get that to work, and the fact that this support was gated behind experimental features in the Rust toolchain and in Firefox made me quickly realize that I was down some crazy path. After all, there are plenty of web apps that do harder things without using experimental web features, so I was definitely doing something wrong.
In the end, I was forced to ask the Rust user forums for advice after seeing that the answer seemed to be making my code async-friendly. I feared approaching this route for lack of knowledge, so I wanted some reassurance that it was indeed the way to go. And… I got that confirmation.
So after some more failed attempts, I took the plunge and made the whole interpreter async-friendly just so that the INPUT
callback could be async. And you know what? It wasn’t that hard after all. With that resolved, making input work was not much harder, and this was one of those “Eureka!” moments that I crave for but rarely get to experience.
And making INPUT
work also unblocked interactive program editing via EDIT
, which brings us to the next step.
Step 4: Abstracting program storage
At this point, the whole language interpreter was working and I knew I had unblocked the main problem to bridge the web gap. But the whole thing was still too unusable to announce. In particular, all commands that deal with program storage were still unusable because they assumed I/O against a file system. I knew not abstracting this early enough would come back to bite me… and it did, but I wasn’t expecting it would happen so soon.
I had to introduce an abstraction to isolate program storage so that commands like DIR
, LOAD
, and SAVE
could do different things depending on the underlying platform. With this abstraction in place, it was pretty easy to add a specialization that uses HTML5’s local storage instead of the local disk.
I got a little bit stuck here as well because Travis CI didn’t want to run my browser tests while my macOS installation did, which made me ask in the Rust user forums again. But, this time, I found the answer but myself a few minutes after, and it stemmed from version inconsistencies in my dependencies.
Step 5: Push on green
The last cool thing that I played with throughout this process was configuring the Travis CI pipelines to push a new version of the web interface as soon as it passes tests and is merged into the master branch.
There is not a lot of magic here (and I think what I did is a bit too “manual” compared to what Travis CI offers), but seeing the whole thing work in an automated fashion is very pleasant. The next step in here is migrating to GitHub Actions—I’m using Travis CI out of inertia—but that yak ought to be shaved another day.
Parting words
To be honest, I have now spent way more time on the frontend aspects of EndBASIC than in the language interpreter itself, which is not what I was originally expecting. And as a result, the language is still lacking some important features—in particular, FOR
loops and function calls so that I can add things like random numbers and timers.
Anyway, with that said, head to:
to give all this a try.
Expect things to break everywhere but know that you can just reload the webpage to start afresh (Ctrl-Alt-Del, anyone?) 😄. Don’t hesitate to send feedback and… if you are willing to help me build EndBASIC, please let me know! I have some pretty strong ideas on what I’d like this whole thing to look like, but there is a ton of work to be done at all levels.
Thanks for listening.