Patrick Dubroy
@dubroy.com
1.5K followers 260 following 520 posts
Programmer & researcher, co-creator of https://ohmjs.org. 🇨🇦 🇩🇪 🇪🇺 Co-author of https://wasmgroundup.com — learn Wasm by building a simple compiler in JavaScript. Prev: CDG/HARC, Google, BumpTop
Posts Media Videos Starter Packs
Pinned
dubroy.com
Here it is — very happy to officially release the book that @marianoguerra.org and I have been working on for the past 2½ years.

If you bought it in early access, thanks for your support! 🙏

If you haven't bought it yet, please check it out!!
wasmgroundup.com
Excited to announce the official launch of our online book, WebAssembly from the Ground Up! 🎉

It's the book we wish we'd had 3 years ago.

No messing with tools and frameworks. It's a hands-on guide to the core of Wasm: the instruction set and module format.

Link below. 👇
Learn Wasm by building a simple compiler in JavaScript. No compiler expertise necessary. All the code is in the book; we'll take you through it step by step. Get your hands dirty and see for yourself what WebAssembly is all about.
Reposted by Patrick Dubroy
tomasp.net
I'm teaching 𝗪𝗿𝗶𝘁𝗲 𝘆𝗼𝘂𝗿 𝗼𝘄𝗻 𝘁𝗶𝗻𝘆 𝗽𝗿𝗼𝗴𝗿𝗮𝗺𝗺𝗶𝗻𝗴 𝘀𝘆𝘀𝘁𝗲𝗺(𝘀)! again. I'll be posting the videos & tasks on YouTube too.

In the first lecture, I explain what's a tiny system, why write one and show plenty of demos!

🎞️ Playlist: www.youtube.com/playlist?lis...
👉 More info: d3s.mff.cuni.cz/teaching/npr...
Write your own tiny programming system(s)! - YouTube
The goal of this course is to teach how fundamental programming language techniques, algorithms and systems work by writing their miniature versions. The cou...
www.youtube.com
dubroy.com
Ah, thanks. Burned by a minor typo 😔
dubroy.com
😄

Btw, I'm planning a larger blog post to talk about how I'm using this in practice, how to deal with fallback in older environments, and how to combine it with FinalizationRegistration to prevent misuse.
dubroy.com
TIL: [JS] Explicit resource management
github.com/pdubroy/til/...

Don't miss the getter trick! It's super helpful and not well-documented.
After learning about FinalizationRegistry, I also learned about the using statement:

The using declaration declares block-scoped local variables that are synchronously disposed. Like const, variables declared with using must be initialized and cannot be reassigned. The variable's value must be either null, undefined, or an object with a [Symbol.dispose]() method. When the variable goes out of scope, the [Symbol.dispose]() method of the object is called, to ensure that resources are freed.

It's useful in scenarios where you might otherwise use a try/finally, e.g.:

let normalMap;
try {
  tex = textureLoader.load('muppet.jpg');
  tex.doSomething();
  // …
} finally {
  tex.dispose();
}
With a using statement, and assuming the texture object implemented [Symbol.dispose](), you could write it like this instead:

using tex = textureLoader.load('muppet.jpg');
tex.doSomething();
[Symbol.dispose]() will be called when tex goes out of scope. Unlike FinalizationRegistry — but just like a finally block — it's guaranteed to be called, so you can depend on it (in environments that support it).

It's currently supported in Chromium-based browsers, Node, and Firefox, but not yet in Safari. It's also been supported in TypeScript since v5.2, which was released in August 2023. Required dispose
A non-obvious thing is how to deal with objects that must be disposed. In other words, if a programmer accidentally writes const instead of using, can we catch it? For many scenarios, we can — by writing a getter for [Symbol.dispose]():

import assert from 'node:assert/strict';

class MyResource {
  constructor() {
    this._using = false;
  }

  doSomething() {
    assert(this._using, 'Did you forget `using`?');
    // …
  }

  get [Symbol.dispose]() {
    this._using = true;
    return () => {
      // Actual dispose logic goes here.
    }
  }
}

using res1 = new MyResource(); // ok

const res2 = new MyResource();
res2.doSomething(); // Throws
dubroy.com
Heh. Seems like a pseudo-formalism but cool nonetheless :-)
dubroy.com
Causal Separation Diagrams (CSDs) from "Inductive Diagrams for Causal Reasoning" by Castello et al (2024)

