seriot.ch

About | Projects | Trail

Programming in PostScript

March 2024
December 2024
February 2025

Why PostScript?

PostScript is a compact, simple, and stack-based interpreted language. It is the precursor to PDF, yet it is significantly more powerful. As a Turing-complete language, it can theoretically compute anything that another Turing-complete language can.

Undoubtedly, PostScript is considered outdated. It's not intended to be used directly by humans, but to be machine-generated and interpreted on printers. There are no real Integrated Development Environments (IDEs) for it, nor are there any substantial debuggers. PostScript lacks a standard method for checking the argument types and return values of procedures. No standard libraries are available. Moreover, the PostScript language has been released in three official versions, and interpreters may also include specific instructions.

Despite these drawbacks, PostScript is stunningly fun to work with. Indeed, engineering is fundamentally about building things up within constraints, devising relevant guidelines and conventions. The lack of complicated language constructs, or huge libs and framework to master, in combination with its vintage charm, simplicity and powerful set of primitives make PostScript an ideal candidate for software engineering pet projects such as PSChess.

This page compiles some of the aspects and techniques I employ when writing PostScript for enjoyment. It is by no means comprehensive and there may be areas for correction or improvement. If you are someone who still enjoys manually coding in PostScript, I would greatly appreciate your feedback.

  1. Dev Environment
  2. Mental Model
  3. Logs
  4. User Input
  5. Immediate User Input
  6. Naming Conventions
  7. Strings
  8. Dictionaries and Scopes
  9. Control Flow and Early Returns
  10. Asserts
  11. Unit Tests
  12. Errors
  13. Stack Traces
  14. Object Oriented Programming
  15. Animated PostScript
  16. Profiling

1. Dev Environment

I couldn't find a decent PostScript IDE on macOS.

After using TextMate for a while, I ended up writing my own PostScript mode and stylesheet for SubEthaEdit.

For your convenience, you may download it here: postscript.seemode.zip.

sokoban.ps opened in SubEthaEdit with PostScript mode.

2. Mental Model

Everything pretty much comes down to piling and unpiling values, and storing values in dictionaries.

%!PS

% put some values in dictionary

/a 3 def
/b 1 def

% put some values on stack

2 5 (asd)

% operators take values on top of stack and through dictionary keys

          % [2 5 (asd)]
length    % [2 5 3]
add       % [2 8]
a sub     % [2 5]
b add     % [2 6]
sub       % [-4]

3. Logs

One of the very first thing we need when programming in PostScript is logs. When the interpreter has a console, which is the case for GhostScript, we can use the following operators, often followed by stop. The last two are GhostScript specific, though:

print       % no newline
=only       % no newline
==only      % no newline, strings in parenthesis

=           % newline
==          % newline, strings in parenthesis

In practice, it is very common to add such markers in code to follow what's going on:

(--1) =
(--2) =

The stack can be printed with pstack, so it's quite common to write pstack stop to check the stack state at a given point.

More elaborated logs capabilities can be built on demand, eg. by printing an array contents:

/x 1 def
/y 2 def

/=== { { =only ( ) =only } forall (\n) print } def

[ [1] (asd) x y ] ===
--nostringval-- asd 1 2

or by implementing a simple, pseudo printf:

/=== {
    {
        dup (%) 0 get eq {
            pop
        } {
            ( ) dup 0 4 -1 roll put
        } ifelse
        =only
    } forall
    (\n) print
} def
[3 4] y x (x: %, y: %, array: %) ===

4. User Input

The lineedit operator allows reading user input.

PostScript Language Reference Manual mentions that this operator may not be available on all interpreters.

%!PS

/printUsage {
    (Usage:) =
    (  a - some stuff) =
    (  b - other stuff) =
    (  q - quit) =
} def

printUsage

{    
    /userInput 128 string def
    (>) print flush
    (%lineedit) (r) file userInput readline
    pop % bool

    token not { () } if

    {
        dup (q) eq { quit } if

        dup (a) eq {
            pop token pop
            (a) =
            exit
        } if

        dup (b) eq {
            pop token pop
            (b) =
            exit
        } if

        clear
        printUsage

        exit

    } loop
} loop

You can get a prompt on a real laser printer by sending this program with netcat:

$ nc 172.16.158.40 9100

(copy-paste the program here)

5. Immediate User Input

Handling immediate keyboard input (without hiting enter) is a bit more subtle. The idea is to have the PostScript program read single characters from standard input.

Here is how if goes using GhostView. Two terminals are used: - First terminal sets up a named pipe (/tmp/p) and pipes both the PostScript code and the pipe content to Ghostview - Second terminal is configured for raw input (no line buffering, no echo) and writes directly to the named pipe

Example:

{
    (%stdin) (r) file read {
        == flush
    } if
} loop

Terminal 1: read pipe

rm /tmp/p; mkfifo /tmp/p; cat x.ps /tmp/p | gv -

Terminal 2: fill pipe

stty raw -echo; cat > /tmp/p

Reference: http://quickies.seriot.ch/?id=604

This is the technique I used to write PSSokoban.

6. Naming Conventions

