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. Alternatively, there is a function pairs-begin
which can be used with this same array of pairs as an upgrade to the traditional << ... >> begin
construct.
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 them in a new local dictionary created and begin
ed 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?