From 232762f8733de66e42061a9c1cb456c29230a165 Mon Sep 17 00:00:00 2001 From: TakalaWang Date: Fri, 9 Oct 2026 16:55:55 +0800 Subject: [PATCH 1/2] fix(runtime): drop interactive writes to an exited peer instead of failing with EPIPE Closes #101. When one side of an interact session exited or closed its stdin, its peer's next write failed with EPIPE on both hosts. A CPython interactor that replied to a contestant that had already exited died with BrokenPipeError and exit 120, and because the stdout wrapper records into the transcript before writing to the pipe, each retried flush added the reply again (five copies). Judges that run both programs natively keep draining each program's output until it exits, so the interactor reads EOF and exits with its own verdict. InteractiveOutput (server) and StreamOutput (browser side Workers) now treat a broken pipe as a successful write: the bytes are already in the transcript and are dropped. Reads from a side that has exited still return the remaining buffered bytes and then EOF. The transcript records each byte a side writes exactly once, up to its output limit, including bytes written after the peer exited, so it depends only on what the writer wrote. Tests cover both hosts: runtime-core unit tests for writes after either side exits and EOF after buffered bytes, server integration tests with C and CPython interactors exiting 42/43 after replying to an exited contestant, and strict-CSP browser fixtures for the same cases plus the reply-before-EOF race. runtime-core_bg.wasm is rebuilt and the runtime identity pins refreshed, which changes cost profiles. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 8 ++ crates/runtime-core/src/interactive.rs | 132 +++++++++++++++++- .../runtime-core/src/run/web_interactive.rs | 7 +- docs/architecture.md | 2 +- docs/library-contract.md | 9 ++ scripts/verify-browser-csp.mjs | 9 +- src/core/runtime-identity.ts | 6 +- src/runner/generated/runtime-core.js | 6 +- src/runner/generated/runtime-core_bg.wasm | 4 +- src/server/judge.integration.test.ts | 96 +++++++++++++ 10 files changed, 267 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4ddf711..0d4fa00 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,14 @@ All notable changes to WASM-OJ are recorded here. Releases follow ## Unreleased +- Interactive writes no longer fail with `EPIPE` after the other side exits or closes its stdin. The + bytes are recorded in the transcript once and dropped, and the writer keeps running, as with a + judge that keeps draining both pipes. An interactor that replies to a contestant that already + exited now reads EOF and exits with its own verdict; a CPython interactor used to exit 120 and + repeat its reply up to five times in the transcript. Reads still return the remaining buffered + bytes and then EOF. This applies to the server and the browser. The refreshed runtime identity + changes cost profiles. + ## 0.2.4 - 2026-10-09 - Fixed a host process crash (`Uncaught Error: write EPIPE`) when `ServerRunner` cancelled or diff --git a/crates/runtime-core/src/interactive.rs b/crates/runtime-core/src/interactive.rs index 77a9f29..735b747 100644 --- a/crates/runtime-core/src/interactive.rs +++ b/crates/runtime-core/src/interactive.rs @@ -121,6 +121,9 @@ struct InteractiveOutput { } impl AsyncWrite for InteractiveOutput { + /// Once the peer has closed its stdin, for example by exiting, the pipe reports a broken + /// pipe. The bytes are already in the transcript, so the write succeeds and they are dropped, + /// as when a judge keeps draining a pipe whose reader is gone. fn poll_write( mut self: Pin<&mut Self>, context: &mut Context<'_>, @@ -128,7 +131,12 @@ impl AsyncWrite for InteractiveOutput { ) -> Poll> { match Pin::new(&mut self.capture).poll_write(context, buffer) { Poll::Ready(Ok(written)) => { - Pin::new(&mut self.pipe).poll_write(context, &buffer[..written]) + match Pin::new(&mut self.pipe).poll_write(context, &buffer[..written]) { + Poll::Ready(Err(error)) if error.kind() == io::ErrorKind::BrokenPipe => { + Poll::Ready(Ok(written)) + } + result => result, + } } result => result, } @@ -1120,6 +1128,128 @@ mod tests { assert_eq!(interactive.metrics.cost, standalone.metrics.cost); } + const DRAIN_STDIN: &str = r#" + (func $drain_stdin (result i32) + (local $total i32) + (loop $again + (i32.store (i32.const 0) (i32.add (i32.const 256) (local.get $total))) + (i32.store (i32.const 4) (i32.const 64)) + (if (call $fd_read (i32.const 0) (i32.const 0) (i32.const 1) (i32.const 8)) + (then (call $exit (i32.const 3)))) + (local.set $total (i32.add (local.get $total) (i32.load (i32.const 8)))) + (br_if $again (i32.load (i32.const 8)))) + local.get $total)"#; + + fn peer_program(body: &str, data: &str) -> Vec { + wat::parse_str(format!( + r#"(module + (import "wasi_snapshot_preview1" "fd_read" + (func $fd_read (param i32 i32 i32 i32) (result i32))) + (import "wasi_snapshot_preview1" "fd_write" + (func $fd_write (param i32 i32 i32 i32) (result i32))) + (import "wasi_snapshot_preview1" "proc_exit" (func $exit (param i32))) + (memory (export "memory") 1) + (data (i32.const 128) "{data}") + {DRAIN_STDIN} + (func $write (param $length i32) (result i32) + (i32.store (i32.const 16) (i32.const 128)) + (i32.store (i32.const 20) (local.get $length)) + (call $fd_write (i32.const 1) (i32.const 16) (i32.const 1) (i32.const 24))) + (func (export "_start") {body}))"# + )) + .unwrap() + } + + fn interact_pair(contestant: Vec, interactor: Vec) -> crate::InteractiveResult { + tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .unwrap() + .block_on(interact(InteractiveRequest { + contestant: program(contestant), + interactor: program(interactor), + determinism: DeterminismConfig { + random_seed: 7, + realtime_epoch_ms: 946_684_800_000, + clock_step_ns: 1_000_000, + }, + })) + .unwrap() + } + + #[test] + fn interactor_writes_to_an_exited_contestant_without_a_broken_pipe() { + let contestant = peer_program("(drop (call $write (i32.const 2)))", "1\\n"); + let interactor = peer_program( + r#"(local $errno i32) + (drop (call $drain_stdin)) + (local.set $errno (call $write (i32.const 8))) + (if (local.get $errno) (then (call $exit (local.get $errno)))) + (if (i32.ne (i32.load (i32.const 24)) (i32.const 8)) (then (call $exit (i32.const 4)))) + (call $exit (i32.const 42))"#, + "correct\\n", + ); + + let result = interact_pair(contestant, interactor); + + assert_eq!(result.contestant.termination, ExecutionTermination::Exited); + assert_eq!(result.contestant.code, 0); + assert_eq!(result.interactor.termination, ExecutionTermination::Exited); + assert_eq!(result.interactor.code, 42); + assert_eq!(result.contestant_to_interactor, b"1\n"); + assert_eq!(result.interactor_to_contestant, b"correct\n"); + assert_eq!(result.interactor.metrics.protocol_bytes, 8); + } + + #[test] + fn contestant_reads_buffered_bytes_then_eof_after_the_interactor_exits() { + let contestant = peer_program( + r#"(local $total i32) + (local.set $total (call $drain_stdin)) + (call $exit (select (i32.const 0) (i32.const 1) + (i32.and + (i32.eq (local.get $total) (i32.const 4)) + (i32.eq (i32.load (i32.const 256)) (i32.const 0x0a657962)))))"#, + "", + ); + let interactor = peer_program("(drop (call $write (i32.const 4)))", "bye\\n"); + + let result = interact_pair(contestant, interactor); + + assert_eq!(result.interactor.termination, ExecutionTermination::Exited); + assert_eq!(result.interactor.code, 0); + assert_eq!(result.contestant.termination, ExecutionTermination::Exited); + assert_eq!(result.contestant.code, 0); + assert_eq!(result.interactor_to_contestant, b"bye\n"); + } + + #[test] + fn contestant_writes_to_an_exited_interactor_without_a_broken_pipe() { + let contestant = peer_program( + r#"(local $round i32) + (local $errno i32) + (drop (call $drain_stdin)) + (loop $again + (local.set $errno (call $write (i32.const 2))) + (if (local.get $errno) (then (call $exit (local.get $errno)))) + (if (i32.ne (i32.load (i32.const 24)) (i32.const 2)) (then (call $exit (i32.const 4)))) + (local.set $round (i32.add (local.get $round) (i32.const 1))) + (br_if $again (i32.lt_u (local.get $round) (i32.const 3)))) + (call $exit (i32.const 42))"#, + "x\\n", + ); + let interactor = peer_program("(drop (call $write (i32.const 4)))", "bye\\n"); + + let result = interact_pair(contestant, interactor); + + assert_eq!(result.interactor.termination, ExecutionTermination::Exited); + assert_eq!(result.interactor.code, 0); + assert_eq!(result.contestant.termination, ExecutionTermination::Exited); + assert_eq!(result.contestant.code, 42); + assert_eq!(result.contestant_to_interactor, b"x\nx\nx\n"); + assert_eq!(result.interactor_to_contestant, b"bye\n"); + } + fn program(wasm: Vec) -> InteractiveProgram { InteractiveProgram { wasm, diff --git a/crates/runtime-core/src/run/web_interactive.rs b/crates/runtime-core/src/run/web_interactive.rs index c16f73b..6820922 100644 --- a/crates/runtime-core/src/run/web_interactive.rs +++ b/crates/runtime-core/src/run/web_interactive.rs @@ -291,13 +291,18 @@ impl std::fmt::Debug for StreamOutput { } impl AsyncWrite for StreamOutput { + /// Drops bytes written after the peer closed its stdin, as native does; they are already in + /// the transcript. fn poll_write( mut self: Pin<&mut Self>, context: &mut Context<'_>, buffer: &[u8], ) -> Poll> { match Pin::new(&mut self.capture).poll_write(context, buffer) { - Poll::Ready(Ok(written)) => Poll::Ready(self.streams.write(&buffer[..written])), + Poll::Ready(Ok(written)) => Poll::Ready(match self.streams.write(&buffer[..written]) { + Err(error) if error.kind() == io::ErrorKind::BrokenPipe => Ok(written), + result => result, + }), result => result, } } diff --git a/docs/architecture.md b/docs/architecture.md index 9406b48..d52e009 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -57,7 +57,7 @@ complete browser Worker-generation boundary. Browser interaction runs each side Worker as a standalone metered run. The two sides exchange bytes through shared-memory ring buffers whose reads block with `Atomics.wait`, so neither side ever yields to the other on one thread. Each ring holds its writer's whole output budget, so a write never waits, as on the server's unbounded -pipes. A poll checks the input without blocking, so another ready subscription is reported first; if +pipes. On both hosts a write after the reader exited is dropped instead of failing. A poll checks the input without blocking, so another ready subscription is reported first; if nothing is ready, a poll with a clock advances the virtual clock to its deadline, as the server does. On both hosts a read from stdin opened with `O_NONBLOCK` waits for input instead of failing with `EAGAIN`. diff --git a/docs/library-contract.md b/docs/library-contract.md index 214a5fa..bd30521 100644 --- a/docs/library-contract.md +++ b/docs/library-contract.md @@ -244,6 +244,15 @@ resource policies, process-local deterministic clocks, and secret inputs mounted interactor side. Either side may be a standalone Wasm module or a runtime bundle such as CPython; runtime bundles that cannot provide streaming fd 0 are rejected for interaction. +A side that has exited, or closed its stdin, no longer reads, but its peer's writes to it still +succeed: the bytes are dropped and the peer keeps running without `EPIPE`, as with a judge that keeps +draining both programs' output until they exit. Reads from a side that has exited, or closed its +stdout, return the bytes still buffered and then EOF. An interactor can therefore reply to a +contestant that already exited, read EOF, and exit with its own verdict. `contestantToInteractor` and +`interactorToContestant` record every byte each side wrote to stdout exactly once, up to its output +limit, including bytes written after the peer exited. A transcript depends only on what its writer +wrote, not on when the reader exited. + Each case executes under the broad hard policy once. Correct output and the same normalized metrics are evaluated against ordered cumulative `baseline`, `efficient`, and `optimal` policies. The portable compute metric is `RunResult.metrics.cost`; wall time is only a safety boundary. diff --git a/scripts/verify-browser-csp.mjs b/scripts/verify-browser-csp.mjs index 5f9a545..a9b138a 100644 --- a/scripts/verify-browser-csp.mjs +++ b/scripts/verify-browser-csp.mjs @@ -96,6 +96,11 @@ const guessInput = secret => ({ args:["/judge/input.txt"], files:{"/judge/input. const guessCpp = { language:"cpp", source:'#include \n#include \nint main(){long long lo=1,hi=1<<20;std::string r;while(lo<=hi){long long mid=(lo+hi)/2;std::cout<>r))return 2;if(r=="=")return 0;if(r=="<")lo=mid+1;else hi=mid-1;}return 1;}' }; const guessC = { language:"c", source:'#include \nint main(void){long long lo=1,hi=1<<20;char r[4];while(lo<=hi){long long mid=(lo+hi)/2;printf("%lld\\n",mid);fflush(stdout);if(scanf("%3s",r)!=1)return 2;if(r[0]==\'=\')return 0;if(r[0]==\'<\')lo=mid+1;else hi=mid-1;}return 1;}' }; const readOne = { language:"c", source:'#include \nint main(void){int x;return scanf("%d",&x)==1?0:1;}' }; +const finalGuess = { language:"c", source:'#include \nint main(void){puts("7");return 0;}' }; +const secretInput = secret => ({ args:["/judge/input.txt"], files:{"/judge/input.txt":`${secret}\n`} }); +const replyInteractor = (language, afterEof) => language === "c" + ? { language, source:`#include \nint main(int argc,char**argv){FILE*f=argc>1?fopen(argv[1],"r"):NULL;long long secret,guess;if(!f||fscanf(f,"%lld",&secret)!=1)return 2;if(scanf("%lld",&guess)!=1)return 3;${afterEof ? "while(getchar()!=EOF){}" : ""}puts(guess==secret?"correct":"wrong");if(fflush(stdout)!=0)return 4;${afterEof ? "" : "if(scanf(\"%lld\",&guess)!=EOF)return 5;"}return guess==secret?42:43;}` } + : { language, source:`import sys\nsecret = int(open(sys.argv[1]).read())\nguess = int(input())\n${afterEof ? "sys.stdin.read()\n" : ""}print("correct" if guess == secret else "wrong", flush=True)\n${afterEof ? "" : "if sys.stdin.read().strip():\n sys.exit(5)\n"}sys.exit(42 if guess == secret else 43)\n` }; const exited = (process, code) => process.termination === "exited" && process.code === code; const guessed = result => exited(result.contestant, 0) && exited(result.interactor, 0) && result.interactorToContestant.endsWith("=\n"); const interactiveFixtures = [ @@ -111,7 +116,9 @@ const interactiveFixtures = [ { label:"interactive-poll-clockless", contestant:{ language:"c", source:'#include \n#include \nint main(void){struct pollfd p[2]={{0,POLLIN,0},{1,POLLOUT,0}};if(poll(p,2,-1)<1||!(p[1].revents&POLLOUT))return 4;puts("ping");fflush(stdout);if(poll(p,1,-1)!=1||!(p[0].revents&POLLIN))return 5;char b[8];if(scanf("%7s",b)!=1)return 2;return b[0]==\'p\'&&b[1]==\'o\'?0:3;}' }, interactor:{ language:"c", source:'#include \nint main(void){char b[8];if(scanf("%7s",b)!=1)return 2;puts("pong");fflush(stdout);return 0;}' }, options:{ contestant:{ resources:{ wallTimeLimitMs:10000 } }, interactor:{ resources:{ wallTimeLimitMs:10000 } } }, check:result => exited(result.contestant, 0) && exited(result.interactor, 0) && result.contestantToInteractor === "ping\n" && result.interactorToContestant === "pong\n" }, { label:"interactive-instruction-limit", contestant:{ language:"cpp", source:'int main(){volatile unsigned long long spin=0;for(;;)spin=spin+1;}' }, interactor:guessInteractor, options:{ contestant:{ resources:{ wallTimeLimitMs:30000 } }, interactor:{ ...guessInput(1), resources:{ wallTimeLimitMs:30000 } } }, check:result => result.contestant.termination === "instruction-limit" && result.contestant.code === 137 && exited(result.interactor, 4) }, { label:"interactive-contestant-exits", contestant:{ language:"c", source:'int main(void){return 0;}' }, interactor:guessInteractor, options:{ interactor:guessInput(1) }, check:result => exited(result.contestant, 0) && exited(result.interactor, 4) && result.contestantToInteractor === "" }, - { label:"interactive-interactor-exits", contestant:{ language:"c", source:'#include \n#include \n#include \nint main(void){char b[8];if(scanf("%7s",b)!=1)return 2;for(int i=0;i<1000000;i++)if(write(1,"x\\n",2)<0)return errno==EPIPE?32:33;return 34;}' }, interactor:{ language:"cpp", source:'#include \nint main(){std::puts("bye");std::fflush(stdout);return 0;}' }, options:{}, check:result => exited(result.contestant, 32) && exited(result.interactor, 0) && result.interactorToContestant === "bye\n" }, + { label:"interactive-interactor-exits-eof", contestant:{ language:"c", source:'#include \n#include \nint main(void){char b[8];if(scanf("%7s",b)!=1||strcmp(b,"bye")!=0)return 2;return scanf("%7s",b)==EOF&&feof(stdin)?0:3;}' }, interactor:{ language:"c", source:'#include \nint main(void){puts("bye");return 0;}' }, options:{}, check:result => exited(result.contestant, 0) && exited(result.interactor, 0) && result.interactorToContestant === "bye\n" }, + { label:"interactive-interactor-exits", contestant:{ language:"c", source:'#include \n#include \n#include \n#include \nint main(void){char b[8];if(scanf("%7s",b)!=1||strcmp(b,"bye")!=0)return 2;if(scanf("%7s",b)!=EOF)return 3;for(int i=0;i<3;i++)if(write(1,"x\\n",2)!=2)return errno==EPIPE?32:33;return 0;}' }, interactor:{ language:"c", source:'#include \nint main(void){puts("bye");return 0;}' }, options:{}, check:result => exited(result.contestant, 0) && exited(result.interactor, 0) && result.contestantToInteractor === "x\nx\nx\n" && result.interactorToContestant === "bye\n" }, + ...[["c", true, 7], ["python", true, 8], ["c", false, 8], ["python", false, 7]].map(([language, afterEof, secret]) => ({ label:`interactive-reply-${afterEof ? "after-exit" : "race"}-${language}`, contestant:finalGuess, interactor:replyInteractor(language, afterEof), options:{ interactor:secretInput(secret) }, check:result => exited(result.contestant, 0) && exited(result.interactor, secret === 7 ? 42 : 43) && result.contestantToInteractor === "7\n" && result.interactorToContestant === (secret === 7 ? "correct\n" : "wrong\n") })), { label:"interactive-output-flood", contestant:{ language:"c", source:'#include \nint main(void){while(fputs("flood\\n",stdout)>=0&&fflush(stdout)==0){}return 0;}' }, interactor:{ language:"c", source:'#include \nint main(void){while(getchar()!=EOF){}return 0;}' }, options:{ contestant:{ resources:{ outputLimitBytes:65536 } } }, check:result => result.contestant.termination === "output-limit" && result.contestant.code === 137 && result.contestantToInteractor.length === 65536 && exited(result.interactor, 0) }, { label:"interactive-wall-time", contestant:readOne, interactor:readOne, options:{ contestant:{ resources:{ wallTimeLimitMs:2000 } }, interactor:{ resources:{ wallTimeLimitMs:2000 } } }, check:(result, elapsedMs) => result.contestant.termination === "wall-time-limit" && result.interactor.termination === "wall-time-limit" && elapsedMs >= 2000 && elapsedMs < 15000 }, { label:"interactive-cancel", contestant:readOne, interactor:readOne, options:{}, cancelAfterMs:1000, recovery:{ contestant:guessC, interactor:guessInteractor, options:{ interactor:guessInput(5) } }, check:(result, elapsedMs) => result.cancelled && elapsedMs < 10000 && guessed(result.recovery) }, diff --git a/src/core/runtime-identity.ts b/src/core/runtime-identity.ts index 6323dee..2dca29e 100644 --- a/src/core/runtime-identity.ts +++ b/src/core/runtime-identity.ts @@ -3,8 +3,8 @@ import { sha256Hex } from "./sha256.ts"; /** Executable runtime components covered by deterministic cost calibration. */ export const WASM_OJ_RUNTIME_COMPONENTS = Object.freeze({ - runtimeCoreWasmSha256: "0ae00fc646f529aafb5d1a68758220e6428f6c565e8e621eeb6c8f14ac5d4a10", - runtimeSourceRootSha256: "a7e9071511d127e3b14920558c7c38c27a1fa19d98906c8476c1df8041a9c3bb", + runtimeCoreWasmSha256: "9fe6417ded9476957ed892eecffb9210fd99aafb3b55d0272ce5c123be43c1e6", + runtimeSourceRootSha256: "8d33f355c9fb44a2f22b501694280fcf64e2c490ea1f9b4ab1195ba2849b62df", wasmerNativeVersion: "7.2.1", wasmerSdkVersion: "0.10.0", wasmerSdkWasmSha256: "49a6646209f5ab5e7c737eac33407d87d9a9959ac83e5ecaaab9261b2323589e", @@ -17,7 +17,7 @@ export const WASM_OJ_RUNTIME_COMPONENTS = Object.freeze({ * identity is admitted into a calibrated release. */ export const WASM_OJ_RUNTIME_IDENTITY_SHA256 = - "e5680beae9bd832ed2b679267250ce1efc7d104dd005bb155135664b40f961fb"; + "9e4f094c5b11450beb19bde39c17fafb3c21475c0f3064f5fa4fecd75ce8c23a"; /** Exact canonical serialization hashed by `WASM_OJ_RUNTIME_IDENTITY_SHA256`. */ export function runtimeIdentityBytes(): Uint8Array { diff --git a/src/runner/generated/runtime-core.js b/src/runner/generated/runtime-core.js index 4aaddfc..f3f45f0 100644 --- a/src/runner/generated/runtime-core.js +++ b/src/runner/generated/runtime-core.js @@ -775,17 +775,17 @@ function __wbg_get_imports() { return ret; }, __wbindgen_cast_0000000000000002: function(arg0, arg1) { - // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [Ref(NamedExternref("Array"))], shim_idx: 2393, ret: Result(Externref), inner_ret: Some(Result(Externref)) }, mutable: true }) -> Externref`. + // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [Ref(NamedExternref("Array"))], shim_idx: 2392, ret: Result(Externref), inner_ret: Some(Result(Externref)) }, mutable: true }) -> Externref`. const ret = makeMutClosure(arg0, arg1, wasm_bindgen_68458880a41dd4bb___convert__closures________invoke___js_sys_c11fba41208799d1___Array__core_9b3796e30d99ddb7___result__Result_js_sys_c11fba41208799d1___Array__wasm_bindgen_68458880a41dd4bb___JsValue___true_); return ret; }, __wbindgen_cast_0000000000000003: function(arg0, arg1) { - // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [Ref(NamedExternref("Array"))], shim_idx: 2393, ret: Result(NamedExternref("Array")), inner_ret: Some(Result(NamedExternref("Array"))) }, mutable: true }) -> Externref`. + // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [Ref(NamedExternref("Array"))], shim_idx: 2392, ret: Result(NamedExternref("Array")), inner_ret: Some(Result(NamedExternref("Array"))) }, mutable: true }) -> Externref`. const ret = makeMutClosure(arg0, arg1, wasm_bindgen_68458880a41dd4bb___convert__closures________invoke___js_sys_c11fba41208799d1___Array__core_9b3796e30d99ddb7___result__Result_js_sys_c11fba41208799d1___Array__wasm_bindgen_68458880a41dd4bb___JsValue___true__2); return ret; }, __wbindgen_cast_0000000000000004: function(arg0, arg1) { - // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [Ref(NamedExternref("Array"))], shim_idx: 2394, ret: Result(Unit), inner_ret: Some(Result(Unit)) }, mutable: true }) -> Externref`. + // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [Ref(NamedExternref("Array"))], shim_idx: 2393, ret: Result(Unit), inner_ret: Some(Result(Unit)) }, mutable: true }) -> Externref`. const ret = makeMutClosure(arg0, arg1, wasm_bindgen_68458880a41dd4bb___convert__closures________invoke___js_sys_c11fba41208799d1___Array__core_9b3796e30d99ddb7___result__Result_____wasm_bindgen_68458880a41dd4bb___JsValue___true_); return ret; }, diff --git a/src/runner/generated/runtime-core_bg.wasm b/src/runner/generated/runtime-core_bg.wasm index bebed1b..a19d849 100644 --- a/src/runner/generated/runtime-core_bg.wasm +++ b/src/runner/generated/runtime-core_bg.wasm @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0ae00fc646f529aafb5d1a68758220e6428f6c565e8e621eeb6c8f14ac5d4a10 -size 14599854 +oid sha256:9fe6417ded9476957ed892eecffb9210fd99aafb3b55d0272ce5c123be43c1e6 +size 14585541 diff --git a/src/server/judge.integration.test.ts b/src/server/judge.integration.test.ts index 8f9f00e..6145bf6 100644 --- a/src/server/judge.integration.test.ts +++ b/src/server/judge.integration.test.ts @@ -217,6 +217,102 @@ describe.skipIf(!enabled)("real server judge contracts", () => { }); }); + it("lets C and CPython interactors reply to a contestant that already exited", { timeout: 300_000 }, async () => { + const contestant = await compileC("final-guess", [ + "#include ", + "int main(void) {", + " puts(\"7\");", + " return 0;", + "}", + ].join("\n")); + const interactors = { + c: await compileC("c-reply-after-exit", [ + "#include ", + "int main(int argc, char **argv) {", + " long long secret = 0, guess = 0;", + " FILE *input = argc > 1 ? fopen(argv[1], \"r\") : NULL;", + " if (!input || fscanf(input, \"%lld\", &secret) != 1) return 2;", + " if (scanf(\"%lld\", &guess) != 1) return 3;", + " while (getchar() != EOF) {}", + " puts(guess == secret ? \"correct\" : \"wrong\");", + " if (fflush(stdout) != 0) return 4;", + " return guess == secret ? 42 : 43;", + "}", + ].join("\n")), + python: await compilePython("python-reply-after-exit", [ + "import sys", + "secret = int(open(sys.argv[1]).read())", + "guess = int(input())", + "sys.stdin.read()", + "print(\"correct\" if guess == secret else \"wrong\", flush=True)", + "sys.exit(42 if guess == secret else 43)", + ].join("\n")), + }; + + for (const [language, interactor] of Object.entries(interactors)) { + for (const [secret, code, reply] of [[7, 42, "correct\n"], [8, 43, "wrong\n"]] as const) { + const result = await engine.interact(contestant, interactor, { + interactor: { args: ["/judge/input.txt"], files: { "/judge/input.txt": `${secret}\n` } }, + }); + expect({ language, secret, result }).toMatchObject({ + language, + secret, + result: { + contestantToInteractor: "7\n", + interactorToContestant: reply, + contestant: { code: 0, termination: "exited" }, + interactor: { code, termination: "exited" }, + }, + }); + } + } + }); + + it("gives the contestant EOF and accepts its writes after the interactor exits", { timeout: 300_000 }, async () => { + const contestant = await compileC("write-after-interactor", [ + "#include ", + "#include ", + "#include ", + "#include ", + "int main(void) {", + " char word[8];", + " if (scanf(\"%7s\", word) != 1 || strcmp(word, \"bye\") != 0) return 2;", + " if (scanf(\"%7s\", word) != EOF || !feof(stdin)) return 3;", + " for (int i = 0; i < 3; ++i)", + " if (write(1, \"x\\n\", 2) != 2) return errno == EPIPE ? 32 : 33;", + " return 0;", + "}", + ].join("\n")); + const interactor = await compileC("early-exit", [ + "#include ", + "int main(void) {", + " puts(\"bye\");", + " return 0;", + "}", + ].join("\n")); + + const result = await engine.interact(contestant, interactor, {}); + + expect(result).toMatchObject({ + contestantToInteractor: "x\nx\nx\n", + interactorToContestant: "bye\n", + contestant: { code: 0, termination: "exited" }, + interactor: { code: 0, termination: "exited" }, + }); + }); + + async function compilePython(name: string, source: string): Promise { + const built = await engine.compile({ + projectId: `judge-integration:${name}`, + name, + language: "python", + entry: "main.py", + files: { "main.py": `${source}\n` }, + }, { cache: false }); + if (!built.success || !built.artifact) throw new Error(`Failed to compile ${name}: ${built.stderr}`); + return built.artifact; + } + async function compileC(name: string, source: string): Promise { const entry = `src/${name}.c`; const input: CompileInput = { From 9b035226d666394856b117983f499667b8f58321 Mon Sep 17 00:00:00 2001 From: TakalaWang Date: Fri, 9 Oct 2026 17:18:34 +0800 Subject: [PATCH 2/2] fix(runtime): give loop functions without locals a local so WebKit optimizes them In WebKit an empty C++ for(;;); took about 25 s to reach the default instruction budget (Chromium 1.3 s, Firefox 2.9 s), so it usually hit the wall limit first and was reported as a wall-time TLE. The meter is not optimized away; the trap does arrive, just late. JavaScriptCore (Safari 26, also the macOS 26.6 jsc shell) never enters optimized code inside a loop of a function that has no parameters and no locals. With --verboseOSR the baseline tier logs "Consider OSREntryPlan" about once per iteration (18 million times in 2e8 cost units) although the OMG entry code exists, so every iteration takes the tier-up slow path: about 14 times slower than BBQ alone (--useOMGJIT=false) and 100 times slower than the same loop in a function with one unused local. The forge meter makes no difference; any loop body behaves the same way. The final instrumentation pass (MeterInitializer, which already re-encodes the module to set the budget) now gives every function that contains a loop but has no parameters or locals one unused i32 local. It changes neither behaviour nor cost. WebKit now stops the empty loop at the budget in about 0.2 s; Chromium and Firefox are unchanged. A unit test covers which functions receive the local, and the strict-CSP suite gains empty-loop fixtures for run and interact that must reach instruction-limit before a 15 s wall limit. runtime-core_bg.wasm is rebuilt and the runtime identity pins refreshed, which changes cost profiles. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 6 + crates/runtime-core/src/meter.rs | 128 +++++++++++++++++++++- docs/architecture.md | 4 +- scripts/verify-browser-csp.mjs | 2 + src/core/runtime-identity.ts | 6 +- src/runner/generated/runtime-core.js | 8 +- src/runner/generated/runtime-core_bg.wasm | 4 +- 7 files changed, 144 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0d4fa00..2574384 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,12 @@ All notable changes to WASM-OJ are recorded here. Releases follow ## Unreleased +- Fixed WebKit taking about 25 s to stop an empty C++ `for(;;);` at the default instruction budget, + so the run usually hit its wall limit instead. JavaScriptCore never enters optimized code inside a + loop of a function that has no parameters or locals, and keeps calling its tier-up slow path, so + such a metered loop ran about 20 times slower than in Chromium. Instrumentation now gives these + functions one unused local, which changes neither behaviour nor cost; WebKit stops the loop at the + budget in about 0.2 s. The refreshed runtime identity changes cost profiles. - Interactive writes no longer fail with `EPIPE` after the other side exits or closes its stdin. The bytes are recorded in the transcript once and dropped, and the writer keeps running, as with a judge that keeps draining both pipes. An interactor that replies to a contestant that already diff --git a/crates/runtime-core/src/meter.rs b/crates/runtime-core/src/meter.rs index 6ca57a7..2ffd9f5 100644 --- a/crates/runtime-core/src/meter.rs +++ b/crates/runtime-core/src/meter.rs @@ -88,13 +88,77 @@ impl std::fmt::Display for MeterInitializationError { impl std::error::Error for MeterInitializationError {} +/// Sets the meter's initial budget and gives every function that has a loop but no parameters or +/// locals one unused `i32` local. JavaScriptCore (Safari 26) never enters optimized code inside a +/// loop of such a function: its baseline tier asks to tier up on almost every iteration and keeps +/// running the slow path, so a metered empty loop runs about 20 times slower than in Chromium and +/// hits the wall deadline before its instruction budget. The local changes neither behaviour nor +/// cost. struct MeterInitializer { budget: i64, + parameterized_types: Vec, + function_types: Vec, + defined_functions: usize, } impl Reencode for MeterInitializer { type Error = MeterInitializationError; + fn parse_type_section( + &mut self, + types: &mut wasm_encoder::TypeSection, + section: wasmparser::TypeSectionReader<'_>, + ) -> Result<(), ReencodeError> { + for group in section.clone() { + for ty in group?.types() { + self.parameterized_types + .push(match &ty.composite_type.inner { + wasmparser::CompositeInnerType::Func(function) => { + !function.params().is_empty() + } + _ => true, + }); + } + } + wasm_encoder::reencode::utils::parse_type_section(self, types, section) + } + + fn parse_function_section( + &mut self, + functions: &mut wasm_encoder::FunctionSection, + section: wasmparser::FunctionSectionReader<'_>, + ) -> Result<(), ReencodeError> { + for function in section.clone() { + self.function_types.push(function?); + } + wasm_encoder::reencode::utils::parse_function_section(self, functions, section) + } + + fn parse_function_body( + &mut self, + code: &mut wasm_encoder::CodeSection, + body: wasmparser::FunctionBody<'_>, + ) -> Result<(), ReencodeError> { + let ordinal = self.defined_functions; + self.defined_functions += 1; + let parameterized = self + .function_types + .get(ordinal) + .and_then(|ty| self.parameterized_types.get(*ty as usize)) + .copied() + .unwrap_or(true); + if parameterized || body.get_locals_reader()?.get_count() != 0 || !has_loop(&body)? { + return wasm_encoder::reencode::utils::parse_function_body(self, code, body); + } + let mut function = wasm_encoder::Function::new([(1, wasm_encoder::ValType::I32)]); + let mut operators = body.get_operators_reader()?; + while !operators.eof() { + function.instruction(&self.parse_instruction(&mut operators)?); + } + code.function(&function); + Ok(()) + } + fn parse_global_section( &mut self, globals: &mut wasm_encoder::GlobalSection, @@ -128,12 +192,27 @@ impl Reencode for MeterInitializer { fn set_initial_meter_budget(wasm: &[u8], budget: i64) -> Result, String> { validate_meter_global_position(wasm)?; let mut module = wasm_encoder::Module::new(); - MeterInitializer { budget } - .parse_core_module(&mut module, wasmparser::Parser::new(0), wasm) - .map_err(|error| format!("failed to initialize weighted meter: {error}"))?; + MeterInitializer { + budget, + parameterized_types: Vec::new(), + function_types: Vec::new(), + defined_functions: 0, + } + .parse_core_module(&mut module, wasmparser::Parser::new(0), wasm) + .map_err(|error| format!("failed to initialize weighted meter: {error}"))?; Ok(module.finish()) } +fn has_loop(body: &wasmparser::FunctionBody<'_>) -> Result { + let mut operators = body.get_operators_reader()?; + while !operators.eof() { + if matches!(operators.read()?, wasmparser::Operator::Loop { .. }) { + return Ok(true); + } + } + Ok(false) +} + fn validate_meter_global_position(wasm: &[u8]) -> Result<(), String> { let mut imported_globals = 0_u32; let mut defined_globals = 0_u32; @@ -409,7 +488,7 @@ mod tests { use super::{METER_MODEL, instrument_wasm, weighted_opcode_cost}; use std::borrow::Cow; use wasm_encoder::{Encode, Section}; - use wasmparser::{ExternalKind, Parser, Payload}; + use wasmparser::{ExternalKind, Parser, Payload, ValType}; #[test] fn instrumentation_adds_the_metering_global() { @@ -488,6 +567,47 @@ mod tests { assert_eq!(weighted_opcode_cost("AtomicFence"), Some(1000)); } + #[test] + fn functions_with_a_loop_and_no_params_or_locals_get_one_unused_local() { + let wasm = wat::parse_str( + r#"(module + (memory (export "memory") 1) + (func (export "_start") (loop (br 0))) + (func (local i64) (loop (br 0))) + (func (param i32) (loop (br 0))) + (func nop) + (func (block (loop (br 1)))))"#, + ) + .unwrap(); + let metered = instrument_wasm(&wasm, 1_000_000).unwrap(); + let locals = Parser::new(0) + .parse_all(&metered.wasm) + .filter_map(Result::ok) + .filter_map(|payload| match payload { + Payload::CodeSectionEntry(body) => Some( + body.get_locals_reader() + .unwrap() + .into_iter() + .map(|local| local.unwrap()) + .collect::>(), + ), + _ => None, + }) + .collect::>(); + let i32_local = vec![(1, ValType::I32)]; + assert_eq!( + locals, + [ + i32_local.clone(), + vec![(1, ValType::I64)], + vec![], + vec![], + i32_local, + vec![], + ] + ); + } + #[test] fn reports_wark_compatible_static_operation_counts() { let wasm = wat::parse_str( diff --git a/docs/architecture.md b/docs/architecture.md index d52e009..ddb911d 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -154,7 +154,9 @@ or toolchain source. Before instantiation the runtime validates the module, removes non-semantic debug/name sections, preserves required runtime metadata, and injects a mutable 64-bit weighted instruction meter. The -budget is present before a start section can execute. Static original-opcode counts and normalized +budget is present before a start section can execute. A function that has a loop but no parameters +or locals also gets one unused local: JavaScriptCore never optimizes such a loop and runs it about 20 +times slower than Chromium, so it would reach the wall deadline before its budget. Static original-opcode counts and normalized cost are reported separately from injected meter instructions. Contract 2 enforces: diff --git a/scripts/verify-browser-csp.mjs b/scripts/verify-browser-csp.mjs index a9b138a..56aade9 100644 --- a/scripts/verify-browser-csp.mjs +++ b/scripts/verify-browser-csp.mjs @@ -90,6 +90,7 @@ fixtures.push( { language:"c", label:"logical-clock-limit", source:'#include \n#include \nint main(){for(int i=0;i<10000;i++) clock();puts("42");}', input:"", termination:"logical-time-limit", resources:{logicalTimeLimitMs:1} }, { language:"c", label:"cpu-work", source:'#include \nint main(){volatile unsigned long long s=0;for(unsigned i=0;i<10000000;i++)s+=i;printf("%llu\\n",s);}', input:"", expected:"49999995000000\n" }, { language:"python", label:"memory-16mb", source:'print(42)', input:"", expected:"42\n", resources:{memoryLimitBytes:16*1024*1024} }, + { language:"cpp", label:"empty-loop-budget", source:'int main(){for(;;);}', input:"", termination:"instruction-limit", resources:{wallTimeLimitMs:15000} }, ); const guessInteractor = { language:"cpp", source:'#include \nint main(int argc,char**argv){std::FILE*f=argc>1?std::fopen(argv[1],"r"):nullptr;long long secret,guess;int limit;if(!f||std::fscanf(f,"%lld %d",&secret,&limit)!=2)return 3;for(int round=0;round");std::fflush(stdout);}return 1;}' }; const guessInput = secret => ({ args:["/judge/input.txt"], files:{"/judge/input.txt":`${secret} 25\n`} }); @@ -115,6 +116,7 @@ const interactiveFixtures = [ { label:"interactive-batch", contestant:{ language:"c", source:'#include \nint main(void){long long x;for(int i=0;i<30000;i++){if(scanf("%lld",&x)!=1)return 2;printf("%lld\\n",2*x);}fflush(stdout);char b[8];if(scanf("%7s",b)!=1)return 3;return b[0]==\'o\'?0:4;}' }, interactor:{ language:"c", source:'#include \nint main(void){for(int i=0;i<30000;i++)printf("%d\\n",1000000+i);fflush(stdout);for(int i=0;i<30000;i++){long long x;if(scanf("%lld",&x)!=1)return 2;if(x!=2LL*(1000000+i))return 1;}puts("ok");fflush(stdout);return 0;}' }, options:{ contestant:{ resources:{ wallTimeLimitMs:20000 } }, interactor:{ resources:{ wallTimeLimitMs:20000 } } }, check:result => exited(result.contestant, 0) && exited(result.interactor, 0) && result.contestantToInteractor.length === 240000 && result.interactorToContestant.length === 240003 && result.interactorToContestant.endsWith("ok\n") }, { label:"interactive-poll-clockless", contestant:{ language:"c", source:'#include \n#include \nint main(void){struct pollfd p[2]={{0,POLLIN,0},{1,POLLOUT,0}};if(poll(p,2,-1)<1||!(p[1].revents&POLLOUT))return 4;puts("ping");fflush(stdout);if(poll(p,1,-1)!=1||!(p[0].revents&POLLIN))return 5;char b[8];if(scanf("%7s",b)!=1)return 2;return b[0]==\'p\'&&b[1]==\'o\'?0:3;}' }, interactor:{ language:"c", source:'#include \nint main(void){char b[8];if(scanf("%7s",b)!=1)return 2;puts("pong");fflush(stdout);return 0;}' }, options:{ contestant:{ resources:{ wallTimeLimitMs:10000 } }, interactor:{ resources:{ wallTimeLimitMs:10000 } } }, check:result => exited(result.contestant, 0) && exited(result.interactor, 0) && result.contestantToInteractor === "ping\n" && result.interactorToContestant === "pong\n" }, { label:"interactive-instruction-limit", contestant:{ language:"cpp", source:'int main(){volatile unsigned long long spin=0;for(;;)spin=spin+1;}' }, interactor:guessInteractor, options:{ contestant:{ resources:{ wallTimeLimitMs:30000 } }, interactor:{ ...guessInput(1), resources:{ wallTimeLimitMs:30000 } } }, check:result => result.contestant.termination === "instruction-limit" && result.contestant.code === 137 && exited(result.interactor, 4) }, + { label:"interactive-empty-loop-budget", contestant:{ language:"cpp", source:'int main(){for(;;);}' }, interactor:readOne, options:{ contestant:{ resources:{ wallTimeLimitMs:15000 } }, interactor:{ resources:{ wallTimeLimitMs:15000 } } }, check:(result, elapsedMs) => result.contestant.termination === "instruction-limit" && result.contestant.code === 137 && exited(result.interactor, 1) && elapsedMs < 10000 }, { label:"interactive-contestant-exits", contestant:{ language:"c", source:'int main(void){return 0;}' }, interactor:guessInteractor, options:{ interactor:guessInput(1) }, check:result => exited(result.contestant, 0) && exited(result.interactor, 4) && result.contestantToInteractor === "" }, { label:"interactive-interactor-exits-eof", contestant:{ language:"c", source:'#include \n#include \nint main(void){char b[8];if(scanf("%7s",b)!=1||strcmp(b,"bye")!=0)return 2;return scanf("%7s",b)==EOF&&feof(stdin)?0:3;}' }, interactor:{ language:"c", source:'#include \nint main(void){puts("bye");return 0;}' }, options:{}, check:result => exited(result.contestant, 0) && exited(result.interactor, 0) && result.interactorToContestant === "bye\n" }, { label:"interactive-interactor-exits", contestant:{ language:"c", source:'#include \n#include \n#include \n#include \nint main(void){char b[8];if(scanf("%7s",b)!=1||strcmp(b,"bye")!=0)return 2;if(scanf("%7s",b)!=EOF)return 3;for(int i=0;i<3;i++)if(write(1,"x\\n",2)!=2)return errno==EPIPE?32:33;return 0;}' }, interactor:{ language:"c", source:'#include \nint main(void){puts("bye");return 0;}' }, options:{}, check:result => exited(result.contestant, 0) && exited(result.interactor, 0) && result.contestantToInteractor === "x\nx\nx\n" && result.interactorToContestant === "bye\n" }, diff --git a/src/core/runtime-identity.ts b/src/core/runtime-identity.ts index 2dca29e..1456ac5 100644 --- a/src/core/runtime-identity.ts +++ b/src/core/runtime-identity.ts @@ -3,8 +3,8 @@ import { sha256Hex } from "./sha256.ts"; /** Executable runtime components covered by deterministic cost calibration. */ export const WASM_OJ_RUNTIME_COMPONENTS = Object.freeze({ - runtimeCoreWasmSha256: "9fe6417ded9476957ed892eecffb9210fd99aafb3b55d0272ce5c123be43c1e6", - runtimeSourceRootSha256: "8d33f355c9fb44a2f22b501694280fcf64e2c490ea1f9b4ab1195ba2849b62df", + runtimeCoreWasmSha256: "96876c8c384b4c6489088302d2d5e9db91464fa171d0665e0e8009fb96fb9105", + runtimeSourceRootSha256: "da89f4eef0d1155e7eebeb0f0ee1d6b598e81c4d2ed2b784b299bf9181c38a72", wasmerNativeVersion: "7.2.1", wasmerSdkVersion: "0.10.0", wasmerSdkWasmSha256: "49a6646209f5ab5e7c737eac33407d87d9a9959ac83e5ecaaab9261b2323589e", @@ -17,7 +17,7 @@ export const WASM_OJ_RUNTIME_COMPONENTS = Object.freeze({ * identity is admitted into a calibrated release. */ export const WASM_OJ_RUNTIME_IDENTITY_SHA256 = - "9e4f094c5b11450beb19bde39c17fafb3c21475c0f3064f5fa4fecd75ce8c23a"; + "07d2d2bf91666336edb4cff570013c32d876307eda523f98545df67ee508805a"; /** Exact canonical serialization hashed by `WASM_OJ_RUNTIME_IDENTITY_SHA256`. */ export function runtimeIdentityBytes(): Uint8Array { diff --git a/src/runner/generated/runtime-core.js b/src/runner/generated/runtime-core.js index f3f45f0..7d31217 100644 --- a/src/runner/generated/runtime-core.js +++ b/src/runner/generated/runtime-core.js @@ -770,22 +770,22 @@ function __wbg_get_imports() { return ret; }, __wbindgen_cast_0000000000000001: function(arg0, arg1) { - // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [Externref], shim_idx: 6916, ret: Result(Unit), inner_ret: Some(Result(Unit)) }, mutable: true }) -> Externref`. + // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [Externref], shim_idx: 6920, ret: Result(Unit), inner_ret: Some(Result(Unit)) }, mutable: true }) -> Externref`. const ret = makeMutClosure(arg0, arg1, wasm_bindgen_68458880a41dd4bb___convert__closures_____invoke___wasm_bindgen_68458880a41dd4bb___JsValue__core_9b3796e30d99ddb7___result__Result_____wasm_bindgen_68458880a41dd4bb___JsError___true_); return ret; }, __wbindgen_cast_0000000000000002: function(arg0, arg1) { - // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [Ref(NamedExternref("Array"))], shim_idx: 2392, ret: Result(Externref), inner_ret: Some(Result(Externref)) }, mutable: true }) -> Externref`. + // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [Ref(NamedExternref("Array"))], shim_idx: 2393, ret: Result(Externref), inner_ret: Some(Result(Externref)) }, mutable: true }) -> Externref`. const ret = makeMutClosure(arg0, arg1, wasm_bindgen_68458880a41dd4bb___convert__closures________invoke___js_sys_c11fba41208799d1___Array__core_9b3796e30d99ddb7___result__Result_js_sys_c11fba41208799d1___Array__wasm_bindgen_68458880a41dd4bb___JsValue___true_); return ret; }, __wbindgen_cast_0000000000000003: function(arg0, arg1) { - // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [Ref(NamedExternref("Array"))], shim_idx: 2392, ret: Result(NamedExternref("Array")), inner_ret: Some(Result(NamedExternref("Array"))) }, mutable: true }) -> Externref`. + // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [Ref(NamedExternref("Array"))], shim_idx: 2393, ret: Result(NamedExternref("Array")), inner_ret: Some(Result(NamedExternref("Array"))) }, mutable: true }) -> Externref`. const ret = makeMutClosure(arg0, arg1, wasm_bindgen_68458880a41dd4bb___convert__closures________invoke___js_sys_c11fba41208799d1___Array__core_9b3796e30d99ddb7___result__Result_js_sys_c11fba41208799d1___Array__wasm_bindgen_68458880a41dd4bb___JsValue___true__2); return ret; }, __wbindgen_cast_0000000000000004: function(arg0, arg1) { - // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [Ref(NamedExternref("Array"))], shim_idx: 2393, ret: Result(Unit), inner_ret: Some(Result(Unit)) }, mutable: true }) -> Externref`. + // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [Ref(NamedExternref("Array"))], shim_idx: 2394, ret: Result(Unit), inner_ret: Some(Result(Unit)) }, mutable: true }) -> Externref`. const ret = makeMutClosure(arg0, arg1, wasm_bindgen_68458880a41dd4bb___convert__closures________invoke___js_sys_c11fba41208799d1___Array__core_9b3796e30d99ddb7___result__Result_____wasm_bindgen_68458880a41dd4bb___JsValue___true_); return ret; }, diff --git a/src/runner/generated/runtime-core_bg.wasm b/src/runner/generated/runtime-core_bg.wasm index a19d849..cd6f5a9 100644 --- a/src/runner/generated/runtime-core_bg.wasm +++ b/src/runner/generated/runtime-core_bg.wasm @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9fe6417ded9476957ed892eecffb9210fd99aafb3b55d0272ce5c123be43c1e6 -size 14585541 +oid sha256:96876c8c384b4c6489088302d2d5e9db91464fa171d0665e0e8009fb96fb9105 +size 14529506