Search code examples
argumentsprocedureghostscriptpostscript

Why does ghostscript hang when passing a procedure to a procedure to another procedure to invoke when using the same argument name?


I am passing a postscript procedure (c) as an argument on the stack to a procedure (a), which then passes this as an argument on the stack to another procedure (b) that invokes the procedure (c).

When I use a dictionary to localize variables and use the same name for the argument in a and b, ghostscript hangs (in the below code, these are procedures a1 and a2 and they both use proc). When I make them different names (in the below code these are procedures a and b which use Aproc and Bproc, respectively), it runs correctly.

Note that I am aware that I can just use the stack to pass down the original argument and avoid the whole exch def step, but in my real world code, I want to capture the stack argument to use locally.

Below is a minimal working example of the issue:

%!PS

/c{
    (C START) =
    (C output) =
    (C END) =
}bind def

/b{
    (B START) =
    1 dict begin
    /Bproc exch def
    Bproc
    end
    (B END) =
}bind def

/a{
    (A START) =
    1 dict begin
    /Aproc exch def
    {Aproc} b
    end
    (A END) =
}bind def

/b2{
    (B2 START) =
    1 dict begin
    /proc exch def
    proc
    end
    (B2 END) =
}bind def

/a2{
    (A2 START) =
    1 dict begin
    /proc exch def
    {proc} b2
    end
    (A2 END) =
}bind def


{c} a

% {c} a2

Here is the output with the above code (different argument names, no issues):

$ gs -dBATCH -dNOPAUSE -sDEVICE=pdfwrite -sOutputFile=test2.pdf test2.ps
GPL Ghostscript 9.54.0 (2021-03-30)
Copyright (C) 2021 Artifex Software, Inc.  All rights reserved.
This software is supplied under the GNU AGPLv3 and comes with NO WARRANTY:
see the file COPYING for details.
A START
B START
C START
C output
C END
B END
A END
$

Change the last 2 lines of the code to the following (comment out first, uncomment out the second):

...
% {c} a

{c} a2

Now ghostscript just hangs:

$ gs -dBATCH -dNOPAUSE -sDEVICE=pdfwrite  -sOutputFile=test2.pdf test2.ps
GPL Ghostscript 9.54.0 (2021-03-30)
Copyright (C) 2021 Artifex Software, Inc.  All rights reserved.
This software is supplied under the GNU AGPLv3 and comes with NO WARRANTY:
see the file COPYING for details.
  C-c C-c: *** Interrupt
$

Environment: OpenSuse:

$ uname -a
Linux localhost.localdomain 5.18.4-1-default #1 SMP PREEMPT_DYNAMIC Wed Jun 15 06:00:33 UTC 2022 (ed6345d) x86_64 x86_64 x86_64 GNU/Linux
$ cat /etc/os-release 
NAME="openSUSE Tumbleweed"
# VERSION="20220619"
ID="opensuse-tumbleweed"
ID_LIKE="opensuse suse"
VERSION_ID="20220619"
PRETTY_NAME="openSUSE Tumbleweed"
ANSI_COLOR="0;32"
CPE_NAME="cpe:/o:opensuse:tumbleweed:20220619"
BUG_REPORT_URL="https://bugs.opensuse.org"
HOME_URL="https://www.opensuse.org/"
DOCUMENTATION_URL="https://en.opensuse.org/Portal:Tumbleweed"
LOGO="distributor-logo-Tumbleweed"

Solution

  • I think you are being tripped up by early and late name binding, which is a subtle point in PostScript and often traps newcomers to the language.

    Because of late name binding you can change the definition of a key/value pair during the course of execution, which can have unexpected effects. This is broadly similar to self-modifying code.

    See section 3.12 Early Name Binding on page 117 of the 3rd Edition PostScript Language Reference Manual for details, but the basic point is right at the first paragraph:

    "Normally, when the PostScript language scanner encounters an executable name in the program being scanned, it simply produces an executable name object; it does not look up the value of the name. It looks up the name only when the name object is executed by the interpreter. The lookup occurs in the dictionaries that are on the dictionary tack at the time of execution."

    Also you aren't quite (I think) doing what you think you are with the executable array tokens '{' and '}'. That isn't putting the definition on the stack, exactly, it is defining an executable array on the stack. Not quite the same thing. In particular it means that the content of the array (between { and }) isn't being executed.

    If you wanted to put the original function on the stack you would do '/proc load'. For example if you wanted to define a function to be the same as showpage you might do:

    /my_showpage /showpage load def
    

    Not:

    /my_showpage {showpage} def
    

    So looking at your failing case.

    In function a2 You start by creating and opening a new dictionary. Said dictionary is only present on the stack, so if you ever pop it off it will go out of scope and be garbage collected (I'm sure you know this of course).

    In that dictionary you create a key '/proc' with the associated value of an executable array. The array contains {c}.

    Then you create a new executable array on the stack {proc} You then execute function b2 with that executable array on the stack.

    b2 starts another new dictionary, again on the stack, and then defines a key/value pair '/proc' with the associated value {proc}.

    You then execute proc. That executes the array, the only thing in it is a reference to 'proc', so that is looked up starting in the current dictionary.

    Oh look! We have a definition in the current dictionary, it's an executable array. So we push that array and execute it. The only thing in it is a reference to 'proc', so we look that up in the current dictionary, and we have one, so we execute that array. Round and round and round we go.....

    You can avoid this by simply changing :

    /a2{
        (A2 START) =
        1 dict begin
        /proc exch def
        {proc} b2
        end
        (A2 END) =
    }bind def
    

    To :

    /a2{
        (A2 START) =
        1 dict begin
        /proc exch def
        /proc load b2
        end
        (A2 END) =
    }bind def