I've gotten so bad about writing these this year. I wanted to talk about two things I'm quite excited about. First up is a huge compiler project I've been doing for the past month to take Gleam and produce wasm.
Gleam Compiler
WebAssembly (Wasm) is a binary instruction format designed to run in browsers and standalone runtimes. It is stack-based, strongly typed, and sandboxed. The same .wasm binary can run in Firefox, Chrome, Node.js, Wasmtime, or any other compliant runtime, a level of portability that native machine code does not offer. But like machine code, Wasm has value types, functions, linear memory, tables, imports, and exports. Generating valid, efficient Wasm from a high-level language requires the same structured compiler work as generating machine code.
Regulus (codename Reggie), an experimental compiler that takes Gleam source code and produces WebAssembly, written in Rust like the Gleam compiler.
Gleam is a functional language with static types, immutable values, and a friendly syntax. It has pattern matching, custom types, generics, and a module system that normally compiles to Erlang bytecode or JavaScript. As a learning exercise, I opted to create a separate project to compile Gleam to WebAssembly instead.
Reggie produces .wasm from Gleam source through seven steps:
- 1.
Produces an in-memory tree-sitter concrete syntax tree
- 2.
Builds a compiler-owned AST
- 3.
resolves module
- 4.
type-checks modules
- 5.
Builds a core Intermediate Representation
- 6.
Generates WAT (WebAssembly Text format)
- 7.
Produces a
.wasmbinary
I want to talk about the most challenging and interesting parts of the whole project.
Name Resolution
In this portion of the pipeline, the resolver walks the AST (step 2) and turns textual name references into known declarations.
It handles lexical scope, imports, module-qualified names, constructors, field names, and visibility rules. The output is a ResolvedModule which is the AST annotated with symbol table data.
Lowering
After the type checker walks the resolved AST and checks that values are used with compatible types, we "lower" (translate) to typed source syntax into a smaller, source-independent representation (core IR). Source-level constructs become explicit runtime operations. For example:
letbindings become local variable allocations and writescaseexpressions become branch nodes with explicit pattern testslet assertbecomes a branch with an explicit failure pathfunction parameters become numbered locals
This IR knows nothing about Gleam syntax. It knows about locals, calls, blocks, branches, and managed-value operations.
Gleam example with use:
pub type User { User(name: String, age: Int) }
fn with_user(user: User, callback: fn(User) -> Int) -> Int {
callback(user)
}
pub fn use_updated_age(age: Int) -> Int {
let user = User(name: "Ada", age: age)
use current <- with_user(user)
let older = User(..current, age: age + 2)
case older {
User(name: _, age: value) -> value
}
}Emission of WAT
The backend translates core IR into WebAssembly Text format (WAT). It maps scalar Gleam types to Wasm value types:
| Gleam type | Wasm ABI |
| -------------------------- | ------------- |
| `Int` | `i64` |
| `Float` | `f64` |
| `Bool` | `i32` |
| `Nil` | no result |
| strings, lists, records, … | `i32` pointer |Managed values like strings, lists, tuples, records, custom types, closures live in WebAssembly linear memory. The backend emits a runtime prelude for modules that use managed values: memory, a bump-allocation heap pointer, and helper functions for allocation, string handling, list operations, equality, and panic.
The above gleam example's wat output (truncated):
Long-Term Goals
The long-term goal here is for a complete Gleam-to-WebAssembly compiler that can compile real Gleam applications like including web backends with Wisp and browser UIs with Lustre to WebAssembly that runs in browsers and cloud runtimes like Cloudflare Workers.
Resume
As I continue my job hunt, I've been updating my "brand" and made a resume site on Astro using the JSON Resume schema. The setup generates three outputs from one data file: a web page, a PDF, and an Open Graph image. It's not a super interesting or complex project but I'm quite happy with how it's turned out. To build it, I adapted Colin Hemphill's nextjs-resume to Astro. He had already solved the layout problems. I used his setup as inspiration and swapped out the framework underneath, while adding a few flourishes to make the project my own.
You can view the finished product here:
The resume lives in a flat data.json file that follows the JSON Resume spec, translated to an Astro content collection.
The PDF generation happens at build time through a static path. It uses @react-pdf/renderer to convert React components into a PDF document. Here's what the PDF ends up looking like:
The OG image also uses React but via using @takumi-rs/image-response. It pulls my name and title from the JSON to create a social media preview card that I think looks pretty cool.