Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
84 commits
Select commit Hold shift + click to select a range
a08a7d8
fix: correct memory operations in AOT compilers
teamchong Jan 12, 2026
3a30b5b
feat: add AOT test files, benchmarks, and generated types
teamchong Jan 12, 2026
48d4cf9
Run pong inside the type checker at ~4fps, one evaluation per frame
teamchong Jul 26, 2026
f5a6985
Support calls, and stop naming block parameters after stack expressions
teamchong Jul 26, 2026
e4474cd
Specialise the operations that are actually cheaper, and measure the …
teamchong Jul 26, 2026
c2d3a4b
Pixel graphics: a 64x48 framebuffer game, one evaluation per frame
teamchong Jul 26, 2026
b053057
Only pass the locals a block actually reads
teamchong Jul 26, 2026
333aade
Cap block depth: 4x on the games, 9.5x on the worst case
teamchong Jul 26, 2026
866c8d8
Sweep the depth cap: 12 -> 6, pixel pong reaches 16 fps
teamchong Jul 26, 2026
adc779f
Write the game for the machine it runs on: 25 fps at 64x48
teamchong Jul 26, 2026
aaa6aed
Record the game as a GIF, and write down what the measurements showed
teamchong Jul 26, 2026
a48ab8d
Unrolled sprites, a trie-width knob, and two negative results
teamchong Jul 26, 2026
37388f0
compile long blocks as pipelines of alias applications, keep nesting …
teamchong Jul 26, 2026
bfe3475
record why fusing the byte store into the trie walk is slower
teamchong Jul 26, 2026
9ed5c07
memory: buffer stores in one trie branch
teamchong Jul 27, 2026
0680a59
arithmetic: specialise the constant cases, and stop naming shallow re…
teamchong Jul 27, 2026
7d19ca3
Delete the pong and gfx detour; point everything at DOOM
teamchong Jul 27, 2026
a0c47ff
Remove everything that is not DOOM; teach the CFG compiler 64-bit
teamchong Jul 27, 2026
28a1269
call_indirect: dispatch on the table slot; DOOM compiles end to end
teamchong Jul 27, 2026
e442079
Restore everything I should not have deleted
teamchong Jul 27, 2026
a4d3554
suspendable calls: a frame stack, so a callee can run out of fuel
teamchong Jul 27, 2026
e2de01f
frames carry whether a return value belongs on their stack, and runs …
teamchong Jul 27, 2026
361c41e
memory.grow has to be part of the state, and 64-bit add/sub/mul were …
teamchong Jul 27, 2026
9fa3b28
the fuel climbs back, and a branch known to be too big is not asked f…
teamchong Jul 27, 2026
f1b1c34
a left shift keeps the wrong end of the string
teamchong Jul 27, 2026
24c3267
a worn-out compiler gets replaced, and can be replaced on a schedule
teamchong Jul 27, 2026
82f08f5
learn how long a compiler lasts instead of asking the caller
teamchong Jul 27, 2026
cb73cb4
stop replacing a compiler that is not worn out, and name what came back
teamchong Jul 27, 2026
df03a70
report where a state stops being a state, not what tends to go wrong
teamchong Jul 27, 2026
c65a503
Raise default chunk fuel to 2048
teamchong Jul 27, 2026
1dde122
Fix i64 division depth blowup and a retry loop that never backed off
teamchong Jul 27, 2026
49fdfbc
Read unwritten memory through to $InitialMemory
teamchong Jul 27, 2026
698f4a8
Multiply i64 magnitudes so FixedMul stops returning never
teamchong Jul 27, 2026
83a8ecd
doom: a 16x16 multiply so FixedMul fits in the depth budget
teamchong Jul 27, 2026
1bc0639
doom: render the checker's screen as a colour PNG
teamchong Jul 27, 2026
13289e5
doom: stream the checker's screen to a browser
teamchong Jul 27, 2026
6d18b7d
doom: show fps in the viewer
teamchong Jul 28, 2026
1bffedf
cfg: let the compiler lifetime climb back
teamchong Jul 28, 2026
1c0ec07
cfg: run at the fuel the checker can actually take
teamchong Jul 28, 2026
df8f7a8
cfg: time the trie split and the forcing read separately
teamchong Jul 28, 2026
b68c5c6
Add i32 with a nibble table instead of 32 recursive steps
teamchong Jul 28, 2026
bc53ffe
Subtract i32 with a nibble table too
teamchong Jul 28, 2026
89bd7aa
Read initial memory through a keyed map, not a trie walk
teamchong Jul 28, 2026
08e5ec9
aot_cfg: check node arity without inferring its children
teamchong Jul 28, 2026
8395380
aot_cfg: match the discarded address bits as literals
teamchong Jul 28, 2026
c53326f
Doom driver: atomic checkpoints, correct type-level arithmetic
teamchong Jul 31, 2026
08b82aa
Memory is an overlay, so a store costs what it wrote
teamchong Jul 31, 2026
0ba571e
Skip the half of a multiply that is zero
teamchong Jul 31, 2026
fa6b34b
Divide 32-bit through the single-pass loop 64-bit already uses
teamchong Jul 31, 2026
d63e950
aot-cfg: stop paying the multiplier's worst case in FixedMul
teamchong Jul 31, 2026
10a283e
drive: bisect the fuel cliff instead of doubling past it
teamchong Jul 31, 2026
30f1684
drive: back off strictly below the fuel that failed
teamchong Jul 31, 2026
754d7b0
stream: rename the page's status variable off window.status
teamchong Jul 31, 2026
db69da1
Break the 1000-instruction ceiling: resume a suspend in types
teamchong Jul 31, 2026
be9f013
Resume without zeroing sbrk: the reset corrupted the allocator
teamchong Jul 31, 2026
b7bd5c0
Stop clearing a "spent" frame buffer on resume: it was live memory
teamchong Jul 31, 2026
b7d3037
Put sbrk's reset back: a spent heap has to be re-initialized, not reused
teamchong Jul 31, 2026
a42ac6f
Turn the trampoline off: it runs a different block than the host does
teamchong Jul 31, 2026
6de412f
A cold run was handing doom an empty address space
teamchong Jul 31, 2026
c115fc9
Parse $InitialMemory for a cold run instead of naming it
teamchong Jul 31, 2026
f27cb2d
Start a cold run on $Absent again
teamchong Jul 31, 2026
bc9a722
Run one segment per chunk: the second one dropped the first's writes
teamchong Jul 31, 2026
8561bec
Carry the page count into a new frame
teamchong Jul 31, 2026
4ac50e8
Divide i32 signed by dividing the magnitudes
teamchong Aug 1, 2026
a3c3e51
Run the real linuxdoom-1.10, and let keys reach it
teamchong Aug 2, 2026
2417034
Let the browser's keys reach doom
teamchong Aug 2, 2026
e3127a7
Latch keypresses so a tap outlives one chunk
teamchong Aug 2, 2026
3a3c5ee
stream: show what the game actually has, not what the browser sent
teamchong Aug 2, 2026
37e6b20
doom/stream: retire press counts once the game has them
teamchong Aug 2, 2026
816a64a
playground: check in a post-init checkpoint so play starts painted
teamchong Aug 2, 2026
8061add
doom: seed the checked-in checkpoint from a cold start, add pnpm run …
teamchong Aug 2, 2026
7540d70
doom: make restart kill the play loop, not just its driver
teamchong Aug 2, 2026
9bea95d
playground: rename play to start, keep live state out of /tmp
teamchong Aug 2, 2026
8a3a8a5
Give the game a release frame between repeated keypresses
teamchong Aug 2, 2026
6dbbeff
doom: forward the whole keyboard, not ten keys
teamchong Aug 2, 2026
386bcbc
doom: make the queued badge a state, not an event
teamchong Aug 2, 2026
35319ac
benchmark: call node from PATH
teamchong Aug 2, 2026
83e9312
Accept newer wabt/clang instead of one exact version
teamchong Aug 2, 2026
062a827
Add root start/restart scripts forwarding to playground
teamchong Aug 3, 2026
0c3e2ef
Raise default fuel to 655360; trie memory removed the 1280 ceiling
teamchong Aug 3, 2026
a259c56
Suspend with the raw memory overlay instead of flushing per chunk
teamchong Aug 3, 2026
3fab178
Bump tsgo to 7.1.0-dev.20260802.1
teamchong Aug 3, 2026
57159eb
Mark retry paths in the drive.ts time split
teamchong Aug 3, 2026
46368bc
Send key presses as edges and let a newer key preempt an unacked one
teamchong Aug 4, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,14 @@ packages/playground/evaluate/results*
packages/playground/evaluate/stats
tsc-output.cpuprofile
packages/playground/evaluate/restart*
trace/
trace/
# per-chunk scratch files written by the type-level runtime driver
packages/playground/cfg/chunk-*.ts
packages/playground/cfg/failing-chunk.ts
packages/playground/cfg/failing-output.txt
# generated by --aot-cfg; rebuild with `cargo run -- --aot-cfg <module>.wasm`
packages/playground/cfg/*.cfg.ts
packages/playground/**/*.cfg.ts
packages/playground/gfx/frames/
# live doom checkpoint; the clean-start seed is doom/first-frame.json.gz and stays tracked
packages/playground/doom/.live/
2 changes: 2 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
@pxpipe pin measure first, evaluate second, and make final decisions purely from the data
@pxpipe pin the only goal is getting Doom playable strictly in TypeScript 7 types—no non-TypeScript shortcuts
1 change: 1 addition & 0 deletions CLAUDE.md
26 changes: 25 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ edition = "2021"
[dependencies]
indexmap = "2.1.0"
wast = "70.0.0"
wasmparser = "0.121.0"
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ In case you're here just heckle, here's some nightmare fuel for you:
- [Add](./packages/ts-type-math/add.ts): this is the cornerstone of the arithmetic in the application. Upon seeing this for the first time, some people have said "I see TypeScript types on the screen, but I don't quite see where _addition_ is taking place". Fair. It's there, though.
- [Divide](./packages/ts-type-math/divide.ts) was _by far_ the hardest instruction to implement to get it up to 64 bits. @teamchong gets all the credit for this one. To get there, we had to build it up one bit at a time to see where it hit performance limitations with [the division playground](./packages/ts-type-math/divide-playground.ts).
- some of the [binary helpers](./packages/ts-type-math/binary.ts) are sorta artsy to look at fullscreen.
- [final-doom-pun-intended](./packages/playground/final-doom-pun-intended/) contains the files where the finish line was crossed. While it was chugging away, we made mockups that demonstrated what it would look like when complete. As it often goes, the mockups ended up being the real thing in the end, haha.
- [final-doom-pun-intended](./packages/playground/final-doom-pun-intended/) contains the files where the finish line was crossed. While it was chugging away, we made mockups that demonstrated what it would look like when complete. As it often goes, the mockups ended up being the real thing in the end, haha. The completed 15,895,321-instruction snapshot evaluates in 1.55 seconds on average: 0.65 FPS (completed snapshot evaluations per second) and 10,276,353 instructions per second across three cold runs with the benchmark in `packages/playground/benchmark.ts`.
- [david-blass-incredibleness](./packages/playground/david-blass-incredibleness.ts), so-named because of a very important bit of insight that @ssalbdivad provided which caused an explosion of progress, is the main dashboard for development of individual tests.
- [conformance-tests](./packages/conformance-tests/) for the full loop (i.e. from C to WASM to types) as well as from straight WASM to types can be found here. They're called conformance tests because the same inputs are tested agains the JavaScript built-in WebAssembly runtime, which was the way I made sure this engine's behavior is correct.
- Many of the instructions were surprisingly straightforward but some of the [control flow instructions](./packages/wasm-to-typescript-types/instructions/control-flow.ts) have some meat on the bone.
Expand Down
45 changes: 45 additions & 0 deletions benchmark.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
#!/bin/bash
# Benchmark script - runs Conway's Game of Life for a fixed number of instructions
# Usage: ./benchmark.sh [instructions]
# Default: 1000 instructions

