2
\$\begingroup\$

This file enhances PostScript with a few new syntactic niceties for defining functions with named arguments and even type-checking.

block

The first part of the enhancement consists of a new control structure called a "block". A block is a list of pairs which will be collected as key/value pairs into a dictionary and then the special key main gets called. This much allows us to elide the 'def' for all functions and data, and also the / decorations on all the function names.

{
    main { f g h }
    f { }
    g { }
    h { }
} block

This code defines a main function and 3 functions that main calls. When block executes, all 4 functions are defined in a dictionary and then main gets called.

Importantly, it lets you put main at the top to aid top-down coding.

func

Another enhancement is the func syntax. A function can be created to accept named arguments by calling func with the array of argument names and the array of the body of the function. To do this within the block construct where normally the contents of the block are not executed but just collected, you can force execution by prefixing the @ sign to func. Any name can be executed "at compile time" with this @ prefix, but only at the top-level.

Using func you can give names to the arguments of functions by enclosing them in curly braces before the function body:

{
    main{
        3 f
        4 5 g
    }
    f {x}{ 1 x div sin } @func
    g {x y}{ x log y log mul } @func
} block

This code creates a function f of one argument x, and a function g of two arguments x and y. The function body is augmented with code which takes these 2 objects from the stack and defines then in a new local dictionary created and begined for this invocation of the function coupled with an end at the end.

For some use-cases like implementing control structures, it is useful to place the end more strategically. The fortuple function illustrates this by using @func-begin which does not add end at the end. It then places the end earlier, before calling back to the p argument.

A function can also declare the types that its arguments must have by enclosing them in parentheses and using executable names for the argument names and literal names for the types:

{
    main {
        3 4 p
    }
    p (x/integer y/integer){ x y add } @func
} block

This augments the function body with code which checks that there are indeed enough objects on the stack, or else it triggers a stackunderflow error. Then it checks that all of the arguments have the expected type, or else it triggers a typecheck error. The types here are written without the letters type at the end; these are added automatically.

You can omit the type name with the parenthesized syntax and it will allow any type for that argument. If you omit all the type names you still get the stackunderflow checking. With any of these errors the name of the user function is reported in the error message for easier debugging.

Implementation

Implementation-wise, the foundation is the pairs construct which is an array which gets traversed with forall. Any name beginning with @ gets the @ stripped off and the remainder gets executed. The results are enclosed in << and >> to create a dictionary.

The first dictionary defines everything to do with pairs including pairs-def which adds the key/value pairs into the current dictionary rather than begin a new dictionary. The next two sections add their functions to this same dictionary.

This justifies (somewhat) the cuddled style of bracing. The whole implementation is split into 3 layers but the result is only 1 dictionary on the dictstack with all of these functions in it.

The middle section defines block and func and all of the functionality for the simple-func style of defining functions. The third section implements two looping control structures which use the simple-func style. These looping functions are then used by the more complex typed-func style.

Many of the functions used to implement all of this are useful in their own right so they are also supplied to the user, like curry compose reduce.

There is some simplistic testing code at the bottom guarded by /debug where. So if this file is simply run from another file, it will skip the testing code. But if the key debug is defined somewhere on the dictstack, then the testing code will execute. So with ghostscript, testing can be invoked with gs -ddebug struct2.ps.

The testing code itself illustrates the overhead of the code added by func. For type checking, it adds a fair amount of code.

%!
% struct2.ps   An enhanced PostScript syntax for defining functions with named,
% type-checked arguments. Using @func within a block or other construct that uses
% 'pairs' accomplishes a sort of compile-time macro expansion of the shorthand function description.
<<
    /pairs-begin { pairs begin        }
    /pairs-def   { pairs {def} forall }
    /pairs       { << exch explode >> }
    /explode     { { @exec } forall   }
    /@exec       { dup type /nametype eq { exec-if-@ } if }
    /exec-if-@   { dup dup length string cvs dup first (@) first eq { exec@ }{ pop } ifelse }
    /first       { 0 get }            /exec@ {  exch pop rest cvn cvx exec  }
    /rest        { 1 1 index length 1 sub getinterval }