PostScript procedures can pop and push values on the stack. To make this clear, I've come up with the following convention:

Example of PostScript procedure name:

_board_fromCoord_toCoord_Move_canMove_

Functional equivalent:

move(board, fromCoord, toCoord) -> canMove

7. Strings

String are assigned by reference. They must be copied whenever needed. In practice, I've found not copying strings to be the root cause of many bugs.

In Python:

s1 = "asd"
s2 = s1     # copy

s1 += "x"

print(s1)   # asdx
print(s2)   # asd

In PostScript:

/s1 (aa) def
/s2 s1 def                          % s2 is same as s1
/s3 s1 dup length string cvs def    % s3 is s1 copy

s1 1 (b) putinterval                % modify s1

s1 =        % ab                    % s1 has changed
s2 =        % ab                    % s2 (s1) has changed
s3 =        % aa                    % s3 hasn't changed

8. Dictionaries and Scopes

PostScript has no stack frame to store local variables. Variables are stored as key-value pairs in the top-most dictionary. By default, variables declared inside a procedure may override variables declared outside of the procedure, and will remain available outside of it.

Example:

/x 1 def        % declares x = 1

/p {            % procedure p
    /x 2 def    % declares x = 2
    x =         % prints 2 
} def

x =             % print 1
p               % p overrides x = 2
x =             % prints 2

In order to avoid side effects, procedures that declare variables can enclose them in a dictionary local to the procedure.

/x 1 def

/p {
    1 dict begin
    /x 2 def
    x =         % 2
    end
} def

x =   % 1
p
x =   % 1

9. Control Flow and Early Returns

