The weekday program of last chapter showed a memory leak in our RPN machine. Every time you call the operator, the string and array constants fill the heap and never go away. This will be a problem if you create operators that are used often.
So we need to remove the entries in the heap when they are not used any more. This is not trivial. How do we know that a string will not be used any more?
A strategy is reference counting. In the RPN, a string or an array can either be referenced by the stack or by a key in a dictionary. For each string or array, the reference counter increases when it is pushed on a stack or assigned to a key. The references counter decreases when it is consumed in the stack or the key in the dictionary is assigned a new value.
We create a new class rpnHeapElement which implements a counter, which is initially 0 and two methods. inc() increases the counter. dec() decreases the counter and checks if the counter is 0. If it is 0, the heap value is set to null.
This removes the values from the heap but will still keep them as null in the array, so the heap array still grows. To contain that, instead of growing the array on each instance of a new string or array, we will reuse the values that are null. We rewrite the constructors to achieve that.
Special case: When we destroy an array, we also have to dec() all elements referenced by the array.
We use this occasion to add unitTest also to the RPN parser to check every branch of the parser is used at least once.
Run
rpn = function(s, context = null ) {
if (!context) {
context = {};
context.stack = [];
context.dict = {};
context.heap = [];
context.heapcounter = [];
context.lasterror = "";
context.currentcode = "";
context.pop = function (...parameters) {
if (this.stack.length < parameters.length) {
this.error("stackunderflow");
return [];
}
const result = [];
for (var p of parameters) {
const data = this.stack.pop();
if (p == "any") {
result.push(data);
} else if (p.includes(data.type)) {
result.push(data);
} else {
this.error("typeerror");
return [];
}
}
return result;
}
context.popArray = function() {
var found = false;
var arr = [];
while (this.stack.length && ! found) {
const value = this.stack.pop();
if (value.type == "mark") {
found = true;
} else {
arr.push(value);
}
}
if (! found) {
return this.error("stackunderflow");
}
arr.reverse();
const a = new rpnArray(arr, this);
a.reference.inc();
this.stack.push(a);
}
context.error = function(s) {
this.lasterror = s;
this.stack.push(new rpnError(s));
return this;
}
}
const list = s.concat(" ").split("");
var state = "start";
var current = "";
var depth = 0;
for (elem of list) {
if (context.lasterror) {
return context;
}
context.currentcode += elem;
switch (state) {
case "start":
if (/[0-9-]/.test(elem)) {
state = "number";
current = elem;
} else if (/[a-zA-Z]/.test(elem)) {
state = "operator";
current = elem;
} else if (elem == "/") {
state = "namestart";
} else if (elem == "{") {
state = "procedure";
} else if (elem == "(") {
state = "string";
} else if (elem == "[") {
context.stack.push(new rpnMark()); //mark
} else if (elem == "]") {
context.popArray();
} else if (elem == "%") {
state = "comment";
} else if (elem.trim() === '') {
// ignore whitespace
} else {
context.error("syntaxerror");
}
break;
case "number":
if (/[0-9]/.test(elem)) {
current += elem;
} else if (elem == ".") {
state = "fraction";
current += elem;
} else if (/[a-zA-Z-]/.test(elem)) {
context.error("syntaxerror");
} else if (elem.trim() === "") {
context.stack.push(new rpnNumber(parseFloat(current)));
state = "start";
current = "";
} else if (elem == "]") {
context.stack.push(new rpnNumber(parseFloat(current)));
state = "start";
current = "";
context.popArray();
} else {
context.error("syntaxerror");
}
break;
case "fraction":
if (/[0-9]/.test(elem)) {
current += elem;
} else if (elem.trim() === "") {
context.stack.push(new rpnNumber(parseFloat(current)));
state = "start";
current = "";
} else if (elem == "]") {
context.stack.push(new rpnNumber(parseFloat(current)));
state = "start";
current = "";
context.popArray();
} else {
context.error("syntaxerror");
}
break;
case "operator":
if (/[a-zA-Z0-9]/.test(elem)) {
current += elem;
} else if (elem.trim() !== "" && elem != "]"){
context.error("syntaxerror");
} else {
const data = context.dict[current];
const op = operators[current];
if (data) {
if (data.type == "procedure") {
context = rpn(data.value, context);
} else if (data.type == "array"){
data.reference.inc();
context.stack.push(data);
} else if (data.type == "string"){
data.reference.inc();
context.stack.push(data);
} else {
context.stack.push(data);
}
} else if (op) {
context = op(context);
} else {
context.error("syntaxerror");
}
state = "start";
current = "";
if (elem == "]") {
context.popArray();
}
}
break;
case "namestart":
if (/[a-zA-Z]/.test(elem)) {
state = "name";
current = elem;
} else {
context.error("syntaxerror");
}
break;
case "name":
if (/[a-zA-Z0-9]/.test(elem)) {
current += elem;
} else if (elem.trim() === "") {
context.stack.push(new rpnName(current));
state = "start";
current = "";
} else if (elem == "]") {
context.stack.push(new rpnName(current));
state = "start";
current = "";
context.popArray();
} else {
context.error("syntaxerror");
}
break;
case "procedure":
if (elem == "}") {
if (depth) {
depth--;
current += elem;
} else {
context.stack.push(new rpnProcedure(current));
state = "start";
current = "";
}
} else if (elem == "{") {
depth++;
current += elem;
} else {
current += elem;
}
break;
case "string":
if (elem == ")") {
if (depth) {
depth--;
current += elem;
} else {
const s = new rpnString(current, context);
s.reference.inc();
context.stack.push(s);
state = "start";
current = "";
}
} else if (elem == "(") {
depth++;
current += elem;
} else {
current += elem;
}
break;
case "comment":
if (elem == endofline ) {
state = "start";
}
} // switch state
} // for
if (state !== "start") {
context.error("syntaxerror");
}
return context;
};
unitTest("123","123");
unitTest("-123","-123");
unitTest("1c","!syntaxerror");
unitTest("1.","1");
unitTest("1.1","1.1");
unitTest("1.1.","!syntaxerror");
unitTest("1.1a","!syntaxerror");
unitTest("/name","/name");
unitTest("/n2","/n2");
unitTest("/n.","!syntaxerror");
unitTest("[ 1 2 3 ]","[1 2 3]");
unitTest("[1 2 3]","[1 2 3]");
unitTest("{a b c}","{a b c}");
unitTest("(abc)","(abc)");
unitTest("(ab(abc)c)","(ab(abc)c)");
unitTest("(ab(abc","!syntaxerror");
unitTest("(ab(abc)","!syntaxerror");
unitTest("(ab)abc)","(ab) !syntaxerror");
unitTest("[(abc) 2 3 [4 5]]","[(abc) 2 3 [4 5]]");
We need to adapt the representation of the heap in the postScriptEditor console.
Run
postScriptEditor = function(code) {
const id = Math.floor(Math.random() * 1000); // create unique id for console.log
const node = document.createElement("DIV"); // build the HTML nodes
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 = "PostScript";
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); // add the node to the parent element
const script = `redirectConsole($id);
run$id = function() {
redirectConsole($id);
const code = document.getElementById("editor$id").value;
const context = rpn(code);
console.log("stack: " + context.stack.reduce((acc,v) => acc + v.dump + " " , " "));
for (key in context.dict) {
console.log(key + " = " + (context.dict[key].dump));
}
if (context.heap.length) {
console.log("heap: " + context.heap.reduce( function(acc, v) {
const val = v.value;
if (Array.isArray(val)) {
return acc + " [" + val.reduce( (acc2, v2) => acc2 + v2.dump + " " , " ") + "] "+v.counter;
} else if (val === null) {
return acc + " null";
} else {
return acc + " (" + val + ") "+v.counter;
}
} , ""));
}
};
document.getElementById("button$id").onclick = run$id;`.replaceAll("$id", id).replaceAll("$code",code);
const scriptnode = document.createElement("SCRIPT");
scriptnode.innerHTML = script;
document.body.appendChild(scriptnode); // run the script
};
Only some operators work on strings and arrays, so we modify these.
Run
operators.def = function(context) {
const [b, a] = context.pop("any", "name");
if (!b) return context;
if (context.dict[a.value]) {
const old = context.dict[a.value];
if (old.type == "array") old.reference.dec();
if (old.type == "string") old.reference.dec();
}
// we do not inc() or dec() for the new, because reference count stays
context.dict[a.value] = b;
return context;
};
unitTest("/foo 1 def 2 foo","2 1");
unitTest("/foo (abc) def 2 foo","2 (abc)");
unitTest("/foo { 72 mul } def 2 foo","144");
unitTest("2 3 def","!typeerror");
unitTest("2 def","2 !stackunderflow");
unitTest("def","!stackunderflow");
unitTest("/a (abc) def /b a def /a (def) def b a","(abc) (def)")
operators.dup = function(context) {
const [a] = context.pop("any");
if (!a) return context;
if (a.type == "array") a.reference.inc();
if (a.type == "string") a.reference.inc();
context.stack.push(a);
context.stack.push(a);
return context;
};
unitTest("1 dup","1 1");
unitTest("(a) dup","(a) (a)");
unitTest("[2 3] dup","[2 3] [2 3]");
unitTest("dup","!stackunderflow");
operators.pop = function(context) {
const [a] = context.pop("any");
if (!a) return context;
if (a.type == "array") a.reference.dec();
if (a.type == "string") a.reference.dec();
return context;
};
unitTest("1 2 pop","1");
unitTest("(a) pop","");
unitTest("[2 3] pop","");
unitTest("pop","!stackunderflow");
operators.exch = function(context) {
const [b, a] = context.pop("any", "any");
if (!b) return context;
context.stack.push(b);
context.stack.push(a);
// reference count does not change
return context;
};
unitTest("2 3 exch","3 2");
unitTest("2 0 exch","0 2");
unitTest("2 -3 exch","-3 2");
unitTest("2 (a) exch","(a) 2");
unitTest("(a) 2 exch","2 (a)");
unitTest("2 exch","2 !stackunderflow");
unitTest("exch","!stackunderflow");
unitTest("(a) dup /b exch def pop b", "(a)");
operators.length = function(context) {
const [a] = context.pop("array,string");
if (!a) return context;
context.stack.push(new rpnNumber(a.value.length));
a.reference.dec();
return context;
};
unitTest("(abc) length","3");
unitTest("[1 2 3] length","3");
unitTest("() length","0");
unitTest("[] length","0");
unitTest("1 length","!typeerror");
unitTest("length","!stackunderflow");
operators.get = function(context) {
const [b, a] = context.pop("number", "array,string");
if (!b) return context;
if (b.value < 0) return context.error("rangerror");
if (b.value >= a.value.length) return context.error("rangerror");
if (a.type == "array") {
const elem = a.value[b.value];
if (elem.reference) elem.reference.inc();
context.stack.push(elem);
} else {
context.stack.push(new rpnNumber(a.value.charCodeAt(b.value)));
}
a.reference.dec();
return context;
};
unitTest("(abc) 1 get","98");
unitTest("[1 2 3] 1 get","2");
unitTest("() 1 get","!rangerror");
unitTest("[] 1 get","!rangerror");
unitTest("(abc) -2 get","!rangerror");
unitTest("[1 2 3] -2 get","!rangerror");
unitTest("(abc) 3 get","!rangerror");
unitTest("[1 2 3] 3 get","!rangerror");
unitTest("1 2 get","!typeerror");
unitTest("(a) get","(a) !stackunderflow");
unitTest("get","!stackunderflow");
operators.getinterval = function(context) {
const [c, b, a] = context.pop("number", "number", "array,string");
if (!c) return context;
if (b.value < 0) return context.error("rangerror");
if (c.value < 0) return context.error("rangerror");
if (b.value + c.value > a.value.length) return context.error("rangerror");
if (a.type == "array") {
const elem = new rpnArray(a.value.slice(b.value, b.value + c.value), context);
elem.reference.inc();
context.stack.push(elem);
} else {
const s = new rpnString(a.value.substr(b.value, c.value), context);
s.reference.inc();
context.stack.push(s);
}
a.reference.dec();
return context;
};
unitTest("(abc) 1 2 getinterval","(bc)");
unitTest("[1 2 3] 1 2 getinterval","[2 3]");
unitTest("(abc) 1 0 getinterval","()");
unitTest("[1 2 3] 1 0 getinterval","[]");
unitTest("() 1 2 getinterval","!rangerror");
unitTest("[] 1 2 getinterval","!rangerror");
unitTest("(abc) -2 2 getinterval","!rangerror");
unitTest("[1 2 3] -2 2 getinterval","!rangerror");
unitTest("(abc) 3 2 getinterval ","!rangerror");
unitTest("[1 2 3] 3 2 getinterval","!rangerror");
unitTest("1 2 2 getinterval","!typeerror");
unitTest("(a) 2 getinterval","(a) 2 !stackunderflow");
unitTest("(a) getinterval","(a) !stackunderflow");
unitTest("getinterval","!stackunderflow");
operators.put = function(context) {
const [c, b, a] = context.pop("any", "number", "array,string");
if (!c) return context;
if (b.value < 0) return context.error("rangerror");
if (b.value >= a.value.length) return context.error("rangerror");
if (a.type == "array") {
const ch = [ c ];
const s = a.value.slice(0, b.value).concat(ch).concat(a.value.slice(b.value + 1, a.value.length));
a.replace(s);
} else {
const ch = String.fromCharCode(c.value);
const s = a.value.slice(0, b.value) + ch + a.value.slice(b.value + 1, a.value.length);
a.replace(s);
}
a.reference.dec();
return context;
};
unitTest("/a (abc) def a 1 97 put a","(aac)");
unitTest("/a [1 2 3] def a 1 1 put a", "[1 1 3]");
unitTest("() 1 98 put","!rangerror");
unitTest("[] 1 2 put","!rangerror");
unitTest("(abc) -2 98 put","!rangerror");
unitTest("[1 2 3] -2 2 put","!rangerror");
unitTest("(abc) 3 98 put","!rangerror");
unitTest("[1 2 3] 3 2 put","!rangerror");
unitTest("1 2 2 put","!typeerror");
unitTest("(a) 2 put","(a) 2 !stackunderflow");
unitTest("(a) put","(a) !stackunderflow");
unitTest("put","!stackunderflow");
operators.putinterval = function(context) {
const [c, b, a] = context.pop("string,array", "number", "array,string");
if (!c) return context;
if (b.value < 0) return context.error("rangerror");
if (b.value + c.value.length > a.value.length) return context.error("rangerror");
if (a.type !== c.type) return context.error("rangerror");
if (a.type == "array") {
const s = a.value.slice(0, b.value).concat(c.value).concat(a.value.slice(b.value + c.value.length, a.value.length));
a.replace(s);
} else {
const s = a.value.slice(0, b.value) + c.value + a.value.slice(b.value + c.value.length , a.value.length);
a.replace(s);
}
a.reference.dec();
c.reference.dec();
return context;
};
unitTest("/a (abc) def a 1 (ab) putinterval a","(aab)");
unitTest("/a [1 2 3] def a 1 [1 2] putinterval a", "[1 1 2]");
unitTest("() 1 (a) putinterval","!rangerror");
unitTest("[] 1 [2] putinterval","!rangerror");
unitTest("(abc) -2 (a) putinterval","!rangerror");
unitTest("[1 2 3] -2 [1 2] putinterval","!rangerror");
unitTest("(abc) 3 (a) putinterval","!rangerror");
unitTest("[1 2 3] 3 [1 2] putinterval","!rangerror");
unitTest("1 2 2 putinterval","1 2 !typeerror");
unitTest("(a) 2 putinterval","(a) 2 !stackunderflow");
unitTest("(a) putinterval","(a) !stackunderflow");
unitTest("putinterval","!stackunderflow");
Run
postScriptEditor(`/foo (abc) def /bar foo def /foo (def) def /bar (ghi) def /bar (jkl) def` )
Basically, the garbage collection seems to work. The heap fills with (abc), (def), (ghi) then (abc) is freed an used by (jkl) and (ghi) is freed.
code
heap
/foo (abc)
(abc)
def
(abc)
/bar foo def
(abc)
/foo (def)
(abc) (def)
def
(abc) (def)
/bar (ghi)
(abc) (def) (ghi)
def
null (def) (ghi)
/bar (jkl)
(jkl) (def) (ghi)
def
(jkl) (def) null
We try the garbage collection on the weekday algorithm.
Run
postScriptEditor(`/weekday {
/year exch def
/month exch def
/day exch def
% four centuries
/bisextiles year 400 idiv 97 mul def
% centuries
/bisextiles year 400 mod 100 idiv 24 mul bisextiles add def
% four years
/bisextiles year 100 mod 4 idiv bisextiles add def
% weekdays labels zero based (mod 7 gives 0-6)
/wd [(sun) (mon) (tue) (wed) (thu) (fri) (sat)] def
% day offset of month, 1-based.
/m [0 0 31 59 90 120 151 181 212 242 273 304 334] def
% is this year bisextile, change value for february
year 4 mod not
year 100 mod
year 400 mod not or and
{ m 2 60 put } if
% put together
year 365 mul
bisextiles add
m month get add
day add
6 add % offset constant to determine experimentally
7 mod
wd exch get }
def
9 7 2024 weekday
20 5 2024 weekday
4 10 2023 weekday
8 2 2018 weekday
8 3 1996 weekday
8 5 1945 weekday
1 8 1891 weekday
` )
Seems to work.
The codebase including the unitTest has now 950 lines ps20240710.js
My Journey to PostScript