INSTRUCTIONS=${1:-1000}

# Backup config
cp packages/playground/evaluate/config.ts packages/playground/evaluate/config.ts.bak

# Modify config for benchmark
sed -i '' "s/stopAt: Infinity/stopAt: $INSTRUCTIONS/" packages/playground/evaluate/config.ts
sed -i '' "s/shouldComputeFullStats: false/shouldComputeFullStats: true/" packages/playground/evaluate/config.ts

echo "=== TypeScript Types WASM Runtime Benchmark ==="
echo "Running $INSTRUCTIONS instructions..."
echo ""

START_TIME=$(date +%s)

# Run the eval
node \
--stack-size=8192 \
--max-old-space-size=16384 \
--max-semi-space-size=16384 \
--import tsx/esm \
packages/playground/evaluate/run.ts 2>&1

END_TIME=$(date +%s)
DURATION=$((END_TIME - START_TIME))

echo ""
echo "=== Results ==="
echo "Total time: ${DURATION}s"
echo "Instructions: $INSTRUCTIONS"
if [ $DURATION -gt 0 ]; then
IPS=$((INSTRUCTIONS / DURATION))
echo "IPS (instructions/sec): $IPS"
fi

# Restore config
mv packages/playground/evaluate/config.ts.bak packages/playground/evaluate/config.ts

