Porffor
Generated from docs/engines/porffor.md when the site was built at 7aa23e2.
Porffor (github.com/CanadaHonk/porffor) compiles JS ahead of time to C. niso’s apps run on it today: the counter, scroll and controls examples and the Svelte fixture compile, run, and produce the same DOM and render stream as on QuickJS, and 39 of the 43 JS binding tests pass. Two fail because Porffor’s WeakRef holds its target strongly and its FinalizationRegistry never calls back (the lifetime tests); that is what blocks shipping, together with the ones listed under What blocks shipping. The other two, the WebSocket tests, cannot run: their scripts carry a server port chosen at run time, and Porffor runs only scripts it was built with. The device apps run on it too: radio on Android and the iOS simulator, and ihatemyphone’s panel and setup pages headless (see Device apps).
Tested with Porffor alpha 22 (a081ebd, 2026-11-05, the newest release, from
curl -fsSL https://porffor.dev/install.sh | sh) on Linux x86_64 with clang 18.
How the binding works
Section titled “How the binding works”Porffor has no embedding API and no eval at runtime, so the engine is part of the program Porffor compiles:
crates/niso-porffor/build.rswrites one JS program:bindings/js/porffor/prelude.js, the glue with its generated part, and every script the program may run, each acaseofeval("<source>"). Porffor compiles a literal eval ahead of time, completion value included.porf cturns the program into one C file, which cc compiles with-Dmain=qe_porf_main.- The prelude includes
bindings/js/porffor/qe_porffor.cas file-scope C (Porffor.c), so the engine reaches the runtime’s static functions: values (jsval), the allocator, arrays and strings,porf_call_dynamicto call a JS function from C, and the setjmp/longjmp exceptions (porf_try_depth,porf_try_ensure,porf_exception). - JS calls C through
Porffor.cstatements in prelude functions, which name JS locals by their bare names (${x}interpolation rendersundefined). Every host function is a JS shim(...args) => __niso_host_call(i, args)over the binding core’s host table. A host function that throws makes the glue’s error andporf_throws it after the C frame has cleaned up. - C calls JS (hooks, the script table, ToNumber and ToString) with
porf_call_dynamicinside a try frame, so no JS exception unwinds through the DOM’s Rust frames. - The GC scans the C stack conservatively up to
porf_c_stack_top. Porffor’smainsets it for the top level only, so each entry from C sets it to a frame above the one that holds JS values. - Microtasks are Porffor’s promise jobs (
p_builtins___Porffor_promise_runJobs);qe_gcrunsporf_gc_collect. The glue now definesqueueMicrotaskitself (Porffor’s runtime only provides it when the outer program names it). - Porffor treats a program naming
fetch,HeadersorResponse, even a binding of its own, as using its own fetch, and compiles in its network runtime (libuv, OpenSSL, ada and uWebSockets), which niso does not link (porffor-own-fetch-pulls-network-runtime). The glue binds its fetch, Headers and Response under other names and defines the globals from them. A script that names them itself would get Porffor’s fetch rather than niso’s, sobuild.rsrefuses a program whose C includes that runtime.URLandURLSearchParamspull in ada the same way, and a top-level class of either name is replaced by Porffor’s own (porffor-own-url-class-replaced), so the generator declares niso’s asNisoURLandNisoURLSearchParamsandXgives them their interface names. A top-levelclass MessageChannelpulls in libuv the same way (porffor-own-messagechannel-class-replaced), so niso’s isNisoMessageChannel. - Module scripts do not run: a module loads its graph at run time, so
qe_eval_modulereports one as an error. - Bytes cross as arrays of numbers:
qe_bytescopies a buffer or view the prelude reads into one, andqe_ret_bytesandqe_push_byteshave the prelude make an ArrayBuffer from one. qe_evalfinds a script byniso_js_script_hash(FNV-1a of its bytes) and fails for a script the program was not built with.- The runtime is one global heap, so a process runs one engine at a time:
after
niso_js_free, a new engine starts the glue again on the same heap (__niso_start), but a secondniso_js_newwhile one is alive fails with “niso: Porffor runs one engine at a time”. - Porffor’s runtime defines its own
navigatorandconsole, and a program’s own global of either name is ignored (porffor-navigator-console-global-ignored), so the glue copies niso’s members onto Porffor’s objects (__niso_onto).
The Rust side is niso-quickjs’s own Js API and niso-run source, built against the
Porffor engine. Without NISO_PORFFOR_SCRIPTS the crate builds without an engine,
so workspace builds do not need Porffor.
# niso-run on Porffor, built with the app it runsNISO_PORFFOR_SCRIPTS=examples/dist/counter.js cargo build --release -p niso-porffortarget/release/niso-run-porffor --dump --tap 60,140 examples/dist/counter.js
# niso-quickjs's dom, lifetime and streaming tests on Porffor: record the# scripts they evaluate, build them in, and run each test in its own processmkdir -p target/porffor/scriptsNISO_JS_RECORD=$PWD/target/porffor/scripts cargo test --release -p niso-quickjs --test dom --test lifetime --test streamingNISO_PORFFOR_SCRIPTS=target/porffor/scripts cargo test --release -p niso-porffor --no-runtarget/release/deps/dom-* --exact class_list_replace
# everything belownode tests/porffor/run.mjsResults
Section titled “Results”node tests/porffor/run.mjs, on a 4-core cloud machine (x86_64, 16 GB), 2026-11-08. Each app is
built into its own niso-run-porffor and run against niso-run (QuickJS-ng) with
--time --repeat 50 --render FILE --dump and the taps below. Startup is from
process start to the first rendered frame. It splits into engine init (DOM,
fonts, engine and glue, mostly not JS), scripts (the app and its timers) and the
first frame. Frame is one tap, the tasks it makes due, and a rendered frame.
The render stream is compared as bytes, and with node handles replaced by tree
paths (handle values depend on when the GC lets the DOM free nodes). “Engine +
app code” is the text and data of QuickJS plus the binding core, against
Porffor’s compiled program (runtime, glue and app) plus the binding core. The
QuickJS binary still loads the app’s JS source at runtime (163 to 175 KB for
these apps).
Every one of the 21 runs of an app must exit cleanly and leave the same DOM and render stream for “runs” to say yes.
Taps: counter 60,140 (the button); scroll 100,300 (a row, no listener); controls 88,289 (Disconnect); svelte none (it clicks its own button).
| app | compiles | runs | DOM matches | render stream matches | compile (porf + cc) | engine + app code, Porffor / QuickJS | stripped binary, Porffor / QuickJS | startup ms, Porffor / QuickJS | engine init ms, Porffor / QuickJS | scripts ms, Porffor / QuickJS | frame ms, Porffor / QuickJS | max RSS MB, Porffor / QuickJS | nodes alive after GC, Porffor / QuickJS |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| baseline | yes | yes | yes | yes | 2.5 s + 66.3 s | 4.10 MB / 1.69 MB | 26.92 MB / 24.55 MB | 47.16 / 63.91 | 46.62 / 63.42 | 0.08 / 0.07 | - / - | 21.2 / 21.5 | 5 / 5 |
| counter | yes | yes | yes | yes | 2.7 s + 92.4 s | 5.25 MB / 1.69 MB | 28.06 MB / 24.55 MB | 43.20 / 74.82 | 39.24 / 65.03 | 1.50 / 7.58 | 0.69 / 0.75 | 26.7 / 24.7 | 145 / 145 |
| scroll | yes | yes | yes | yes | 2.9 s + 94.6 s | 5.35 MB / 1.69 MB | 28.15 MB / 24.55 MB | 53.23 / 75.15 | 46.19 / 63.98 | 5.95 / 10.95 | 0.15 / 0.08 | 25.7 / 24.4 | 110 / 109 |
| controls | yes | yes | yes | yes | 2.8 s + 93.0 s | 5.22 MB / 1.69 MB | 28.03 MB / 24.55 MB | 29.98 / 58.24 | 25.33 / 47.90 | 1.63 / 8.21 | 0.10 / 0.10 | 25.8 / 24.4 | 133 / 132 |
| svelte | yes | yes | yes | yes, modulo node handles | 2.9 s + 93.4 s | 5.39 MB / 1.69 MB | 28.19 MB / 24.55 MB | 58.11 / 81.47 | 49.66 / 69.24 | 2.26 / 8.62 | - / - | 25.9 / 24.7 | 61 / 60 |
Medians of 21 runs; frame time is the mean over 50 rounds of the taps (tap, due tasks, render frame).
What the numbers say:
- Running the app is where AOT pays: scripts take 1 to 5 ms on Porffor and 7 to 11 ms on QuickJS, which parses and interprets 163 to 175 KB of Svelte first. Startup as a whole is dominated by engine init (font loading and DOM setup, about 40 to 50 ms and noisy on this machine; the controls row shows the noise), so the end-to-end gain is smaller.
- Frames are layout and rendering; the JS in a tap is a small part. Porffor is within noise of QuickJS (0.70 against 0.73 ms for a counter tap), not faster.
- Each app costs about 45 s to build (porf ~1.5 s, clang ~40 s at -O3), against none for QuickJS. The dev loop stays on QuickJS.
- Porffor’s code is larger than QuickJS’s interpreter: about 3.1 to 3.3 MB for the runtime, glue and a Svelte app, against 1.24 MB for QuickJS plus the binding core (which also needs the app’s source). Whole stripped binaries are 1.7 to 1.9 MB larger.
- Nodes alive after a full GC differ by one per app here, because these pages detach almost nothing. The lifetime tests show the general case: nothing JS ever wrapped is freed.
Binding tests
Section titled “Binding tests”| test | Porffor |
|---|---|
| dom::aria_attributes_reflect_and_internals_hold_their_own | pass |
| dom::attr_nodes | pass |
| dom::base_element_sets_the_base_url | pass |
| dom::class_list_replace | pass |
| dom::customized_built_in_elements | pass |
| dom::document_replacements_are_validated | pass |
| dom::elements_by_namespace | pass |
| dom::eval_string_returns_the_completion_value | pass |
| dom::fixed_boxes_stay_put_when_the_viewport_scrolls | pass |
| dom::forms_own_their_controls_and_submit | pass |
| dom::html_collections_have_named_items | pass |
| dom::html_element_interfaces | pass |
| dom::inputs_keep_checkedness | pass |
| dom::labels_and_metas_reflect_for_and_http_equiv | pass |
| dom::lone_surrogates_become_replacement_characters | pass |
| dom::move_before | pass |
| dom::namespace_lookups | pass |
| dom::native_buttons_click_when_the_host_presses_them | pass |
| dom::native_controls_report_user_changes_as_trusted_events | pass |
| dom::native_scrolls_fire_scroll_events_in_the_rendering_update | pass |
| dom::other_documents | pass |
| dom::parsed_doctype | pass |
| dom::selects_have_one_selected_option | pass |
| dom::tables_keep_sections_and_rows_in_place | pass |
| dom::taps_click_through_svg_icons_but_never_disabled_controls | pass |
| dom::taps_fire_trusted_mouse_events_at_the_hit_element | pass |
| dom::text_fields_report_typing_and_focus_as_events | pass |
| dom::timers_and_animation_frames_run_in_the_dom_event_loop | pass |
| dom::token_list_replace_checks_empty_tokens_first | pass |
| dom::token_lists_are_iterable | pass |
| dom::url_search_params_iterate_live | pass |
| dom::variadic_mutations_do_not_leak | pass |
| lifetime::detached_nodes_live_while_js_holds_them | FAIL: assertion left == right failed: the detached tree is freed once JS drops it |
| lifetime::errors_are_reported | pass |
| lifetime::expandos_survive_while_connected | FAIL: the removed node’s wrapper is released |
| lifetime::identity_and_listeners | pass |
| streaming::canvas_pixels_round_trip | pass |
| streaming::dataset_maps_data_attributes | pass |
| streaming::host_pointers_become_pointer_events_with_capture | pass |
| streaming::keys_and_wheels_reach_the_page | pass |
| streaming::media_queries_match_the_viewport_and_report_changes | pass |
| streaming::web_socket_failures | FAIL: script threw (not compiled ahead of time) |
| streaming::web_sockets_exchange_messages_and_close | FAIL: script threw (not compiled ahead of time) |
The 39 that pass are every DOM test, the lifetime tests that do not need a
wrapper to be collected, and the streaming tests that do not open a
WebSocket. Each test runs in its own process (one engine at a time); the
scripts they evaluate are recorded from the QuickJS run with
NISO_JS_RECORD. The WebSocket tests put their echo server’s port, picked
when the test runs, into their scripts, so the build never has the script the
run evaluates.
On Linux x86_64 (alpha 22, 2026-11-09, tests/porffor/run.mjs --runs 3), 45
of 51 binding tests pass: the 4 above fail, and two modules:: tests:
modules::with_a_socket_open_a_module_answering_each_read_neither_waits_for_the_timer_nor_spins, for the WebSocket tests’ reason: its script carries the port of a server the test starts.modules::a_module_event_runs_between_a_chain_of_tasks(a chain of 3000 IndexedDB gets with a module event in between), with no message; its cause isn’t known yet.
Blob, AbortController and crypto.subtle
Section titled “Blob, AbortController and crypto.subtle”On Porffor alpha 22 (macOS arm64, debug build, 2026-11-09) the binding tests
still pass 40 of 44, the same 4 failing as on main. A probe script built into
niso-run-porffor and run with --url http://localhost/ prints the same as on
QuickJS. It constructs a Blob and a File, reads blob.text(), creates a blob
URL, aborts an AbortController (the listener runs, and the reason is an
AbortError), calls AbortSignal.timeout, getRandomValues and randomUUID,
and calls crypto.subtle‘s digest and an ECDSA generateKey with a pkcs8
exportKey (138 bytes, the browsers’ encoding). After the move to AWS-LC,
a second probe (an RSASSA-PKCS1-v1_5 key’s generation, signature,
verification and JWK export, and AES-GCM with a 128-bit IV and a 64-bit tag)
prints the same on both engines, and the binding tests are still 40 of 44.
After the merge of main’s native modules and workers, 42 of 46 pass, the same
4 failing. That count records the module tests’ scripts too:
tests/porffor/run.mjs records only the dom, lifetime and streaming tests, so
it reports modules::calls_convert_values and
modules::promises_and_events_are_tasks as failing.
Porffor has its own WebCrypto on OpenSSL. A binding named CryptoKey, or a
function or variable named crypto, makes porf c compile it in, and the
bundle then includes openssl/evp.h. That is the same trap as fetch’s names.
So the glue’s class is bound as NisoCryptoKey (its name is still
CryptoKey), and the crypto getter’s function is getCrypto (renamed to
get crypto).
The aliases only keep the glue’s own names out of that path. They do nothing
for an app’s scripts, which are compiled into the same bundle: any script
that names crypto or CryptoKey pulls in OpenSSL. On alpha 22, porf c
of console.log(2) emits no OpenSSL, while typeof crypto, a local
const crypto = 1, a class CryptoKey {} and
globalThis.crypto.getRandomValues(...) each emit #include <openssl/evp.h>,
err.h and rand.h. So such an app needs OpenSSL to build, next to the
AWS-LC niso already links. Until Porffor lets a program opt out of its
WebCrypto, an app for Porffor must not name either identifier.
The evaluate hook comes after the two WebAssembly hooks in
qe_porf_bind and __niso_bind, which are positional.
A script that names fetch, Headers or Response still fails the build, as
on main, so Response.blob() and fetching blob URLs are not reachable on
Porffor.
A fetch with a signal keeps its abort algorithm on the signal until every body of its response and their clones is read, or collected unread, which a FinalizationRegistry reports. Porffor’s never calls back, so there a response whose body is never read keeps its abort algorithm, and with it the body’s bytes, for as long as the signal lives.
Device apps
Section titled “Device apps”Radio (examples/radio) and ihatemyphone’s panel and setup pages
(examples/ihatemyphone, the niso:phone module) build and run with Porffor
as the engine. Neither app’s source changed. QuickJS stays the default; Porffor
is chosen per build:
NISO_ENGINE=porffor hosts/android/apk.sh ... # and build.sh, native.shNISO_ENGINE=porffor hosts/apple/build.sh ...NISO_ENGINE=porffor examples/ihatemyphone/build.shNISO_ENGINE=porffor turns on the porffor feature of niso-app, niso-android
and niso-jvm and points NISO_PORFFOR_SCRIPTS at the app’s bundle, so the
app’s scripts are compiled into the library. Three changes made that work:
build.rsrewrites the generatedporf_data_fileto find its data withdladdrin the library that holds it, since Porffor assumes the main executable (porffor-shared-library-assumes-main-executable). It uses the host’s clang only for native builds, so cross builds take the target’s cc (the NDK, Xcode).- iOS SDKs have no
sys/random.h, which Porffor’s C includes, sobindings/js/porffor/ios/sys/random.hdeclaresgetentropy, which iOS’s libSystem exports (porffor-ios-sys-random-missing). - The glue copies niso’s
navigatorandconsolemembers onto Porffor’s own objects (see How the binding works). Without it, radio’snavigator.mediaSessionwas undefined.
Radio calls fetch but builds: Porffor compiles app scripts inside a literal
eval, so their fetch does not pull in Porffor’s network runtime.
Two commands rerun the comparison:
node tests/porffor/apps.mjs # headless: niso-run and the JVM page testsnode tests/porffor/devices.mjs [android] [ios] [build] [measure] [--runs N]Headless
Section titled “Headless”tests/porffor/apps.mjs on macOS arm64 (M-series, a heavily loaded host,
2026-11-08). Radio’s station API is answered from tests/porffor/radio-playing.json;
the panel and setup pages run their JVM tests against a fake phone module.
| page | runs | DOM matches | render stream matches |
|---|---|---|---|
| radio | yes | yes | yes |
| panel | yes | yes | yes, except 3 frames that differ between two QuickJS runs too |
| setup | yes | yes | yes |
| radio, medians of 11 runs | Porffor | QuickJS |
|---|---|---|
| startup | 64.05 ms | 90.73 ms |
| engine init | 45.61 ms | 70.38 ms |
| scripts | 1.38 ms | 6.40 ms |
| max RSS | 36.3 MB | 35.5 MB |
| niso-run binary | 39.47 MB | 32.51 MB |
Building the three pages into one Porffor program takes porf 2.4 s and cc 54 to 98 s.
iOS simulator
Section titled “iOS simulator”devices.mjs ios measure --runs 7, iPhone 17 simulator, iOS 27, 2026-11-08
(host load average about 30 to 45). Radio is tapped to play by the probe’s
button position; it must show Pause and become the Now Playing item at rate 1.
| radio | Porffor | QuickJS |
|---|---|---|
| Play tap | Play → Pause | Play → Pause |
| Now Playing item, rate | おもかげ, 1 | おもかげ, 1 |
| cold start, median of 7 | 509 ms | 534 ms |
| physical footprint | 31.4 MB | 31.7 MB |
| Niso.app | 56.0 MB | 51.1 MB |
| executable, stripped | 40.1 MB | 36.1 MB |
Cold starts (ms): Porffor 537 504 486 522 509 513 503; QuickJS 549 514 534 537 542 518 524. An earlier run of 7 gave 500 against 537 ms.
Android emulator
Section titled “Android emulator”Android 17 emulator, 2026-11-08. Radio’s Porffor release APK cold-starts,
renders, and plays: tapping Play puts the media session in PLAYING with the
song as its metadata, as on QuickJS. Cold start times were not usable (the host’s
load average was 53 to 66 and two starts took 3.8 s and 29 s), so there are no
Android timings yet; ihatemyphone’s APKs build on Porffor but have not run on
the emulator yet.
| size | Porffor | QuickJS |
|---|---|---|
| radio release APK, entries compressed | 30.0 MB | 26.0 MB |
radio libniso_android.so, stripped |
25.5 MB | 21.4 MB |
| ihatemyphone debug APK, entries compressed | 81.5 MB | 76.2 MB |
| ihatemyphone library, unstripped release | 43.9 MB | 38.6 MB |
What the device apps say
Section titled “What the device apps say”- Both apps behave the same on both engines wherever they ran: same DOM and render stream headless, and radio plays on the simulator and the emulator.
- Startup gains less on a device than headless: the scripts’ 5 ms saved is most of what Porffor wins, and a cold start on iOS is about 500 ms of which the engine is a small part (509 against 534 ms, within the host’s noise).
- Memory is the same within 1 MB.
- Porffor adds about 4 MB to an app (the library or executable, stripped), and about 5 MB to radio’s APK and .app.
- ihatemyphone needs two engines at once when its setup screen is open and the panel opens; on Porffor the second fails (see What blocks shipping).
- The release APK build (
hosts/android/build.shasapk.shruns it) did not pass its Gradle properties to Gradle, so release APKs failed with “Cannot query the value of Gradle property ‘nisoJava’”; fixed here.
What blocks shipping
Section titled “What blocks shipping”- WeakRef and FinalizationRegistry. Every wrapper the glue makes stays
alive (its WeakRef in the wrapper cache is strong) and is never finalized,
so
host.unrefnever runs and the DOM never frees a node JS once touched. Both lifetime tests fail for this. Long-running apps (the editor milestone) leak every node they create and drop. Repros:repros/porffor-weakref-holds-target,repros/porffor-finalizationregistry-never-cleans. WeakMap is already weak in Porffor’s GC, so the machinery exists. - No embedding API. The binding depends on runtime internals: static
functions it reaches only by being compiled into Porffor’s C file,
renaming
main,porf_c_stack_top, the try-frame protocol and the promise job runner’s symbol name. Any Porffor release can break it. Shipping needs a supported way to run the top level from a host, call functions both ways, root values held from C, and drain jobs. - One engine at a time. Porffor’s heap and globals are the process’s.
Pages can follow one another (a new engine restarts the glue on the same
heap), but two at once cannot: ihatemyphone’s panel cannot open while its
setup screen is showing in the same process, and an app with a widget
extension in-process, or tests that make several DOMs, need a process each
(the test runner runs one test per process).
It is also why there are no workers: a dedicated worker is a second engine
on its own thread, and Porffor’s runtime is not reentrant across threads. On
Porffor
new Worker()throws a NotSupportedError DOMException, andimportScriptscannot be reached. MessageChannel, BroadcastChannel andwindow.postMessageneed no second engine and go through the same glue as on QuickJS; the glue compiles on Porffor, but no Porffor test runs them yet. A setTimeout string handler runs as a classic script throughhost.evalScript, so on Porffor it must be one of the scripts the program was built with (NISO_JS_RECORDrecords it). Workers would need the runtime’s heap and globals to become per-thread state; their scripts could be compiled ahead of time with the page’s, as module workers name their script statically. - Scripts are fixed at build time. There is no runtime eval, so a host can
only run scripts it was built with. That is fine for a shipped app bundle,
but the scripts run as direct evals: their top-level declarations do not
become globals as they would in a classic script, because indirect eval is
broken (repros
porffor-indirect-eval-closure-crash,porffor-indirect-eval-var-not-global). Runtime globals only eval code uses are missing (porffor-eval-misses-runtime-globals; niso now definesqueueMicrotaskin the glue). - iOS is verified on the simulator only. Radio runs on the iOS simulator
(see Device apps); not on a phone yet. Porffor reserves a
4 GB arena (
PORF_ARENA_RESERVE, mapped at a fixed hint), which may need care within a phone’s address space limits; the C itself is portable and has no JIT.
Also found: a named class expression created in a function that is handed
globalThis leaks its name as a global (porffor-class-expression-name-leaks).
niso’s glue declares its classes, except Headers and Response, whose leaked
names are the globals the glue defines from them anyway.
WebAssembly
Section titled “WebAssembly”Porffor has no WebAssembly global. The JS API requires Memory.buffer to be
an ArrayBuffer over the memory’s own bytes, and Porffor’s ArrayBuffers live in
its own heap with no way to wrap external memory, so the engine’s
qe_can_alias returns false and the glue leaves WebAssembly undefined.
Scripts see what a browser without WebAssembly shows them, rather than an API
that copies memory and silently loses writes. A copying Memory.buffer, or
an ArrayBuffer that Porffor allocates and wasmi then uses as linear memory, would
need either a Porffor API for external buffers or a wasmi memory backed by
foreign bytes, neither of which exists.
Building with alpha 22 (2026-11-08)
Section titled “Building with alpha 22 (2026-11-08)”With main merged, the WebAssembly branch builds on Porffor alpha 22 (a081ebd)
and passes 40 of the 44 binding tests (debug build, macOS arm64, the recipe
under How the binding works): main’s 39, plus
lifetime::hidden_containing_blocks_let_go_of_freed_boxes. The 4 that fail are
the same two lifetime and two WebSocket tests.
Two things were needed:
- Porffor binds the two WebAssembly hooks after
microtask, whereQE_HOOK_WASM_CALLandQE_HOOK_WASM_TASKsit in the hook table (qe_porf_bindand__niso_bindare positional), and implementsqe_push_str. - A parameter named
modulein the WebAssembly glue madeporf ccompile in ada, sobuild.rsrefused the program as if a script namedURL. Found by bisecting the bundle; a lone(module) => …does not do it, so there is no repro. The glue names those parametersmoduleObject.
Repros
Section titled “Repros”Each directory under repros/ has repro.js, expected.txt (node), actual.txt
(Porffor alpha 22) and ISSUE.md, ready to file. None duplicates an open or
closed issue in CanadaHonk/porffor (searched 2026-11-08; #400, a class
expression’s name inside its own class, is fixed and different). Porffor’s
AI_POLICY.md asks for AI use to be disclosed and issue text written by a person:
the drafts say how they were made and should be reviewed and rewritten before
filing.
| repro | what |
|---|---|
| porffor-weakref-holds-target | deref() still returns the target after it was collected |
| porffor-finalizationregistry-never-cleans | cleanup callbacks never run; targets are held strongly |
| porffor-indirect-eval-closure-crash | (0, eval)('const x = 1; const f = () => x; f()') crashes the compiler |
| porffor-indirect-eval-var-not-global | indirect eval’s var is not a global, even to its own closures |
| porffor-eval-misses-runtime-globals | queueMicrotask, setTimeout, structuredClone are undefined in eval code |
| porffor-class-expression-name-leaks | a class expression’s name becomes a global and shadows the real one |
| porffor-own-fetch-pulls-network-runtime | a program’s own fetch compiles in libuv, OpenSSL, ada and uWebSockets |
| porffor-own-url-class-replaced | a top-level class URL is replaced by Porffor’s URL, and compiles in ada |
| porffor-own-messagechannel-class-replaced | a top-level class MessageChannel compiles in libuv |
| porffor-navigator-console-global-ignored | a program’s own navigator or console global is ignored for Porffor’s |
| porffor-shared-library-assumes-main-executable | porf_data_file finds its data next to the main executable, not the library that holds it |
| porffor-ios-sys-random-missing | the generated C includes sys/random.h, which iOS SDKs lack |
| porffor-date-now-hook | a request, not a bug (ISSUE.md only): Date.now() reads the system clock and a host cannot supply its own, so it does not skip ahead with niso’s virtual time |