mirror of
https://github.com/mozilla/pdf.js.git
synced 2026-04-12 00:04:02 +02:00
Merge pull request #21010 from calixteman/ps_js
Add an interpreter for optimized ps code
This commit is contained in:
commit
399fce6471
@ -2136,6 +2136,22 @@ class PDFDocument {
|
||||
return obj;
|
||||
}
|
||||
|
||||
if (dict.get("FunctionType") === 4) {
|
||||
const source = value.getString();
|
||||
value.reset();
|
||||
const domain = dict.get("Domain") ?? [];
|
||||
const range = dict.get("Range") ?? [];
|
||||
obj.psFunction = true;
|
||||
obj.source = source;
|
||||
obj.psLines = InternalViewerUtils.tokenizePSSource(source);
|
||||
obj.jsCode = InternalViewerUtils.postScriptToJSCode(
|
||||
source,
|
||||
domain,
|
||||
range
|
||||
);
|
||||
return obj;
|
||||
}
|
||||
|
||||
obj.bytes = value.getString();
|
||||
return obj;
|
||||
}
|
||||
|
||||
@ -25,6 +25,7 @@ import {
|
||||
} from "../shared/util.js";
|
||||
import { PostScriptLexer, PostScriptParser } from "./ps_parser.js";
|
||||
import { BaseStream } from "./base_stream.js";
|
||||
import { buildPostScriptJsFunction } from "./postscript/js_evaluator.js";
|
||||
import { buildPostScriptWasmFunction } from "./postscript/wasm_compiler.js";
|
||||
import { isNumberArray } from "./core_utils.js";
|
||||
import { LocalFunctionCache } from "./image_utils.js";
|
||||
@ -370,8 +371,8 @@ class PDFFunction {
|
||||
throw new FormatError("No range.");
|
||||
}
|
||||
|
||||
if (factory.useWasm) {
|
||||
try {
|
||||
try {
|
||||
if (factory.useWasm) {
|
||||
const wasmFn = buildPostScriptWasmFunction(
|
||||
fn.getString(),
|
||||
domain,
|
||||
@ -380,9 +381,14 @@ class PDFFunction {
|
||||
if (wasmFn) {
|
||||
return wasmFn; // (src, srcOffset, dest, destOffset) → void
|
||||
}
|
||||
} catch {
|
||||
// Fall through to the existing interpreter-based path.
|
||||
} else {
|
||||
const jsFn = buildPostScriptJsFunction(fn.getString(), domain, range);
|
||||
if (jsFn) {
|
||||
return jsFn; // (src, srcOffset, dest, destOffset) → void
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Fall through to the existing interpreter-based path.
|
||||
}
|
||||
|
||||
warn("Unable to compile PS function, using interpreter");
|
||||
|
||||
@ -16,6 +16,13 @@
|
||||
import { Cmd, Dict, EOF, Name, Ref } from "./primitives.js";
|
||||
import { Lexer, Parser } from "./parser.js";
|
||||
import { OPS, shadow } from "../shared/util.js";
|
||||
import {
|
||||
parsePostScriptFunction,
|
||||
PS_NODE,
|
||||
PS_VALUE_TYPE,
|
||||
PSStackToTree,
|
||||
} from "./postscript/ast.js";
|
||||
import { Lexer as PsLexer, TOKEN } from "./postscript/lexer.js";
|
||||
import { BaseStream } from "./base_stream.js";
|
||||
import { EvaluatorPreprocessor } from "./evaluator.js";
|
||||
|
||||
@ -26,6 +33,262 @@ if (
|
||||
throw new Error("Not implemented: InternalViewerUtils");
|
||||
}
|
||||
|
||||
// JS operator precedence levels.
|
||||
const PREC = {
|
||||
ATOM: 100, // literals, identifiers, calls — never need parens
|
||||
UNARY: 14,
|
||||
POW: 13, // right-associative
|
||||
MUL: 12,
|
||||
ADD: 11,
|
||||
SHIFT: 10,
|
||||
CMP: 9,
|
||||
EQ: 8,
|
||||
BAND: 7,
|
||||
BXOR: 6,
|
||||
BOR: 5,
|
||||
TERNARY: 3,
|
||||
};
|
||||
|
||||
// Wrap left (or commutative) operand when child prec < op prec.
|
||||
function _wrapLeft(child, opPrec) {
|
||||
return child.prec < opPrec ? `(${child.expr})` : child.expr;
|
||||
}
|
||||
|
||||
// Wrap right operand (or left of **) when child prec <= op prec.
|
||||
function _wrapRight(child, opPrec) {
|
||||
return child.prec <= opPrec ? `(${child.expr})` : child.expr;
|
||||
}
|
||||
|
||||
function _nodeToExpr(node, argNames, cseMap) {
|
||||
if (cseMap?.has(node)) {
|
||||
return { expr: cseMap.get(node), prec: PREC.ATOM };
|
||||
}
|
||||
switch (node.type) {
|
||||
case PS_NODE.arg:
|
||||
return { expr: argNames[node.index], prec: PREC.ATOM };
|
||||
case PS_NODE.const: {
|
||||
const v = node.value;
|
||||
return {
|
||||
expr: String(typeof v === "boolean" ? Number(v) : v),
|
||||
prec: PREC.ATOM,
|
||||
};
|
||||
}
|
||||
case PS_NODE.unary:
|
||||
return _unaryToExpr(node, argNames, cseMap);
|
||||
case PS_NODE.binary:
|
||||
return _binaryToExpr(node, argNames, cseMap);
|
||||
case PS_NODE.ternary:
|
||||
return _ternaryToExpr(node, argNames, cseMap);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function _unaryToExpr(node, argNames, cseMap) {
|
||||
const { op, operand, valueType } = node;
|
||||
if (op === TOKEN.cvr) {
|
||||
return _nodeToExpr(operand, argNames, cseMap);
|
||||
}
|
||||
const x = _nodeToExpr(operand, argNames, cseMap);
|
||||
if (x === null) {
|
||||
return null;
|
||||
}
|
||||
switch (op) {
|
||||
case TOKEN.abs:
|
||||
return { expr: `Math.abs(${x.expr})`, prec: PREC.ATOM };
|
||||
case TOKEN.neg:
|
||||
return { expr: `-${_wrapLeft(x, PREC.UNARY)}`, prec: PREC.UNARY };
|
||||
case TOKEN.ceiling:
|
||||
return { expr: `Math.ceil(${x.expr})`, prec: PREC.ATOM };
|
||||
case TOKEN.floor:
|
||||
return { expr: `Math.floor(${x.expr})`, prec: PREC.ATOM };
|
||||
case TOKEN.round:
|
||||
return {
|
||||
expr: `Math.floor(${_wrapLeft(x, PREC.ADD)} + 0.5)`,
|
||||
prec: PREC.ATOM,
|
||||
};
|
||||
case TOKEN.truncate:
|
||||
return { expr: `Math.trunc(${x.expr})`, prec: PREC.ATOM };
|
||||
case TOKEN.sqrt:
|
||||
return { expr: `Math.sqrt(${x.expr})`, prec: PREC.ATOM };
|
||||
case TOKEN.sin:
|
||||
return {
|
||||
expr: `Math.sin(${_wrapLeft(x, PREC.MUL)} % 360 * (Math.PI / 180))`,
|
||||
prec: PREC.ATOM,
|
||||
};
|
||||
case TOKEN.cos:
|
||||
return {
|
||||
expr: `Math.cos(${_wrapLeft(x, PREC.MUL)} % 360 * (Math.PI / 180))`,
|
||||
prec: PREC.ATOM,
|
||||
};
|
||||
case TOKEN.ln:
|
||||
return { expr: `Math.log(${x.expr})`, prec: PREC.ATOM };
|
||||
case TOKEN.log:
|
||||
return { expr: `Math.log10(${x.expr})`, prec: PREC.ATOM };
|
||||
case TOKEN.cvi:
|
||||
return { expr: `(Math.trunc(${x.expr}) | 0)`, prec: PREC.ATOM };
|
||||
case TOKEN.not:
|
||||
if (valueType === PS_VALUE_TYPE.boolean) {
|
||||
return {
|
||||
expr: `(${_wrapLeft(x, PREC.EQ)} === 0 ? 1 : 0)`,
|
||||
prec: PREC.ATOM,
|
||||
};
|
||||
}
|
||||
if (valueType === PS_VALUE_TYPE.numeric) {
|
||||
return {
|
||||
expr: `~(${_wrapLeft(x, PREC.BOR)} | 0)`,
|
||||
prec: PREC.UNARY,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function _binaryToExpr(node, argNames, cseMap) {
|
||||
const { op, first, second } = node;
|
||||
if (op === TOKEN.bitshift) {
|
||||
if (first.type !== PS_NODE.const || !Number.isInteger(first.value)) {
|
||||
return null;
|
||||
}
|
||||
const s = _nodeToExpr(second, argNames, cseMap);
|
||||
if (s === null) {
|
||||
return null;
|
||||
}
|
||||
const amt = first.value;
|
||||
const base = `(${_wrapLeft(s, PREC.BOR)} | 0)`;
|
||||
if (amt > 0) {
|
||||
return { expr: `${base} << ${amt}`, prec: PREC.SHIFT };
|
||||
}
|
||||
if (amt < 0) {
|
||||
return { expr: `${base} >> ${-amt}`, prec: PREC.SHIFT };
|
||||
}
|
||||
return { expr: base, prec: PREC.ATOM };
|
||||
}
|
||||
// second is left operand (below on stack), first is right (top).
|
||||
const a = _nodeToExpr(second, argNames, cseMap);
|
||||
const b = _nodeToExpr(first, argNames, cseMap);
|
||||
if (a === null || b === null) {
|
||||
return null;
|
||||
}
|
||||
switch (op) {
|
||||
case TOKEN.add:
|
||||
return {
|
||||
expr: `${_wrapLeft(a, PREC.ADD)} + ${_wrapLeft(b, PREC.ADD)}`,
|
||||
prec: PREC.ADD,
|
||||
};
|
||||
case TOKEN.sub:
|
||||
return {
|
||||
expr: `${_wrapLeft(a, PREC.ADD)} - ${_wrapRight(b, PREC.ADD)}`,
|
||||
prec: PREC.ADD,
|
||||
};
|
||||
case TOKEN.mul:
|
||||
return {
|
||||
expr: `${_wrapLeft(a, PREC.MUL)} * ${_wrapLeft(b, PREC.MUL)}`,
|
||||
prec: PREC.MUL,
|
||||
};
|
||||
case TOKEN.div:
|
||||
return {
|
||||
expr:
|
||||
`(${b.expr} !== 0 ? ` +
|
||||
`${_wrapLeft(a, PREC.MUL)} / ${_wrapRight(b, PREC.MUL)} : 0)`,
|
||||
prec: PREC.ATOM,
|
||||
};
|
||||
case TOKEN.idiv:
|
||||
return {
|
||||
expr:
|
||||
`(${b.expr} !== 0 ? ` +
|
||||
`Math.trunc(${_wrapLeft(a, PREC.MUL)} / ${_wrapRight(b, PREC.MUL)}) : 0)`,
|
||||
prec: PREC.ATOM,
|
||||
};
|
||||
case TOKEN.mod:
|
||||
return {
|
||||
expr:
|
||||
`(${b.expr} !== 0 ? ` +
|
||||
`${_wrapLeft(a, PREC.MUL)} % ${_wrapRight(b, PREC.MUL)} : 0)`,
|
||||
prec: PREC.ATOM,
|
||||
};
|
||||
case TOKEN.exp:
|
||||
return {
|
||||
expr: `${_wrapRight(a, PREC.POW)} ** ${_wrapLeft(b, PREC.POW)}`,
|
||||
prec: PREC.POW,
|
||||
};
|
||||
case TOKEN.eq:
|
||||
return {
|
||||
expr: `(${_wrapLeft(a, PREC.CMP)} === ${_wrapLeft(b, PREC.CMP)} ? 1 : 0)`,
|
||||
prec: PREC.ATOM,
|
||||
};
|
||||
case TOKEN.ne:
|
||||
return {
|
||||
expr: `(${_wrapLeft(a, PREC.CMP)} !== ${_wrapLeft(b, PREC.CMP)} ? 1 : 0)`,
|
||||
prec: PREC.ATOM,
|
||||
};
|
||||
case TOKEN.gt:
|
||||
return {
|
||||
expr: `(${_wrapLeft(a, PREC.CMP)} > ${_wrapLeft(b, PREC.CMP)} ? 1 : 0)`,
|
||||
prec: PREC.ATOM,
|
||||
};
|
||||
case TOKEN.ge:
|
||||
return {
|
||||
expr: `(${_wrapLeft(a, PREC.CMP)} >= ${_wrapLeft(b, PREC.CMP)} ? 1 : 0)`,
|
||||
prec: PREC.ATOM,
|
||||
};
|
||||
case TOKEN.lt:
|
||||
return {
|
||||
expr: `(${_wrapLeft(a, PREC.CMP)} < ${_wrapLeft(b, PREC.CMP)} ? 1 : 0)`,
|
||||
prec: PREC.ATOM,
|
||||
};
|
||||
case TOKEN.le:
|
||||
return {
|
||||
expr: `(${_wrapLeft(a, PREC.CMP)} <= ${_wrapLeft(b, PREC.CMP)} ? 1 : 0)`,
|
||||
prec: PREC.ATOM,
|
||||
};
|
||||
case TOKEN.and:
|
||||
return {
|
||||
expr: `(${_wrapLeft(a, PREC.BOR)} | 0) & (${_wrapLeft(b, PREC.BOR)} | 0)`,
|
||||
prec: PREC.BAND,
|
||||
};
|
||||
case TOKEN.or:
|
||||
return {
|
||||
expr: `(${_wrapLeft(a, PREC.BOR)} | 0) | (${_wrapLeft(b, PREC.BOR)} | 0)`,
|
||||
prec: PREC.BOR,
|
||||
};
|
||||
case TOKEN.xor:
|
||||
return {
|
||||
expr: `(${_wrapLeft(a, PREC.BOR)} | 0) ^ (${_wrapLeft(b, PREC.BOR)} | 0)`,
|
||||
prec: PREC.BXOR,
|
||||
};
|
||||
case TOKEN.atan:
|
||||
// atan2 result in degrees [0, 360).
|
||||
return {
|
||||
expr: `(Math.atan2(${a.expr}, ${b.expr}) * (180 / Math.PI) + 360) % 360`,
|
||||
prec: PREC.MUL,
|
||||
};
|
||||
case TOKEN.min:
|
||||
return { expr: `Math.min(${a.expr}, ${b.expr})`, prec: PREC.ATOM };
|
||||
case TOKEN.max:
|
||||
return { expr: `Math.max(${a.expr}, ${b.expr})`, prec: PREC.ATOM };
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function _ternaryToExpr(node, argNames, cseMap) {
|
||||
const cond = _nodeToExpr(node.cond, argNames, cseMap);
|
||||
const then = _nodeToExpr(node.then, argNames, cseMap);
|
||||
const otherwise = _nodeToExpr(node.otherwise, argNames, cseMap);
|
||||
if (cond === null || then === null || otherwise === null) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
expr:
|
||||
`(${_wrapLeft(cond, PREC.EQ)} !== 0 ? ` +
|
||||
`${_wrapLeft(then, PREC.TERNARY)} : ${_wrapLeft(otherwise, PREC.TERNARY)})`,
|
||||
prec: PREC.ATOM,
|
||||
};
|
||||
}
|
||||
|
||||
const InternalViewerUtils = {
|
||||
tokenizeStream(stream, xref) {
|
||||
const tokens = [];
|
||||
@ -125,6 +388,130 @@ const InternalViewerUtils = {
|
||||
return { instructions, cmdNames };
|
||||
},
|
||||
|
||||
// Tokenize a PS Type 4 source into display lines: each line groups args with
|
||||
// the operator that consumes them; braces get their own indented lines.
|
||||
tokenizePSSource(source) {
|
||||
const lexer = new PsLexer(source);
|
||||
const lines = [];
|
||||
let indent = 0;
|
||||
let buffer = [];
|
||||
|
||||
const flush = () => {
|
||||
if (buffer.length > 0) {
|
||||
lines.push({ indent, tokens: buffer });
|
||||
buffer = [];
|
||||
}
|
||||
};
|
||||
|
||||
while (true) {
|
||||
const tok = lexer.next();
|
||||
if (tok.id === TOKEN.eof) {
|
||||
break;
|
||||
}
|
||||
if (tok.id === TOKEN.lbrace) {
|
||||
flush();
|
||||
lines.push({ indent, tokens: [{ type: "brace", value: "{" }] });
|
||||
indent++;
|
||||
} else if (tok.id === TOKEN.rbrace) {
|
||||
flush();
|
||||
indent = Math.max(0, indent - 1);
|
||||
lines.push({ indent, tokens: [{ type: "brace", value: "}" }] });
|
||||
} else if (tok.id === TOKEN.number) {
|
||||
buffer.push({ type: "number", value: tok.value });
|
||||
} else if (tok.id === TOKEN.true) {
|
||||
buffer.push({ type: "boolean", value: true });
|
||||
} else if (tok.id === TOKEN.false) {
|
||||
buffer.push({ type: "boolean", value: false });
|
||||
} else if (tok.value !== null) {
|
||||
buffer.push({ type: "cmd", value: tok.value });
|
||||
flush();
|
||||
}
|
||||
}
|
||||
flush();
|
||||
return lines;
|
||||
},
|
||||
|
||||
postScriptToJSCode(source, domain, range) {
|
||||
const program = parsePostScriptFunction(source);
|
||||
const nIn = domain.length >> 1;
|
||||
const nOut = range.length >> 1;
|
||||
const outputs = new PSStackToTree().evaluate(program, nIn);
|
||||
if (!outputs || outputs.length < nOut) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Named input variables: single input → "x", multiple → "x0", "x1", …
|
||||
const argNames =
|
||||
nIn === 1 ? ["x"] : Array.from({ length: nIn }, (_, i) => `x${i}`);
|
||||
|
||||
// Build cseMap in topological order using shared marks from the AST.
|
||||
const cseMap = new Map();
|
||||
const tmpDecls = [];
|
||||
let tmpIdx = 0;
|
||||
const visited = new Set();
|
||||
const ensureShared = node => {
|
||||
if (
|
||||
!node ||
|
||||
node.type === PS_NODE.arg ||
|
||||
node.type === PS_NODE.const ||
|
||||
visited.has(node)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
visited.add(node);
|
||||
switch (node.type) {
|
||||
case PS_NODE.unary:
|
||||
ensureShared(node.operand);
|
||||
break;
|
||||
case PS_NODE.binary:
|
||||
ensureShared(node.first);
|
||||
ensureShared(node.second);
|
||||
break;
|
||||
case PS_NODE.ternary:
|
||||
ensureShared(node.cond);
|
||||
ensureShared(node.then);
|
||||
ensureShared(node.otherwise);
|
||||
break;
|
||||
}
|
||||
if (node.shared) {
|
||||
const result = _nodeToExpr(node, argNames, cseMap);
|
||||
if (result !== null) {
|
||||
const name = `t${tmpIdx++}`;
|
||||
cseMap.set(node, name);
|
||||
tmpDecls.push(` const ${name} = ${result.expr};`);
|
||||
}
|
||||
}
|
||||
};
|
||||
for (let i = 0; i < nOut; i++) {
|
||||
ensureShared(outputs[i]);
|
||||
}
|
||||
|
||||
const decls = argNames.map(
|
||||
(name, i) => ` const ${name} = src[srcOffset + ${i}];`
|
||||
);
|
||||
|
||||
const assignments = [];
|
||||
for (let i = 0; i < nOut; i++) {
|
||||
const result = _nodeToExpr(outputs[i], argNames, cseMap);
|
||||
if (result === null) {
|
||||
return null;
|
||||
}
|
||||
const min = range[i * 2];
|
||||
const max = range[i * 2 + 1];
|
||||
assignments.push(
|
||||
` dest[destOffset + ${i}] = ` +
|
||||
`Math.max(Math.min(${result.expr}, ${max}), ${min});`
|
||||
);
|
||||
}
|
||||
|
||||
const lines = [...decls, ""];
|
||||
if (tmpDecls.length > 0) {
|
||||
lines.push(...tmpDecls, "");
|
||||
}
|
||||
lines.push(...assignments);
|
||||
return `(src, srcOffset, dest, destOffset) => {\n${lines.join("\n")}\n}`;
|
||||
},
|
||||
|
||||
tokenToJSObject(obj) {
|
||||
if (obj instanceof Cmd) {
|
||||
return { type: "cmd", value: obj.cmd };
|
||||
|
||||
@ -664,7 +664,7 @@ class PSStackToTree {
|
||||
for (const [node, count] of refCount) {
|
||||
if (count > 1) {
|
||||
node.shared = true;
|
||||
node.sharedCount = count; // remaining-use tracking in backends
|
||||
node.sharedCount = count;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
550
src/core/postscript/js_evaluator.js
Normal file
550
src/core/postscript/js_evaluator.js
Normal file
@ -0,0 +1,550 @@
|
||||
/* Copyright 2026 Mozilla Foundation
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
parsePostScriptFunction,
|
||||
PS_NODE,
|
||||
PS_VALUE_TYPE,
|
||||
PSStackToTree,
|
||||
} from "./ast.js";
|
||||
import { TOKEN } from "./lexer.js";
|
||||
|
||||
// Consecutive integers for a dense jump table in _execute.
|
||||
// 2-slot: ARG, CONST, IF, JUMP, SHIFT. 4-slot: STORE. All others: 1 slot.
|
||||
const OP = {
|
||||
ARG: 0, // [ARG, idx]
|
||||
CONST: 1, // [CONST, val]
|
||||
STORE: 2, // [STORE, slot, min, max] clamp(pop()) → mem[slot]
|
||||
IF: 3, // [IF, target] jump when top-of-stack === 0
|
||||
JUMP: 4, // [JUMP, target] unconditional
|
||||
ABS: 5,
|
||||
NEG: 6,
|
||||
CEIL: 7,
|
||||
FLOOR: 8,
|
||||
ROUND: 9, // floor(x + 0.5)
|
||||
TRUNC: 10,
|
||||
NOT_B: 11, // boolean NOT
|
||||
NOT_N: 12, // bitwise NOT
|
||||
SQRT: 13,
|
||||
SIN: 14, // degrees in/out
|
||||
COS: 15,
|
||||
LN: 16,
|
||||
LOG10: 17,
|
||||
CVI: 18,
|
||||
SHIFT: 19, // [SHIFT, amount] +ve = left, −ve = right
|
||||
// Binary ops: second below, first on top; result = second OP first.
|
||||
ADD: 20,
|
||||
SUB: 21,
|
||||
MUL: 22,
|
||||
DIV: 23, // 0 when divisor is 0
|
||||
IDIV: 24, // 0 when divisor is 0
|
||||
MOD: 25, // 0 when divisor is 0
|
||||
POW: 26,
|
||||
EQ: 27,
|
||||
NE: 28,
|
||||
GT: 29,
|
||||
GE: 30,
|
||||
LT: 31,
|
||||
LE: 32,
|
||||
AND: 33,
|
||||
OR: 34,
|
||||
XOR: 35,
|
||||
ATAN: 36, // atan2(second, first) → degrees [0, 360)
|
||||
MIN: 37,
|
||||
MAX: 38,
|
||||
TEE_TMP: 39, // [TEE_TMP, slot] peek top of stack → tmp[slot], leave on stack
|
||||
LOAD_TMP: 40, // [LOAD_TMP, slot] push tmp[slot]
|
||||
};
|
||||
|
||||
const _DEG_TO_RAD = Math.PI / 180;
|
||||
const _RAD_TO_DEG = 180 / Math.PI;
|
||||
|
||||
class PsJsCompiler {
|
||||
// Safe because JS is single-threaded.
|
||||
static #stack = new Float64Array(64);
|
||||
|
||||
static #tmp = new Float64Array(64);
|
||||
|
||||
constructor(domain, range) {
|
||||
this.nIn = domain.length >> 1;
|
||||
this.nOut = range.length >> 1;
|
||||
this.range = range;
|
||||
this.ir = [];
|
||||
this._tmpMap = new Map(); // node → tmp slot index (CSE)
|
||||
this._nextTmp = 0;
|
||||
}
|
||||
|
||||
_compileNode(node) {
|
||||
if (node.shared) {
|
||||
const cached = this._tmpMap.get(node);
|
||||
if (cached !== undefined) {
|
||||
this.ir.push(OP.LOAD_TMP, cached);
|
||||
return true;
|
||||
}
|
||||
if (!this._compileNodeImpl(node)) {
|
||||
return false;
|
||||
}
|
||||
const slot = this._nextTmp++;
|
||||
this._tmpMap.set(node, slot);
|
||||
this.ir.push(OP.TEE_TMP, slot);
|
||||
return true;
|
||||
}
|
||||
return this._compileNodeImpl(node);
|
||||
}
|
||||
|
||||
_compileNodeImpl(node) {
|
||||
switch (node.type) {
|
||||
case PS_NODE.arg:
|
||||
this.ir.push(OP.ARG, node.index);
|
||||
return true;
|
||||
|
||||
case PS_NODE.const: {
|
||||
const v = node.value;
|
||||
this.ir.push(OP.CONST, typeof v === "boolean" ? Number(v) : v);
|
||||
return true;
|
||||
}
|
||||
|
||||
case PS_NODE.unary:
|
||||
return this._compileUnary(node);
|
||||
|
||||
case PS_NODE.binary:
|
||||
return this._compileBinary(node);
|
||||
|
||||
case PS_NODE.ternary:
|
||||
return this._compileTernary(node);
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
_compileUnary(node) {
|
||||
const { op, operand, valueType } = node;
|
||||
|
||||
// cvr is a no-op — values are already f64.
|
||||
if (op === TOKEN.cvr) {
|
||||
return this._compileNode(operand);
|
||||
}
|
||||
|
||||
if (!this._compileNode(operand)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (op) {
|
||||
case TOKEN.abs:
|
||||
this.ir.push(OP.ABS);
|
||||
break;
|
||||
case TOKEN.neg:
|
||||
this.ir.push(OP.NEG);
|
||||
break;
|
||||
case TOKEN.ceiling:
|
||||
this.ir.push(OP.CEIL);
|
||||
break;
|
||||
case TOKEN.floor:
|
||||
this.ir.push(OP.FLOOR);
|
||||
break;
|
||||
case TOKEN.round:
|
||||
this.ir.push(OP.ROUND);
|
||||
break;
|
||||
case TOKEN.truncate:
|
||||
this.ir.push(OP.TRUNC);
|
||||
break;
|
||||
case TOKEN.sqrt:
|
||||
this.ir.push(OP.SQRT);
|
||||
break;
|
||||
case TOKEN.sin:
|
||||
this.ir.push(OP.SIN);
|
||||
break;
|
||||
case TOKEN.cos:
|
||||
this.ir.push(OP.COS);
|
||||
break;
|
||||
case TOKEN.ln:
|
||||
this.ir.push(OP.LN);
|
||||
break;
|
||||
case TOKEN.log:
|
||||
this.ir.push(OP.LOG10);
|
||||
break;
|
||||
case TOKEN.cvi:
|
||||
this.ir.push(OP.CVI);
|
||||
break;
|
||||
case TOKEN.not:
|
||||
if (valueType === PS_VALUE_TYPE.boolean) {
|
||||
this.ir.push(OP.NOT_B);
|
||||
} else if (valueType === PS_VALUE_TYPE.numeric) {
|
||||
this.ir.push(OP.NOT_N);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
_compileBinary(node) {
|
||||
const { op, first, second } = node;
|
||||
|
||||
// bitshift requires a constant shift amount.
|
||||
if (op === TOKEN.bitshift) {
|
||||
if (first.type !== PS_NODE.const || !Number.isInteger(first.value)) {
|
||||
return false;
|
||||
}
|
||||
if (!this._compileNode(second)) {
|
||||
return false;
|
||||
}
|
||||
this.ir.push(OP.SHIFT, first.value);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!this._compileNode(second)) {
|
||||
return false;
|
||||
}
|
||||
if (!this._compileNode(first)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (op) {
|
||||
case TOKEN.add:
|
||||
this.ir.push(OP.ADD);
|
||||
break;
|
||||
case TOKEN.sub:
|
||||
this.ir.push(OP.SUB);
|
||||
break;
|
||||
case TOKEN.mul:
|
||||
this.ir.push(OP.MUL);
|
||||
break;
|
||||
case TOKEN.div:
|
||||
this.ir.push(OP.DIV);
|
||||
break;
|
||||
case TOKEN.idiv:
|
||||
this.ir.push(OP.IDIV);
|
||||
break;
|
||||
case TOKEN.mod:
|
||||
this.ir.push(OP.MOD);
|
||||
break;
|
||||
case TOKEN.exp:
|
||||
this.ir.push(OP.POW);
|
||||
break;
|
||||
case TOKEN.eq:
|
||||
this.ir.push(OP.EQ);
|
||||
break;
|
||||
case TOKEN.ne:
|
||||
this.ir.push(OP.NE);
|
||||
break;
|
||||
case TOKEN.gt:
|
||||
this.ir.push(OP.GT);
|
||||
break;
|
||||
case TOKEN.ge:
|
||||
this.ir.push(OP.GE);
|
||||
break;
|
||||
case TOKEN.lt:
|
||||
this.ir.push(OP.LT);
|
||||
break;
|
||||
case TOKEN.le:
|
||||
this.ir.push(OP.LE);
|
||||
break;
|
||||
case TOKEN.and:
|
||||
this.ir.push(OP.AND);
|
||||
break;
|
||||
case TOKEN.or:
|
||||
this.ir.push(OP.OR);
|
||||
break;
|
||||
case TOKEN.xor:
|
||||
this.ir.push(OP.XOR);
|
||||
break;
|
||||
case TOKEN.atan:
|
||||
this.ir.push(OP.ATAN);
|
||||
break;
|
||||
case TOKEN.min:
|
||||
this.ir.push(OP.MIN);
|
||||
break;
|
||||
case TOKEN.max:
|
||||
this.ir.push(OP.MAX);
|
||||
break;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
_compileTernary(node) {
|
||||
if (!this._compileNode(node.cond)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.ir.push(OP.IF, 0);
|
||||
const ifPatch = this.ir.length - 1;
|
||||
|
||||
if (!this._compileNode(node.then)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.ir.push(OP.JUMP, 0);
|
||||
const jumpPatch = this.ir.length - 1;
|
||||
|
||||
this.ir[ifPatch] = this.ir.length; // IF jumps here on false
|
||||
if (!this._compileNode(node.otherwise)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.ir[jumpPatch] = this.ir.length; // JUMP lands here
|
||||
return true;
|
||||
}
|
||||
|
||||
compile(program) {
|
||||
const outputs = new PSStackToTree().evaluate(program, this.nIn);
|
||||
if (!outputs || outputs.length < this.nOut) {
|
||||
return null;
|
||||
}
|
||||
|
||||
for (let i = 0; i < this.nOut; i++) {
|
||||
if (!this._compileNode(outputs[i])) {
|
||||
return null;
|
||||
}
|
||||
const min = this.range[i * 2];
|
||||
const max = this.range[i * 2 + 1];
|
||||
this.ir.push(OP.STORE, i, min, max);
|
||||
}
|
||||
|
||||
return new Float64Array(this.ir);
|
||||
}
|
||||
|
||||
static execute(ir, src, srcOffset, dest, destOffset) {
|
||||
let ip = 0,
|
||||
sp = 0;
|
||||
const n = ir.length;
|
||||
const stack = PsJsCompiler.#stack;
|
||||
const tmp = PsJsCompiler.#tmp;
|
||||
|
||||
while (ip < n) {
|
||||
switch (ir[ip++] | 0) {
|
||||
case OP.ARG:
|
||||
stack[sp++] = src[srcOffset + (ir[ip++] | 0)];
|
||||
break;
|
||||
case OP.CONST:
|
||||
stack[sp++] = ir[ip++];
|
||||
break;
|
||||
case OP.STORE: {
|
||||
const slot = ir[ip++] | 0;
|
||||
const min = ir[ip++];
|
||||
const max = ir[ip++];
|
||||
dest[destOffset + slot] = Math.max(Math.min(stack[--sp], max), min);
|
||||
break;
|
||||
}
|
||||
case OP.IF: {
|
||||
const tgt = ir[ip++];
|
||||
if (stack[--sp] === 0) {
|
||||
ip = tgt;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case OP.JUMP:
|
||||
ip = ir[ip];
|
||||
break;
|
||||
case OP.ABS:
|
||||
stack[sp - 1] = Math.abs(stack[sp - 1]);
|
||||
break;
|
||||
case OP.NEG:
|
||||
stack[sp - 1] = -stack[sp - 1];
|
||||
break;
|
||||
case OP.CEIL:
|
||||
stack[sp - 1] = Math.ceil(stack[sp - 1]);
|
||||
break;
|
||||
case OP.FLOOR:
|
||||
stack[sp - 1] = Math.floor(stack[sp - 1]);
|
||||
break;
|
||||
case OP.ROUND:
|
||||
stack[sp - 1] = Math.floor(stack[sp - 1] + 0.5);
|
||||
break;
|
||||
case OP.TRUNC:
|
||||
stack[sp - 1] = Math.trunc(stack[sp - 1]);
|
||||
break;
|
||||
case OP.NOT_B:
|
||||
stack[sp - 1] = stack[sp - 1] !== 0 ? 0 : 1;
|
||||
break;
|
||||
case OP.NOT_N:
|
||||
stack[sp - 1] = ~(stack[sp - 1] | 0);
|
||||
break;
|
||||
case OP.SQRT:
|
||||
stack[sp - 1] = Math.sqrt(stack[sp - 1]);
|
||||
break;
|
||||
case OP.SIN:
|
||||
stack[sp - 1] = Math.sin((stack[sp - 1] % 360) * _DEG_TO_RAD);
|
||||
break;
|
||||
case OP.COS:
|
||||
stack[sp - 1] = Math.cos((stack[sp - 1] % 360) * _DEG_TO_RAD);
|
||||
break;
|
||||
case OP.LN:
|
||||
stack[sp - 1] = Math.log(stack[sp - 1]);
|
||||
break;
|
||||
case OP.LOG10:
|
||||
stack[sp - 1] = Math.log10(stack[sp - 1]);
|
||||
break;
|
||||
case OP.CVI:
|
||||
stack[sp - 1] = Math.trunc(stack[sp - 1]) | 0;
|
||||
break;
|
||||
case OP.SHIFT: {
|
||||
const amt = ir[ip++];
|
||||
const v = stack[sp - 1] | 0;
|
||||
if (amt > 0) {
|
||||
stack[sp - 1] = v << amt;
|
||||
} else if (amt < 0) {
|
||||
stack[sp - 1] = v >> -amt;
|
||||
} else {
|
||||
stack[sp - 1] = v;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case OP.ADD: {
|
||||
const b = stack[--sp];
|
||||
stack[sp - 1] += b;
|
||||
break;
|
||||
}
|
||||
case OP.SUB: {
|
||||
const b = stack[--sp];
|
||||
stack[sp - 1] -= b;
|
||||
break;
|
||||
}
|
||||
case OP.MUL: {
|
||||
const b = stack[--sp];
|
||||
stack[sp - 1] *= b;
|
||||
break;
|
||||
}
|
||||
case OP.DIV: {
|
||||
const b = stack[--sp];
|
||||
stack[sp - 1] = b !== 0 ? stack[sp - 1] / b : 0;
|
||||
break;
|
||||
}
|
||||
case OP.IDIV: {
|
||||
const b = stack[--sp];
|
||||
stack[sp - 1] = b !== 0 ? Math.trunc(stack[sp - 1] / b) : 0;
|
||||
break;
|
||||
}
|
||||
case OP.MOD: {
|
||||
const b = stack[--sp];
|
||||
stack[sp - 1] = b !== 0 ? stack[sp - 1] % b : 0;
|
||||
break;
|
||||
}
|
||||
case OP.POW: {
|
||||
const b = stack[--sp];
|
||||
stack[sp - 1] **= b;
|
||||
break;
|
||||
}
|
||||
case OP.EQ: {
|
||||
const b = stack[--sp];
|
||||
stack[sp - 1] = stack[sp - 1] === b ? 1 : 0;
|
||||
break;
|
||||
}
|
||||
case OP.NE: {
|
||||
const b = stack[--sp];
|
||||
stack[sp - 1] = stack[sp - 1] !== b ? 1 : 0;
|
||||
break;
|
||||
}
|
||||
case OP.GT: {
|
||||
const b = stack[--sp];
|
||||
stack[sp - 1] = stack[sp - 1] > b ? 1 : 0;
|
||||
break;
|
||||
}
|
||||
case OP.GE: {
|
||||
const b = stack[--sp];
|
||||
stack[sp - 1] = stack[sp - 1] >= b ? 1 : 0;
|
||||
break;
|
||||
}
|
||||
case OP.LT: {
|
||||
const b = stack[--sp];
|
||||
stack[sp - 1] = stack[sp - 1] < b ? 1 : 0;
|
||||
break;
|
||||
}
|
||||
case OP.LE: {
|
||||
const b = stack[--sp];
|
||||
stack[sp - 1] = stack[sp - 1] <= b ? 1 : 0;
|
||||
break;
|
||||
}
|
||||
case OP.AND: {
|
||||
const b = stack[--sp] | 0;
|
||||
stack[sp - 1] = (stack[sp - 1] | 0) & b;
|
||||
break;
|
||||
}
|
||||
case OP.OR: {
|
||||
const b = stack[--sp] | 0;
|
||||
stack[sp - 1] = stack[sp - 1] | 0 | b;
|
||||
break;
|
||||
}
|
||||
case OP.XOR: {
|
||||
const b = stack[--sp] | 0;
|
||||
stack[sp - 1] = (stack[sp - 1] | 0) ^ b;
|
||||
break;
|
||||
}
|
||||
case OP.ATAN: {
|
||||
const b = stack[--sp];
|
||||
const deg = Math.atan2(stack[sp - 1], b) * _RAD_TO_DEG;
|
||||
stack[sp - 1] = deg < 0 ? deg + 360 : deg;
|
||||
break;
|
||||
}
|
||||
case OP.MIN: {
|
||||
const b = stack[--sp];
|
||||
stack[sp - 1] = Math.min(stack[sp - 1], b);
|
||||
break;
|
||||
}
|
||||
case OP.MAX: {
|
||||
const b = stack[--sp];
|
||||
stack[sp - 1] = Math.max(stack[sp - 1], b);
|
||||
break;
|
||||
}
|
||||
case OP.TEE_TMP:
|
||||
tmp[ir[ip++] | 0] = stack[sp - 1];
|
||||
break;
|
||||
case OP.LOAD_TMP:
|
||||
stack[sp++] = tmp[ir[ip++] | 0];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} source
|
||||
* @param {number[]} domain – flat [min0,max0, …]
|
||||
* @param {number[]} range – flat [min0,max0, …]
|
||||
* @returns {Float64Array|null}
|
||||
*/
|
||||
function compilePostScriptToIR(source, domain, range) {
|
||||
return new PsJsCompiler(domain, range).compile(
|
||||
parsePostScriptFunction(source)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Same calling convention as the Wasm wrapper:
|
||||
* fn(src, srcOffset, dest, destOffset)
|
||||
*
|
||||
* @param {string} source
|
||||
* @param {number[]} domain – flat [min0,max0, …]
|
||||
* @param {number[]} range – flat [min0,max0, …]
|
||||
* @returns {Function|null}
|
||||
*/
|
||||
function buildPostScriptJsFunction(source, domain, range) {
|
||||
const ir = compilePostScriptToIR(source, domain, range);
|
||||
if (!ir) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (src, srcOffset, dest, destOffset) => {
|
||||
PsJsCompiler.execute(ir, src, srcOffset, dest, destOffset);
|
||||
};
|
||||
}
|
||||
|
||||
export { buildPostScriptJsFunction, compilePostScriptToIR };
|
||||
@ -138,13 +138,8 @@ const _mathImportObject = {
|
||||
Math: Object.fromEntries(MATH_IMPORTS.map(([name]) => [name, Math[name]])),
|
||||
};
|
||||
|
||||
// The compiler.
|
||||
//
|
||||
// After PSStackToTree converts the parser AST into a stack-free expression
|
||||
// tree, the compiler walks each output tree node recursively and emits Wasm
|
||||
// instructions that leave exactly one f64 value on the Wasm operand stack.
|
||||
// PsTernaryNode compiles to a value-returning `if/else/end` block — no
|
||||
// branch-buffer swapping or local-merging is needed.
|
||||
// Walks each PSStackToTree output node and emits Wasm, leaving one f64 per
|
||||
// output on the Wasm operand stack. Ternary nodes compile to if/else/end.
|
||||
class PsWasmCompiler {
|
||||
static #initialized = false;
|
||||
|
||||
@ -257,11 +252,12 @@ class PsWasmCompiler {
|
||||
this._nOut = range.length >> 1;
|
||||
this._range = range;
|
||||
this._code = [];
|
||||
// Params 0..nIn-1 are automatically locals; extras start at _nextLocal.
|
||||
|
||||
// Params 0..nIn-1 are locals; extra locals start at _nextLocal.
|
||||
this._nextLocal = this._nIn;
|
||||
|
||||
this._freeLocals = [];
|
||||
// node → {local, remaining} for shared sub-expression caching (CSE).
|
||||
this._sharedLocals = new Map();
|
||||
this._sharedLocals = new Map(); // node → {local, remaining} for CSE
|
||||
}
|
||||
|
||||
// Wasm emit helpers
|
||||
|
||||
@ -35,6 +35,7 @@ import {
|
||||
PsTernaryNode,
|
||||
PsUnaryNode,
|
||||
} from "../../src/core/postscript/ast.js";
|
||||
import { buildPostScriptJsFunction } from "../../src/core/postscript/js_evaluator.js";
|
||||
|
||||
// Precision argument for toBeCloseTo() in trigonometric tests.
|
||||
const TRIGONOMETRY_EPS = 1e-10;
|
||||
@ -195,15 +196,23 @@ describe("PostScript Type 4 lexer, parser, and Wasm compiler", function () {
|
||||
* For single-output functions returns a scalar; for multi-output an Array.
|
||||
*/
|
||||
function compileAndRun(src, domain, range, args) {
|
||||
const fn = buildPostScriptWasmFunction(src, domain, range);
|
||||
if (!fn) {
|
||||
const wasmFn = buildPostScriptWasmFunction(src, domain, range);
|
||||
const jsFn = buildPostScriptJsFunction(src, domain, range);
|
||||
if (!wasmFn) {
|
||||
expect(jsFn).toBeNull();
|
||||
return null;
|
||||
}
|
||||
expect(jsFn).not.toBeNull();
|
||||
const nOut = range.length >> 1;
|
||||
const srcBuf = new Float64Array(args);
|
||||
const dest = new Float64Array(nOut);
|
||||
fn(srcBuf, 0, dest, 0);
|
||||
return nOut === 1 ? dest[0] : Array.from(dest);
|
||||
const wasmDest = new Float64Array(nOut);
|
||||
const jsDest = new Float64Array(nOut);
|
||||
wasmFn(srcBuf, 0, wasmDest, 0);
|
||||
jsFn(srcBuf, 0, jsDest, 0);
|
||||
for (let i = 0; i < nOut; i++) {
|
||||
expect(jsDest[i]).toBeCloseTo(wasmDest[i], 10);
|
||||
}
|
||||
return nOut === 1 ? wasmDest[0] : Array.from(wasmDest);
|
||||
}
|
||||
|
||||
function readULEB128(bytes, offset) {
|
||||
@ -257,186 +266,161 @@ describe("PostScript Type 4 lexer, parser, and Wasm compiler", function () {
|
||||
// Arithmetic.
|
||||
|
||||
it("compiles add", async function () {
|
||||
const r = await compileAndRun(
|
||||
"{ add }",
|
||||
[0, 1, 0, 1],
|
||||
[0, 2],
|
||||
[0.3, 0.7]
|
||||
);
|
||||
const r = compileAndRun("{ add }", [0, 1, 0, 1], [0, 2], [0.3, 0.7]);
|
||||
expect(r).toBeCloseTo(1.0, 9);
|
||||
});
|
||||
|
||||
it("compiles sub", async function () {
|
||||
const r = await compileAndRun(
|
||||
"{ sub }",
|
||||
[0, 1, 0, 1],
|
||||
[0, 1],
|
||||
[0.8, 0.3]
|
||||
);
|
||||
const r = compileAndRun("{ sub }", [0, 1, 0, 1], [0, 1], [0.8, 0.3]);
|
||||
expect(r).toBeCloseTo(0.5, 9);
|
||||
});
|
||||
|
||||
it("compiles mul", async function () {
|
||||
const r = await compileAndRun("{ 0.5 mul }", [0, 1], [0, 1], [0.4]);
|
||||
const r = compileAndRun("{ 0.5 mul }", [0, 1], [0, 1], [0.4]);
|
||||
expect(r).toBeCloseTo(0.2, 9);
|
||||
});
|
||||
|
||||
it("compiles div", async function () {
|
||||
const r = await compileAndRun("{ div }", [0, 10, 0, 10], [0, 10], [6, 3]);
|
||||
const r = compileAndRun("{ div }", [0, 10, 0, 10], [0, 10], [6, 3]);
|
||||
expect(r).toBeCloseTo(2, 9);
|
||||
});
|
||||
|
||||
it("div by zero returns 0", async function () {
|
||||
const r = await compileAndRun("{ div }", [0, 10, 0, 10], [0, 10], [5, 0]);
|
||||
const r = compileAndRun("{ div }", [0, 10, 0, 10], [0, 10], [5, 0]);
|
||||
expect(r).toBe(0);
|
||||
});
|
||||
|
||||
it("compiles idiv", async function () {
|
||||
const r = await compileAndRun(
|
||||
"{ idiv }",
|
||||
[0, 10, 1, 10],
|
||||
[0, 10],
|
||||
[7, 2]
|
||||
);
|
||||
const r = compileAndRun("{ idiv }", [0, 10, 1, 10], [0, 10], [7, 2]);
|
||||
expect(r).toBeCloseTo(3, 9);
|
||||
});
|
||||
|
||||
it("idiv by zero returns 0", async function () {
|
||||
const r = await compileAndRun(
|
||||
"{ idiv }",
|
||||
[0, 10, 0, 10],
|
||||
[0, 10],
|
||||
[5, 0]
|
||||
);
|
||||
const r = compileAndRun("{ idiv }", [0, 10, 0, 10], [0, 10], [5, 0]);
|
||||
expect(r).toBe(0);
|
||||
});
|
||||
|
||||
it("compiles mod", async function () {
|
||||
const r = await compileAndRun("{ mod }", [0, 10, 1, 10], [0, 10], [7, 3]);
|
||||
const r = compileAndRun("{ mod }", [0, 10, 1, 10], [0, 10], [7, 3]);
|
||||
expect(r).toBeCloseTo(1, 9);
|
||||
});
|
||||
|
||||
it("mod by zero returns 0", async function () {
|
||||
const r = await compileAndRun("{ mod }", [0, 10, 0, 10], [0, 10], [5, 0]);
|
||||
const r = compileAndRun("{ mod }", [0, 10, 0, 10], [0, 10], [5, 0]);
|
||||
expect(r).toBe(0);
|
||||
});
|
||||
|
||||
it("compiles mod with constant divisor", async function () {
|
||||
// { 3 mod } — divisor is a compile-time constant, exercises the
|
||||
// constant-divisor branch in _compileModNode.
|
||||
const r = await compileAndRun("{ 3 mod }", [0, 10], [0, 3], [7]);
|
||||
const r = compileAndRun("{ 3 mod }", [0, 10], [0, 3], [7]);
|
||||
expect(r).toBeCloseTo(1, 9); // 7 mod 3 = 1
|
||||
});
|
||||
|
||||
it("compiles integer xor (bitwise)", async function () {
|
||||
// { 5 xor } with an integer-typed arg — exercises the non-boolean path in
|
||||
// _compileBitwiseOperandI32 and the xor case in _compileBitwiseNode.
|
||||
const r = await compileAndRun("{ 5 xor }", [-128, 127], [-128, 127], [3]);
|
||||
const r = compileAndRun("{ 5 xor }", [-128, 127], [-128, 127], [3]);
|
||||
expect(r).toBeCloseTo(6, 9); // 3 XOR 5 = 6
|
||||
});
|
||||
|
||||
it("compiles neg", async function () {
|
||||
// neg applied to a variable — the optimizer cannot fold this.
|
||||
// abs(neg(x)) is optimized to abs(x), so test neg alone.
|
||||
const r = await compileAndRun("{ neg }", [-1, 1], [-1, 1], [-0.5]);
|
||||
const r = compileAndRun("{ neg }", [-1, 1], [-1, 1], [-0.5]);
|
||||
expect(r).toBeCloseTo(0.5, 9);
|
||||
});
|
||||
|
||||
it("compiles neg and abs", async function () {
|
||||
const r = await compileAndRun("{ neg abs }", [-1, 1], [0, 1], [-0.8]);
|
||||
const r = compileAndRun("{ neg abs }", [-1, 1], [0, 1], [-0.8]);
|
||||
expect(r).toBeCloseTo(0.8, 9);
|
||||
});
|
||||
|
||||
it("compiles cvi (truncate to integer)", async function () {
|
||||
const r = await compileAndRun("{ 1.7 add cvi }", [0, 2], [0, 4], [0.5]);
|
||||
const r = compileAndRun("{ 1.7 add cvi }", [0, 2], [0, 4], [0.5]);
|
||||
expect(r).toBeCloseTo(2, 9); // trunc(0.5 + 1.7) = trunc(2.2) = 2
|
||||
});
|
||||
|
||||
it("compiles cvr (identity on reals)", async function () {
|
||||
const r = await compileAndRun("{ cvr }", [0, 1], [0, 1], [0.7]);
|
||||
const r = compileAndRun("{ cvr }", [0, 1], [0, 1], [0.7]);
|
||||
expect(r).toBeCloseTo(0.7, 9);
|
||||
});
|
||||
|
||||
// Math.
|
||||
|
||||
it("compiles sqrt", async function () {
|
||||
const r = await compileAndRun("{ sqrt }", [0, 100], [0, 10], [9]);
|
||||
const r = compileAndRun("{ sqrt }", [0, 100], [0, 10], [9]);
|
||||
expect(r).toBeCloseTo(3, 9);
|
||||
});
|
||||
|
||||
it("compiles floor", async function () {
|
||||
const r = await compileAndRun("{ floor }", [-2, 2], [-2, 2], [1.7]);
|
||||
const r = compileAndRun("{ floor }", [-2, 2], [-2, 2], [1.7]);
|
||||
expect(r).toBeCloseTo(1, 9);
|
||||
});
|
||||
|
||||
it("compiles ceiling", async function () {
|
||||
const r = await compileAndRun("{ ceiling }", [-2, 2], [-2, 2], [1.2]);
|
||||
const r = compileAndRun("{ ceiling }", [-2, 2], [-2, 2], [1.2]);
|
||||
expect(r).toBeCloseTo(2, 9);
|
||||
});
|
||||
|
||||
it("compiles round", async function () {
|
||||
const r = await compileAndRun("{ round }", [-2, 2], [-2, 2], [1.5]);
|
||||
const r = compileAndRun("{ round }", [-2, 2], [-2, 2], [1.5]);
|
||||
expect(r).toBeCloseTo(2, 9);
|
||||
});
|
||||
|
||||
it("round uses round-half-up (0.5 rounds to 1, -0.5 rounds to 0)", async function () {
|
||||
const r1 = await compileAndRun("{ round }", [-2, 2], [-2, 2], [0.5]);
|
||||
const r1 = compileAndRun("{ round }", [-2, 2], [-2, 2], [0.5]);
|
||||
expect(r1).toBe(1);
|
||||
const r2 = await compileAndRun("{ round }", [-2, 2], [-2, 2], [-0.5]);
|
||||
const r2 = compileAndRun("{ round }", [-2, 2], [-2, 2], [-0.5]);
|
||||
expect(r2).toBe(0);
|
||||
});
|
||||
|
||||
it("compiles truncate", async function () {
|
||||
const r = await compileAndRun("{ truncate }", [-2, 2], [-2, 2], [-1.9]);
|
||||
const r = compileAndRun("{ truncate }", [-2, 2], [-2, 2], [-1.9]);
|
||||
expect(r).toBeCloseTo(-1, 9);
|
||||
});
|
||||
|
||||
it("compiles ln", async function () {
|
||||
const r = await compileAndRun("{ ln }", [0.001, 10], [-10, 10], [Math.E]);
|
||||
const r = compileAndRun("{ ln }", [0.001, 10], [-10, 10], [Math.E]);
|
||||
expect(r).toBeCloseTo(1, 9);
|
||||
});
|
||||
|
||||
it("compiles log (base 10)", async function () {
|
||||
const r = await compileAndRun("{ log }", [0.001, 1000], [-3, 3], [100]);
|
||||
const r = compileAndRun("{ log }", [0.001, 1000], [-3, 3], [100]);
|
||||
expect(r).toBeCloseTo(2, 9);
|
||||
});
|
||||
|
||||
it("compiles exp (base ^ exponent)", async function () {
|
||||
const r = await compileAndRun(
|
||||
"{ exp }",
|
||||
[0, 10, 0, 10],
|
||||
[0, 2000],
|
||||
[2, 10]
|
||||
);
|
||||
const r = compileAndRun("{ exp }", [0, 10, 0, 10], [0, 2000], [2, 10]);
|
||||
expect(r).toBeCloseTo(1024, 6);
|
||||
});
|
||||
|
||||
it("compiles x ^ -1 → 1/x (strength reduction)", async function () {
|
||||
const r = await compileAndRun("{ -1 exp }", [0.1, 10], [0.1, 10], [2]);
|
||||
const r = compileAndRun("{ -1 exp }", [0.1, 10], [0.1, 10], [2]);
|
||||
expect(r).toBeCloseTo(0.5, 9); // 1/2 = 0.5
|
||||
});
|
||||
|
||||
it("compiles x ^ 3 → (x*x)*x (strength reduction)", async function () {
|
||||
const r = await compileAndRun("{ 3 exp }", [0, 10], [0, 1000], [2]);
|
||||
const r = compileAndRun("{ 3 exp }", [0, 10], [0, 1000], [2]);
|
||||
expect(r).toBeCloseTo(8, 9); // 2^3 = 8
|
||||
});
|
||||
|
||||
it("compiles x ^ 4 → (x*x)*(x*x) (strength reduction)", async function () {
|
||||
// x^4 uses CSE: x*x is computed once and squared — exercises the
|
||||
// local_tee/local_get path in _compileStandardBinaryNode.
|
||||
const r = await compileAndRun("{ 4 exp }", [0, 10], [0, 10000], [2]);
|
||||
const r = compileAndRun("{ 4 exp }", [0, 10], [0, 10000], [2]);
|
||||
expect(r).toBeCloseTo(16, 9); // 2^4 = 16
|
||||
});
|
||||
|
||||
// Trigonometry (degrees).
|
||||
|
||||
it("compiles sin (degrees)", async function () {
|
||||
const r = await compileAndRun("{ sin }", [-360, 360], [-1, 1], [90]);
|
||||
const r = compileAndRun("{ sin }", [-360, 360], [-1, 1], [90]);
|
||||
expect(r).toBeCloseTo(1, TRIGONOMETRY_EPS);
|
||||
});
|
||||
|
||||
it("compiles cos (degrees)", async function () {
|
||||
const r = await compileAndRun("{ cos }", [-360, 360], [-1, 1], [0]);
|
||||
const r = compileAndRun("{ cos }", [-360, 360], [-1, 1], [0]);
|
||||
expect(r).toBeCloseTo(1, TRIGONOMETRY_EPS);
|
||||
});
|
||||
|
||||
@ -452,18 +436,13 @@ describe("PostScript Type 4 lexer, parser, and Wasm compiler", function () {
|
||||
|
||||
it("compiles atan (degrees, result in [0,360))", async function () {
|
||||
// atan(1, 1) = 45°
|
||||
const r = await compileAndRun(
|
||||
"{ atan }",
|
||||
[-10, 10, -10, 10],
|
||||
[0, 360],
|
||||
[1, 1]
|
||||
);
|
||||
const r = compileAndRun("{ atan }", [-10, 10, -10, 10], [0, 360], [1, 1]);
|
||||
expect(r).toBeCloseTo(45, 6);
|
||||
});
|
||||
|
||||
it("atan normalizes negative angles to [0,360)", async function () {
|
||||
// atan(-1, 1) would be -45°, should become 315°
|
||||
const r = await compileAndRun(
|
||||
const r = compileAndRun(
|
||||
"{ atan }",
|
||||
[-10, 10, -10, 10],
|
||||
[0, 360],
|
||||
@ -475,33 +454,23 @@ describe("PostScript Type 4 lexer, parser, and Wasm compiler", function () {
|
||||
// Stack operators.
|
||||
|
||||
it("compiles dup", async function () {
|
||||
const r = await compileAndRun("{ dup mul }", [0, 1], [0, 1], [0.5]);
|
||||
const r = compileAndRun("{ dup mul }", [0, 1], [0, 1], [0.5]);
|
||||
expect(r).toBeCloseTo(0.25, 9);
|
||||
});
|
||||
|
||||
it("compiles exch", async function () {
|
||||
const r = await compileAndRun(
|
||||
"{ exch div }",
|
||||
[0, 10, 0, 10],
|
||||
[0, 10],
|
||||
[1, 2]
|
||||
);
|
||||
const r = compileAndRun("{ exch div }", [0, 10, 0, 10], [0, 10], [1, 2]);
|
||||
expect(r).toBeCloseTo(2, 9); // 2 / 1
|
||||
});
|
||||
|
||||
it("compiles pop", async function () {
|
||||
const r = await compileAndRun(
|
||||
"{ pop }",
|
||||
[0, 1, 0, 1],
|
||||
[0, 1],
|
||||
[0.3, 0.7]
|
||||
);
|
||||
const r = compileAndRun("{ pop }", [0, 1, 0, 1], [0, 1], [0.3, 0.7]);
|
||||
expect(r).toBeCloseTo(0.3, 9); // 0.7 popped, 0.3 remains
|
||||
});
|
||||
|
||||
it("compiles copy", async function () {
|
||||
// { 1 copy add }: one input a → stack [a, a] → add → [2a]
|
||||
const r = await compileAndRun("{ 1 copy add }", [0, 1], [0, 2], [0.4]);
|
||||
const r = compileAndRun("{ 1 copy add }", [0, 1], [0, 2], [0.4]);
|
||||
expect(r).toBeCloseTo(0.8, 9);
|
||||
});
|
||||
|
||||
@ -592,7 +561,7 @@ describe("PostScript Type 4 lexer, parser, and Wasm compiler", function () {
|
||||
});
|
||||
|
||||
it("compiles 3-input function", async function () {
|
||||
const r = await compileAndRun(
|
||||
const r = compileAndRun(
|
||||
"{ add add }",
|
||||
[0, 1, 0, 1, 0, 1],
|
||||
[0, 3],
|
||||
@ -602,7 +571,7 @@ describe("PostScript Type 4 lexer, parser, and Wasm compiler", function () {
|
||||
});
|
||||
|
||||
it("compiles 4-input function", async function () {
|
||||
const r = await compileAndRun(
|
||||
const r = compileAndRun(
|
||||
"{ add add add }",
|
||||
[0, 1, 0, 1, 0, 1, 0, 1],
|
||||
[0, 4],
|
||||
@ -612,7 +581,7 @@ describe("PostScript Type 4 lexer, parser, and Wasm compiler", function () {
|
||||
});
|
||||
|
||||
it("compiles 5-input function (default caller path)", async function () {
|
||||
const r = await compileAndRun(
|
||||
const r = compileAndRun(
|
||||
"{ add add add add }",
|
||||
[0, 1, 0, 1, 0, 1, 0, 1, 0, 1],
|
||||
[0, 5],
|
||||
@ -624,38 +593,38 @@ describe("PostScript Type 4 lexer, parser, and Wasm compiler", function () {
|
||||
// Comparison / boolean.
|
||||
|
||||
it("compiles eq", async function () {
|
||||
const r = await compileAndRun("{ eq }", [0, 1, 0, 1], [0, 1], [0.5, 0.5]);
|
||||
const r = compileAndRun("{ eq }", [0, 1, 0, 1], [0, 1], [0.5, 0.5]);
|
||||
expect(r).toBeCloseTo(1, 9);
|
||||
});
|
||||
|
||||
it("compiles ne (not-equal)", async function () {
|
||||
const r = await compileAndRun("{ 0.5 ne }", [0, 1], [0, 1], [0.3]);
|
||||
const r = compileAndRun("{ 0.5 ne }", [0, 1], [0, 1], [0.3]);
|
||||
expect(r).toBeCloseTo(1, 9); // 0.3 ≠ 0.5 → true → 1
|
||||
});
|
||||
|
||||
it("compiles lt (less-than)", async function () {
|
||||
const r = await compileAndRun("{ 0.5 lt }", [0, 1], [0, 1], [0.3]);
|
||||
const r = compileAndRun("{ 0.5 lt }", [0, 1], [0, 1], [0.3]);
|
||||
expect(r).toBeCloseTo(1, 9); // 0.3 < 0.5 → true → 1
|
||||
});
|
||||
|
||||
it("compiles ge (greater-or-equal)", async function () {
|
||||
const r = await compileAndRun("{ 0.5 ge }", [0, 1], [0, 1], [0.7]);
|
||||
const r = compileAndRun("{ 0.5 ge }", [0, 1], [0, 1], [0.7]);
|
||||
expect(r).toBeCloseTo(1, 9); // 0.7 ≥ 0.5 → true → 1
|
||||
});
|
||||
|
||||
it("compiles gt", async function () {
|
||||
const r = await compileAndRun("{ gt }", [0, 1, 0, 1], [0, 1], [0.8, 0.3]);
|
||||
const r = compileAndRun("{ gt }", [0, 1, 0, 1], [0, 1], [0.8, 0.3]);
|
||||
expect(r).toBeCloseTo(1, 9);
|
||||
});
|
||||
|
||||
it("compiles le", async function () {
|
||||
const r = await compileAndRun("{ le }", [0, 1, 0, 1], [0, 1], [0.3, 0.8]);
|
||||
const r = compileAndRun("{ le }", [0, 1, 0, 1], [0, 1], [0.3, 0.8]);
|
||||
expect(r).toBeCloseTo(1, 9);
|
||||
});
|
||||
|
||||
it("compiles true and false literals", async function () {
|
||||
const t = await compileAndRun("{ true }", [], [0, 1], []);
|
||||
const f = await compileAndRun("{ false }", [], [0, 1], []);
|
||||
const t = compileAndRun("{ true }", [], [0, 1], []);
|
||||
const f = compileAndRun("{ false }", [], [0, 1], []);
|
||||
expect(t).toBeCloseTo(1, 9);
|
||||
expect(f).toBeCloseTo(0, 9);
|
||||
});
|
||||
@ -663,7 +632,7 @@ describe("PostScript Type 4 lexer, parser, and Wasm compiler", function () {
|
||||
// Conditionals.
|
||||
|
||||
it("compiles ifelse — true branch taken", async function () {
|
||||
const r = await compileAndRun(
|
||||
const r = compileAndRun(
|
||||
"{ dup 0.5 gt { 2 mul } { 0.5 mul } ifelse }",
|
||||
[0, 1],
|
||||
[0, 2],
|
||||
@ -673,7 +642,7 @@ describe("PostScript Type 4 lexer, parser, and Wasm compiler", function () {
|
||||
});
|
||||
|
||||
it("compiles ifelse — false branch taken", async function () {
|
||||
const r = await compileAndRun(
|
||||
const r = compileAndRun(
|
||||
"{ dup 0.5 gt { 2 mul } { 0.5 mul } ifelse }",
|
||||
[0, 1],
|
||||
[0, 2],
|
||||
@ -684,7 +653,7 @@ describe("PostScript Type 4 lexer, parser, and Wasm compiler", function () {
|
||||
|
||||
it("compiles if — condition true", async function () {
|
||||
// { dup 1 gt { pop 1 } if } — clamp x to 1 from above
|
||||
const r = await compileAndRun(
|
||||
const r = compileAndRun(
|
||||
"{ dup 1 gt { pop 1 } if }",
|
||||
[0, 2],
|
||||
[0, 2],
|
||||
@ -694,7 +663,7 @@ describe("PostScript Type 4 lexer, parser, and Wasm compiler", function () {
|
||||
});
|
||||
|
||||
it("compiles if — condition false", async function () {
|
||||
const r = await compileAndRun(
|
||||
const r = compileAndRun(
|
||||
"{ dup 1 gt { pop 1 } if }",
|
||||
[0, 2],
|
||||
[0, 2],
|
||||
@ -733,29 +702,19 @@ describe("PostScript Type 4 lexer, parser, and Wasm compiler", function () {
|
||||
|
||||
it("clamps output to declared range", async function () {
|
||||
// mul exceeds range [0, 0.5] → result clamped
|
||||
const r = await compileAndRun(
|
||||
"{ add }",
|
||||
[0, 1, 0, 1],
|
||||
[0, 0.5],
|
||||
[0.4, 0.4]
|
||||
);
|
||||
const r = compileAndRun("{ add }", [0, 1, 0, 1], [0, 0.5], [0.4, 0.4]);
|
||||
expect(r).toBeCloseTo(0.5, 9);
|
||||
});
|
||||
|
||||
// Bitwise.
|
||||
|
||||
it("compiles bitshift left (literal shift)", async function () {
|
||||
const r = await compileAndRun("{ 3 bitshift }", [0, 256], [0, 256], [1]);
|
||||
const r = compileAndRun("{ 3 bitshift }", [0, 256], [0, 256], [1]);
|
||||
expect(r).toBeCloseTo(8, 9); // 1 << 3
|
||||
});
|
||||
|
||||
it("compiles bitshift right (negative literal shift)", async function () {
|
||||
const r = await compileAndRun(
|
||||
"{ -2 bitshift }",
|
||||
[-256, 256],
|
||||
[-256, 256],
|
||||
[8]
|
||||
);
|
||||
const r = compileAndRun("{ -2 bitshift }", [-256, 256], [-256, 256], [8]);
|
||||
expect(r).toBeCloseTo(2, 9); // 8 >> 2
|
||||
});
|
||||
|
||||
@ -764,7 +723,7 @@ describe("PostScript Type 4 lexer, parser, and Wasm compiler", function () {
|
||||
// b |= 0x80 branch in _emitULEB128.
|
||||
// Wasm i32.shl uses shift % 32, so 128 % 32 = 0 →
|
||||
// left-shift by 0 = identity.
|
||||
const r = await compileAndRun(
|
||||
const r = compileAndRun(
|
||||
"{ 128 bitshift }",
|
||||
[-1000, 1000],
|
||||
[-1000, 1000],
|
||||
@ -779,7 +738,7 @@ describe("PostScript Type 4 lexer, parser, and Wasm compiler", function () {
|
||||
// exercising the byte |= 0x80 branch in unsignedLEB128.
|
||||
const src =
|
||||
"{ 1 add 1 add 1 add 1 add 1 add 1 add 1 add 1 add 1 add 1 add 1 add 1 add 1 add }";
|
||||
const r = await compileAndRun(src, [0, 1], [0, 14], [0]);
|
||||
const r = compileAndRun(src, [0, 1], [0, 14], [0]);
|
||||
expect(r).toBeCloseTo(13, 9);
|
||||
});
|
||||
|
||||
@ -807,13 +766,13 @@ describe("PostScript Type 4 lexer, parser, and Wasm compiler", function () {
|
||||
|
||||
it("compiles boolean not (logical NOT)", async function () {
|
||||
// 0.5 0.5 eq → true (1.0); not → false (0.0)
|
||||
const r = await compileAndRun("{ dup eq not }", [0, 1], [0, 1], [0.5]);
|
||||
const r = compileAndRun("{ dup eq not }", [0, 1], [0, 1], [0.5]);
|
||||
expect(r).toBeCloseTo(0, 9);
|
||||
});
|
||||
|
||||
it("compiles integer not (bitwise NOT)", async function () {
|
||||
// ~5 = -6
|
||||
const r = await compileAndRun("{ not }", [-256, 256], [-256, 256], [5]);
|
||||
const r = compileAndRun("{ not }", [-256, 256], [-256, 256], [5]);
|
||||
expect(r).toBeCloseTo(-6, 9);
|
||||
});
|
||||
|
||||
@ -821,7 +780,7 @@ describe("PostScript Type 4 lexer, parser, and Wasm compiler", function () {
|
||||
|
||||
it("ifelse with comparison condition (true branch)", async function () {
|
||||
// x > 0.5: comparison emitted directly as i32, no f64 round-trip.
|
||||
const r = await compileAndRun(
|
||||
const r = compileAndRun(
|
||||
"{ 0.5 gt { 1 } { 0 } ifelse }",
|
||||
[0, 1],
|
||||
[0, 1],
|
||||
@ -831,7 +790,7 @@ describe("PostScript Type 4 lexer, parser, and Wasm compiler", function () {
|
||||
});
|
||||
|
||||
it("ifelse with comparison condition (false branch)", async function () {
|
||||
const r = await compileAndRun(
|
||||
const r = compileAndRun(
|
||||
"{ 0.5 gt { 1 } { 0 } ifelse }",
|
||||
[0, 1],
|
||||
[0, 1],
|
||||
@ -846,9 +805,9 @@ describe("PostScript Type 4 lexer, parser, and Wasm compiler", function () {
|
||||
// node is a PsBinaryNode(and) — exercises the i32_trunc_f64_s fallback
|
||||
// in _compileNodeAsBoolI32.
|
||||
const src = "{ dup 0.3 gt exch 0.7 lt and { 1 } { 0 } ifelse }";
|
||||
const r0 = await compileAndRun(src, [0, 1], [0, 1], [0.5]);
|
||||
const r0 = compileAndRun(src, [0, 1], [0, 1], [0.5]);
|
||||
expect(r0).toBeCloseTo(1, 9); // 0.5 in (0.3, 0.7)
|
||||
const r1 = await compileAndRun(src, [0, 1], [0, 1], [0.2]);
|
||||
const r1 = compileAndRun(src, [0, 1], [0, 1], [0.2]);
|
||||
expect(r1).toBeCloseTo(0, 9); // 0.2 outside range
|
||||
});
|
||||
|
||||
@ -856,9 +815,9 @@ describe("PostScript Type 4 lexer, parser, and Wasm compiler", function () {
|
||||
// not(and(two comparisons)) — exercises the boolean `not` path in
|
||||
// _compileNodeAsBoolI32 (recursive call + i32.eqz).
|
||||
const src = "{ dup 0.3 gt exch 0.7 lt and not { 1 } { 0 } ifelse }";
|
||||
const r0 = await compileAndRun(src, [0, 1], [0, 1], [0.5]);
|
||||
const r0 = compileAndRun(src, [0, 1], [0, 1], [0.5]);
|
||||
expect(r0).toBeCloseTo(0, 9); // 0.5 in (0.3, 0.7) → not → false
|
||||
const r1 = await compileAndRun(src, [0, 1], [0, 1], [0.2]);
|
||||
const r1 = compileAndRun(src, [0, 1], [0, 1], [0.2]);
|
||||
expect(r1).toBeCloseTo(1, 9); // 0.2 outside range → not → true
|
||||
});
|
||||
|
||||
@ -868,18 +827,18 @@ describe("PostScript Type 4 lexer, parser, and Wasm compiler", function () {
|
||||
// and(x>0.3, x<0.7) used as the direct output (not as ternary condition)
|
||||
// — exercises _compileBitwiseOperandI32 with boolean operands.
|
||||
const src = "{ dup 0.3 gt exch 0.7 lt and }";
|
||||
const r0 = await compileAndRun(src, [0, 1], [0, 1], [0.5]);
|
||||
const r0 = compileAndRun(src, [0, 1], [0, 1], [0.5]);
|
||||
expect(r0).toBeCloseTo(1, 9);
|
||||
const r1 = await compileAndRun(src, [0, 1], [0, 1], [0.2]);
|
||||
const r1 = compileAndRun(src, [0, 1], [0, 1], [0.2]);
|
||||
expect(r1).toBeCloseTo(0, 9);
|
||||
});
|
||||
|
||||
it("boolean-or of two comparisons as standalone output", async function () {
|
||||
// or(x<0.3, x>0.7): true when x is outside [0.3, 0.7].
|
||||
const src = "{ dup 0.3 lt exch 0.7 gt or }";
|
||||
const r0 = await compileAndRun(src, [0, 1], [0, 1], [0.2]);
|
||||
const r0 = compileAndRun(src, [0, 1], [0, 1], [0.2]);
|
||||
expect(r0).toBeCloseTo(1, 9);
|
||||
const r1 = await compileAndRun(src, [0, 1], [0, 1], [0.5]);
|
||||
const r1 = compileAndRun(src, [0, 1], [0, 1], [0.5]);
|
||||
expect(r1).toBeCloseTo(0, 9);
|
||||
});
|
||||
|
||||
@ -888,9 +847,9 @@ describe("PostScript Type 4 lexer, parser, and Wasm compiler", function () {
|
||||
// _compileNodeAsBoolI32 for the operand, eliminating all f64/i32
|
||||
// round-trips.
|
||||
const src = "{ dup 0.3 gt exch 0.7 lt and not }";
|
||||
const r0 = await compileAndRun(src, [0, 1], [0, 1], [0.5]);
|
||||
const r0 = compileAndRun(src, [0, 1], [0, 1], [0.5]);
|
||||
expect(r0).toBeCloseTo(0, 9); // inside → and=true → not=false
|
||||
const r1 = await compileAndRun(src, [0, 1], [0, 1], [0.2]);
|
||||
const r1 = compileAndRun(src, [0, 1], [0, 1], [0.2]);
|
||||
expect(r1).toBeCloseTo(1, 9); // outside → and=false → not=true
|
||||
});
|
||||
|
||||
@ -899,11 +858,11 @@ describe("PostScript Type 4 lexer, parser, and Wasm compiler", function () {
|
||||
// Each ternary condition goes through _compileNodeAsBoolI32.
|
||||
const src =
|
||||
"{ dup 0.3 lt { pop 0 } { dup 0.7 gt { pop 1 } { pop 0.5 } ifelse } ifelse }";
|
||||
const r0 = await compileAndRun(src, [0, 1], [0, 1], [0.1]);
|
||||
const r0 = compileAndRun(src, [0, 1], [0, 1], [0.1]);
|
||||
expect(r0).toBeCloseTo(0, 9);
|
||||
const r1 = await compileAndRun(src, [0, 1], [0, 1], [0.9]);
|
||||
const r1 = compileAndRun(src, [0, 1], [0, 1], [0.9]);
|
||||
expect(r1).toBeCloseTo(1, 9);
|
||||
const r2 = await compileAndRun(src, [0, 1], [0, 1], [0.5]);
|
||||
const r2 = compileAndRun(src, [0, 1], [0, 1], [0.5]);
|
||||
expect(r2).toBeCloseTo(0.5, 9);
|
||||
});
|
||||
|
||||
@ -911,9 +870,9 @@ describe("PostScript Type 4 lexer, parser, and Wasm compiler", function () {
|
||||
// or(x<0.3, x>0.7) as ternary condition — exercises the TOKEN.or case
|
||||
// in the boolean and/or/xor branch of _compileNodeAsBoolI32.
|
||||
const src = "{ dup 0.3 lt exch 0.7 gt or { 1 } { 0 } ifelse }";
|
||||
const r0 = await compileAndRun(src, [0, 1], [0, 1], [0.2]);
|
||||
const r0 = compileAndRun(src, [0, 1], [0, 1], [0.2]);
|
||||
expect(r0).toBeCloseTo(1, 9); // 0.2 < 0.3 → true
|
||||
const r1 = await compileAndRun(src, [0, 1], [0, 1], [0.5]);
|
||||
const r1 = compileAndRun(src, [0, 1], [0, 1], [0.5]);
|
||||
expect(r1).toBeCloseTo(0, 9); // 0.5 inside → false
|
||||
});
|
||||
|
||||
@ -922,10 +881,10 @@ describe("PostScript Type 4 lexer, parser, and Wasm compiler", function () {
|
||||
// _compileNodeAsBoolI32; true when exactly one condition holds.
|
||||
const src = "{ dup 0.5 lt exch 0.3 gt xor { 1 } { 0 } ifelse }";
|
||||
// x=0.4: 0.4<0.5=true, 0.4>0.3=true → xor=false → 0
|
||||
const r0 = await compileAndRun(src, [0, 1], [0, 1], [0.4]);
|
||||
const r0 = compileAndRun(src, [0, 1], [0, 1], [0.4]);
|
||||
expect(r0).toBeCloseTo(0, 9);
|
||||
// x=0.2: 0.2<0.5=true, 0.2>0.3=false → xor=true → 1
|
||||
const r1 = await compileAndRun(src, [0, 1], [0, 1], [0.2]);
|
||||
const r1 = compileAndRun(src, [0, 1], [0, 1], [0.2]);
|
||||
expect(r1).toBeCloseTo(1, 9);
|
||||
});
|
||||
|
||||
@ -933,19 +892,9 @@ describe("PostScript Type 4 lexer, parser, and Wasm compiler", function () {
|
||||
// The condition is the raw input arg (numeric, not boolean), so
|
||||
// _compileNodeAsBoolI32 falls through to the general path and emits
|
||||
// f64.const 0 / f64.ne to convert to i32.
|
||||
const r0 = await compileAndRun(
|
||||
"{ { 1 } { 0 } ifelse }",
|
||||
[0, 1],
|
||||
[0, 1],
|
||||
[0.7]
|
||||
);
|
||||
const r0 = compileAndRun("{ { 1 } { 0 } ifelse }", [0, 1], [0, 1], [0.7]);
|
||||
expect(r0).toBeCloseTo(1, 9); // 0.7 ≠ 0 → truthy → 1
|
||||
const r1 = await compileAndRun(
|
||||
"{ { 1 } { 0 } ifelse }",
|
||||
[0, 1],
|
||||
[0, 1],
|
||||
[0]
|
||||
);
|
||||
const r1 = compileAndRun("{ { 1 } { 0 } ifelse }", [0, 1], [0, 1], [0]);
|
||||
expect(r1).toBeCloseTo(0, 9); // 0 → falsy → 0
|
||||
});
|
||||
|
||||
@ -955,34 +904,29 @@ describe("PostScript Type 4 lexer, parser, and Wasm compiler", function () {
|
||||
// (x+1)^2: the x^2→x*x strength reduction creates PsBinaryNode(mul,
|
||||
// add_node, add_node) where add_node is non-trivial — exercises the
|
||||
// local_tee/local_get path in _compileStandardBinaryNode.
|
||||
const r = await compileAndRun("{ 1 add 2 exp }", [0, 10], [0, 100], [3]);
|
||||
const r = compileAndRun("{ 1 add 2 exp }", [0, 10], [0, 100], [3]);
|
||||
expect(r).toBeCloseTo(16, 9); // (3+1)^2 = 16
|
||||
});
|
||||
|
||||
it("shared non-trivial operand uses local_tee (x+2)^2 via dup", async function () {
|
||||
// `2 add dup mul`: dup of the add node gives PsBinaryNode(mul, add, add)
|
||||
// with the same reference twice — exercises the local_tee/local_get path.
|
||||
const r = await compileAndRun(
|
||||
"{ 2 add dup mul }",
|
||||
[0, 10],
|
||||
[0, 100],
|
||||
[3]
|
||||
);
|
||||
const r = compileAndRun("{ 2 add dup mul }", [0, 10], [0, 100], [3]);
|
||||
expect(r).toBeCloseTo(25, 9); // (3+2)^2 = 25
|
||||
});
|
||||
|
||||
it("compiles x^3 without changing behavior", async function () {
|
||||
const r = await compileAndRun("{ 3 exp }", [0, 10], [0, 1000], [2]);
|
||||
const r = compileAndRun("{ 3 exp }", [0, 10], [0, 1000], [2]);
|
||||
expect(r).toBeCloseTo(8, 9);
|
||||
});
|
||||
|
||||
it("compiles x^4 without changing behavior", async function () {
|
||||
const r = await compileAndRun("{ 4 exp }", [0, 10], [0, 10000], [2]);
|
||||
const r = compileAndRun("{ 4 exp }", [0, 10], [0, 10000], [2]);
|
||||
expect(r).toBeCloseTo(16, 9);
|
||||
});
|
||||
|
||||
it("compiles x^-1 without changing behavior", async function () {
|
||||
const r = await compileAndRun("{ -1 exp }", [1, 10], [0, 1], [4]);
|
||||
const r = compileAndRun("{ -1 exp }", [1, 10], [0, 1], [4]);
|
||||
expect(r).toBeCloseTo(0.25, 9);
|
||||
});
|
||||
|
||||
@ -1031,19 +975,19 @@ describe("PostScript Type 4 lexer, parser, and Wasm compiler", function () {
|
||||
// min/max fold and related runtime tests.
|
||||
|
||||
it("compiles x^0.25 → sqrt(sqrt(x))", async function () {
|
||||
const r = await compileAndRun("{ 0.25 exp }", [0, 16], [0, 2], [16]);
|
||||
const r = compileAndRun("{ 0.25 exp }", [0, 16], [0, 2], [16]);
|
||||
expect(r).toBeCloseTo(2, 9); // 16^0.25 = 2
|
||||
});
|
||||
|
||||
it("compiles neg(a − b) → b − a", async function () {
|
||||
// neg(x - 3) = 3 - x; at x=1 → 2
|
||||
const r = await compileAndRun("{ 3 sub neg }", [0, 10], [0, 10], [1]);
|
||||
const r = compileAndRun("{ 3 sub neg }", [0, 10], [0, 10], [1]);
|
||||
expect(r).toBeCloseTo(2, 9);
|
||||
});
|
||||
|
||||
it("compiles min(max(x, 0.8), 0.5) → constant 0.5", async function () {
|
||||
// Absorption: max result always >= 0.8 > 0.5, so min is always 0.5.
|
||||
const r = await compileAndRun(
|
||||
const r = compileAndRun(
|
||||
"{ dup 0.8 lt { pop 0.8 } { } ifelse " +
|
||||
"dup 0.5 gt { pop 0.5 } { } ifelse }",
|
||||
[0, 1],
|
||||
@ -1055,14 +999,14 @@ describe("PostScript Type 4 lexer, parser, and Wasm compiler", function () {
|
||||
|
||||
it("min/max fold: upper clamp emits f64.min", async function () {
|
||||
// x > 1 → clamp to 1; x ≤ 1 → pass through.
|
||||
const r1 = await compileAndRun(
|
||||
const r1 = compileAndRun(
|
||||
"{ dup 1 gt { pop 1 } { } ifelse }",
|
||||
[0, 2],
|
||||
[0, 2],
|
||||
[2]
|
||||
);
|
||||
expect(r1).toBeCloseTo(1, 9);
|
||||
const r2 = await compileAndRun(
|
||||
const r2 = compileAndRun(
|
||||
"{ dup 1 gt { pop 1 } { } ifelse }",
|
||||
[0, 2],
|
||||
[0, 2],
|
||||
@ -1073,14 +1017,14 @@ describe("PostScript Type 4 lexer, parser, and Wasm compiler", function () {
|
||||
|
||||
it("min/max fold: lower clamp emits f64.max", async function () {
|
||||
// x < 0 → clamp to 0; x ≥ 0 → pass through.
|
||||
const r1 = await compileAndRun(
|
||||
const r1 = compileAndRun(
|
||||
"{ dup 0 lt { pop 0 } { } ifelse }",
|
||||
[-1, 1],
|
||||
[0, 1],
|
||||
[-0.5]
|
||||
);
|
||||
expect(r1).toBeCloseTo(0, 9);
|
||||
const r2 = await compileAndRun(
|
||||
const r2 = compileAndRun(
|
||||
"{ dup 0 lt { pop 0 } { } ifelse }",
|
||||
[-1, 1],
|
||||
[0, 1],
|
||||
|
||||
@ -157,6 +157,7 @@
|
||||
font-size: 0.8em;
|
||||
color: var(--muted-color);
|
||||
user-select: none;
|
||||
height: 22px;
|
||||
}
|
||||
|
||||
.mlc-num-item.mlc-match {
|
||||
|
||||
@ -179,6 +179,7 @@
|
||||
display: block;
|
||||
white-space: nowrap;
|
||||
padding-inline-start: 0.5em;
|
||||
height: 22px;
|
||||
}
|
||||
|
||||
.raw-bytes-stream {
|
||||
|
||||
@ -235,6 +235,32 @@ class TreeView {
|
||||
* into the ref's children container, avoiding an extra toggle level).
|
||||
*/
|
||||
#buildChildren(value, doc, container) {
|
||||
if (this.#isPSFunction(value)) {
|
||||
for (const [k, v] of Object.entries(value.dict)) {
|
||||
container.append(this.#renderNode(k, v, doc));
|
||||
}
|
||||
const srcNode = this.#makeNodeEl("source");
|
||||
const srcLabel = `[PostScript, ${value.psLines.length} lines]`;
|
||||
const srcLabelEl = this.#makeSpan("stream-label", srcLabel);
|
||||
srcNode.append(
|
||||
this.#makeExpandable(srcLabelEl, srcLabel, c =>
|
||||
this.#buildPSFunctionPanel(value, c, srcLabelEl)
|
||||
)
|
||||
);
|
||||
container.append(srcNode);
|
||||
if (value.jsCode !== null) {
|
||||
const jsNode = this.#makeNodeEl("js");
|
||||
const jsLabel = "[JS equivalent]";
|
||||
const jsLabelEl = this.#makeSpan("stream-label", jsLabel);
|
||||
jsNode.append(
|
||||
this.#makeExpandable(jsLabelEl, jsLabel, c =>
|
||||
this.#buildJSCodePanel(value.jsCode, c)
|
||||
)
|
||||
);
|
||||
container.append(jsNode);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (this.#isStream(value)) {
|
||||
for (const [k, v] of Object.entries(value.dict)) {
|
||||
container.append(this.#renderNode(k, v, doc));
|
||||
@ -310,6 +336,8 @@ class TreeView {
|
||||
span.append(this.#makeSpan("bracket", "]"));
|
||||
return span;
|
||||
}
|
||||
case "brace":
|
||||
return this.#makeSpan("bracket", token.value);
|
||||
case "dict": {
|
||||
const span = document.createElement("span");
|
||||
span.className = "token-dict";
|
||||
@ -355,6 +383,8 @@ class TreeView {
|
||||
return String(token.value);
|
||||
case "null":
|
||||
return "null";
|
||||
case "brace":
|
||||
return token.value;
|
||||
case "array":
|
||||
return `[ ${token.value.map(t => this.#tokenToText(t)).join(" ")} ]`;
|
||||
case "dict": {
|
||||
@ -470,6 +500,96 @@ class TreeView {
|
||||
return btn;
|
||||
}
|
||||
|
||||
// Fills container with a PostScript source panel (indented, token-coloured).
|
||||
#buildPSSourcePanel(psLines, container, actions = null) {
|
||||
const mc = new MultilineView({
|
||||
total: psLines.length,
|
||||
lineClass: "content-stream ps-source-stream",
|
||||
getText: i => {
|
||||
const { tokens } = psLines[i];
|
||||
return tokens.map(t => this.#tokenToText(t)).join(" ");
|
||||
},
|
||||
actions,
|
||||
makeLineEl: (i, isHighlighted) => {
|
||||
const line = document.createElement("div");
|
||||
line.className = "content-stm-instruction";
|
||||
if (isHighlighted) {
|
||||
line.classList.add("mlc-match");
|
||||
}
|
||||
const content = document.createElement("span");
|
||||
const { indent, tokens } = psLines[i];
|
||||
if (indent > 0) {
|
||||
content.style.paddingInlineStart = `${indent * 1.5}em`;
|
||||
}
|
||||
for (let j = 0; j < tokens.length; j++) {
|
||||
if (j > 0) {
|
||||
content.append(document.createTextNode(" "));
|
||||
}
|
||||
content.append(this.#renderToken(tokens[j]));
|
||||
}
|
||||
line.append(content);
|
||||
return line;
|
||||
},
|
||||
});
|
||||
container.append(mc.element);
|
||||
return mc;
|
||||
}
|
||||
|
||||
// Fills container with a JS code panel (plain monospace lines).
|
||||
#buildJSCodePanel(jsCode, container, actions = null) {
|
||||
const lines = jsCode.split("\n");
|
||||
while (lines.at(-1) === "") {
|
||||
lines.pop();
|
||||
}
|
||||
const mc = new MultilineView({
|
||||
total: lines.length,
|
||||
lineClass: "content-stream js-code-stream",
|
||||
getText: i => lines[i],
|
||||
actions,
|
||||
makeLineEl: (i, isHighlighted) => {
|
||||
const el = document.createElement("div");
|
||||
el.className = "content-stm-instruction";
|
||||
if (isHighlighted) {
|
||||
el.classList.add("mlc-match");
|
||||
}
|
||||
el.append(document.createTextNode(lines[i]));
|
||||
return el;
|
||||
},
|
||||
});
|
||||
container.append(mc.element);
|
||||
return mc;
|
||||
}
|
||||
|
||||
// PS source panel with parsed/raw toggle and an expandable JS equivalent.
|
||||
#buildPSFunctionPanel(val, container, labelEl = null) {
|
||||
let isParsed = true;
|
||||
let currentPanel = null;
|
||||
const rawLines = val.source.split(/\r?\n|\r/);
|
||||
if (rawLines.at(-1) === "") {
|
||||
rawLines.pop();
|
||||
}
|
||||
const parsedLabel = `[PostScript, ${val.psLines.length} lines]`;
|
||||
const rawLabel = `[PostScript, ${rawLines.length} raw lines]`;
|
||||
|
||||
const rebuild = () => {
|
||||
currentPanel?.destroy();
|
||||
currentPanel = null;
|
||||
container.replaceChildren();
|
||||
if (labelEl) {
|
||||
labelEl.textContent = isParsed ? parsedLabel : rawLabel;
|
||||
}
|
||||
const btn = this.#makeParseToggleBtn(isParsed, () => {
|
||||
isParsed = !isParsed;
|
||||
rebuild();
|
||||
});
|
||||
currentPanel = isParsed
|
||||
? this.#buildPSSourcePanel(val.psLines, container, btn)
|
||||
: this.#buildRawBytesPanel(val.source, container, btn);
|
||||
};
|
||||
|
||||
rebuild();
|
||||
}
|
||||
|
||||
// Fills container with the content stream panel (parsed or raw), with a
|
||||
// toggle button in the toolbar that swaps the view in-place.
|
||||
#buildContentStreamPanel(val, container, labelEl = null) {
|
||||
@ -538,6 +658,15 @@ class TreeView {
|
||||
return this.#renderContentStream(value);
|
||||
}
|
||||
|
||||
// PostScript Type 4 function stream
|
||||
if (this.#isPSFunction(value)) {
|
||||
return this.#renderExpandable(
|
||||
"[PostScript Function]",
|
||||
"stream-label",
|
||||
container => this.#buildChildren(value, doc, container)
|
||||
);
|
||||
}
|
||||
|
||||
// Stream → expandable showing dict entries + byte count or image preview
|
||||
if (this.#isStream(value)) {
|
||||
return this.#renderExpandable("[Stream]", "stream-label", container =>
|
||||
@ -841,6 +970,17 @@ class TreeView {
|
||||
#isFormXObjectStream(val) {
|
||||
return this.#isStream(val) && val.contentStream === true;
|
||||
}
|
||||
|
||||
// PostScript Type 4 function: { dict, psFunction: true, psLines, jsCode }.
|
||||
#isPSFunction(val) {
|
||||
return (
|
||||
val !== null &&
|
||||
typeof val === "object" &&
|
||||
!Array.isArray(val) &&
|
||||
Object.prototype.hasOwnProperty.call(val, "dict") &&
|
||||
val.psFunction === true
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export { TreeView };
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user