echo ""
echo "Baseline captured. Run again after changes to compare."
2 changes: 1 addition & 1 deletion debug.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import ts from 'typescript';
import ts from 'typescript-legacy';
import path from 'path';

// Define the path to your specific TypeScript file you want to analyze
Expand Down
Binary file added docs/pixel-pong.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions mise.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[tools]
node = "22"
15 changes: 10 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
},
"scripts": {
"browser": "pnpm --filter playground run browser",
"start": "pnpm --filter playground run start",
"restart": "pnpm --filter playground run restart",
"clean": "rm -f .tsbuildinfo && rm -rf packages/playground/evaluate/results && rm -rf target && rm -rf trace",
"preinstall": "npx only-allow pnpm",
"eval": "pnpm --filter playground --silent run eval",
Expand All @@ -18,9 +20,12 @@
"test:create-new": "pnpm --filter conformance-tests run test:create-new",
"test:rust": "cargo test",
"test": "pnpm run test:rust && CI=true pnpm run test:math && CI=true pnpm run test:conformance && pnpm run build",
"build": "node --stack-size=8100 --max-old-space-size=8192 --max-semi-space-size=8192 ./node_modules/typescript/lib/tsc",
"build": "tsc",
"build:trace": "pnpm run build --generateTrace trace",
"build:force": "MICHIGAN_TYPESCRIPT=true time tsc --incremental false --tsBuildInfoFile null --generateCpuProfile tsc-output.cpuprofile"
"build:force": "time tsc --incremental false --tsBuildInfoFile null --pprofDir tsc-profile",
"doom": "cargo run --quiet --release -- --aot-cfg packages/playground/doom/doom.wasm",
"doom:verify": "cargo run --quiet --release -- --aot-cfg packages/playground/doom/doom.wasm && WASM=packages/playground/doom/doom.wasm MODULE=packages/playground/doom/doom.cfg.ts node --stack-size=16384 --max-old-space-size=8192 --import tsx/esm packages/playground/cfg/verify.ts",
"conform": "cargo build --quiet && node --stack-size=16384 --max-old-space-size=8192 --import tsx/esm packages/playground/cfg/conform.ts"
},
"dependencies": {
"@biomejs/biome": "^1.9.3",
Expand All @@ -33,13 +38,13 @@
"ts-node": "^10.9.2",
"tsx": "^4.19.1",
"type-testing": "^0.2.0",
"typescript": "5.6.3",
"typescript": "7.1.0-dev.20260802.1",
"typescript-legacy": "npm:typescript@5.6.3",
"vitest": "^2.1.3"
},
"pnpm": {
"patchedDependencies": {
"type-testing@0.2.0": "patches/type-testing@0.2.0.patch",
"typescript@5.6.3": "patches/typescript@5.6.3.patch"
"type-testing@0.2.0": "patches/type-testing@0.2.0.patch"
}
},
"packageManager": "pnpm@9.6.0+sha512.38dc6fba8dba35b39340b9700112c2fe1e12f10b17134715a4aa98ccf7bb035e76fd981cf0bb384dfa98f8d6af5481c2bef2f4266a24bfa20c34eb7147ce0b5e"
Expand Down
10 changes: 10 additions & 0 deletions packages/conformance-tests/benchmark-aot-only.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/**
* AOT-only benchmark
*/
import type { entry } from './from-wat/loop.aot'

