mirror of
https://github.com/mozilla/pdf.js.git
synced 2026-05-31 15:21:00 +02:00
The main goal is to remove the eval-based interpreter. In order to have some good performances, the new parser performs some optimizations on the AST (similar to the ones in the previous implementation), and the Wasm compiler generates code for the optimized AST. For now, in case of errors or unsupported features, the Wasm compiler returns null and the old interpreter is used as a fallback. Few things are still missing: - a wasm-based interpreter using a stack (in case the ps code isn't stack-free); - a better js implementation in case of disabled wasm. but they will be added in follow-up patches.
1308 lines
38 KiB
JavaScript
1308 lines
38 KiB
JavaScript
/* 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 { FormatError, warn } from "../../shared/util.js";
|
||
import { Lexer, TOKEN } from "./lexer.js";
|
||
|
||
// Value types for tree nodes — used to select the correct code-generation
|
||
// path for type-sensitive operators (currently `not`).
|
||
const PS_VALUE_TYPE = {
|
||
numeric: 0, // known to be a number (f64 in Wasm)
|
||
boolean: 1, // known to be a boolean (0.0 = false, 1.0 = true in f64)
|
||
unknown: 2, // indeterminate at compile time
|
||
};
|
||
|
||
// AST node type constants
|
||
|
||
const PS_NODE = {
|
||
// Parser AST node types (produced by Parser / parsePostScriptFunction)
|
||
program: 0,
|
||
block: 1,
|
||
number: 2,
|
||
operator: 3,
|
||
if: 4,
|
||
ifelse: 5,
|
||
// Tree AST node types (produced by PSStackToTree)
|
||
arg: 6,
|
||
const: 7,
|
||
unary: 8,
|
||
binary: 9,
|
||
ternary: 10,
|
||
};
|
||
|
||
// AST node classes
|
||
|
||
class PsNode {
|
||
constructor(type) {
|
||
this.type = type;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* The root node. Wraps the outermost `{ … }` of a Type 4 function body.
|
||
*/
|
||
class PsProgram extends PsNode {
|
||
constructor(body) {
|
||
super(PS_NODE.program);
|
||
/** @type {PsBlock} */
|
||
this.body = body;
|
||
}
|
||
}
|
||
|
||
class PsBlock extends PsNode {
|
||
constructor(instructions) {
|
||
super(PS_NODE.block);
|
||
/** @type {Array<PsNumber|PsOperator|PsIf|PsIfElse>} */
|
||
this.instructions = instructions;
|
||
}
|
||
}
|
||
|
||
class PsNumber extends PsNode {
|
||
/** @param {number} value */
|
||
constructor(value) {
|
||
super(PS_NODE.number);
|
||
this.value = value;
|
||
}
|
||
}
|
||
|
||
/** A regular PS operator (not `if` / `ifelse`). */
|
||
class PsOperator extends PsNode {
|
||
/** @param {number} op — one of the TOKEN.* constants from lexer.js */
|
||
constructor(op) {
|
||
super(PS_NODE.operator);
|
||
this.op = op;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* `<cond> { thenBlock } if`
|
||
*
|
||
* The condition value is consumed from the operand stack at runtime.
|
||
*/
|
||
class PsIf extends PsNode {
|
||
/** @param {PsBlock} then */
|
||
constructor(then) {
|
||
super(PS_NODE.if);
|
||
this.then = then;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* `<cond> { thenBlock } { elseBlock } ifelse`
|
||
*
|
||
* The condition value is consumed from the operand stack at runtime.
|
||
*/
|
||
class PsIfElse extends PsNode {
|
||
/**
|
||
* @param {PsBlock} then
|
||
* @param {PsBlock} otherwise
|
||
*/
|
||
constructor(then, otherwise) {
|
||
super(PS_NODE.ifelse);
|
||
this.then = then;
|
||
this.otherwise = otherwise;
|
||
}
|
||
}
|
||
|
||
// Tree AST node classes (produced by PSStackToTree)
|
||
|
||
/**
|
||
* A function input argument. `index` is the zero-based position in the
|
||
* domain — in0 has index 0, in1 has index 1, etc.
|
||
*/
|
||
class PsArgNode extends PsNode {
|
||
/** @param {number} index */
|
||
constructor(index) {
|
||
super(PS_NODE.arg);
|
||
this.index = index;
|
||
this.valueType = PS_VALUE_TYPE.numeric;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* A folded constant — a numeric or boolean literal that is known at
|
||
* compile time.
|
||
*/
|
||
class PsConstNode extends PsNode {
|
||
/** @param {number|boolean} value */
|
||
constructor(value) {
|
||
super(PS_NODE.const);
|
||
this.value = value;
|
||
this.valueType =
|
||
typeof value === "boolean"
|
||
? PS_VALUE_TYPE.boolean
|
||
: PS_VALUE_TYPE.numeric;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* A unary operation.
|
||
*/
|
||
class PsUnaryNode extends PsNode {
|
||
/**
|
||
* @param {number} op — TOKEN.* constant
|
||
* @param {PsNode} operand
|
||
* @param {number} [valueType]
|
||
*/
|
||
constructor(op, operand, valueType = PS_VALUE_TYPE.unknown) {
|
||
super(PS_NODE.unary);
|
||
this.op = op;
|
||
this.operand = operand;
|
||
this.valueType = valueType;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* A binary operation.
|
||
*
|
||
* `first` was the top-of-stack operand (popped first);
|
||
* `second` was the operand just below it (popped second).
|
||
*
|
||
* For non-commutative operators the mathematical meaning is
|
||
* second OP first
|
||
* e.g. `a b sub` → second = a, first = b → a − b.
|
||
*/
|
||
class PsBinaryNode extends PsNode {
|
||
/**
|
||
* @param {number} op — TOKEN.* constant
|
||
* @param {PsNode} first — was on top of stack
|
||
* @param {PsNode} second — was below top
|
||
* @param {number} [valueType]
|
||
*/
|
||
constructor(op, first, second, valueType = PS_VALUE_TYPE.unknown) {
|
||
super(PS_NODE.binary);
|
||
this.op = op;
|
||
this.first = first;
|
||
this.second = second;
|
||
this.valueType = valueType;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* A conditional expression: `cond ? then : otherwise`.
|
||
*
|
||
* Represents both PostScript `if` (where `otherwise` is the pre-existing
|
||
* stack value that would remain unchanged when the condition is false) and
|
||
* `ifelse` constructs, after the stack-to-tree conversion.
|
||
*/
|
||
class PsTernaryNode extends PsNode {
|
||
/**
|
||
* @param {PsNode} cond
|
||
* @param {PsNode} then
|
||
* @param {PsNode} otherwise
|
||
* @param {number} [valueType]
|
||
*/
|
||
constructor(cond, then, otherwise, valueType = PS_VALUE_TYPE.unknown) {
|
||
super(PS_NODE.ternary);
|
||
this.cond = cond;
|
||
this.then = then;
|
||
this.otherwise = otherwise;
|
||
this.valueType = valueType;
|
||
}
|
||
}
|
||
|
||
class Parser {
|
||
constructor(lexer) {
|
||
this.lexer = lexer;
|
||
this._token = null;
|
||
}
|
||
|
||
static _isRegularOperator(id) {
|
||
return id >= TOKEN.true && id < TOKEN.if;
|
||
}
|
||
|
||
// Fetch the next token from the lexer.
|
||
_advance() {
|
||
this._token = this.lexer.next();
|
||
}
|
||
|
||
// Assert that the current token has the given id, consume it, and return it.
|
||
_expect(id) {
|
||
if (this._token.id !== id) {
|
||
throw new FormatError(
|
||
`PostScript function: expected token id ${id}, got ${this._token.id}.`
|
||
);
|
||
}
|
||
const tok = this._token;
|
||
this._advance();
|
||
return tok;
|
||
}
|
||
|
||
/**
|
||
* Parse the full Type 4 function body.
|
||
*
|
||
* Grammar (simplified):
|
||
* program ::= '{' block '}'
|
||
* block ::= instruction*
|
||
* instruction ::= number
|
||
* | operator (any PS_OPERATOR except if / ifelse)
|
||
* | '{' block '}' 'if'
|
||
* | '{' block '}' '{' block '}' 'ifelse'
|
||
*
|
||
* @returns {PsProgram}
|
||
*/
|
||
parse() {
|
||
this._advance();
|
||
this._expect(TOKEN.lbrace);
|
||
const block = this._parseBlock();
|
||
this._expect(TOKEN.rbrace);
|
||
if (this._token.id !== TOKEN.eof) {
|
||
warn("PostScript function: unexpected content after closing brace.");
|
||
}
|
||
return new PsProgram(block);
|
||
}
|
||
|
||
_parseBlock() {
|
||
const instructions = [];
|
||
|
||
while (true) {
|
||
const tok = this._token;
|
||
switch (tok.id) {
|
||
case TOKEN.number:
|
||
instructions.push(new PsNumber(tok.value));
|
||
this._advance();
|
||
break;
|
||
|
||
case TOKEN.lbrace: {
|
||
// Start of a sub-procedure: must be followed by 'if' or '{ } ifelse'.
|
||
this._advance();
|
||
const thenBlock = this._parseBlock();
|
||
this._expect(TOKEN.rbrace);
|
||
|
||
if (this._token.id === TOKEN.if) {
|
||
this._advance();
|
||
instructions.push(new PsIf(thenBlock));
|
||
} else if (this._token.id === TOKEN.lbrace) {
|
||
this._advance();
|
||
const elseBlock = this._parseBlock();
|
||
this._expect(TOKEN.rbrace);
|
||
this._expect(TOKEN.ifelse);
|
||
instructions.push(new PsIfElse(thenBlock, elseBlock));
|
||
} else {
|
||
throw new FormatError(
|
||
"PostScript function: a procedure block must be followed by 'if' or '{…} ifelse'."
|
||
);
|
||
}
|
||
break;
|
||
}
|
||
|
||
case TOKEN.rbrace:
|
||
case TOKEN.eof:
|
||
// End of this block; let the caller consume the '}'.
|
||
return new PsBlock(instructions);
|
||
|
||
case TOKEN.if:
|
||
case TOKEN.ifelse:
|
||
// 'if'/'ifelse' without a preceding block.
|
||
throw new FormatError(
|
||
`PostScript function: unexpected '${tok.value}' operator.`
|
||
);
|
||
|
||
default:
|
||
if (Parser._isRegularOperator(tok.id)) {
|
||
instructions.push(new PsOperator(tok.id));
|
||
this._advance();
|
||
break;
|
||
}
|
||
throw new FormatError(
|
||
`PostScript function: unexpected token id ${tok.id}.`
|
||
);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Convenience function: tokenize and parse a PostScript Type 4 function body
|
||
* given as a plain string (already decoded from the PDF stream).
|
||
*
|
||
* @param {string} source
|
||
* @returns {PsProgram}
|
||
*/
|
||
function parsePostScriptFunction(source) {
|
||
return new Parser(new Lexer(source)).parse();
|
||
}
|
||
|
||
// Stack-to-tree transformation
|
||
|
||
/**
|
||
* Structural equality for tree nodes.
|
||
* Returns true when `a` and `b` represent the same sub-expression.
|
||
* Reference equality (`a === b`) is checked first, so shared nodes
|
||
* produced by `dup` are handled in O(1).
|
||
*/
|
||
function _nodesEqual(a, b) {
|
||
if (a === b) {
|
||
return true;
|
||
}
|
||
if (a.type !== b.type) {
|
||
return false;
|
||
}
|
||
switch (a.type) {
|
||
case PS_NODE.arg:
|
||
return a.index === b.index;
|
||
case PS_NODE.const:
|
||
return a.value === b.value;
|
||
case PS_NODE.unary:
|
||
return a.op === b.op && _nodesEqual(a.operand, b.operand);
|
||
case PS_NODE.binary:
|
||
return (
|
||
a.op === b.op &&
|
||
_nodesEqual(a.first, b.first) &&
|
||
_nodesEqual(a.second, b.second)
|
||
);
|
||
case PS_NODE.ternary:
|
||
return (
|
||
_nodesEqual(a.cond, b.cond) &&
|
||
_nodesEqual(a.then, b.then) &&
|
||
_nodesEqual(a.otherwise, b.otherwise)
|
||
);
|
||
default:
|
||
return false;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Evaluate a binary PostScript operator on two compile-time-known values.
|
||
* `a` is the second operand (was below top); `b` is the first (was on top).
|
||
* Returns `undefined` when the operation cannot be safely folded.
|
||
*/
|
||
function _evalBinaryConst(op, a, b) {
|
||
switch (op) {
|
||
case TOKEN.add:
|
||
return a + b;
|
||
case TOKEN.sub:
|
||
return a - b;
|
||
case TOKEN.mul:
|
||
return a * b;
|
||
case TOKEN.div:
|
||
return b !== 0 ? a / b : 0; // div by zero → 0
|
||
case TOKEN.idiv:
|
||
return b !== 0 ? Math.trunc(a / b) : 0; // div by zero → 0
|
||
case TOKEN.mod:
|
||
return b !== 0 ? a - Math.trunc(a / b) * b : 0; // div by zero → 0
|
||
case TOKEN.exp: {
|
||
const r = a ** b;
|
||
return Number.isFinite(r) ? r : undefined;
|
||
}
|
||
case TOKEN.atan: {
|
||
// PostScript: dy dx atan → angle in degrees in [0, 360)
|
||
let deg = Math.atan2(a, b) * (180 / Math.PI);
|
||
if (deg < 0) {
|
||
deg += 360;
|
||
}
|
||
return deg;
|
||
}
|
||
case TOKEN.eq:
|
||
return a === b;
|
||
case TOKEN.ne:
|
||
return a !== b;
|
||
case TOKEN.gt:
|
||
return a > b;
|
||
case TOKEN.ge:
|
||
return a >= b;
|
||
case TOKEN.lt:
|
||
return a < b;
|
||
case TOKEN.le:
|
||
return a <= b;
|
||
case TOKEN.and:
|
||
return typeof a === "boolean" ? a && b : (a & b) | 0;
|
||
case TOKEN.or:
|
||
return typeof a === "boolean" ? a || b : a | b | 0;
|
||
case TOKEN.xor:
|
||
return typeof a === "boolean" ? a !== b : (a ^ b) | 0;
|
||
case TOKEN.bitshift:
|
||
return b >= 0 ? (a << b) | 0 : (a >> -b) | 0;
|
||
case TOKEN.min:
|
||
return Math.min(a, b);
|
||
case TOKEN.max:
|
||
return Math.max(a, b);
|
||
default:
|
||
return undefined;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Evaluate a unary PostScript operator on a compile-time-known value.
|
||
* Returns `undefined` when the operation cannot be safely folded.
|
||
*/
|
||
function _evalUnaryConst(op, v) {
|
||
switch (op) {
|
||
case TOKEN.abs:
|
||
return Math.abs(v);
|
||
case TOKEN.neg:
|
||
return -v;
|
||
case TOKEN.ceiling:
|
||
return Math.ceil(v);
|
||
case TOKEN.floor:
|
||
return Math.floor(v);
|
||
case TOKEN.round:
|
||
return Math.round(v);
|
||
case TOKEN.truncate:
|
||
return Math.trunc(v);
|
||
case TOKEN.sqrt: {
|
||
const r = Math.sqrt(v);
|
||
return Number.isFinite(r) ? r : undefined;
|
||
}
|
||
case TOKEN.sin:
|
||
return Math.sin(((v % 360) * Math.PI) / 180);
|
||
case TOKEN.cos:
|
||
return Math.cos(((v % 360) * Math.PI) / 180);
|
||
case TOKEN.ln: {
|
||
const r = Math.log(v);
|
||
return Number.isFinite(r) ? r : undefined;
|
||
}
|
||
case TOKEN.log: {
|
||
const r = Math.log10(v);
|
||
return Number.isFinite(r) ? r : undefined;
|
||
}
|
||
case TOKEN.cvi:
|
||
return Math.trunc(v);
|
||
case TOKEN.cvr:
|
||
return v;
|
||
case TOKEN.not:
|
||
return typeof v === "boolean" ? !v : ~v;
|
||
default:
|
||
return undefined;
|
||
}
|
||
}
|
||
|
||
// Maximum number of nodes allowed on the virtual stack at any point during
|
||
// the stack-to-tree conversion. Programs that exceed this are rejected.
|
||
const MAX_STACK_SIZE = 100;
|
||
|
||
// Determine the PS_VALUE_TYPE of a unary operation's result.
|
||
// `not` propagates its operand's type (boolean not → boolean, integer not →
|
||
// numeric); every other unary op always yields a numeric result.
|
||
function _unaryValueType(op, operandType) {
|
||
return op === TOKEN.not ? operandType : PS_VALUE_TYPE.numeric;
|
||
}
|
||
|
||
// Determine the PS_VALUE_TYPE of a binary operation's result.
|
||
function _binaryValueType(op, firstType, secondType) {
|
||
switch (op) {
|
||
// Comparison operators always produce a boolean.
|
||
case TOKEN.eq:
|
||
case TOKEN.ne:
|
||
case TOKEN.gt:
|
||
case TOKEN.ge:
|
||
case TOKEN.lt:
|
||
case TOKEN.le:
|
||
return PS_VALUE_TYPE.boolean;
|
||
// and / or / xor preserve the type when both operands are the same known
|
||
// type (both boolean or both numeric); otherwise the type is unknown.
|
||
case TOKEN.and:
|
||
case TOKEN.or:
|
||
case TOKEN.xor:
|
||
return firstType === secondType && firstType !== PS_VALUE_TYPE.unknown
|
||
? firstType
|
||
: PS_VALUE_TYPE.unknown;
|
||
// All arithmetic / bitshift operators produce a numeric result.
|
||
default:
|
||
return PS_VALUE_TYPE.numeric;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Converts a stack-based PostScript parser AST (PsProgram) into a stack-free
|
||
* expression tree.
|
||
*
|
||
* The virtual operand stack is initialized with one PsArgNode per function
|
||
* input; each instruction then manipulates the stack just as it would at
|
||
* runtime, but instead of numbers the stack holds tree nodes.
|
||
*
|
||
* Algebraic optimizations are applied eagerly as each node is constructed:
|
||
* constant folding, identity/absorbing elements, and double-negation
|
||
* elimination.
|
||
*
|
||
* When the program finishes the remaining stack entries are the output
|
||
* expressions — one per function output channel.
|
||
*
|
||
* Usage:
|
||
* const outputs = new PSStackToTree().evaluate(program, numInputs);
|
||
*/
|
||
class PSStackToTree {
|
||
static #binaryOps = null;
|
||
|
||
static #unaryOps = null;
|
||
|
||
static #idempotentUnary = null;
|
||
|
||
static #negatedComparison = null;
|
||
|
||
static #init() {
|
||
// Binary operator ids — used by _evalOp.
|
||
PSStackToTree.#binaryOps = new Set([
|
||
TOKEN.add,
|
||
TOKEN.sub,
|
||
TOKEN.mul,
|
||
TOKEN.div,
|
||
TOKEN.idiv,
|
||
TOKEN.mod,
|
||
TOKEN.exp,
|
||
TOKEN.atan,
|
||
TOKEN.eq,
|
||
TOKEN.ne,
|
||
TOKEN.gt,
|
||
TOKEN.ge,
|
||
TOKEN.lt,
|
||
TOKEN.le,
|
||
TOKEN.and,
|
||
TOKEN.or,
|
||
TOKEN.xor,
|
||
TOKEN.bitshift,
|
||
]);
|
||
// Unary operator ids.
|
||
PSStackToTree.#unaryOps = new Set([
|
||
TOKEN.abs,
|
||
TOKEN.neg,
|
||
TOKEN.ceiling,
|
||
TOKEN.floor,
|
||
TOKEN.round,
|
||
TOKEN.truncate,
|
||
TOKEN.sqrt,
|
||
TOKEN.sin,
|
||
TOKEN.cos,
|
||
TOKEN.ln,
|
||
TOKEN.log,
|
||
TOKEN.cvi,
|
||
TOKEN.cvr,
|
||
TOKEN.not,
|
||
]);
|
||
// Unary operators where f(f(x)) = f(x) — applying them twice is the same
|
||
// as applying them once.
|
||
PSStackToTree.#idempotentUnary = new Set([
|
||
TOKEN.abs,
|
||
TOKEN.ceiling,
|
||
TOKEN.cvi,
|
||
TOKEN.cvr,
|
||
TOKEN.floor,
|
||
TOKEN.round,
|
||
TOKEN.truncate,
|
||
]);
|
||
// Maps each comparison operator to its logical negation.
|
||
// Used to simplify not(comparison) → negated-comparison.
|
||
PSStackToTree.#negatedComparison = new Map([
|
||
[TOKEN.eq, TOKEN.ne],
|
||
[TOKEN.ne, TOKEN.eq],
|
||
[TOKEN.lt, TOKEN.ge],
|
||
[TOKEN.le, TOKEN.gt],
|
||
[TOKEN.gt, TOKEN.le],
|
||
[TOKEN.ge, TOKEN.lt],
|
||
]);
|
||
}
|
||
|
||
/**
|
||
* @param {PsProgram} program
|
||
* @param {number} numInputs — number of domain values placed on the stack
|
||
* before the program runs (i.e. the length of the domain array / 2).
|
||
* @returns {Array<PsNode>} — one tree node per output value.
|
||
*/
|
||
evaluate(program, numInputs) {
|
||
if (!PSStackToTree.#binaryOps) {
|
||
PSStackToTree.#init();
|
||
}
|
||
this._failed = false;
|
||
if (numInputs > MAX_STACK_SIZE) {
|
||
return null;
|
||
}
|
||
const stack = [];
|
||
for (let i = 0; i < numInputs; i++) {
|
||
stack.push(new PsArgNode(i));
|
||
}
|
||
this._evalBlock(program.body, stack);
|
||
return this._failed ? null : stack;
|
||
}
|
||
|
||
_evalBlock(block, stack) {
|
||
this._evalBlockFrom(block.instructions, 0, stack);
|
||
}
|
||
|
||
/**
|
||
* Core evaluation loop. Processes `instructions[startIdx…]` in order,
|
||
* mutating `stack` as each instruction executes.
|
||
*
|
||
* When a `{ body } if` instruction grows the stack (the PostScript "early
|
||
* exit / guard" idiom), the remaining instructions in the current array are
|
||
* evaluated on **both** the true-branch stack and the false-branch stack,
|
||
* then the two results are merged into PsTernaryNodes. This handles
|
||
* patterns like:
|
||
*
|
||
* cond { pop R G B sentinel } if
|
||
* … more guards …
|
||
* sentinel 0 gt { defaultR defaultG defaultB } if
|
||
*/
|
||
_evalBlockFrom(instructions, startIdx, stack) {
|
||
for (let idx = startIdx; idx < instructions.length; idx++) {
|
||
if (this._failed) {
|
||
break;
|
||
}
|
||
const instr = instructions[idx];
|
||
switch (instr.type) {
|
||
case PS_NODE.number:
|
||
stack.push(new PsConstNode(instr.value));
|
||
if (stack.length > MAX_STACK_SIZE) {
|
||
this._failed = true;
|
||
}
|
||
break;
|
||
|
||
case PS_NODE.operator:
|
||
this._evalOp(instr.op, stack);
|
||
break;
|
||
|
||
case PS_NODE.if: {
|
||
// Pop condition, snapshot the stack, run the then-block on a copy,
|
||
// then merge.
|
||
if (stack.length < 1) {
|
||
this._failed = true;
|
||
break;
|
||
}
|
||
const cond = stack.pop();
|
||
const saved = stack.slice();
|
||
this._evalBlock(instr.then, stack);
|
||
if (this._failed) {
|
||
break;
|
||
}
|
||
if (stack.length === saved.length) {
|
||
// Normal case: depth preserved — positions that changed become
|
||
// PsTernaryNode(cond, thenValue, originalValue).
|
||
for (let i = 0; i < stack.length; i++) {
|
||
if (stack[i] !== saved[i]) {
|
||
stack[i] = this._makeTernary(cond, stack[i], saved[i]);
|
||
}
|
||
}
|
||
} else if (stack.length > saved.length) {
|
||
// "Guard / early-exit" pattern: the if-body pushed extra values.
|
||
if (cond.type === PS_NODE.const) {
|
||
// Condition is a compile-time constant: short-circuit without
|
||
// forking. For a false condition restore the saved stack; for a
|
||
// true condition keep the body result already on `stack`.
|
||
if (!cond.value) {
|
||
stack.length = 0;
|
||
stack.push(...saved);
|
||
}
|
||
break;
|
||
}
|
||
// Non-constant condition: evaluate the *rest* of this block on
|
||
// both the true-branch stack and the false-branch stack, then
|
||
// merge the two results into PsTernaryNodes.
|
||
const trueStack = stack.slice();
|
||
this._evalBlockFrom(instructions, idx + 1, trueStack);
|
||
if (this._failed) {
|
||
break;
|
||
}
|
||
const falseStack = saved;
|
||
this._evalBlockFrom(instructions, idx + 1, falseStack);
|
||
if (this._failed) {
|
||
break;
|
||
}
|
||
if (trueStack.length !== falseStack.length) {
|
||
// The two paths produced different stack depths. For
|
||
// well-formed PostScript functions this happens when the
|
||
// remaining code still has a "default value" guard that fires
|
||
// unconditionally for one path but not the other. Pad the
|
||
// shorter result with PsConstNode(0) so both have the same
|
||
// length; the padding zeros end up in ternary branches that
|
||
// are never selected at runtime.
|
||
const zero = new PsConstNode(0);
|
||
while (trueStack.length < falseStack.length) {
|
||
trueStack.push(zero);
|
||
}
|
||
while (falseStack.length < trueStack.length) {
|
||
falseStack.push(zero);
|
||
}
|
||
}
|
||
stack.length = 0;
|
||
for (let i = 0; i < trueStack.length; i++) {
|
||
stack.push(this._makeTernary(cond, trueStack[i], falseStack[i]));
|
||
}
|
||
return; // Remaining instructions already consumed above.
|
||
} else {
|
||
// Stack-shrinking if — cannot represent as a tree.
|
||
this._failed = true;
|
||
}
|
||
break;
|
||
}
|
||
|
||
case PS_NODE.ifelse: {
|
||
// Pop condition; run each branch on an independent copy of the
|
||
// current stack; zip the two resulting stacks into PsTernaryNodes
|
||
// wherever the branches disagree.
|
||
if (stack.length < 1) {
|
||
this._failed = true;
|
||
break;
|
||
}
|
||
const cond = stack.pop();
|
||
const snapshot = stack.slice();
|
||
|
||
const thenStack = snapshot.slice();
|
||
this._evalBlock(instr.then, thenStack);
|
||
if (this._failed) {
|
||
break;
|
||
}
|
||
|
||
const elseStack = snapshot.slice();
|
||
this._evalBlock(instr.otherwise, elseStack);
|
||
if (this._failed) {
|
||
break;
|
||
}
|
||
|
||
if (thenStack.length !== elseStack.length) {
|
||
// Pad the shorter branch with zeros so both have the same depth.
|
||
// For well-formed functions the extra zeros land in branches that
|
||
// are never selected at runtime.
|
||
const zero = new PsConstNode(0);
|
||
while (thenStack.length < elseStack.length) {
|
||
thenStack.push(zero);
|
||
}
|
||
while (elseStack.length < thenStack.length) {
|
||
elseStack.push(zero);
|
||
}
|
||
}
|
||
stack.length = 0;
|
||
for (let i = 0; i < thenStack.length; i++) {
|
||
stack.push(this._makeTernary(cond, thenStack[i], elseStack[i]));
|
||
}
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
_evalOp(op, stack) {
|
||
if (PSStackToTree.#binaryOps.has(op)) {
|
||
if (stack.length < 2) {
|
||
this._failed = true;
|
||
return;
|
||
}
|
||
const first = stack.pop();
|
||
const second = stack.pop();
|
||
stack.push(this._makeBinary(op, first, second));
|
||
return;
|
||
}
|
||
|
||
if (PSStackToTree.#unaryOps.has(op)) {
|
||
if (stack.length < 1) {
|
||
this._failed = true;
|
||
return;
|
||
}
|
||
stack.push(this._makeUnary(op, stack.pop()));
|
||
return;
|
||
}
|
||
|
||
switch (op) {
|
||
case TOKEN.true:
|
||
stack.push(new PsConstNode(true));
|
||
if (stack.length > MAX_STACK_SIZE) {
|
||
this._failed = true;
|
||
}
|
||
break;
|
||
|
||
case TOKEN.false:
|
||
stack.push(new PsConstNode(false));
|
||
if (stack.length > MAX_STACK_SIZE) {
|
||
this._failed = true;
|
||
}
|
||
break;
|
||
|
||
case TOKEN.dup:
|
||
if (stack.length < 1) {
|
||
this._failed = true;
|
||
break;
|
||
}
|
||
stack.push(stack.at(-1));
|
||
if (stack.length > MAX_STACK_SIZE) {
|
||
this._failed = true;
|
||
}
|
||
break;
|
||
|
||
case TOKEN.exch: {
|
||
if (stack.length < 2) {
|
||
this._failed = true;
|
||
break;
|
||
}
|
||
const a = stack.pop();
|
||
const b = stack.pop();
|
||
stack.push(a, b);
|
||
break;
|
||
}
|
||
|
||
case TOKEN.pop:
|
||
if (stack.length < 1) {
|
||
this._failed = true;
|
||
break;
|
||
}
|
||
stack.pop();
|
||
break;
|
||
|
||
case TOKEN.copy: {
|
||
if (stack.length < 1) {
|
||
this._failed = true;
|
||
break;
|
||
}
|
||
const nNode = stack.pop();
|
||
if (nNode.type === PS_NODE.const) {
|
||
const n = nNode.value | 0;
|
||
if (n === 0) {
|
||
// n === 0 is a no-op
|
||
} else if (n < 0 || n > stack.length) {
|
||
this._failed = true;
|
||
} else {
|
||
stack.push(...stack.slice(-n));
|
||
if (stack.length > MAX_STACK_SIZE) {
|
||
this._failed = true;
|
||
}
|
||
}
|
||
} else {
|
||
// Runtime n — cannot resolve at compile time.
|
||
this._failed = true;
|
||
}
|
||
break;
|
||
}
|
||
|
||
case TOKEN.index: {
|
||
if (stack.length < 1) {
|
||
this._failed = true;
|
||
break;
|
||
}
|
||
const nNode = stack.pop();
|
||
if (nNode.type === PS_NODE.const) {
|
||
const n = nNode.value | 0;
|
||
if (n < 0 || n >= stack.length) {
|
||
this._failed = true;
|
||
} else {
|
||
// 0 index = dup of top; n index = copy of nth element from top
|
||
stack.push(stack.at(-n - 1));
|
||
}
|
||
} else {
|
||
// Runtime n — cannot resolve at compile time.
|
||
this._failed = true;
|
||
}
|
||
break;
|
||
}
|
||
|
||
case TOKEN.roll: {
|
||
if (stack.length < 2) {
|
||
this._failed = true;
|
||
break;
|
||
}
|
||
const jNode = stack.pop();
|
||
const nNode = stack.pop();
|
||
if (nNode.type === PS_NODE.const && jNode.type === PS_NODE.const) {
|
||
const n = nNode.value | 0;
|
||
if (n === 0) {
|
||
// n === 0 is a no-op
|
||
} else if (n < 0 || n > stack.length) {
|
||
this._failed = true;
|
||
} else {
|
||
// Normalize j into [0, n): positive j moves the top element(s) to
|
||
// the bottom of the window.
|
||
const j = (((jNode.value | 0) % n) + n) % n;
|
||
if (j > 0) {
|
||
const slice = stack.splice(-n, n);
|
||
// slice[n-j…n-1] → new bottom; slice[0…n-j-1] → new top.
|
||
stack.push(...slice.slice(n - j), ...slice.slice(0, n - j));
|
||
}
|
||
}
|
||
} else {
|
||
// Runtime n or j — cannot resolve at compile time.
|
||
this._failed = true;
|
||
}
|
||
break;
|
||
}
|
||
|
||
default:
|
||
this._failed = true;
|
||
break;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Create a binary tree node, applying optimizations eagerly:
|
||
*
|
||
* 1. Constant folding — both operands are PsConstNode → fold to PsConstNode.
|
||
* 2. Reflexive simplifications — x−x→0, x xor x→0, x eq x→true, etc.
|
||
* 3. Algebraic simplifications with one known operand — identity elements
|
||
* (x+0→x, x*1→x, …), absorbing elements (x*0→0, x and false→false, …),
|
||
* and strength reductions (x*-1→neg(x), x^0.5→sqrt(x), x^2→x*x, …).
|
||
*
|
||
* Recall: `first` was on top of the stack (right operand for non-commutative
|
||
* ops), `second` was below (left operand). So `a b sub` → second=a, first=b
|
||
* → a − b.
|
||
*/
|
||
_makeBinary(op, first, second) {
|
||
// 1. Constant folding
|
||
if (first.type === PS_NODE.const && second.type === PS_NODE.const) {
|
||
const v = _evalBinaryConst(op, second.value, first.value);
|
||
if (v !== undefined) {
|
||
return new PsConstNode(v);
|
||
}
|
||
}
|
||
|
||
// 2. Reflexive simplifications: both operands are the same expression.
|
||
if (_nodesEqual(first, second)) {
|
||
switch (op) {
|
||
case TOKEN.sub:
|
||
return new PsConstNode(0); // x − x → 0
|
||
case TOKEN.xor:
|
||
// Boolean operands: true xor true = false xor false = false.
|
||
// Integer operands: n xor n = 0.
|
||
return new PsConstNode(
|
||
first.valueType === PS_VALUE_TYPE.boolean ? false : 0
|
||
);
|
||
// TOKEN.mod, TOKEN.div, TOKEN.idiv are NOT simplified here:
|
||
// x op x is undefined when x = 0, so we cannot fold without knowing
|
||
// that x is non-zero.
|
||
case TOKEN.and:
|
||
case TOKEN.or:
|
||
return first; // x and x → x; x or x → x
|
||
case TOKEN.min:
|
||
case TOKEN.max:
|
||
return first; // min(x,x) → x; max(x,x) → x
|
||
case TOKEN.eq:
|
||
case TOKEN.ge:
|
||
case TOKEN.le:
|
||
return new PsConstNode(true);
|
||
case TOKEN.ne:
|
||
case TOKEN.gt:
|
||
case TOKEN.lt:
|
||
return new PsConstNode(false);
|
||
}
|
||
}
|
||
|
||
// 3. Algebraic simplifications — b = first.value, a = second.value.
|
||
if (first.type === PS_NODE.const) {
|
||
const b = first.value;
|
||
switch (op) {
|
||
case TOKEN.add:
|
||
if (b === 0) {
|
||
return second; // x + 0 → x
|
||
}
|
||
break;
|
||
case TOKEN.sub:
|
||
if (b === 0) {
|
||
return second; // x − 0 → x
|
||
}
|
||
break;
|
||
case TOKEN.mul:
|
||
if (b === 1) {
|
||
return second; // x * 1 → x
|
||
}
|
||
if (b === 0) {
|
||
return first; // x * 0 → 0 (reuse the PsConstNode(0))
|
||
}
|
||
if (b === -1) {
|
||
return this._makeUnary(TOKEN.neg, second); // x * -1 → neg(x)
|
||
}
|
||
break;
|
||
case TOKEN.div:
|
||
// x / c → x * (1/c): replace division by a constant with the
|
||
// equivalent multiplication (1/1=1 is caught by the mul identity).
|
||
if (b !== 0) {
|
||
return this._makeBinary(TOKEN.mul, new PsConstNode(1 / b), second);
|
||
}
|
||
break;
|
||
case TOKEN.idiv:
|
||
if (b === 1) {
|
||
return second; // x idiv 1 → x
|
||
}
|
||
break;
|
||
case TOKEN.exp:
|
||
if (b === 1) {
|
||
return second; // x ^ 1 → x
|
||
}
|
||
if (b === -1) {
|
||
return this._makeBinary(TOKEN.div, second, new PsConstNode(1));
|
||
}
|
||
if (b === 0.5) {
|
||
return this._makeUnary(TOKEN.sqrt, second); // x ^ 0.5 → sqrt(x)
|
||
}
|
||
if (b === 0.25) {
|
||
// x ^ 0.25 → sqrt(sqrt(x)): two native f64.sqrt calls instead
|
||
// of the pow() import.
|
||
const sqrtOnce = this._makeUnary(TOKEN.sqrt, second);
|
||
return this._makeUnary(TOKEN.sqrt, sqrtOnce);
|
||
}
|
||
if (b === 2) {
|
||
// x ^ 2 → x * x: avoids the pow() import call entirely.
|
||
return this._makeBinary(TOKEN.mul, second, second);
|
||
}
|
||
if (b === 3) {
|
||
// x ^ 3 → (x * x) * x: avoids the pow() import call entirely.
|
||
return this._makeBinary(
|
||
TOKEN.mul,
|
||
this._makeBinary(TOKEN.mul, second, second),
|
||
second
|
||
);
|
||
}
|
||
if (b === 4) {
|
||
// x ^ 4 → (x * x) * (x * x): avoids the pow() import call entirely.
|
||
const square = this._makeBinary(TOKEN.mul, second, second);
|
||
return this._makeBinary(TOKEN.mul, square, square);
|
||
}
|
||
if (b === 0) {
|
||
return new PsConstNode(1); // x ^ 0 → 1
|
||
}
|
||
break;
|
||
case TOKEN.and:
|
||
if (b === true) {
|
||
return second; // x and true → x
|
||
}
|
||
if (b === false) {
|
||
return first; // x and false → false
|
||
}
|
||
break;
|
||
case TOKEN.or:
|
||
if (b === false) {
|
||
return second; // x or false → x
|
||
}
|
||
if (b === true) {
|
||
return first; // x or true → true
|
||
}
|
||
break;
|
||
case TOKEN.min:
|
||
// min(max(x, c2), c1) where c2 ≥ c1 → c1:
|
||
// max(x, c2) ≥ c2 ≥ c1, so min with c1 always returns c1.
|
||
if (
|
||
second.type === PS_NODE.binary &&
|
||
second.op === TOKEN.max &&
|
||
second.first.type === PS_NODE.const &&
|
||
second.first.value >= b
|
||
) {
|
||
return first;
|
||
}
|
||
break;
|
||
case TOKEN.max:
|
||
// max(min(x, c1), c2) where c2 ≥ c1 → c2:
|
||
// min(x, c1) ≤ c1 ≤ c2, so max with c2 always returns c2.
|
||
if (
|
||
second.type === PS_NODE.binary &&
|
||
second.op === TOKEN.min &&
|
||
second.first.type === PS_NODE.const &&
|
||
second.first.value <= b
|
||
) {
|
||
return first;
|
||
}
|
||
break;
|
||
}
|
||
}
|
||
|
||
if (second.type === PS_NODE.const) {
|
||
const a = second.value;
|
||
switch (op) {
|
||
case TOKEN.add:
|
||
if (a === 0) {
|
||
return first; // 0 + x → x
|
||
}
|
||
break;
|
||
case TOKEN.sub:
|
||
if (a === 0) {
|
||
return this._makeUnary(TOKEN.neg, first); // 0 − x → neg(x)
|
||
}
|
||
break;
|
||
case TOKEN.mul:
|
||
if (a === 1) {
|
||
return first; // 1 * x → x
|
||
}
|
||
if (a === 0) {
|
||
return second; // 0 * x → 0 (reuse the PsConstNode(0))
|
||
}
|
||
if (a === -1) {
|
||
return this._makeUnary(TOKEN.neg, first); // -1 * x → neg(x)
|
||
}
|
||
break;
|
||
case TOKEN.and:
|
||
if (a === true) {
|
||
return first; // true and x → x
|
||
}
|
||
if (a === false) {
|
||
return second; // false and x → false
|
||
}
|
||
break;
|
||
case TOKEN.or:
|
||
if (a === false) {
|
||
return first; // false or x → x
|
||
}
|
||
if (a === true) {
|
||
return second; // true or x → true
|
||
}
|
||
break;
|
||
}
|
||
}
|
||
|
||
return new PsBinaryNode(
|
||
op,
|
||
first,
|
||
second,
|
||
_binaryValueType(op, first.valueType, second.valueType)
|
||
);
|
||
}
|
||
|
||
/**
|
||
* Create a unary tree node, applying optimizations eagerly:
|
||
*
|
||
* 1. Constant folding.
|
||
* 2. not(comparison) → negated comparison: not(a eq b) → a ne b, etc.
|
||
* 3. neg(a − b) → b − a.
|
||
* 4. Double-negation: neg(neg(x)) → x, not(not(x)) → x.
|
||
* 5. abs(neg(x)) → abs(x).
|
||
* 6. Idempotent: f(f(x)) → f(x) for abs, ceiling, floor, round, etc.
|
||
*/
|
||
_makeUnary(op, operand) {
|
||
// 1. Constant folding
|
||
if (operand.type === PS_NODE.const) {
|
||
const v = _evalUnaryConst(op, operand.value);
|
||
if (v !== undefined) {
|
||
return new PsConstNode(v);
|
||
}
|
||
}
|
||
|
||
// 2.
|
||
if (op === TOKEN.not && operand.type === PS_NODE.binary) {
|
||
const negated = PSStackToTree.#negatedComparison.get(operand.op);
|
||
if (negated !== undefined) {
|
||
return new PsBinaryNode(
|
||
negated,
|
||
operand.first,
|
||
operand.second,
|
||
PS_VALUE_TYPE.boolean
|
||
);
|
||
}
|
||
}
|
||
|
||
// 3. (_makeBinary may fold further if one operand is 0)
|
||
if (
|
||
op === TOKEN.neg &&
|
||
operand.type === PS_NODE.binary &&
|
||
operand.op === TOKEN.sub
|
||
) {
|
||
return this._makeBinary(TOKEN.sub, operand.second, operand.first);
|
||
}
|
||
|
||
if (operand.type === PS_NODE.unary) {
|
||
// 4. (not(not(x)) only reachable when x is not a comparison)
|
||
if (
|
||
(op === TOKEN.neg && operand.op === TOKEN.neg) ||
|
||
(op === TOKEN.not && operand.op === TOKEN.not)
|
||
) {
|
||
return operand.operand;
|
||
}
|
||
// 5.
|
||
if (op === TOKEN.abs && operand.op === TOKEN.neg) {
|
||
return this._makeUnary(TOKEN.abs, operand.operand);
|
||
}
|
||
// 6.
|
||
if (PSStackToTree.#idempotentUnary.has(op) && op === operand.op) {
|
||
return operand;
|
||
}
|
||
}
|
||
|
||
return new PsUnaryNode(op, operand, _unaryValueType(op, operand.valueType));
|
||
}
|
||
|
||
/**
|
||
* Create a ternary node, applying optimizations eagerly:
|
||
*
|
||
* 1. Constant condition — fold to the live branch.
|
||
* 2. Identical branches — the condition is irrelevant, return either branch.
|
||
* 3. Boolean branch constants — `cond ? true : false` → cond,
|
||
* `cond ? false : true` → not(cond).
|
||
* 4. Ternary → branchless min/max when the condition compares two numeric
|
||
* expressions that are also the two branches.
|
||
*/
|
||
_makeTernary(cond, then, otherwise) {
|
||
// 1. Constant condition
|
||
if (cond.type === PS_NODE.const) {
|
||
return cond.value ? then : otherwise;
|
||
}
|
||
// 2. Both branches are the same expression
|
||
if (_nodesEqual(then, otherwise)) {
|
||
return then;
|
||
}
|
||
// 3. Boolean branch constants
|
||
if (then.type === PS_NODE.const && otherwise.type === PS_NODE.const) {
|
||
if (then.value === true && otherwise.value === false) {
|
||
return cond; // cond ? true : false → cond
|
||
}
|
||
if (then.value === false && otherwise.value === true) {
|
||
return this._makeUnary(TOKEN.not, cond); // cond ? false : true → !cond
|
||
}
|
||
}
|
||
|
||
// 4. Ternary → branchless min/max folding.
|
||
//
|
||
// When the condition is a numeric comparison between two expressions A and
|
||
// B, and the two branches are exactly those two expressions (in some
|
||
// order), the ternary collapses to a single f64.min / f64.max instruction:
|
||
//
|
||
// (A gt B) ? B : A → min(A, B) (A ge B) ? B : A → min(A, B)
|
||
// (A lt B) ? B : A → max(A, B) (A le B) ? B : A → max(A, B)
|
||
// (A gt B) ? A : B → max(A, B) (A ge B) ? A : B → max(A, B)
|
||
// (A lt B) ? A : B → min(A, B) (A le B) ? A : B → min(A, B)
|
||
//
|
||
// Here A = cond.second (left operand) and B = cond.first (right operand),
|
||
// following the PS stack convention: `A B gt` → second=A, first=B.
|
||
if (cond.type === PS_NODE.binary) {
|
||
const { op: cop, first: cf, second: cs } = cond;
|
||
if (cop === TOKEN.gt || cop === TOKEN.ge) {
|
||
// cond: cs > cf
|
||
if (_nodesEqual(then, cf) && _nodesEqual(otherwise, cs)) {
|
||
return this._makeBinary(TOKEN.min, cf, cs); // cs>cf ? cf:cs → min
|
||
}
|
||
if (_nodesEqual(then, cs) && _nodesEqual(otherwise, cf)) {
|
||
return this._makeBinary(TOKEN.max, cf, cs); // cs>cf ? cs:cf → max
|
||
}
|
||
} else if (cop === TOKEN.lt || cop === TOKEN.le) {
|
||
// cond: cs < cf
|
||
if (_nodesEqual(then, cf) && _nodesEqual(otherwise, cs)) {
|
||
return this._makeBinary(TOKEN.max, cf, cs); // cs<cf ? cf:cs → max
|
||
}
|
||
if (_nodesEqual(then, cs) && _nodesEqual(otherwise, cf)) {
|
||
return this._makeBinary(TOKEN.min, cf, cs); // cs<cf ? cs:cf → min
|
||
}
|
||
}
|
||
}
|
||
|
||
return new PsTernaryNode(
|
||
cond,
|
||
then,
|
||
otherwise,
|
||
then.valueType === otherwise.valueType
|
||
? then.valueType
|
||
: PS_VALUE_TYPE.unknown
|
||
);
|
||
}
|
||
}
|
||
|
||
export {
|
||
parsePostScriptFunction,
|
||
Parser,
|
||
PS_NODE,
|
||
PS_VALUE_TYPE,
|
||
PsArgNode,
|
||
PsBinaryNode,
|
||
PsBlock,
|
||
PsConstNode,
|
||
PsIf,
|
||
PsIfElse,
|
||
PsNode,
|
||
PsNumber,
|
||
PsOperator,
|
||
PsProgram,
|
||
PSStackToTree,
|
||
PsTernaryNode,
|
||
PsUnaryNode,
|
||
};
|