Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Debugging PostScript

How can I do debugging in PostScript? I can use GhostView/GhostScript on Linux, but I can't view the stack, dictionaries, etc.

like image 575
Mark Harrison Avatar asked Sep 11 '12 23:09

Mark Harrison


3 Answers

minimal debugger

I just discovered a really useful hack.

<<
/break{ /hook /pause load store }
/cont{ /hook {} store }
/doprompt{
    (\nbreak>)print
    flush(%lineedit)(r)file
    cvx {exec}stopped pop }
/pause{ doprompt }
/hook{}
>> begin

It's an 8-line EMBEDDABLE debugger. I cooked this up for my 8086 emulator. Put hook in the main loop, put break in a procedure to trigger a pause at the next hook (I put it in suspicious OPCODE procs, and hook is where the break takes its first effect). Hook calls pause and pause calls doprompt which gives you a one-line "break>" prompt. Typing cont here will clear the hook and keep spinning, not executing any pauses until another break is encountered. You can also inspect values and execute code at the prompt, but n.b. execution resumes when you hit enter, so if you need additional lines, call doprompt or pause at the end of the line. Errors are ignored while executing the command (you don't want the debugger to crash the program, that'd be stupid!). I suppose I could combine pause and doprompt and eliminate a name; but the goal here is not machine efficiency but a clear collection of concepts: this code, to be at all useful in debugging other code, needs to be easy to scan and verify.

How is this a debugger, it just reads a line?!

Remember you've got = and == to investigate values. forall and get to bust up arrays and stuff. To really find out where you are, do countexecstack array execstack == for a readable dump of the whole caboodle. That is, a backtrace of the current position in execution stack which contains the tails of all partially-executed procedures and files waiting to be resumed when the current frame returns.


"printf"

There's quite a lot of debugging that can be done without a debugger per se merely by instrumenting your program (adding printfs, so to speak).

Just now I ran into an error that my debugger couldn't help me with because the debugger itself was crashing on some overly-clever stuff like

/E [ 0 0 10 ] def %eye point
/crackE { % set pointers into E
    /ex E 0 1 getinterval cvx def
    /ey E 1 1 getinterval cvx def
    /ez E 2 1 getinterval cvx def
} def crackE

So the actual error I was investigating was

GPL Ghostscript 8.62 (2008-02-29)
Copyright (C) 2008 Artifex Software, Inc.  All rights reserved.
This software comes with NO WARRANTY: see the file PUBLIC for details.
Error: /stackunderflow in --forall--
Operand stack:
   --nostringval--
Execution stack:
   %interp_exit   .runexec2   --nostringval--   --nostringval--   --nostringval--   2   %stopped_push   --nostringval--   --nostringval--   --nostringval--   false   1   %stopped_push   1905   1   3   %oparray_pop   1904   1   3   %oparray_pop   1888   1   3   %oparray_pop   1771   1   3   %oparray_pop   --nostringval--   %errorexec_pop   .runexec2   --nostringval--   --nostringval--   --nostringval--   2   %stopped_push   --nostringval--   --nostringval--   --nostringval--   0.238095   0.047619   0.952381   --nostringval--   %for_real_continue   68.5714   17.1429   360.048   --nostringval--   %for_real_continue   --nostringval--
Dictionary stack:
   --dict:1151/1684(ro)(G)--   --dict:0/20(G)--   --dict:121/200(L)--   --dict:13/20(L)--   --dict:1/2(L)--
Current allocation mode is local
Last OS error: 2
Current file position is 3241
GPL Ghostscript 8.62: Unrecoverable error, exit code 1

And all I really need to know is what that --nostringval-- thing on the operand stack really is.

So I put this at the start of the program

/forall { pstack()= forall } bind def

and run it again and now

{MO matmul 0 --get-- --aload-- --pop-- proj action}

Error: /stackunderflow in --forall--
Operand stack:
   --nostringval--
...

Just before the error is the final stackdump (using ==) which tells me that I have a procedure body missing its dataset.

pstack is a bit blunt compared to something like this

