Until now, all operations of our PostScript machine are strictly linear. All code is executed once and without conditions. But without conditional execution, the machine is not Turning complete.

This problem is very easy to solve. We do have to modify our interpreter only slightly. We have to build a counter for the depth of the sequence, because a sequence may have sub-sequences.

What we need, is values (true, false), operators that can compare (equal, bigger, lower) and reverse (not) and operators. There will not be a Boolean type: 1 is true and 0 is false. We create an utility function rpnComparator to compare values.

Run
operators = {};
operators.add = function(context) {
const b = context.stack.pop() ?? 0;
const a = context.stack.pop() ?? 0;
context.stack.push(a + b);
return context;
};
operators.sub = function(context) {
const b = context.stack.pop() ?? 0;
const a = context.stack.pop() ?? 0;
context.stack.push(a - b);
return context;
};
operators.mul = function(context) {
const b = context.stack.pop() ?? 0;
const a = context.stack.pop() ?? 0;
context.stack.push(a * b);
return context;
};
operators.div = function(context) {
const b = context.stack.pop() ?? 0;
const a = context.stack.pop() ?? 0;
context.stack.push(a / b);
return context;
};
operators.sin = function(context) {
const a = context.stack.pop() ?? 0;
context.stack.push(Math.sin(a * 3.1415926536 / 180));
return context;
};
operators.cos = function(context) {
const a = context.stack.pop() ?? 0;
context.stack.push(Math.cos(a * 3.1415926536 / 180));
return context;
};
operators.def = function(context) {
const b = context.stack.pop().toString() ?? "";
const a = context.stack.pop() ?? "undefined";
context.dict[a] = b;
return context;
}
operators.dup = function(context) {
const a = context.stack.pop() ?? 0;
context.stack.push(a);
context.stack.push(a);
return context;
};
operators.pop = function(context) {
const a = context.stack.pop() ?? 0;
return context;
};
operators.exch = function(context) {
const b = context.stack.pop() ?? 0;
const a = context.stack.pop() ?? 0;
context.stack.push(b);
context.stack.push(a);
return context;
};

Run
operators.true = function(context) {
context.stack.push(1);
return context;
};
operators.false = function(context) {
context.stack.push(0);
return context;
};
operators.not = function(context) {
const a = context.stack.pop() ?? 0;
context.stack.push(a ? 0 : 1);
return context;
}
rpnComparator = function(a,b) {
// returns 1 for bigger, -1 for lower and 0 for equal
// strings are compared alphabetically lowercase, except when equal all case
// all non empty strings are higher than numbers
// empty strings are lower than numbers
// NaN is lowest
// undefined and null should not happen
if(typeof a == "number" && typeof b == "number") {
if (a > b) return 1;
if (a < b) return -1;
return 0;
} else if (typeof a == "number") {
return -1;
} else if (typeof b == "number") {
return 1;
} else {
const alow = a.toLowerCase();
const blow = b.toLowerCase();
if (alow > blow) return 1;
if (alow < blow) return -1;
if (a > b) return 1;
if (a < b) return -1;
return 0;
}
}
operators.eq = function(context) {
const b = context.stack.pop() ?? 0;
const a = context.stack.pop() ?? 0;
context.stack.push(rpnComparator(a,b) == 0 ? 1 : 0);
return context;
};
operators.ne = function(context) {
const b = context.stack.pop() ?? 0;
const a = context.stack.pop() ?? 0;
context.stack.push(rpnComparator(a,b) != 0 ? 1 : 0);
return context;
};
operators.gt = function(context) {
const b = context.stack.pop() ?? 0;
const a = context.stack.pop() ?? 0;
context.stack.push(rpnComparator(a,b) == 1 ? 1 : 0);
return context;
};
operators.ge = function(context) {
const b = context.stack.pop() ?? 0;
const a = context.stack.pop() ?? 0;
context.stack.push(rpnComparator(a,b) >= 0 ? 1 : 0);
return context;
};
operators.lt = function(context) {
const b = context.stack.pop() ?? 0;
const a = context.stack.pop() ?? 0;
context.stack.push(rpnComparator(a,b) == -1 ? 1 : 0);
return context;
};
operators.le = function(context) {
const b = context.stack.pop() ?? 0;
const a = context.stack.pop() ?? 0;
context.stack.push(rpnComparator(a,b) <= 0 ? 1 : 0);
return context;
};

We try it out.

Run
postScriptEditor("/foo 1 def /bar 2 def foo bar eq foo bar gt foo bar lt")

We now are ready to construct the first conditional operator if.

Run
operators.if = function(context) {
const doif = context.stack.pop() ?? 0;
const condition = context.stack.pop() ?? 0;
var context2 = context;
if (condition) {
context2 = rpn(doif, context);
}
return context2;
};
operators.ifelse = function(context) {
const doelse = context.stack.pop() ?? 0;
const doif = context.stack.pop() ?? 0;
const condition = context.stack.pop() ?? 0;
var context2;
if (condition) {
context2 = rpn(doif, context);
} else {
context2 = rpn(doelse, context);
}
return context2;
};

We try it out.

Run
postScriptEditor("/foo 1 def /bar 2 def foo bar eq { 99 } { 42 } ifelse ")

We can build a recursive function like factorial.

Run
postScriptEditor("/factorial { dup 0 eq { pop 1 } { dup 1 sub factorial mul } ifelse } def 6 factorial ")

It is better to use the stack and not variables in recursion because of the variable scope. Note that there is no global scope on this page, so when you run just factorial here, you get an error.

Run
postScriptEditor("5 factorial ")

We can also invent a mod operator.

Run
postScriptEditor("/mod { /d exch def dup d ge { d sub d mod } if } def 24 5 mod");

Can we make also the greatest divisor? Stack hhandling is challenging, but gcd is quite similar to mod. Note that you can space and comment your code to make it readable.

Run
postScriptEditor(`% m n gcd g
% takes number m and n from the stack and pushes the greatest common divisor
/gcd {
/n exch def dup n ge {
% gcd(m, n) = gcd(m, m-n)
n sub n gcd
} {
dup 0 eq {
% gcd (0, n) = n
pop n
} {
% gcd(m, n) = gcd(n, m)
n exch gcd
} ifelse
} ifelse
} def
25 30 gcd`);

The current code is now 359 lines ps20240628.js

My Journey to PostScript