dl.acm.org/doi/pdf/10.1...
The image contains six rectangular tiles arranged in two rows of three, each with a blue border and red dashed outline:

**Top row:**
- **tick**: Shows a horizontal line with a black dot in the middle

**fork**: Shows two lines diverging from a single point on the left, spreading outward to the right

**join**: Shows two lines converging from separate points on the left to meet at a single point on the right

**Bottom row:**
- **noop**: Shows two parallel horizontal lines

**swap**: Shows two lines crossing each other in an X pattern

**assoc**: Shows curved lines that appear to rearrange or regroup connections

At the bottom of the image is a larger diagram showing how these atomic steps can be combined, featuring a rectangular grid with curved and straight lines demonstrating the composition of multiple atomic steps into a complete CSD structure.
dubroy.com
I like this —

A case for learning GPU programming with a compute-first mindset: themaister.net/blog/2025/10...
I would argue we should start teaching compute with a debugger/profiler first mindset, building up the understanding of how GPUs execute code, and eventually introduce the fixed-function rasterization pipeline as a specialization once all the fundamentals are already in place. The raster pipeline was simple enough to teach 20 years ago, but those days are long gone, and unless you plan to hack on pre-historic games as a hobby project, it’s an extremely large API surface to learn.
dubroy.com
Been a big fan of mise for a while now, but now I discovered that it can replace make? *swoon*

mise.jdx.dev/tasks/task-c...
sources
Type: string | string[]
Files or directories that this task uses as input, if this and outputs is defined, mise will skip executing tasks where the modification time of the oldest output file is newer than the modification time of the newest source file. This is useful for tasks that are expensive to run and only need to be run when their inputs change.

The task itself will be automatically added as a source, so if you edit the definition that will also cause the task to be run.

This is also used in mise watch to know which files/directories to watch.

This can be specified with relative paths to the config file and/or with glob patterns, e.g.: src/**/*.rs. Ensure you don't go crazy with adding a ton of files in a glob though—mise has to scan each and every one to check the timestamp.
Reposted by Patrick Dubroy
chrisshank.com
You can just make things on the web collaborative by adding a single HTML attribute...
dubroy.com
TIL: WebAssembly library initialization patterns
→https://github.com/pdubroy/til/blob/main/js/2025-10-05-WebAssembly-library-initialization-patterns.md
I was curious how people are shipping WebAssembly as part of JavaScript libraries. There are two main questions:

How to bundle or fetch the .wasm payload, which may be relatively large.
How to deal with module instantiation, which is typically async.
In this post, I'm only concerned with #2 — how to deal with module instantion.

Async factory function as default export
Emscripten's modularized output option produces a module whose default export is an async factory function:

import MyLib from './my-lib.js';
const myLib = await MyLib();
Required async init
Another pattern I've seen is to export an async initialization function, which you're required to call before using the library. E.g., in esbuild-wasm:

import * as esbuild from 'esbuild-wasm'

await esbuild.initialize({
  wasmURL: './node_modules/esbuild-wasm/esbuild.wasm',
})
And in the Automerge unbundled example:

import * as AutomergeRepo from "https://esm.sh/@automerge/react@2.2.0/slim?bundle-deps";

await AutomergeRepo.initializeWasm(
  fetch("https://esm.sh/@automerge/automerge/dist/automerge.wasm")
);
wasm-bindgen without a bundler:

import init, { add } from './my_lib.js';
await init();
Hidden async init
PGlite has a nice variation on the "required async init" pattern. You can synchronously initialize a PGlite instance:

import { PGlite } from '@electric-sql/pglite'
const pg = new PGlite();
Internally, the constructor instantiates a Wasm module and stores the promise in the object's waitReady field. Most of the methods on the PGlite are async, and they await the promise. Top-level await
In principle, a library could use top-level await to do the initialization step internally, rather than requiring it in the application code. I'm not aware of any libraries that use this approach directly though.

It does have transitive effects on any code that imports the module:

Here’s what happens when you use top-level await in a module:

The execution of the current module is deferred until the awaited promise is resolved.
The execution of the parent module is deferred until the child module that called await, and all its siblings, export bindings.
The sibling modules, and siblings of parent modules, are able to continue executing in the same synchronous order — assuming there are no cycles or other awaited promises in the graph.
The module that called await resumes its execution after the awaited promise resolves.
The parent module and subsequent trees continue to execute in a synchronous order as long as there are no other awaited promises.
In other words, for the application code, it's as if it awaited the init function at the top of the module.