>> begin {
    block { pairs-begin main end }
    func  { 1 index type /stringtype eq { typed-func }{ simple-func } ifelse }
    simple-func { func-begin { end } compose }
    typed-func  { exch args-and-types reverse { make-type-name } map check-stack 3 1 roll
                  exch simple-func   compose }
    func-begin  { exch reverse /args-begin load curry exch compose }
    args-begin  { dup length dict begin { exch def } forall }
    args-and-types { /was_x false def [ exch { each-specifier } fortokens fix-last ] dup args exch types  }
    each-specifier { dup xcheck /is_x exch def  is_x was_x and { null exch } if  /was_x is_x def }
    fix-last       { counttomark 2 mod 1 eq { null } if }
    check-stack    { {pop} 4 index cvlit { cvx /stackunderflow signalerror } curry compose
                     /if cvx 2 array astore cvx {check-count} exch compose curry
                     3 index cvlit { cvx /typecheck signalerror } curry
                     /if cvx 2 array astore cvx {check-types} exch compose compose }
    check-count { dup length count 2 sub gt }
    check-types { dup length 1 add copy true exch { check-type and } forall exch pop not }
    check-type  { dup null eq { 3 -1 roll pop pop true }{ 3 -1 roll type eq } ifelse }
    make-type-name { dup type /nametype eq { dup length 4 add string dup dup 4 2 roll cvs
                     2 copy 0 exch putinterval length (type) putinterval cvn } if }
    args    { [ exch 2 { 0 get } fortuple ] }
    types   { [ exch 2 { 1 get } fortuple ] }
    map     { 1 index xcheck 3 1 roll  [ 3 1 roll forall ]  exch {cvx} if }
    reduce  { exch dup first exch rest 3 -1 roll forall }
    rreduce { exch aload length 1 sub dup 3 add -1 roll repeat }
    curry   { [ 3 1 roll {} forall ] cvx } @pop
            { dup length 1 add array dup 0 5 -1 roll put dup 1 4 -1 roll putinterval cvx }
    compose { 2 array astore cvx { {} forall } map } @pop
            { 1 index length 1 index length add array  dup 0 4 index putinterval
              dup 4 -1 roll length 4 -1 roll putinterval cvx }
    reverse { [ exch dup length 1 sub -1 0 { 2 copy get 3 1 roll pop } for pop ] }
} pairs-def {
    fortokens {src proc}{ { src token {exch /src exch store}{exit}ifelse  proc } loop } @func
    fortuple {a n p}{ 0 n /a load length 1 sub
        { /a exch /n getinterval /p exec } {load-if-literal-name} map end for
    } @func-begin
    load-if-literal-name { dup type /nametype eq 1 index xcheck not and { load } if }
} pairs-def

/debug where{pop}{currentfile flushfile}ifelse
{
    - sub + add * mul    %:= {exch def}    += {dup load 3 -1 roll + store}

    var 2 3 @add


    f  {x y z}{ x  y z +  *      } @func

    f' {x y z}{ x  y z +  *  end } @func-begin

    f'' { {z y x}args-begin  x y z + *  end }


    g(x/integer y/integer z/real){ x y z + * } @func

    g' {
        [/realtype/integertype/integertype]
        check-count { pop /g cvx /stackunderflow signalerror } if
        check-types { /g cvx /typecheck signalerror } if

        {z y x}args-begin  x y z + *  end
    }


    h(x y z){ x y z + * } @func %@dup @==

    h' {
        [null null null]
        check-count { pop /h cvx /stackunderflow signalerror } if
        check-types { /h cvx /typecheck signalerror } if

        {z y x}args-begin  x y z + *  end
    }


    main {
        var ==
        [ 1 2 3 4 5 ] { - } rreduce ==
        / =
        3 4 5 f ==
        3 4 5 f' ==
        3 4 5 f'' ==
        / =
        3 4 5.0 g =
        3 4 5.0 g' =
        { 3 4 5 g = } stopped { $error /errorname get =only ( in ) print $error /command get = } if
        / =
        clear
        { 3 4 h  = } stopped { $error /errorname get =only ( in ) print $error /command get = } if
        clear
        3 4 5 h =
        { 3.0 4.0 5.0 h = } stopped { $error /errorname get =only ( in ) print $error /command get = } if
        { 3.0 4.0 5.0 h' = } stopped { $error /errorname get =only ( in ) print $error /command get = } if
        quit
    }

} block

The output from the testing code:

$ gsnd -q -ddebug struct2.ps
5
3

27
27
27

27.0
27.0
typecheck in g

stackunderflow in h
27
27.0
27.0

Are there improvements to make to the implementation or the behavior? Currently the simple-func style does not check that there are enough arguments but just tries to define them assuming that they're there. Would it be better to add this checking, or is it better to have this low-overhead version which does not add (possibly wasteful) checks?

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.