type R1 = entry<[2]> // 16
type R2 = entry<[3]> // 24
type R3 = entry<[5]> // 40

export type Results = [R1, R2, R3]
37 changes: 37 additions & 0 deletions packages/conformance-tests/benchmark-comparison.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/**
* Apples-to-apples benchmark: AOT vs Interpreter
*
* Both compute the same thing: loop(2) = 2 * 2 * 2 * 2 = 16
*
* Run: npx tsc --noEmit --extendedDiagnostics benchmark-comparison.ts
*/

// === INTERPRETER VERSION ===
// Uses the full wasm-to-typescript-types runtime
import type { entry as InterpreterEntry } from './from-wat/loop'

type InterpreterResult = InterpreterEntry<[2]>
// Forces full VM execution: parsing, stack simulation, memory, etc.


// === AOT VERSION ===
// Direct type computation, no VM overhead
import type { entry as AotEntry } from './from-wat/loop.aot'

type AotResult = AotEntry<[2]>
// Direct: $loop_0<$p0, $p0, 0, $p0> recursively computes result


// Verify both produce same result
type _CheckSame = InterpreterResult extends AotResult
? AotResult extends InterpreterResult
? true
: false
: false

// Export to force evaluation
export type Results = {
interpreter: InterpreterResult // Should be 16
aot: AotResult // Should be 16
same: _CheckSame // Should be true
}
21 changes: 21 additions & 0 deletions packages/conformance-tests/benchmark-doom.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/**
* Doom benchmark: AOT vs Interpreter
*
* Run AOT only:
* npx tsc --noEmit --extendedDiagnostics benchmark-doom-aot.ts
*
* Run Interpreter only:
* npx tsc --noEmit --extendedDiagnostics benchmark-doom-interpreter.ts
*
* Compare the "Types" and "Check time" metrics
*/