ES Module integration
Both Node and Deno now support importing Wasm modules directly:

import { add } from "./add.wasm";

console.log(add(1, 2));
My understanding is that it's equivalent to the following code:

const wasmInstance = await WebAssembly.instantiateStreaming(fetch("./add.wasm"));
const { add } = wasmInstance;
AFAIK, it has the same implications on module execution as an explicit top-level await does.

Sychronous instantiation
A final option is to synchronously instantiate the module — either using Node's readFileSync to load from a file, or by embedding the module as base64:

const bytes = Uint8Array.from(atob('AGFzbQEAAAABBwFgAn9/AX8DAgEABwcBA2FkZAAACgkBBwAgACABags='), c => c.charCodeAt(0));
const inst = new WebAssembly.Instance(new WebAssembly.Module(bytes));
console.log(inst.exports.add(2, 3)); // Prints 5
dubroy.com
Hmmm…reading a bit more about this, it seems that it’s not really recommended to use top-level await in libraries.

The explicit async init approach, like Automerge and esbuild use, seems like a good pattern though.
dubroy.com
Or maybe I need to implement tiering up…it starts interpreting your grammar, compiles it to Wasm on a worker, then jumps into the compiled version 😄
dubroy.com
Yeah good question.
dubroy.com
It is. But it's about Ohm grammars which will be compiled at runtime to Wasm. In the future when you do ohm.grammar(`G { }`) it will generate a Wasm module and then instantiate it.
dubroy.com
Interesting, thanks! This is useful…though the Ohm case is a bit different, because it's instantiating grammars that needs to become async, not the Ohm module itself.

But top-level await seems like the right pattern for other libraries which need to instantiate an Ohm grammar.
dubroy.com
Thanks, was just looking at Automerge! Can you point me to an example though? I couldn't easily find details on how they are handling it.
dubroy.com
In the new version, it will be instantiating a Wasm module, which has (or at least, historically had…) the possibility of blocking the main thread for quite a long time.
dubroy.com
JS folks: best way to deal with a public API (in an NPM package) going from sync → async by default?

1️⃣ New major version (`doXyz` becomes async; add `doXyzSync`)
2️⃣ New major version, remove `doXyz`, add `doXyzSync` and `doXyzAsync`
3️⃣ Something else?
dubroy.com
*rides 20 km*

“Ok, so I wonder why…ohhhhh”
dubroy.com
Going for a bike ride remains one of my most powerful debugging techniques.
dubroy.com
Man, I still really like a lot of the ideas behind @cuelang.org. (But I need to find a good excuse to use it properly to see how it fares in practice.)
Philosophy and principles

Types are Values

CUE does not distinguish between values and types. This is a powerful notion that allows CUE to define ultra-detailed constraints, but it also simplifies things considerably: there is no separate schema or data definition language to learn and related language constructs such as sum types, enums, and even null coalescing collapse onto a single construct.

Below is a demonstration of this concept. On the left one can see a JSON object (in CUE syntax) with some properties about the city of Moscow. The middle column shows a possible schema for any municipality. On the right one sees a mix between data and schema as is exemplary of CUE.

Data

Copy code
moscow: {
	name:    "Moscow"
	pop:     11.92M
	capital: true
}
Schema

Copy code
municipality: {
	name:    string
	pop:     int
	capital: bool
}
CUE

Copy code
largeCapital: {
	name:    string
	pop:     >5M
	capital: true
}
In general, in CUE one starts with a broad definition of a type, describing all possible instances. One then narrows down these definitions, possibly by combining constraints from different sources (departments, users), until a concrete data instance remains.
dubroy.com
oooh, this looks super useful.
A heading reading "JSON with labels and numbers", followed by the following monspace-formatted text:

[
  ["Bananas", 5],
  ["Apples", 4],
  ["Oranges", 3]
] Bar chart comparing fruit quantities across three categories. The chart shows Bananas with the highest value at 5 units (purple bar), Apples with 4 units (pink bar), and Oranges with 3 units (teal bar). The y-axis shows "Units" ranging from 0 to 5.5, and the x-axis shows "Run #". A legend on the right identifies the three fruit types with corresponding colors. Below the chart, there are checkboxes for each fruit type, and statistical comparisons showing that Oranges are 1.33 times lower than the highest value and 1.67 times lower than another reference point