It is quite common that we need to exit a procedure early depending on some condition. Or that we need an elsif operator (which doesn't exist in PostScript) such as in the following Python code:

def compare(a, b):
    if a < b:
        return 1
    elif a > b:
        return -1
    else: # a == b
        return 0

Using the ifelse PostScript operator will result in ugly cascading code blocks:

/_a_b_Compare_c_ {
    /b exch def
    /a exch def

    a b gt {
        1
    } {
        a b lt {
            -1
        } {
            0
        } ifelse    
    } ifelse

} def

We can implement the same behaviour with exit statements inside a main loop:

/_a_b_Compare_c_ {

    /b exch def
    /a exch def

    {
        a b gt {  1 exit } if
        a b lt { -1 exit } if
        0 exit
    } loop

} def 

10. Asserts

Here is an example of the assertions I use:

/AssertTypeBool {
    type dup /booleantype ne {
        (type error, expect boolean, got ) print == quit
    } if
} bind def

11. Unit Tests

Here is an example of unit test construct I use.

The idea is to have a procedure taking the test name and one or two code blocks.

For instance, to assert that two values are equal, we can use:

/_s_p1_p2_AssertEqual {
    /p2 exch def
    /p1 exch def
    /s exch def

    p1 exec p2 exec eq { (--) =only s =only (: PASS) = }{ s =only (: FAIL) = p1 =only ( ne ) =only p2 = quit } ifelse
} def

The test will look like:

(TEST TestBoard.1) { testBoard (h1) _board_coord_Piece_p_ } { (R) } _s_p1_p2_AssertEqual

If the test fails, the program will stop and typically display:

TEST TestBoard.1: FAIL
R ne K

12. Errors

GhostScript errors are rather cryptic. Most information are garbage. Only the first 2 lines really matters: the error and the stack.

Error: /typecheck in --forall--
Operand stack:   --nostringval--   --nostringval--
Execution stack:   %interp_exit   .runexec2   --nostringval--   --nostringval--   --nostringval--   2   %stopped_push   --nostringval--   --nostringval--   --nostringval--   false   1   %stopped_push   1944   1   3   %oparray_pop   1943   1   3   %oparray_pop   1928   1   3   %oparray_pop   1801   1   3   %oparray_pop   --nostringval--   %errorexec_pop   .runexec2   --nostringval--   --nostringval--   --nostringval--   2   %stopped_push   --nostringval--   --nostringval--   %loop_continue   --nostringval--   --nostringval--   %loop_continue   --nostringval--   --nostringval--   --nostringval--   --nostringval--   --nostringval--   --nostringval--
Dictionary stack:   --dict:749/1123(ro)(G)--   --dict:0/20(G)--   --dict:207/300(L)--   --dict:2/2(L)--
Current allocation mode is local
Current file position is 5285
GPL Ghostscript 10.02.1: Unrecoverable error, exit code 1

In this case, an Error: /typecheck occurred in command forall at file position 5285. The file position can be checked in BBEdit with cmd-J :5285.

Alas, I found the file position reported by GhostScript to be unreliable, stopping at first procedure call level, and hard to track when including files with the run operator.

In order to know which procedure the error occured and how we got there, we can store the name of the current procedure:

/lastProc (-) def
/!{ /lastProc exch def } def

/CreateOrTruncateInputFile {
    (CreateOrTruncateInputFile)!
    % ...
} bind def

and redefine the error handling procedure:

errordict /handleerror {
  (-- Error occured!) ==
  (Procedure: ) print lastProc ==
  (Error    : ) print $error /errorname get ==
  (Command  : ) print $error /command get ==
  (Operands : ) print $error /ostack get ==
  (Dicts    : ) print $error /dstack get ==
  (Exec     : ) print $error /estack get ==    
  stop
} put

Errors will then look like:

-- Error occured! 
Procedure: (CheckFullLine_b_rowsFull_)
Error    : /stackunderflow
Command  : --put--
Operands : [false false]
(...)

13. Stack Traces

Conceptually, this technique consists in keeping a global stack, pushing and popping the names of the procedure as they are entered or exited.

We can implement the stack with an array and a top-of-stack index.

/CALLSTACK 20 array def
/CS_INDEX 0 def            % callstack index

We add relevant procedures to put, pop and print.

/CS_PRINT {
    5 dict begin
    (\nCALLSTACK:) =
    0 1 CSI {
        /idx exch def
        /line CALLSTACK idx get def
        idx ==only ( ) =only line =
    } for
    end
} def

/CS_PUT {
    1 dict begin
    /s exch def
    CALLSTACK CS_INDEX s put    
    end
    /CSI CSI 1 add def
} def

/CS_POP {        
    CALLSTACK CS_INDEX null put
    /CS_INDEX CS_INDEX 1 sub def
} def

Now, we can use CS_PUT and CS_POP as first and last calls in procedures. Eg:

/_p_a_AddToCaptured {
    (/_p_a_AddToCaptured) CS_PUT

    % ...

    CS_POP
} def

This way, we can call CS_PRINT in assertions, or in error hooks as suggested in the previous section.

/su_ { errordict/stackunderflow } bind def 
errordict/stackunderflow { CS_PRINT su_ } put

A stackunderflow error will then display valuable information:

CALLSTACK:
0 RunBoardLogicTests
1 TestCapture2
2 /_board_fc_tc_dry_Move_canMove_squaresStatus_msg_
3 /ChangePlayer_playerColor_
4 /_whiteOrBlack_Invert_s_

We can hook specific errors such as typecheck:

/_tc { errordict/typecheck } bind def     % save original procedure
errordict/typecheck { CS_PRINT _tc } put  % redefine the procedure

We don't need to hook all errors, mostly undefined, rangecheck, stackunderflow and typecheck. I suggest not to mess up too much with errordict and enable (uncomment) these hooks only when investigating a specific error. Additionally, not all errors behave exactly the same way, and hooking them may require specific adjustments.

14. Object Oriented Programming

It can be tempting to build abstractions to get closer to the OOP world, which is likely for familiar that stack-based programming to 2020's programmers.

The idea is to use PostScript dictionaries to store both instance variables and methods.

Example with my implementation of a ring buffer:

/call { exch begin load exec end } def

/undoManager 10 dict def

undoManager begin
    /MAX 3 def
    /ringBuffer MAX array def
    /head 0 def
    /print { ringBuffer == } def
    /do {
        ringBuffer head 3 -1 roll put
        /head head 1 add MAX mod def
    } def
    /reset { /ringBuffer MAX array def } def
    /undo {
        /head head 0 eq { MAX 1 sub } { head 1 sub } ifelse def
        ringBuffer head get % result
        ringBuffer head null put
    } def
end

3 undoManager /do call
4 undoManager /do call
5 undoManager /do call
6 undoManager /do call

undoManager /print call
[6 4 5]
undoManager /undo call ==
[null 4 5]

We can continue in this direction and build objects as dictionaries with a _methods key associated with a dictionary of methods.

/objMethods 
<<
    /addOne { 
        /_self exch def
        _self/ivar1 _self/ivar1 get 1 add put 
    }
>> def

/call {
    1 index /_methods get 
    exch get exec
} def

/d1 
<<
    /ivar1 1
    /ivar2 2
    /_methods objMethods
>> def

/d2 
<<
    /ivar1 10
    /ivar2 20
    /_methods objMethods
>> def

% Use object 1
d1/ivar1 get ==
d1/addOne call
d1/ivar1 get ==

% Use object 2
d2/ivar1 get ==
d2/addOne call
d2/ivar1 get ==
1
2
10
11

With that in place, we could go as far a writing the Objective-C runtime.

However, it rarely makes sense to fight the language and frameworks, and I've always managed to replace objects by a much more elegant constructs leveraging PostScript idioms.

15. Animated PostScript

PostScript wasn't originally designed for animation purposes. However, I came up with a nice technique to create smooth animations using GhostView (gv), a PostScript viewer.

The key technique is using a loop that where each iteration:

For instance, here is how to display an animated bouncing ball:

Run with gv -quiet anim.ps

%!PS
%%BoundingBox: 0 0 320 240

/sleep { 100000 mul {} repeat } def

/x 50 def

0 1 300 1 sub {
    pop

    /x x 1 add def

    gsave

    1 setgray
    0 0 320 240 rectfill

    x 100 45 0 360 arc
    0.5 setgray fill

    grestore

    flushpage

    10 sleep
} for

References:

16. Profiling

Basic profiling capabilities can be built with the realtime operator.