// === AOT VERSION ===
// Direct type expressions, no VM simulation
import type { entry as AotEntry } from '../playground/doom/doom.aot'

type AotResult = AotEntry<[]>

export type Results = {
aot: AotResult
}
6 changes: 6 additions & 0 deletions packages/conformance-tests/from-c/aot-only.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import type { entry } from './c-add.aot';

type R1 = entry<[5, 3]>;
type R2 = entry<[100, 200]>;

export type Results = [R1, R2];
28 changes: 28 additions & 0 deletions packages/conformance-tests/from-c/benchmark-aot.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/**
* Benchmark: AOT vs Interpreter
*
* This file is used to compare TypeScript instantiation counts between
* the interpreter-based approach and the AOT-compiled approach.
*
* Run with: pnpm build:trace
* Then check the trace file for instantiation counts.
*/

import type { entry as entryInterpreter } from './c-add'
import type { entry as entryAot } from './c-add.aot'

// Interpreter version - requires full VM execution
type InterpreterResult1 = entryInterpreter<[5, 3]>
type InterpreterResult2 = entryInterpreter<[100, 200]>
type InterpreterResult3 = entryInterpreter<[1000, 2000]>

// AOT version - direct type computation
type AotResult1 = entryAot<[5, 3]>
type AotResult2 = entryAot<[100, 200]>
type AotResult3 = entryAot<[1000, 2000]>

// Force evaluation
export type Results = {
interpreter: [InterpreterResult1, InterpreterResult2, InterpreterResult3]
aot: [AotResult1, AotResult2, AotResult3]
}
10 changes: 10 additions & 0 deletions packages/conformance-tests/from-c/c-add.aot.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { describe, test, expectTypeOf } from "vitest"
import type { entry } from "./c-add.aot"

describe("c-add AOT", () => {
test("adds two numbers", () => {
expectTypeOf<entry<[5, 3]>>().toEqualTypeOf<8>()
expectTypeOf<entry<[0, 0]>>().toEqualTypeOf<0>()
expectTypeOf<entry<[1, 1]>>().toEqualTypeOf<2>()
})
})
15 changes: 15 additions & 0 deletions packages/conformance-tests/from-c/c-add.aot.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import type { Wasm, WasmValue, Convert } from 'ts-type-math'

type $unreachable = never

type $entry_impl<$p0 extends WasmValue, $p1 extends WasmValue> =
Wasm.I32Add<$p0, $p1>

export type entry<Args extends [number, number]> =
Convert.WasmValue.ToTSNumber<
$entry_impl<
Convert.U32Decimal.ToU32Binary<Args[0]>,
Convert.U32Decimal.ToU32Binary<Args[1]>
>,
'i32'
>
Loading