From 1c7710707b31678329999cd79a07016e24ab98c4 Mon Sep 17 00:00:00 2001 From: cp20 <47262658+cp-20@users.noreply.github.com> Date: Fri, 9 Oct 2026 18:31:04 +0900 Subject: [PATCH 1/2] fix(eval): return compiled modules before namespace conversion --- c/interface.c | 7 +- .../quickjs-emscripten/src/quickjs.test.ts | 65 +++++++++++++++++++ 2 files changed, 69 insertions(+), 3 deletions(-) diff --git a/c/interface.c b/c/interface.c index 4546fc58f..da9d6c85f 100644 --- a/c/interface.c +++ b/c/interface.c @@ -1098,8 +1098,10 @@ MaybeAsync(JSValue *) QTS_Eval(JSContext *ctx, BorrowedHeapChar *js_code, size_t // Error - nothing more to do. JS_IsException(eval_result) // Non-module eval - return the result - || (evalFlags & JS_EVAL_TYPE_MODULE) == 0) { - QTS_DEBUG("QTS_Eval: non-module or exception") + || (evalFlags & JS_EVAL_TYPE_MODULE) == 0 + // Compiled code has not been evaluated; return it without accessing module. + || (evalFlags & JS_EVAL_FLAG_COMPILE_ONLY) != 0) { + QTS_DEBUG("QTS_Eval: non-module, compile-only, or exception") return jsvalue_to_heap(eval_result); } @@ -1114,7 +1116,6 @@ MaybeAsync(JSValue *) QTS_Eval(JSContext *ctx, BorrowedHeapChar *js_code, size_t // quickjs@2024-01-14 evaluating module // produced a promise (state == JS_PROMISE_FULFILLED) - // quickjs in compile mode // quickjs-ng before rebasing on quickjs@2024-01-14 // not a promise. || (state == -1)) { diff --git a/packages/quickjs-emscripten/src/quickjs.test.ts b/packages/quickjs-emscripten/src/quickjs.test.ts index f603c256c..495f8e64f 100644 --- a/packages/quickjs-emscripten/src/quickjs.test.ts +++ b/packages/quickjs-emscripten/src/quickjs.test.ts @@ -541,6 +541,49 @@ function contextTests(getContext: GetTestContext, options: ContextTestOptions = }) describe(".evalCode", () => { + describe("compileOnly", () => { + for (const type of ["global", "module"] as const) { + it(`${type}: returns disposable compiled code without executing it`, () => { + vm.unwrapResult(vm.evalCode("globalThis.compileOnlyRuns = 0")).dispose() + const code = + type === "module" + ? "globalThis.compileOnlyRuns++; export const value = 42" + : "globalThis.compileOnlyRuns++; 42" + const compiled = vm.unwrapResult( + vm.evalCode(code, "compiled.js", { type, compileOnly: true }), + ) + assert.equal(vm.typeof(compiled), "unknown") + compiled.dispose() + assert.equal(vm.getProp(vm.global, "compileOnlyRuns").consume(vm.getNumber), 0) + }) + } + + it("module: preserves syntax errors", () => { + const result = vm.evalCode("export const =", "invalid.js", { + type: "module", + compileOnly: true, + }) + assert(result.error, "Compilation should return a syntax error") + assert.equal(result.error.consume(vm.dump).name, "SyntaxError") + }) + + it("module: can compile, interrupt evaluation, and dispose", () => { + const code = "while (true) {}" + vm.unwrapResult( + vm.evalCode(code, "compiled.js", { type: "module", compileOnly: true }), + ).dispose() + let calls = 0 + vm.runtime.setInterruptHandler(() => ++calls > 10) + const result = vm.evalCode(code, "executed.js", { type: "module" }) + assert(result.error, "Evaluation should be interrupted") + assertError(result.error.consume(vm.dump), { + name: "InternalError", + message: "interrupted", + }) + assert(calls > 10, "Reached the interrupt budget") + }) + }) + it("on success: returns { value: success }", () => { const value = vm.unwrapResult(vm.evalCode(`["this", "should", "work"].join(' ')`)) assert.equal(vm.getString(value), "this should work") @@ -1268,6 +1311,28 @@ function asyncContextTests( }) describe("module loading", () => { + it("compileOnly: returns disposable compiled code without executing it", async () => { + vm.unwrapResult(vm.evalCode("globalThis.compileOnlyRuns = 0")).dispose() + const result = await vm.evalCodeAsync( + "globalThis.compileOnlyRuns++; export const value = 42", + "compiled.js", + { type: "module", compileOnly: true }, + ) + const compiled = vm.unwrapResult(result) + assert.equal(vm.typeof(compiled), "unknown") + compiled.dispose() + assert.equal(vm.getProp(vm.global, "compileOnlyRuns").consume(vm.getNumber), 0) + }) + + it("compileOnly: preserves syntax errors", async () => { + const result = await vm.evalCodeAsync("export const =", "invalid.js", { + type: "module", + compileOnly: true, + }) + assert(result.error, "Compilation should return a syntax error") + assert.equal(result.error.consume(vm.dump).name, "SyntaxError") + }) + it("supports async module loading", async () => { async function testBody() { let moduleLoaderCalls = 0 From 6a7eaf80aa9c6b0a21b7117000092c72436498f8 Mon Sep 17 00:00:00 2001 From: cp20 <47262658+cp-20@users.noreply.github.com> Date: Fri, 9 Oct 2026 18:41:02 +0900 Subject: [PATCH 2/2] test(eval): drop interrupt case that passes without the fix --- .../quickjs-emscripten/src/quickjs.test.ts | 18 ++---------------- 1 file changed, 2 insertions(+), 16 deletions(-) diff --git a/packages/quickjs-emscripten/src/quickjs.test.ts b/packages/quickjs-emscripten/src/quickjs.test.ts index 495f8e64f..1cfcb6c4a 100644 --- a/packages/quickjs-emscripten/src/quickjs.test.ts +++ b/packages/quickjs-emscripten/src/quickjs.test.ts @@ -552,6 +552,7 @@ function contextTests(getContext: GetTestContext, options: ContextTestOptions = const compiled = vm.unwrapResult( vm.evalCode(code, "compiled.js", { type, compileOnly: true }), ) + // Compiled code is bytecode, not a JS value, so it has no JS type. assert.equal(vm.typeof(compiled), "unknown") compiled.dispose() assert.equal(vm.getProp(vm.global, "compileOnlyRuns").consume(vm.getNumber), 0) @@ -566,22 +567,6 @@ function contextTests(getContext: GetTestContext, options: ContextTestOptions = assert(result.error, "Compilation should return a syntax error") assert.equal(result.error.consume(vm.dump).name, "SyntaxError") }) - - it("module: can compile, interrupt evaluation, and dispose", () => { - const code = "while (true) {}" - vm.unwrapResult( - vm.evalCode(code, "compiled.js", { type: "module", compileOnly: true }), - ).dispose() - let calls = 0 - vm.runtime.setInterruptHandler(() => ++calls > 10) - const result = vm.evalCode(code, "executed.js", { type: "module" }) - assert(result.error, "Evaluation should be interrupted") - assertError(result.error.consume(vm.dump), { - name: "InternalError", - message: "interrupted", - }) - assert(calls > 10, "Reached the interrupt budget") - }) }) it("on success: returns { value: success }", () => { @@ -1319,6 +1304,7 @@ function asyncContextTests( { type: "module", compileOnly: true }, ) const compiled = vm.unwrapResult(result) + // Compiled code is bytecode, not a JS value, so it has no JS type. assert.equal(vm.typeof(compiled), "unknown") compiled.dispose() assert.equal(vm.getProp(vm.global, "compileOnlyRuns").consume(vm.getNumber), 0)