TL;DR: I made a budget Pascal compiler to WebAssembly so that I can play a hangman game that my friends and I made 10 years ago. Check out the demo and the github repository.
About a month ago, I was reorganizing my old files in my laptop when I found something interesting. It was a console-based hangman game that my friends and I made in Pascal as a final project for intro to programming class1 back in 2011. I had just finished reading Crafting Interpreters by Robert Nystrom, so I thought it would be fun to move to compilers and try to compile the hangman game to WebAssembly. Here are some of many interesting things I learned and made during the development.
Choosing and “budgeting” the features
Making a full-fledge Pascal compiler is a very time-consuming task. I want the project to be small enough that I can finish it in 4-6 weeks, so I decided to support only a subset of Pascal features and language constructs (hence, “budget”). I chose which features to implement based on three principles:
- The compiler should be able to compile the hangman game without any changes to the game’s source code. This means I need to handle things that normally I don’t handle like files, output formatting, standard library methods like
pos
,clrscr
,readkey
, and so on. - The compiler should be able to handle things that “naturally” exist given the chosen features. For example, while the game source code doesn’t have any recursion call or use any floating-point number type, I think it would be weird not implementing those. However, things like dynamic length array, dynamic memory allocation, pointers, and fully-implemented set type are surplus to the requirements. This rationale is quite arbitrary but I settled on it.
- The compiler should compile a strict subset of Pascal. This means while it can’t compile some Pascal programs, all programs that it can compile should be compilable by other full-feature Pascal compiler like FreePascal. There should be no program that is valid for this compiler but invalid for other compilers. This is easier said than done and I’m still not 100% sure if the implementation is indeed a strict subset.
The full detail of chosen features can be found in the repository’s readme. In summary, the compiler can handle:
- Basic data types like
integer
,real
,char
,boolean
,string
,array
,record
andfile
- Variable, constant, type alias, and subroutine (procedure / function) declaration
- Basic expression, subroutine call and control flow statement
- Internal declaration (e.g. procedure inside procedure) with local / global scoping
- A few standard library method call
Generating WebAssembly binary
I wanted the hangman game to be playable on a web page. There are three approaches2 to do it: (1) make a virtual machine that interprets and runs the Pascal program, (2) transpile the Pascal program to Javascript and then run it using Function object, and (3) compile the Pascal program to WebAssembly. I chose the third option because I’m interested in WebAssembly for quite some time and it seems more fun and challenging than “just” making a VM or transpiling to Javascript.
While it is possible to just manually produce WebAssembly binary bytecodes, I used the binaryen-js library because it’s easier and it also have validation and optimization features. The downside is that it is quite large, about 5 MB even after packed using parceljs. It also uses tree representation for validating and optimizing the WebAssembly module, so a few expressions like multivalue tuples and manual stack manipulation are a little bit harder to express. At the time I didn’t realize there’s also wabt.js, so it