Summary
On perry compile --target web, Perry's output diverges dramatically depending on whether the file: dependency path in package.json points inside or outside the project root. The same source tree, same perry 0.5.178, same engine produces two different broken builds:
package.json bloom dep |
modules found |
wasm size |
ffi imports |
WASM valid? |
runtime behavior |
"file:./vendor/bloom/" |
1 native |
216 902 B |
9 |
✅ OK |
runs but every bloom/core-wrapped call silently no-ops (see below) |
"file:../engine/" |
10 native |
413 500 B |
140 |
❌ FAIL |
WebAssembly.compile(): Compiling function #687 failed: expected 0 elements on the stack for fallthru, found 130 |
Real-world impact: Bloom Jump (the game) uses the first path in CI → ships to https://bloomengine.dev/jump/ as a white screen, because runGame(callback)'s bloom_run_game FFI import was stripped.
Repro
Project layout:
src/main.ts — imports runGame, initWindow, loadSound, … from "bloom/core", "bloom/audio", etc., plus declares some bloom_* functions directly (declare function bloom_load_texture(...)).
package.json — has one dependency: "bloom": "file:<path>".
perry.toml — standard project config.
Compile the identical src/main.ts twice, only changing package.json's bloom dep path:
A. "bloom": "file:./vendor/bloom/" (engine cloned/symlinked into vendor/bloom/ inside the project)
$ perry compile --target web src/main.ts -o out
Found 1 module(s): 1 native, 0 JavaScript
Generating WebAssembly...
WASM output: out.html (715.7 KB)
$ wasm-tools dump out.wasm | grep -cE 'module: "ffi"'
9
The only ffi imports emitted are the 9 declare function bloom_*(...) in main.ts. Every FFI call routed through the bloom/core (and sibling) module's re-exports (runGame → bloom_run_game, initWindow → bloom_init_window, loadSound → bloom_load_sound, getPlatform → bloom_get_platform, …) is stripped. WASM compiles and runs, but the affected calls silently do nothing — in the jump game, runGame's web branch never registers a frame callback, so the canvas stays blank and the game never starts.
B. "bloom": "file:../engine/" (engine living outside the project)
$ perry compile --target web src/main.ts -o out
Found 10 module(s): 10 native, 0 JavaScript
Generating WebAssembly...
WASM output: out.html (1232.2 KB)
$ wasm-tools dump out.wasm | grep -cE 'module: "ffi"'
140
$ node -e 'WebAssembly.compile(require("fs").readFileSync("out.wasm")).catch(e => console.log(e.message))'
WebAssembly.compile(): Compiling function #687 failed: expected 0 elements on the stack for fallthru, found 130 @+408287
Perry now discovers all 10 bloom submodules and emits 140 ffi imports — correct. But the resulting WASM function #687 has a stack-fallthru validation error, so the browser refuses to instantiate the module. (The exact "found N" count varies with engine state: 130 with my local engine checkout, 103 with a fresh origin/main clone.)
Expected
Both paths should produce the same valid WASM — either both with the full 140 imports, or both with the subset that survives DCE, but with consistent DCE such that the game's call graph isn't partially severed.
Environment
I can hand over the full compiled out.wasm files from both A and B if that helps diagnose — just point me at where to drop them.
Workaround
None from the jump side that I've found:
- Adding
declare function bloom_run_game(callback: number): void; + bloom_run_game(cb as any) directly in main.ts does get the import emitted under path A — but only under path B (which then hits the #687 codegen bug).
- Rewriting every
bloom/core call as a direct declare function in main.ts would require ~30 declarations and still only fixes path A.
The fix needs to land in Perry's web-target codegen.
Summary
On
perry compile --target web, Perry's output diverges dramatically depending on whether thefile:dependency path inpackage.jsonpoints inside or outside the project root. The same source tree, sameperry 0.5.178, same engine produces two different broken builds:package.jsonbloomdepffiimports"file:./vendor/bloom/"bloom/core-wrapped call silently no-ops (see below)"file:../engine/"WebAssembly.compile(): Compiling function #687 failed: expected 0 elements on the stack for fallthru, found 130Real-world impact: Bloom Jump (the game) uses the first path in CI → ships to https://bloomengine.dev/jump/ as a white screen, because
runGame(callback)'sbloom_run_gameFFI import was stripped.Repro
Project layout:
src/main.ts— importsrunGame,initWindow,loadSound, … from"bloom/core","bloom/audio", etc., plus declares somebloom_*functions directly (declare function bloom_load_texture(...)).package.json— has one dependency:"bloom": "file:<path>".perry.toml— standard project config.Compile the identical
src/main.tstwice, only changingpackage.json'sbloomdep path:A.
"bloom": "file:./vendor/bloom/"(engine cloned/symlinked intovendor/bloom/inside the project)The only
ffiimports emitted are the 9declare function bloom_*(...)inmain.ts. Every FFI call routed through thebloom/core(and sibling) module's re-exports (runGame → bloom_run_game,initWindow → bloom_init_window,loadSound → bloom_load_sound,getPlatform → bloom_get_platform, …) is stripped. WASM compiles and runs, but the affected calls silently do nothing — in the jump game,runGame's web branch never registers a frame callback, so the canvas stays blank and the game never starts.B.
"bloom": "file:../engine/"(engine living outside the project)Perry now discovers all 10 bloom submodules and emits 140
ffiimports — correct. But the resulting WASM function#687has a stack-fallthru validation error, so the browser refuses to instantiate the module. (The exact "found N" count varies with engine state: 130 with my local engine checkout, 103 with a fresh origin/main clone.)Expected
Both paths should produce the same valid WASM — either both with the full 140 imports, or both with the subset that survives DCE, but with consistent DCE such that the game's call graph isn't partially severed.
Environment
perry 0.5.178(testedperry-macos-aarch64,perry-macos-x86_64; CI usesperry-linux-x86_64, same symptoms per the deployed artifact)origin/mainfresh clone and a local checkout — both trigger B's codegen bug)I can hand over the full compiled
out.wasmfiles from both A and B if that helps diagnose — just point me at where to drop them.Workaround
None from the jump side that I've found:
declare function bloom_run_game(callback: number): void;+bloom_run_game(cb as any)directly inmain.tsdoes get the import emitted under path A — but only under path B (which then hits the #687 codegen bug).bloom/corecall as a directdeclare functioninmain.tswould require ~30 declarations and still only fixes path A.The fix needs to land in Perry's web-target codegen.