/args { dup 1 add copy -1 1 { -1 roll ==only ( ) print } for } def
/forall { 2 args (forall)= forall } bind def

which would be more useful for tracking down erroneous data in apparently working code. This is also the way very early versions of Distiller produced optimized .ps files by defining only the drawing operations to dump themselves, the rest of the calculation is "distilled" out.

Some tricks

()= %print a newline
=string %built-in 128-byte buffer used by = and ==
/object =string cvs print %convert object to string and print without newline
/stack { count dup 1 add copy { = } repeat pop } def % this is the code for the stack operator
66 (#) dup 0 3 index put print %m non-destructively print a "char"

[I wrote '=' instead of 'stack' here earlier. A bad blunder. Edit: added missing pop to /stack.]


errordict hacking

Another way to investigate an error is to change the error handler. To investigate the /stackunderflow error described above I could have used

errordict/stackunderflow{dup == /stackunderflow signalerror}put

instead of specializing forall. To learn about this rather arcane aspect of postscript, read up on errordict stop and stopped. And interactively, take a peek at errordict{exch =only ==}forall. signalerror in ghostscript is called .error in Adobe interpreters. Its job is to take the snapshots of the stacks, and then call stop to pop the exec stack. So the dup == here and the pstack above are essentially the same 'moment' of the error, before the stop. Your interactive session (and the preceding program in gs's normal mode) are bracketed deeper on the exec stack with the equivalent of //your-program stopped { handleerror } if. It is handleerror which uses the snapshots (after the program has been otherwise purged) to print the error report with its uninformative stack printouts.

There are replacements you can find for handleerror which you can (ehandle.ps)run at the beginning of the erring program to produce different styled error reports.

Inspect $error

I just discovered this while re-reading my examples here. You can also investigate the stacks after the error, if the interpreter is still giving you a prompt. Error information is saved in the $error dictionary, including the snapshots of the stacks.

GS>[ 1 2 3 ] [4 5 6] bogus
Error: /undefined in bogus
Operand stack:
   --nostringval--   --nostringval--
Execution stack:
   %interp_exit   .runexec2   --nostringval--   --nostringval--   --nostringval--   2   %stopped_push   --nostringval--   --nostringval--   %loop_continue   --nostringval--   --nostringval--   false   1   %stopped_push   .runexec2   --nostringval--   --nostringval--   --nostringval--   2   %stopped_push   --nostringval--
Dictionary stack:
   --dict:1168/1684(ro)(G)--   --dict:0/20(G)--   --dict:77/200(L)--
Current allocation mode is local
Current file position is 24
GS<2>

GS<2>$error{pop ==}forall
/dstack
/recordstacks
/globalmode
/newerror
/.nosetlocal
/estack
/errorinfo
/.inerror
/SubstituteFont
/position
/binary
/ostack
/command
/errorname
GS<2>$error/ostack get ==
[[1 2 3] [4 5 6]]
GS<2>

Of course, here the objects were still on the stack. But $error is there to peek at. Don't try this: $error ===. TMI.

One very useful bit of information you can get from $error is a pretty-print of /estack, the copy of the execution stack at the point of error.

PS<3>$error /estack get ==
[ --quit--{ pop --quit--} false { quitflag false --def---dict- /
execdepth 2 --copy----get--1 --sub----put----end---dict- /doclose false 
--put--interrupt } --loop----cvx--[ /quitflag false --def---dict- /
newerror false --put--/prompt --load----stopped--{ (
Error during prompt execution
)--print--handleerror --exit--} --if--{ 
mark /stmtfile (%statementedit)(r)--file----def--} --stopped--{ --
cleartomark---dict- /newerror --get--{ -dict- /errorname --get--/
undefinedfilename --ne--{ handleerror } --if---dict- /newerror false --
put----exit--} --if--} { --pop--stmtfile --end--{ --cvx----exec--} --
stopped---dict- --begin--{ handleerror stmtfile --closefile--} --if--} 
--ifelse--checkquit ] { checkquit } { -dict- --begin--{ handleerror 
stmtfile --closefile--} --if--} false -file- -file- -file- --repeat----
cvx--[ randcurve randwidth randcolor stroke ] 1 { flushpage newpath } { 
newpath } --forall----cvx--[ dup length 2 gt { [ currentcolordict DEVICE
 /nativecolorspace get get exec counttomark 2 add -1 roll DEVICE dup /
FillPoly get exec pop pstack ()= flushpage } { pop } ifelse ] [ ] { pop 
pstack ()= flushpage } { x_max width 0.50 add def (
intersect polygon edges with scanlines)= /P poly poly length 1 sub get 
def [ poly { Q exch def x_max miny floor cvi 0.50 add 1 maxy ceiling cvi
 0.50 sub { 1 index exch -0.50 1 index 4 2 roll P aload pop Q aload pop 
.intersect { 2 array astore exch } if } for pop /P Q def } forall ] (
sort scanline intersection list)= dup { 1 index 1 get 1 index 1 get eq 
{ exch 0 get exch 0 get lt } { exch 1 get exch 1 get lt } ifelse } qsort
 (set pixels on each scanline)= aload length 2 idiv { exch aload pop 3 2
 roll aload pop /USEDRAWLINE where { pop r g b 7 3 roll currentdict 
DrawLine } { pop 3 2 roll exch 1 exch dup width ge { pop width 1 sub } 
if { r g b 4 3 roll 2 index currentdict PutPix } for pop } ifelse } 
repeat end } --forall----cvx--[ aload pop .maxmin ] [ [ 16 154 ] [ 16 
154 ] ] { pop .maxmin } ] 
PS<3>

Now most of it will probably be gibberish, and the top few pieces may not even be readable. This output is from my own postscript interpreter which is under construction and all objects have full access. But don't look at the top. Look at the bottom. The very last element of the array is the topmost element of the stack. It's the piece of code that would have come next had /command not gone and done /errorname. That little postscript fragment can help you locate where in the source the problem lies. In my case above, I need to search my source for calls to .maxmin preceded by pop preceded by .. whatever the error was.

call executive to get a prompt

If you have a serial or telnet session with an interpreter in a printer, you can type executive and hit enter a few times. It may not echo the letters of executive as you type. Have no fear, but spell correctly. It should give you a greeting and a prompt.

With ghostscript, running the program with no arguments will give you the same sort of executive session. You can then (yourfile)run and you should still get a prompt after errors, allowing you to inspect $error as described above.

If that doesn't work, you can try running executive twice. This adds an extra level of error-handling (another stopped {handlerror} if on the exec stack). This may help in hunting more bizarre errors.

a step-wise debugger

I have available a source-level stepwise debugger that should run in any Level 2 compliant PostScript Interpreter.

It can also be used to generate stack traces as shown in this answer on TeX.SE.

like image 169
luser droog Avatar answered Nov 27 '22 15:11

luser droog


On OS X 10.7.5, preview gives me no details when it bombs, but /usr/bin/pstopdf does give me a stack dump to stdout and that's where pstack goes, too.

If I open the pdf file in preview, then changing back to preview after running pstopdf will refresh the view for the newly created pdf file.

It's not high-tech, but you can iterate pretty fast.

like image 30
asusu Avatar answered Nov 27 '22 14:11

asusu


Emacs includes PostScript tools. It includes tools for sending the postscript interpreter your currently selected text, and you can also enter commands directly into that same interpreter, for, say, querying the operand stack, or things like that.

This might not be what you are looking for, though, because it might be harder to use than you are willing to work with. Set up correctly, though, with different buffers for all the things you want to monitor, scripts and macros to do things, etc, it will do everything you want it to. I am not sure, but there may be stuff elsewhere on the net that can help you set it up.

EDIT: The main way I have used Emacs to debug postscript is in doing the following: I can copy-paste segments of the program from the file buffer into the interpreter buffer as a way to step through my program. I can also use it to tell me things about the operand stack, using commands to print out its contents and such. I can also add debugging statements to the code (like dup == etc.) that will output into the interpreter buffer, because I have been somewhat unable to figure out how to view stdout as the program is executed using other environments.

like image 23
AJMansfield Avatar answered Nov 27 '22 15:11

AJMansfield