This document covers how to implement new PostScript operators in PostForge. Every operator must follow these patterns for PostScript compliance.
- Implementation: Add the operator function to the appropriate file in
postforge/operators/(e.g., math operators inmath.py, path operators inpath.py). - Registration: Add a tuple to the
opslist inpostforge/operators/dict.py, insidecreate_system_dict(), under the appropriate commented section and in alphabetical order.
# In create_system_dict(), in the ops list:
("operator_name", ps.Operator, ps_module.function_name),For example, in the file operators section:
("read", ps.Operator, ps_file.read),
("readline", ps.Operator, ps_file.readline),
("readstring", ps.Operator, ps_file.readstring),
("run", ps.Operator, ps_file.run),Most operator functions are named after the PostScript operator they implement
(e.g., def showpage(ctxt, ostack)). When the PostScript name conflicts with
a Python keyword or built-in, prefix the function with ps_:
ps_def(Python keyworddef)ps_dict(Python built-indict)ps_exec,ps_exit,ps_if,ps_for(Python keywords)ps_and,ps_not,ps_copy(Python built-ins)ps_image,ps_imagemask,ps_colorimage(convention for image operators)
This prefix affects error reporting — see the Error Handling section below.
Before writing any code:
- Read the operator's entry in the PostScript Language Reference Manual (start with the Second Edition, then check the Third Edition for updates)
- Identify all error conditions listed for the operator
- Note the exact stack effect notation
- Document any special behaviors or edge cases
Every operator must validate all operands before popping anything from the stack. This is a fundamental PostScript compliance requirement — when an error occurs, the operand stack must be in its original state so error handlers can inspect the operands that caused the failure.
Use negative indexing to peek at operands without modifying the stack:
def operator_name(ctxt, ostack):
"""
[Exact PLRM description from Section 8.2]
Stack: [exact PLRM stack notation]
Errors: [complete list from PLRM]
"""
# STEP 1: Validate stack depth
if len(ostack) < 2:
return ps_error.e(ctxt, ps_error.STACKUNDERFLOW, operator_name.__name__)
# STEP 2: Validate operand types (peek with negative indexing)
if ostack[-1].TYPE not in ps.NUMERIC_TYPES:
return ps_error.e(ctxt, ps_error.TYPECHECK, operator_name.__name__)
if ostack[-2].TYPE != ps.T_ARRAY:
return ps_error.e(ctxt, ps_error.TYPECHECK, operator_name.__name__)
# STEP 3: Validate access permissions (if applicable)
if ostack[-2].access() < ps.ACCESS_READ_ONLY:
return ps_error.e(ctxt, ps_error.INVALIDACCESS, operator_name.__name__)
# STEP 4: Any operator-specific validations from PLRM
# STEP 5: ONLY after all validation passes — pop and execute
value = ostack.pop()
array = ostack.pop()
# ... perform the actual work ...Never pop operands before validation is complete:
# WRONG — stack is corrupted if the second check fails
operand1 = ostack.pop()
if ostack[-1].TYPE != ps.T_ARRAY:
return ps_error.e(ctxt, ps_error.TYPECHECK, ...)For every operator, check the applicable conditions from this list:
| Error | Condition |
|---|---|
stackunderflow |
Not enough operands on the stack |
stackoverflow |
Result would exceed operand stack capacity |
typecheck |
Wrong operand type |
invalidaccess |
Access permission violation |
rangecheck |
Value outside acceptable range |
limitcheck |
Implementation limit exceeded |
Plus any operator-specific errors documented in the PLRM.
Use ps_error.e() for all PostScript errors. Never raise Python exceptions
for PostScript error conditions.
return ps_error.e(ctxt, ps_error.TYPECHECK, operator_name.__name__)The third argument is the operator name string for error reporting. Normally
use __name__ to avoid redundant string allocations. However, for functions
with a ps_ prefix, use a hardcoded string of the actual PostScript operator
name:
# Normal case — __name__ gives the correct PostScript name
def showpage(ctxt, ostack):
return ps_error.e(ctxt, ps_error.TYPECHECK, showpage.__name__) # "showpage"
# ps_ prefix — __name__ would give "ps_image", but PostScript name is "image"
def ps_image(ctxt, ostack):
return ps_error.e(ctxt, ps_error.TYPECHECK, "image")
# Helper called on behalf of a parent operator — pass parent name as parameter
def _image_helper(ctxt, ostack, op_name):
return ps_error.e(ctxt, ps_error.TYPECHECK, op_name)Use the pre-defined type groupings from postforge/core/types/constants.py
for efficient type checking:
| Constant | Types | Use for |
|---|---|---|
NUMERIC_TYPES |
Int, Real | Numeric operands |
ARRAY_TYPES |
Array, PackedArray | Array operands |
COMPOSITE_TYPES |
Array, Dict, String, PackedArray | Composite objects |
STREAM_TYPES |
File, String | I/O operations |
CONTAINER_TYPES |
Array, Dict, PackedArray | Container objects |
IMMUTABLE_TYPES |
Int, Real, Bool, Null, Name | Read-only objects |
# Preferred — use type groups
if ostack[-1].TYPE in ps.NUMERIC_TYPES:
# Avoid — multiple individual comparisons
if ostack[-1].TYPE == ps.T_INT or ostack[-1].TYPE == ps.T_REAL:Operator functions always receive ctxt (the execution context) as the first
parameter, followed by ostack (the operand stack):
def operator_name(ctxt, ostack):Some operators also receive the execution stack, in which case the order is
ctxt, e_stack, ostack. Follow existing operators in the same file for
the correct signature.
Each operator must include a docstring with the exact PLRM description. This
is what the help operator displays in interactive mode:
def readstring(ctxt, ostack):
"""
file string **readstring** substring bool
Reads characters from file into string until either the
entire string has been filled or an end-of-file indication
is encountered...
Stack: file string readstring substring bool
Errors: invalidaccess, ioerror, rangecheck, stackunderflow, typecheck
"""Use the exact PLRM wording — do not paraphrase. This ensures specification compliance and makes it easy to verify against the manual.
Before considering an operator complete:
- PLRM consulted — all error conditions identified
- All validation implemented before any pops
- Every PLRM error condition handled
- No Python exceptions raised during PostScript error conditions
- PostScript tests written (see Testing Guide)
- Registered in
create_system_dict()inpostforge/operators/dict.py