mirror of
https://github.com/mozilla/pdf.js.git
synced 2026-07-10 09:05:50 +02:00
Add an interpreter for optimized ps code
It'll be used as a fallback when wasm is disabled. And add in the debugger a view for the generated js code and one for the ps code.
This commit is contained in:
parent
58b807d8e8
commit
9f3de1edf6
@ -2136,6 +2136,22 @@ class PDFDocument {
|
|||||||
return obj;
|
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();
|
obj.bytes = value.getString();
|
||||||
return obj;
|
return obj;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -25,6 +25,7 @@ import {
|
|||||||
} from "../shared/util.js";
|
} from "../shared/util.js";
|
||||||
import { PostScriptLexer, PostScriptParser } from "./ps_parser.js";
|
import { PostScriptLexer, PostScriptParser } from "./ps_parser.js";
|
||||||
import { BaseStream } from "./base_stream.js";
|
import { BaseStream } from "./base_stream.js";
|
||||||
|
import { buildPostScriptJsFunction } from "./postscript/js_evaluator.js";
|
||||||
import { buildPostScriptWasmFunction } from "./postscript/wasm_compiler.js";
|
import { buildPostScriptWasmFunction } from "./postscript/wasm_compiler.js";
|
||||||
import { isNumberArray } from "./core_utils.js";
|
import { isNumberArray } from "./core_utils.js";
|
||||||
import { LocalFunctionCache } from "./image_utils.js";
|
import { LocalFunctionCache } from "./image_utils.js";
|
||||||
@ -370,8 +371,8 @@ class PDFFunction {
|
|||||||
throw new FormatError("No range.");
|
throw new FormatError("No range.");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (factory.useWasm) {
|
try {
|
||||||
try {
|
if (factory.useWasm) {
|
||||||
const wasmFn = buildPostScriptWasmFunction(
|
const wasmFn = buildPostScriptWasmFunction(
|
||||||
fn.getString(),
|
fn.getString(),
|
||||||
domain,
|
domain,
|
||||||
@ -380,9 +381,14 @@ class PDFFunction {
|
|||||||
if (wasmFn) {
|
if (wasmFn) {
|
||||||
return wasmFn; // (src, srcOffset, dest, destOffset) → void
|
return wasmFn; // (src, srcOffset, dest, destOffset) → void
|
||||||
}
|
}
|
||||||
} catch {
|
} else {
|
||||||
// Fall through to the existing interpreter-based path.
|
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");
|
warn("Unable to compile PS function, using interpreter");
|
||||||
|
|||||||
@ -16,6 +16,13 @@
|
|||||||
import { Cmd, Dict, EOF, Name, Ref } from "./primitives.js";
|
import { Cmd, Dict, EOF, Name, Ref } from "./primitives.js";
|
||||||
import { Lexer, Parser } from "./parser.js";
|
import { Lexer, Parser } from "./parser.js";
|
||||||
import { OPS, shadow } from "../shared/util.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 { BaseStream } from "./base_stream.js";
|
||||||
import { EvaluatorPreprocessor } from "./evaluator.js";
|
import { EvaluatorPreprocessor } from "./evaluator.js";
|
||||||
|
|
||||||
@ -26,6 +33,262 @@ if (
|
|||||||
throw new Error("Not implemented: InternalViewerUtils");
|
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 = {
|
const InternalViewerUtils = {
|
||||||
tokenizeStream(stream, xref) {
|
tokenizeStream(stream, xref) {
|
||||||
const tokens = [];
|
const tokens = [];
|
||||||
@ -125,6 +388,130 @@ const InternalViewerUtils = {
|
|||||||
return { instructions, cmdNames };
|
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) {
|
tokenToJSObject(obj) {
|
||||||
if (obj instanceof Cmd) {
|
if (obj instanceof Cmd) {
|
||||||
return { type: "cmd", value: obj.cmd };
|
return { type: "cmd", value: obj.cmd };
|
||||||
|
|||||||
@ -664,7 +664,7 @@ class PSStackToTree {
|
|||||||
for (const [node, count] of refCount) {
|
for (const [node, count] of refCount) {
|
||||||
if (count > 1) {
|
if (count > 1) {
|
||||||
node.shared = true;
|
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]])),
|
Math: Object.fromEntries(MATH_IMPORTS.map(([name]) => [name, Math[name]])),
|
||||||
};
|
};
|
||||||
|
|
||||||
// The compiler.
|
// 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.
|
||||||
// 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.
|
|
||||||
class PsWasmCompiler {
|
class PsWasmCompiler {
|
||||||
static #initialized = false;
|
static #initialized = false;
|
||||||
|
|
||||||
@ -257,11 +252,12 @@ class PsWasmCompiler {
|
|||||||
this._nOut = range.length >> 1;
|
this._nOut = range.length >> 1;
|
||||||
this._range = range;
|
this._range = range;
|
||||||
this._code = [];
|
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._nextLocal = this._nIn;
|
||||||
|
|
||||||
this._freeLocals = [];
|
this._freeLocals = [];
|
||||||
// node → {local, remaining} for shared sub-expression caching (CSE).
|
this._sharedLocals = new Map(); // node → {local, remaining} for CSE
|
||||||
this._sharedLocals = new Map();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wasm emit helpers
|
// Wasm emit helpers
|
||||||
|
|||||||
@ -35,6 +35,7 @@ import {
|
|||||||
PsTernaryNode,
|
PsTernaryNode,
|
||||||
PsUnaryNode,
|
PsUnaryNode,
|
||||||
} from "../../src/core/postscript/ast.js";
|
} from "../../src/core/postscript/ast.js";
|
||||||
|
import { buildPostScriptJsFunction } from "../../src/core/postscript/js_evaluator.js";
|
||||||
|
|
||||||
// Precision argument for toBeCloseTo() in trigonometric tests.
|
// Precision argument for toBeCloseTo() in trigonometric tests.
|
||||||
const TRIGONOMETRY_EPS = 1e-10;
|
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.
|
* For single-output functions returns a scalar; for multi-output an Array.
|
||||||
*/
|
*/
|
||||||
function compileAndRun(src, domain, range, args) {
|
function compileAndRun(src, domain, range, args) {
|
||||||
const fn = buildPostScriptWasmFunction(src, domain, range);
|
const wasmFn = buildPostScriptWasmFunction(src, domain, range);
|
||||||
if (!fn) {
|
const jsFn = buildPostScriptJsFunction(src, domain, range);
|
||||||
|
if (!wasmFn) {
|
||||||
|
expect(jsFn).toBeNull();
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
expect(jsFn).not.toBeNull();
|
||||||
const nOut = range.length >> 1;
|
const nOut = range.length >> 1;
|
||||||
const srcBuf = new Float64Array(args);
|
const srcBuf = new Float64Array(args);
|
||||||
const dest = new Float64Array(nOut);
|
const wasmDest = new Float64Array(nOut);
|
||||||
fn(srcBuf, 0, dest, 0);
|
const jsDest = new Float64Array(nOut);
|
||||||
return nOut === 1 ? dest[0] : Array.from(dest);
|
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) {
|
function readULEB128(bytes, offset) {
|
||||||
@ -257,186 +266,161 @@ describe("PostScript Type 4 lexer, parser, and Wasm compiler", function () {
|
|||||||
// Arithmetic.
|
// Arithmetic.
|
||||||
|
|
||||||
it("compiles add", async function () {
|
it("compiles add", async function () {
|
||||||
const r = await compileAndRun(
|
const r = compileAndRun("{ add }", [0, 1, 0, 1], [0, 2], [0.3, 0.7]);
|
||||||
"{ add }",
|
|
||||||
[0, 1, 0, 1],
|
|
||||||
[0, 2],
|
|
||||||
[0.3, 0.7]
|
|
||||||
);
|
|
||||||
expect(r).toBeCloseTo(1.0, 9);
|
expect(r).toBeCloseTo(1.0, 9);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("compiles sub", async function () {
|
it("compiles sub", async function () {
|
||||||
const r = await compileAndRun(
|
const r = compileAndRun("{ sub }", [0, 1, 0, 1], [0, 1], [0.8, 0.3]);
|
||||||
"{ sub }",
|
|
||||||
[0, 1, 0, 1],
|
|
||||||
[0, 1],
|
|
||||||
[0.8, 0.3]
|
|
||||||
);
|
|
||||||
expect(r).toBeCloseTo(0.5, 9);
|
expect(r).toBeCloseTo(0.5, 9);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("compiles mul", async function () {
|
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);
|
expect(r).toBeCloseTo(0.2, 9);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("compiles div", async function () {
|
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);
|
expect(r).toBeCloseTo(2, 9);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("div by zero returns 0", async function () {
|
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);
|
expect(r).toBe(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("compiles idiv", async function () {
|
it("compiles idiv", async function () {
|
||||||
const r = await compileAndRun(
|
const r = compileAndRun("{ idiv }", [0, 10, 1, 10], [0, 10], [7, 2]);
|
||||||
"{ idiv }",
|
|
||||||
[0, 10, 1, 10],
|
|
||||||
[0, 10],
|
|
||||||
[7, 2]
|
|
||||||
);
|
|
||||||
expect(r).toBeCloseTo(3, 9);
|
expect(r).toBeCloseTo(3, 9);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("idiv by zero returns 0", async function () {
|
it("idiv by zero returns 0", async function () {
|
||||||
const r = await compileAndRun(
|
const r = compileAndRun("{ idiv }", [0, 10, 0, 10], [0, 10], [5, 0]);
|
||||||
"{ idiv }",
|
|
||||||
[0, 10, 0, 10],
|
|
||||||
[0, 10],
|
|
||||||
[5, 0]
|
|
||||||
);
|
|
||||||
expect(r).toBe(0);
|
expect(r).toBe(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("compiles mod", async function () {
|
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);
|
expect(r).toBeCloseTo(1, 9);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("mod by zero returns 0", async function () {
|
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);
|
expect(r).toBe(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("compiles mod with constant divisor", async function () {
|
it("compiles mod with constant divisor", async function () {
|
||||||
// { 3 mod } — divisor is a compile-time constant, exercises the
|
// { 3 mod } — divisor is a compile-time constant, exercises the
|
||||||
// constant-divisor branch in _compileModNode.
|
// 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
|
expect(r).toBeCloseTo(1, 9); // 7 mod 3 = 1
|
||||||
});
|
});
|
||||||
|
|
||||||
it("compiles integer xor (bitwise)", async function () {
|
it("compiles integer xor (bitwise)", async function () {
|
||||||
// { 5 xor } with an integer-typed arg — exercises the non-boolean path in
|
// { 5 xor } with an integer-typed arg — exercises the non-boolean path in
|
||||||
// _compileBitwiseOperandI32 and the xor case in _compileBitwiseNode.
|
// _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
|
expect(r).toBeCloseTo(6, 9); // 3 XOR 5 = 6
|
||||||
});
|
});
|
||||||
|
|
||||||
it("compiles neg", async function () {
|
it("compiles neg", async function () {
|
||||||
// neg applied to a variable — the optimizer cannot fold this.
|
// neg applied to a variable — the optimizer cannot fold this.
|
||||||
// abs(neg(x)) is optimized to abs(x), so test neg alone.
|
// 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);
|
expect(r).toBeCloseTo(0.5, 9);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("compiles neg and abs", async function () {
|
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);
|
expect(r).toBeCloseTo(0.8, 9);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("compiles cvi (truncate to integer)", async function () {
|
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
|
expect(r).toBeCloseTo(2, 9); // trunc(0.5 + 1.7) = trunc(2.2) = 2
|
||||||
});
|
});
|
||||||
|
|
||||||
it("compiles cvr (identity on reals)", async function () {
|
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);
|
expect(r).toBeCloseTo(0.7, 9);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Math.
|
// Math.
|
||||||
|
|
||||||
it("compiles sqrt", async function () {
|
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);
|
expect(r).toBeCloseTo(3, 9);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("compiles floor", async function () {
|
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);
|
expect(r).toBeCloseTo(1, 9);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("compiles ceiling", async function () {
|
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);
|
expect(r).toBeCloseTo(2, 9);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("compiles round", async function () {
|
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);
|
expect(r).toBeCloseTo(2, 9);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("round uses round-half-up (0.5 rounds to 1, -0.5 rounds to 0)", async function () {
|
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);
|
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);
|
expect(r2).toBe(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("compiles truncate", async function () {
|
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);
|
expect(r).toBeCloseTo(-1, 9);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("compiles ln", async function () {
|
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);
|
expect(r).toBeCloseTo(1, 9);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("compiles log (base 10)", async function () {
|
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);
|
expect(r).toBeCloseTo(2, 9);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("compiles exp (base ^ exponent)", async function () {
|
it("compiles exp (base ^ exponent)", async function () {
|
||||||
const r = await compileAndRun(
|
const r = compileAndRun("{ exp }", [0, 10, 0, 10], [0, 2000], [2, 10]);
|
||||||
"{ exp }",
|
|
||||||
[0, 10, 0, 10],
|
|
||||||
[0, 2000],
|
|
||||||
[2, 10]
|
|
||||||
);
|
|
||||||
expect(r).toBeCloseTo(1024, 6);
|
expect(r).toBeCloseTo(1024, 6);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("compiles x ^ -1 → 1/x (strength reduction)", async function () {
|
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
|
expect(r).toBeCloseTo(0.5, 9); // 1/2 = 0.5
|
||||||
});
|
});
|
||||||
|
|
||||||
it("compiles x ^ 3 → (x*x)*x (strength reduction)", async function () {
|
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
|
expect(r).toBeCloseTo(8, 9); // 2^3 = 8
|
||||||
});
|
});
|
||||||
|
|
||||||
it("compiles x ^ 4 → (x*x)*(x*x) (strength reduction)", async function () {
|
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
|
// x^4 uses CSE: x*x is computed once and squared — exercises the
|
||||||
// local_tee/local_get path in _compileStandardBinaryNode.
|
// 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
|
expect(r).toBeCloseTo(16, 9); // 2^4 = 16
|
||||||
});
|
});
|
||||||
|
|
||||||
// Trigonometry (degrees).
|
// Trigonometry (degrees).
|
||||||
|
|
||||||
it("compiles sin (degrees)", async function () {
|
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);
|
expect(r).toBeCloseTo(1, TRIGONOMETRY_EPS);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("compiles cos (degrees)", async function () {
|
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);
|
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 () {
|
it("compiles atan (degrees, result in [0,360))", async function () {
|
||||||
// atan(1, 1) = 45°
|
// atan(1, 1) = 45°
|
||||||
const r = await compileAndRun(
|
const r = compileAndRun("{ atan }", [-10, 10, -10, 10], [0, 360], [1, 1]);
|
||||||
"{ atan }",
|
|
||||||
[-10, 10, -10, 10],
|
|
||||||
[0, 360],
|
|
||||||
[1, 1]
|
|
||||||
);
|
|
||||||
expect(r).toBeCloseTo(45, 6);
|
expect(r).toBeCloseTo(45, 6);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("atan normalizes negative angles to [0,360)", async function () {
|
it("atan normalizes negative angles to [0,360)", async function () {
|
||||||
// atan(-1, 1) would be -45°, should become 315°
|
// atan(-1, 1) would be -45°, should become 315°
|
||||||
const r = await compileAndRun(
|
const r = compileAndRun(
|
||||||
"{ atan }",
|
"{ atan }",
|
||||||
[-10, 10, -10, 10],
|
[-10, 10, -10, 10],
|
||||||
[0, 360],
|
[0, 360],
|
||||||
@ -475,33 +454,23 @@ describe("PostScript Type 4 lexer, parser, and Wasm compiler", function () {
|
|||||||
// Stack operators.
|
// Stack operators.
|
||||||
|
|
||||||
it("compiles dup", async function () {
|
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);
|
expect(r).toBeCloseTo(0.25, 9);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("compiles exch", async function () {
|
it("compiles exch", async function () {
|
||||||
const r = await compileAndRun(
|
const r = compileAndRun("{ exch div }", [0, 10, 0, 10], [0, 10], [1, 2]);
|
||||||
"{ exch div }",
|
|
||||||
[0, 10, 0, 10],
|
|
||||||
[0, 10],
|
|
||||||
[1, 2]
|
|
||||||
);
|
|
||||||
expect(r).toBeCloseTo(2, 9); // 2 / 1
|
expect(r).toBeCloseTo(2, 9); // 2 / 1
|
||||||
});
|
});
|
||||||
|
|
||||||
it("compiles pop", async function () {
|
it("compiles pop", async function () {
|
||||||
const r = await compileAndRun(
|
const r = compileAndRun("{ pop }", [0, 1, 0, 1], [0, 1], [0.3, 0.7]);
|
||||||
"{ pop }",
|
|
||||||
[0, 1, 0, 1],
|
|
||||||
[0, 1],
|
|
||||||
[0.3, 0.7]
|
|
||||||
);
|
|
||||||
expect(r).toBeCloseTo(0.3, 9); // 0.7 popped, 0.3 remains
|
expect(r).toBeCloseTo(0.3, 9); // 0.7 popped, 0.3 remains
|
||||||
});
|
});
|
||||||
|
|
||||||
it("compiles copy", async function () {
|
it("compiles copy", async function () {
|
||||||
// { 1 copy add }: one input a → stack [a, a] → add → [2a]
|
// { 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);
|
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 () {
|
it("compiles 3-input function", async function () {
|
||||||
const r = await compileAndRun(
|
const r = compileAndRun(
|
||||||
"{ add add }",
|
"{ add add }",
|
||||||
[0, 1, 0, 1, 0, 1],
|
[0, 1, 0, 1, 0, 1],
|
||||||
[0, 3],
|
[0, 3],
|
||||||
@ -602,7 +571,7 @@ describe("PostScript Type 4 lexer, parser, and Wasm compiler", function () {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("compiles 4-input function", async function () {
|
it("compiles 4-input function", async function () {
|
||||||
const r = await compileAndRun(
|
const r = compileAndRun(
|
||||||
"{ add add add }",
|
"{ add add add }",
|
||||||
[0, 1, 0, 1, 0, 1, 0, 1],
|
[0, 1, 0, 1, 0, 1, 0, 1],
|
||||||
[0, 4],
|
[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 () {
|
it("compiles 5-input function (default caller path)", async function () {
|
||||||
const r = await compileAndRun(
|
const r = compileAndRun(
|
||||||
"{ add add add add }",
|
"{ add add add add }",
|
||||||
[0, 1, 0, 1, 0, 1, 0, 1, 0, 1],
|
[0, 1, 0, 1, 0, 1, 0, 1, 0, 1],
|
||||||
[0, 5],
|
[0, 5],
|
||||||
@ -624,38 +593,38 @@ describe("PostScript Type 4 lexer, parser, and Wasm compiler", function () {
|
|||||||
// Comparison / boolean.
|
// Comparison / boolean.
|
||||||
|
|
||||||
it("compiles eq", async function () {
|
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);
|
expect(r).toBeCloseTo(1, 9);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("compiles ne (not-equal)", async function () {
|
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
|
expect(r).toBeCloseTo(1, 9); // 0.3 ≠ 0.5 → true → 1
|
||||||
});
|
});
|
||||||
|
|
||||||
it("compiles lt (less-than)", async function () {
|
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
|
expect(r).toBeCloseTo(1, 9); // 0.3 < 0.5 → true → 1
|
||||||
});
|
});
|
||||||
|
|
||||||
it("compiles ge (greater-or-equal)", async function () {
|
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
|
expect(r).toBeCloseTo(1, 9); // 0.7 ≥ 0.5 → true → 1
|
||||||
});
|
});
|
||||||
|
|
||||||
it("compiles gt", async function () {
|
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);
|
expect(r).toBeCloseTo(1, 9);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("compiles le", async function () {
|
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);
|
expect(r).toBeCloseTo(1, 9);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("compiles true and false literals", async function () {
|
it("compiles true and false literals", async function () {
|
||||||
const t = await compileAndRun("{ true }", [], [0, 1], []);
|
const t = compileAndRun("{ true }", [], [0, 1], []);
|
||||||
const f = await compileAndRun("{ false }", [], [0, 1], []);
|
const f = compileAndRun("{ false }", [], [0, 1], []);
|
||||||
expect(t).toBeCloseTo(1, 9);
|
expect(t).toBeCloseTo(1, 9);
|
||||||
expect(f).toBeCloseTo(0, 9);
|
expect(f).toBeCloseTo(0, 9);
|
||||||
});
|
});
|
||||||
@ -663,7 +632,7 @@ describe("PostScript Type 4 lexer, parser, and Wasm compiler", function () {
|
|||||||
// Conditionals.
|
// Conditionals.
|
||||||
|
|
||||||
it("compiles ifelse — true branch taken", async function () {
|
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 }",
|
"{ dup 0.5 gt { 2 mul } { 0.5 mul } ifelse }",
|
||||||
[0, 1],
|
[0, 1],
|
||||||
[0, 2],
|
[0, 2],
|
||||||
@ -673,7 +642,7 @@ describe("PostScript Type 4 lexer, parser, and Wasm compiler", function () {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("compiles ifelse — false branch taken", async 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 }",
|
"{ dup 0.5 gt { 2 mul } { 0.5 mul } ifelse }",
|
||||||
[0, 1],
|
[0, 1],
|
||||||
[0, 2],
|
[0, 2],
|
||||||
@ -684,7 +653,7 @@ describe("PostScript Type 4 lexer, parser, and Wasm compiler", function () {
|
|||||||
|
|
||||||
it("compiles if — condition true", async function () {
|
it("compiles if — condition true", async function () {
|
||||||
// { dup 1 gt { pop 1 } if } — clamp x to 1 from above
|
// { 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 }",
|
"{ dup 1 gt { pop 1 } if }",
|
||||||
[0, 2],
|
[0, 2],
|
||||||
[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 () {
|
it("compiles if — condition false", async function () {
|
||||||
const r = await compileAndRun(
|
const r = compileAndRun(
|
||||||
"{ dup 1 gt { pop 1 } if }",
|
"{ dup 1 gt { pop 1 } if }",
|
||||||
[0, 2],
|
[0, 2],
|
||||||
[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 () {
|
it("clamps output to declared range", async function () {
|
||||||
// mul exceeds range [0, 0.5] → result clamped
|
// mul exceeds range [0, 0.5] → result clamped
|
||||||
const r = await compileAndRun(
|
const r = compileAndRun("{ add }", [0, 1, 0, 1], [0, 0.5], [0.4, 0.4]);
|
||||||
"{ add }",
|
|
||||||
[0, 1, 0, 1],
|
|
||||||
[0, 0.5],
|
|
||||||
[0.4, 0.4]
|
|
||||||
);
|
|
||||||
expect(r).toBeCloseTo(0.5, 9);
|
expect(r).toBeCloseTo(0.5, 9);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Bitwise.
|
// Bitwise.
|
||||||
|
|
||||||
it("compiles bitshift left (literal shift)", async function () {
|
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
|
expect(r).toBeCloseTo(8, 9); // 1 << 3
|
||||||
});
|
});
|
||||||
|
|
||||||
it("compiles bitshift right (negative literal shift)", async function () {
|
it("compiles bitshift right (negative literal shift)", async function () {
|
||||||
const r = await compileAndRun(
|
const r = compileAndRun("{ -2 bitshift }", [-256, 256], [-256, 256], [8]);
|
||||||
"{ -2 bitshift }",
|
|
||||||
[-256, 256],
|
|
||||||
[-256, 256],
|
|
||||||
[8]
|
|
||||||
);
|
|
||||||
expect(r).toBeCloseTo(2, 9); // 8 >> 2
|
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.
|
// b |= 0x80 branch in _emitULEB128.
|
||||||
// Wasm i32.shl uses shift % 32, so 128 % 32 = 0 →
|
// Wasm i32.shl uses shift % 32, so 128 % 32 = 0 →
|
||||||
// left-shift by 0 = identity.
|
// left-shift by 0 = identity.
|
||||||
const r = await compileAndRun(
|
const r = compileAndRun(
|
||||||
"{ 128 bitshift }",
|
"{ 128 bitshift }",
|
||||||
[-1000, 1000],
|
[-1000, 1000],
|
||||||
[-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.
|
// exercising the byte |= 0x80 branch in unsignedLEB128.
|
||||||
const src =
|
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 }";
|
"{ 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);
|
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 () {
|
it("compiles boolean not (logical NOT)", async function () {
|
||||||
// 0.5 0.5 eq → true (1.0); not → false (0.0)
|
// 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);
|
expect(r).toBeCloseTo(0, 9);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("compiles integer not (bitwise NOT)", async function () {
|
it("compiles integer not (bitwise NOT)", async function () {
|
||||||
// ~5 = -6
|
// ~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);
|
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 () {
|
it("ifelse with comparison condition (true branch)", async function () {
|
||||||
// x > 0.5: comparison emitted directly as i32, no f64 round-trip.
|
// 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.5 gt { 1 } { 0 } ifelse }",
|
||||||
[0, 1],
|
[0, 1],
|
||||||
[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 () {
|
it("ifelse with comparison condition (false branch)", async function () {
|
||||||
const r = await compileAndRun(
|
const r = compileAndRun(
|
||||||
"{ 0.5 gt { 1 } { 0 } ifelse }",
|
"{ 0.5 gt { 1 } { 0 } ifelse }",
|
||||||
[0, 1],
|
[0, 1],
|
||||||
[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
|
// node is a PsBinaryNode(and) — exercises the i32_trunc_f64_s fallback
|
||||||
// in _compileNodeAsBoolI32.
|
// in _compileNodeAsBoolI32.
|
||||||
const src = "{ dup 0.3 gt exch 0.7 lt and { 1 } { 0 } ifelse }";
|
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)
|
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
|
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
|
// not(and(two comparisons)) — exercises the boolean `not` path in
|
||||||
// _compileNodeAsBoolI32 (recursive call + i32.eqz).
|
// _compileNodeAsBoolI32 (recursive call + i32.eqz).
|
||||||
const src = "{ dup 0.3 gt exch 0.7 lt and not { 1 } { 0 } ifelse }";
|
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
|
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
|
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)
|
// and(x>0.3, x<0.7) used as the direct output (not as ternary condition)
|
||||||
// — exercises _compileBitwiseOperandI32 with boolean operands.
|
// — exercises _compileBitwiseOperandI32 with boolean operands.
|
||||||
const src = "{ dup 0.3 gt exch 0.7 lt and }";
|
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);
|
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);
|
expect(r1).toBeCloseTo(0, 9);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("boolean-or of two comparisons as standalone output", async function () {
|
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].
|
// 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 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);
|
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);
|
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
|
// _compileNodeAsBoolI32 for the operand, eliminating all f64/i32
|
||||||
// round-trips.
|
// round-trips.
|
||||||
const src = "{ dup 0.3 gt exch 0.7 lt and not }";
|
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
|
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
|
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.
|
// Each ternary condition goes through _compileNodeAsBoolI32.
|
||||||
const src =
|
const src =
|
||||||
"{ dup 0.3 lt { pop 0 } { dup 0.7 gt { pop 1 } { pop 0.5 } ifelse } ifelse }";
|
"{ 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);
|
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);
|
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);
|
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
|
// or(x<0.3, x>0.7) as ternary condition — exercises the TOKEN.or case
|
||||||
// in the boolean and/or/xor branch of _compileNodeAsBoolI32.
|
// in the boolean and/or/xor branch of _compileNodeAsBoolI32.
|
||||||
const src = "{ dup 0.3 lt exch 0.7 gt or { 1 } { 0 } ifelse }";
|
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
|
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
|
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.
|
// _compileNodeAsBoolI32; true when exactly one condition holds.
|
||||||
const src = "{ dup 0.5 lt exch 0.3 gt xor { 1 } { 0 } ifelse }";
|
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
|
// 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);
|
expect(r0).toBeCloseTo(0, 9);
|
||||||
// x=0.2: 0.2<0.5=true, 0.2>0.3=false → xor=true → 1
|
// 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);
|
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
|
// The condition is the raw input arg (numeric, not boolean), so
|
||||||
// _compileNodeAsBoolI32 falls through to the general path and emits
|
// _compileNodeAsBoolI32 falls through to the general path and emits
|
||||||
// f64.const 0 / f64.ne to convert to i32.
|
// f64.const 0 / f64.ne to convert to i32.
|
||||||
const r0 = await compileAndRun(
|
const r0 = compileAndRun("{ { 1 } { 0 } ifelse }", [0, 1], [0, 1], [0.7]);
|
||||||
"{ { 1 } { 0 } ifelse }",
|
|
||||||
[0, 1],
|
|
||||||
[0, 1],
|
|
||||||
[0.7]
|
|
||||||
);
|
|
||||||
expect(r0).toBeCloseTo(1, 9); // 0.7 ≠ 0 → truthy → 1
|
expect(r0).toBeCloseTo(1, 9); // 0.7 ≠ 0 → truthy → 1
|
||||||
const r1 = await compileAndRun(
|
const r1 = compileAndRun("{ { 1 } { 0 } ifelse }", [0, 1], [0, 1], [0]);
|
||||||
"{ { 1 } { 0 } ifelse }",
|
|
||||||
[0, 1],
|
|
||||||
[0, 1],
|
|
||||||
[0]
|
|
||||||
);
|
|
||||||
expect(r1).toBeCloseTo(0, 9); // 0 → falsy → 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,
|
// (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
|
// add_node, add_node) where add_node is non-trivial — exercises the
|
||||||
// local_tee/local_get path in _compileStandardBinaryNode.
|
// 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
|
expect(r).toBeCloseTo(16, 9); // (3+1)^2 = 16
|
||||||
});
|
});
|
||||||
|
|
||||||
it("shared non-trivial operand uses local_tee (x+2)^2 via dup", async function () {
|
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)
|
// `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.
|
// with the same reference twice — exercises the local_tee/local_get path.
|
||||||
const r = await compileAndRun(
|
const r = compileAndRun("{ 2 add dup mul }", [0, 10], [0, 100], [3]);
|
||||||
"{ 2 add dup mul }",
|
|
||||||
[0, 10],
|
|
||||||
[0, 100],
|
|
||||||
[3]
|
|
||||||
);
|
|
||||||
expect(r).toBeCloseTo(25, 9); // (3+2)^2 = 25
|
expect(r).toBeCloseTo(25, 9); // (3+2)^2 = 25
|
||||||
});
|
});
|
||||||
|
|
||||||
it("compiles x^3 without changing behavior", async function () {
|
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);
|
expect(r).toBeCloseTo(8, 9);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("compiles x^4 without changing behavior", async function () {
|
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);
|
expect(r).toBeCloseTo(16, 9);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("compiles x^-1 without changing behavior", async function () {
|
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);
|
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.
|
// min/max fold and related runtime tests.
|
||||||
|
|
||||||
it("compiles x^0.25 → sqrt(sqrt(x))", async function () {
|
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
|
expect(r).toBeCloseTo(2, 9); // 16^0.25 = 2
|
||||||
});
|
});
|
||||||
|
|
||||||
it("compiles neg(a − b) → b − a", async function () {
|
it("compiles neg(a − b) → b − a", async function () {
|
||||||
// neg(x - 3) = 3 - x; at x=1 → 2
|
// 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);
|
expect(r).toBeCloseTo(2, 9);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("compiles min(max(x, 0.8), 0.5) → constant 0.5", async function () {
|
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.
|
// 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.8 lt { pop 0.8 } { } ifelse " +
|
||||||
"dup 0.5 gt { pop 0.5 } { } ifelse }",
|
"dup 0.5 gt { pop 0.5 } { } ifelse }",
|
||||||
[0, 1],
|
[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 () {
|
it("min/max fold: upper clamp emits f64.min", async function () {
|
||||||
// x > 1 → clamp to 1; x ≤ 1 → pass through.
|
// x > 1 → clamp to 1; x ≤ 1 → pass through.
|
||||||
const r1 = await compileAndRun(
|
const r1 = compileAndRun(
|
||||||
"{ dup 1 gt { pop 1 } { } ifelse }",
|
"{ dup 1 gt { pop 1 } { } ifelse }",
|
||||||
[0, 2],
|
[0, 2],
|
||||||
[0, 2],
|
[0, 2],
|
||||||
[2]
|
[2]
|
||||||
);
|
);
|
||||||
expect(r1).toBeCloseTo(1, 9);
|
expect(r1).toBeCloseTo(1, 9);
|
||||||
const r2 = await compileAndRun(
|
const r2 = compileAndRun(
|
||||||
"{ dup 1 gt { pop 1 } { } ifelse }",
|
"{ dup 1 gt { pop 1 } { } ifelse }",
|
||||||
[0, 2],
|
[0, 2],
|
||||||
[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 () {
|
it("min/max fold: lower clamp emits f64.max", async function () {
|
||||||
// x < 0 → clamp to 0; x ≥ 0 → pass through.
|
// x < 0 → clamp to 0; x ≥ 0 → pass through.
|
||||||
const r1 = await compileAndRun(
|
const r1 = compileAndRun(
|
||||||
"{ dup 0 lt { pop 0 } { } ifelse }",
|
"{ dup 0 lt { pop 0 } { } ifelse }",
|
||||||
[-1, 1],
|
[-1, 1],
|
||||||
[0, 1],
|
[0, 1],
|
||||||
[-0.5]
|
[-0.5]
|
||||||
);
|
);
|
||||||
expect(r1).toBeCloseTo(0, 9);
|
expect(r1).toBeCloseTo(0, 9);
|
||||||
const r2 = await compileAndRun(
|
const r2 = compileAndRun(
|
||||||
"{ dup 0 lt { pop 0 } { } ifelse }",
|
"{ dup 0 lt { pop 0 } { } ifelse }",
|
||||||
[-1, 1],
|
[-1, 1],
|
||||||
[0, 1],
|
[0, 1],
|
||||||
|
|||||||
@ -157,6 +157,7 @@
|
|||||||
font-size: 0.8em;
|
font-size: 0.8em;
|
||||||
color: var(--muted-color);
|
color: var(--muted-color);
|
||||||
user-select: none;
|
user-select: none;
|
||||||
|
height: 22px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mlc-num-item.mlc-match {
|
.mlc-num-item.mlc-match {
|
||||||
|
|||||||
@ -179,6 +179,7 @@
|
|||||||
display: block;
|
display: block;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
padding-inline-start: 0.5em;
|
padding-inline-start: 0.5em;
|
||||||
|
height: 22px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.raw-bytes-stream {
|
.raw-bytes-stream {
|
||||||
|
|||||||
@ -235,6 +235,32 @@ class TreeView {
|
|||||||
* into the ref's children container, avoiding an extra toggle level).
|
* into the ref's children container, avoiding an extra toggle level).
|
||||||
*/
|
*/
|
||||||
#buildChildren(value, doc, container) {
|
#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)) {
|
if (this.#isStream(value)) {
|
||||||
for (const [k, v] of Object.entries(value.dict)) {
|
for (const [k, v] of Object.entries(value.dict)) {
|
||||||
container.append(this.#renderNode(k, v, doc));
|
container.append(this.#renderNode(k, v, doc));
|
||||||
@ -310,6 +336,8 @@ class TreeView {
|
|||||||
span.append(this.#makeSpan("bracket", "]"));
|
span.append(this.#makeSpan("bracket", "]"));
|
||||||
return span;
|
return span;
|
||||||
}
|
}
|
||||||
|
case "brace":
|
||||||
|
return this.#makeSpan("bracket", token.value);
|
||||||
case "dict": {
|
case "dict": {
|
||||||
const span = document.createElement("span");
|
const span = document.createElement("span");
|
||||||
span.className = "token-dict";
|
span.className = "token-dict";
|
||||||
@ -355,6 +383,8 @@ class TreeView {
|
|||||||
return String(token.value);
|
return String(token.value);
|
||||||
case "null":
|
case "null":
|
||||||
return "null";
|
return "null";
|
||||||
|
case "brace":
|
||||||
|
return token.value;
|
||||||
case "array":
|
case "array":
|
||||||
return `[ ${token.value.map(t => this.#tokenToText(t)).join(" ")} ]`;
|
return `[ ${token.value.map(t => this.#tokenToText(t)).join(" ")} ]`;
|
||||||
case "dict": {
|
case "dict": {
|
||||||
@ -470,6 +500,96 @@ class TreeView {
|
|||||||
return btn;
|
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
|
// Fills container with the content stream panel (parsed or raw), with a
|
||||||
// toggle button in the toolbar that swaps the view in-place.
|
// toggle button in the toolbar that swaps the view in-place.
|
||||||
#buildContentStreamPanel(val, container, labelEl = null) {
|
#buildContentStreamPanel(val, container, labelEl = null) {
|
||||||
@ -538,6 +658,15 @@ class TreeView {
|
|||||||
return this.#renderContentStream(value);
|
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
|
// Stream → expandable showing dict entries + byte count or image preview
|
||||||
if (this.#isStream(value)) {
|
if (this.#isStream(value)) {
|
||||||
return this.#renderExpandable("[Stream]", "stream-label", container =>
|
return this.#renderExpandable("[Stream]", "stream-label", container =>
|
||||||
@ -841,6 +970,17 @@ class TreeView {
|
|||||||
#isFormXObjectStream(val) {
|
#isFormXObjectStream(val) {
|
||||||
return this.#isStream(val) && val.contentStream === true;
|
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 };
|
export { TreeView };
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user