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 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;
};
editorfactory = function(fn, label, code) {
const id = Math.floor(Math.random() * 1000);
const lt = String.fromCharCode(60); //wikitext filters tags
const node = document.createElement("DIV");
node.id = "id"+id;
node.className = "psmain";
const node2 = document.createElement("DIV");
node2.className = "editzone";
node.appendChild(node2);
const node3 = document.createElement("DIV");
node3.className = "editheader";
node3.innerHTML = label;
node2.appendChild(node3);
const node4 = document.createElement("FORM");
node2.appendChild(node4);
const node5 = document.createElement("BUTTON");
node5.type = "button";
node5.id = "button" + id;
node5.innerHTML = "Run";
node4.appendChild(node5);
const node6 = document.createElement("TEXTAREA");
node6.id = "editor" + id;
node6.className = "pseditor";
node6.innerHTML = code;
node6.rows = code.split(endofline).length + 1;
node4.appendChild(node6);
const node7 = document.createElement("DIV");
node7.id = "console" + id;
node7.className = "jsconsole";
node.appendChild(node7);
console.log(node.outerHTML);
const script = `redirectConsole($id);
run$id = function() {
redirectConsole($id);
const code = document.getElementById("editor$id").value;
var context = $fn(code);
console.log("stack: " + context.stack);
for (key in context.dict) {
console.log(key + " = " + context.dict[key]);
}
};
document.getElementById("button$id").onclick = run$id;`.replaceAll("$id", id).replaceAll("$fn",fn.name).replaceAll("$code",code);
const scriptnode = document.createElement("SCRIPT");
scriptnode.innerHTML = script;
document.body.appendChild(scriptnode);
};
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;
}
comparator = 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(comparator(a,b) == 0 ? 1 : 0);
return context;
};
operators.neq = function(context) {
const b = context.stack.pop() ?? 0;
const a = context.stack.pop() ?? 0;
context.stack.push(comparator(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(comparator(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(comparator(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(comparator(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(comparator(a,b) <= 0 ? 1 : 0);
return context;
};
We try it out.
Run
editorfactory(rpn,"RPN","/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
editorfactory(rpn,"RPN","/foo 1 def /bar 2 def foo bar eq { 99 } { 42 } ifelse ")
We can build a recursive functiom like factorial.
Run
editorfactory(rpn,"RPN","/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
editorfactory(rpn,"RPN","5 factorial ")
We can also invent a mod operator.
Run
editorfactory(rpn,"RPN","/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
editorfactory(rpn,"RPN",`% 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`);
My Journey to PostScript