diff --git a/src/core/postscript/wasm_compiler.js b/src/core/postscript/wasm_compiler.js index 1b552c3b877f2..bdc7b555fb9e5 100644 --- a/src/core/postscript/wasm_compiler.js +++ b/src/core/postscript/wasm_compiler.js @@ -283,6 +283,21 @@ class PsWasmCompiler { } while (n !== 0); } + // `i32.const` immediates are signed LEB128 (Wasm spec), so they must be + // emitted with sign extension — the unsigned encoder mis-encodes any value + // whose final 7-bit group has bit 0x40 set (e.g. 64 → 0x40 → decoded as −64). + _emitSLEB128(n) { + for (;;) { + const b = n & 0x7f; + n >>= 7; // arithmetic shift keeps the sign bit + if ((n === 0 && (b & 0x40) === 0) || (n === -1 && (b & 0x40) !== 0)) { + this._code.push(b); + return; + } + this._code.push(b | 0x80); + } + } + _emitF64Const(value) { this._code.push(OP.f64_const); PsWasmCompiler.#f64View.setFloat64(0, value, true /* little-endian */); @@ -532,11 +547,11 @@ class PsWasmCompiler { const shift = first.value; if (shift > 0) { code.push(OP.i32_const); - this._emitULEB128(shift); + this._emitSLEB128(shift); code.push(OP.i32_shl); } else if (shift < 0) { code.push(OP.i32_const); - this._emitULEB128(-shift); + this._emitSLEB128(-shift); code.push(OP.i32_shr_s); } code.push(OP.f64_convert_i32_s); @@ -870,7 +885,7 @@ class PsWasmCompiler { const min = this._range[i * 2]; const max = this._range[i * 2 + 1]; code.push(OP.i32_const); - this._emitULEB128(i * 8); + this._emitSLEB128(i * 8); if (!this._compileNode(outputs[i])) { return null; } diff --git a/test/pdfs/.gitignore b/test/pdfs/.gitignore index e25516a81e193..2d5724f6e7f2c 100644 --- a/test/pdfs/.gitignore +++ b/test/pdfs/.gitignore @@ -1,6 +1,7 @@ *.pdf *.error +!postscript_type4_many_outputs.pdf !boundingBox_invalid.pdf !pdkids.pdf !tracemonkey.pdf diff --git a/test/pdfs/postscript_type4_many_outputs.pdf b/test/pdfs/postscript_type4_many_outputs.pdf new file mode 100644 index 0000000000000..23d775b4490a4 Binary files /dev/null and b/test/pdfs/postscript_type4_many_outputs.pdf differ diff --git a/test/test_manifest.json b/test/test_manifest.json index c60a97fdfdd90..59433084a8f7c 100644 --- a/test/test_manifest.json +++ b/test/test_manifest.json @@ -1,4 +1,11 @@ [ + { + "id": "postscript-type4-many-outputs", + "file": "pdfs/postscript_type4_many_outputs.pdf", + "md5": "90c1ebd35acc6c7d01f4fdb0ae0bef82", + "rounds": 1, + "type": "eq" + }, { "id": "bug1755201", "file": "pdfs/bug1755201.pdf", diff --git a/test/unit/postscript_spec.js b/test/unit/postscript_spec.js index cdfdcc0b5d488..b58c277bab15e 100644 --- a/test/unit/postscript_spec.js +++ b/test/unit/postscript_spec.js @@ -1049,6 +1049,26 @@ describe("PostScript Type 4 lexer, parser, and Wasm compiler", function () { ); expect(r2).toBeCloseTo(0.5, 9); }); + + it("compiles functions with 9+ outputs (signed i32.const store offset)", async function () { + // Regression: each output's f64.store address is emitted as + // `i32.const (i * 8)`. i32.const immediates are *signed* LEB128, so the + // 9th output offset (64) must not be written with the unsigned encoder, + // which yields the byte 0x40 that Wasm decodes as -64 → out-of-bounds + // store → runtime trap. compileAndRun throws if the Wasm function traps. + for (const nOut of [9, 10, 16, 20]) { + const range = []; + for (let i = 0; i < nOut; i++) { + range.push(0, 1000); + } + const src = "{" + " dup".repeat(nOut - 1) + " }"; + const out = compileAndRun(src, [0, 1], range, [0.5]); + expect(out.length).toBe(nOut); + for (const value of out) { + expect(value).toBeCloseTo(0.5, 10); + } + } + }); }); // PSStackToTree