// and | elements
var heapRows = myViz.domRootD3.select('#heap')
.selectAll('table.heapRow')
.data(curToplevelLayout, function(objLst) {
return objLst[0]; // return first element, which is the row ID tag
})
// update an existing heap row
var heapColumns = heapRows
//.each(function(objLst, i) { console.log('UPDATE ROW:', objLst, i); })
.selectAll('td')
.data(function(d, i) {return d.slice(1, d.length);}, /* map over each row, skipping row ID tag */
function(objID) {return objID;} /* each object ID is unique for constancy */)
// ENTER
heapColumns.enter().append('td')
.attr('class', 'toplevelHeapObject')
.attr('id', function(d, i) {return 'toplevel_heap_object_' + d;})
// remember that the enter selection is added to the update
// selection so that we can process it later ...
// UPDATE
heapColumns
.order() // VERY IMPORTANT to put in the order corresponding to data elements
.each(function(objID, i) {
//console.log('NEW/UPDATE ELT', objID);
// TODO: add a smoother transition in the future
// Right now, just delete the old element and render a new one in its place
$(this).empty();
renderCompoundObject(objID, $(this), true);
})
// EXIT
heapColumns.exit()
.remove()
// insert new heap rows
heapRows.enter().append('table')
//.each(function(objLst, i) {console.log('NEW ROW:', objLst, i);})
.attr('class', 'heapRow')
.selectAll('td')
.data(function(d, i) {return d.slice(1, d.length);}, /* map over each row, skipping row ID tag */
function(objID) {return objID;} /* each object ID is unique for constancy */)
.enter().append('td')
.attr('class', 'toplevelHeapObject')
.attr('id', function(d, i) {return 'toplevel_heap_object_' + d;})
.each(function(objID, i) {
//console.log('NEW ELT', objID);
// TODO: add a smoother transition in the future
renderCompoundObject(objID, $(this), true);
});
// remove deleted rows
heapRows.exit()
//.each(function(objLst, i) {console.log('DEL ROW:', objLst, i);})
.remove();
function renderNestedObject(obj, d3DomElement) {
if (isPrimitiveType(obj)) {
renderPrimitiveObject(obj, d3DomElement);
}
else {
renderCompoundObject(getRefID(obj), d3DomElement, false);
}
}
function renderPrimitiveObject(obj, d3DomElement) {
var typ = typeof obj;
if (obj == null) {
d3DomElement.append('None');
}
else if (typ == "number") {
d3DomElement.append('' + obj + '');
}
else if (typ == "boolean") {
if (obj) {
d3DomElement.append('True');
}
else {
d3DomElement.append('False');
}
}
else if (typ == "string") {
// escape using htmlspecialchars to prevent HTML/script injection
var literalStr = htmlspecialchars(obj);
// print as a double-quoted string literal
literalStr = literalStr.replace(new RegExp('\"', 'g'), '\\"'); // replace ALL
literalStr = '"' + literalStr + '"';
d3DomElement.append('' + literalStr + '');
}
else {
assert(false);
}
}
function renderCompoundObject(objID, d3DomElement, isTopLevel) {
if (!isTopLevel && renderedObjectIDs.has(objID)) {
// render jsPlumb arrow source since this heap object has already been rendered
// (or will be rendered soon)
// add a stub so that we can connect it with a connector later.
// IE needs this div to be NON-EMPTY in order to properly
// render jsPlumb endpoints, so that's why we add an " "!
var srcDivID = 'heap_pointer_src_' + heap_pointer_src_id;
heap_pointer_src_id++; // just make sure each source has a UNIQUE ID
d3DomElement.append(' ');
assert(!connectionEndpointIDs.has(srcDivID));
connectionEndpointIDs.set(srcDivID, 'heap_object_' + objID);
assert(!heapConnectionEndpointIDs.has(srcDivID));
heapConnectionEndpointIDs.set(srcDivID, 'heap_object_' + objID);
return; // early return!
}
// wrap ALL compound objects in a heapObject div so that jsPlumb
// connectors can point to it:
d3DomElement.append('');
d3DomElement = myViz.domRoot.find('#heap_object_' + objID);
renderedObjectIDs.set(objID, 1);
var obj = curEntry.heap[objID];
assert($.isArray(obj));
if (obj[0] == 'LIST' || obj[0] == 'TUPLE' || obj[0] == 'SET' || obj[0] == 'DICT') {
var label = obj[0].toLowerCase();
assert(obj.length >= 1);
if (obj.length == 1) {
d3DomElement.append('empty ' + label + ' ');
}
else {
d3DomElement.append('' + label + ' ');
d3DomElement.append('');
var tbl = d3DomElement.children('table');
if (obj[0] == 'LIST' || obj[0] == 'TUPLE') {
tbl.append(' | |
');
var headerTr = tbl.find('tr:first');
var contentTr = tbl.find('tr:last');
$.each(obj, function(ind, val) {
if (ind < 1) return; // skip type tag and ID entry
// add a new column and then pass in that newly-added column
// as d3DomElement to the recursive call to child:
headerTr.append('');
headerTr.find('td:last').append(ind - 1);
contentTr.append(' | ');
renderNestedObject(val, contentTr.find('td:last'));
});
}
else if (obj[0] == 'SET') {
// create an R x C matrix:
var numElts = obj.length - 1;
// gives roughly a 3x5 rectangular ratio, square is too, err,
// 'square' and boring
var numRows = Math.round(Math.sqrt(numElts));
if (numRows > 3) {
numRows -= 1;
}
var numCols = Math.round(numElts / numRows);
// round up if not a perfect multiple:
if (numElts % numRows) {
numCols += 1;
}
jQuery.each(obj, function(ind, val) {
if (ind < 1) return; // skip 'SET' tag
if (((ind - 1) % numCols) == 0) {
tbl.append('
');
}
var curTr = tbl.find('tr:last');
curTr.append(' | ');
renderNestedObject(val, curTr.find('td:last'));
});
}
else if (obj[0] == 'DICT') {
$.each(obj, function(ind, kvPair) {
if (ind < 1) return; // skip 'DICT' tag
tbl.append(' | |
');
var newRow = tbl.find('tr:last');
var keyTd = newRow.find('td:first');
var valTd = newRow.find('td:last');
var key = kvPair[0];
var val = kvPair[1];
renderNestedObject(key, keyTd);
renderNestedObject(val, valTd);
});
}
}
}
else if (obj[0] == 'INSTANCE' || obj[0] == 'CLASS') {
var isInstance = (obj[0] == 'INSTANCE');
var headerLength = isInstance ? 2 : 3;
assert(obj.length >= headerLength);
if (isInstance) {
d3DomElement.append('' + obj[1] + ' instance
');
}
else {
var superclassStr = '';
if (obj[2].length > 0) {
superclassStr += ('[extends ' + obj[2].join(', ') + '] ');
}
d3DomElement.append('' + obj[1] + ' class ' + superclassStr + '
');
}
if (obj.length > headerLength) {
var lab = isInstance ? 'inst' : 'class';
d3DomElement.append('');
var tbl = d3DomElement.children('table');
$.each(obj, function(ind, kvPair) {
if (ind < headerLength) return; // skip header tags
tbl.append(' | |
');
var newRow = tbl.find('tr:last');
var keyTd = newRow.find('td:first');
var valTd = newRow.find('td:last');
// the keys should always be strings, so render them directly (and without quotes):
assert(typeof kvPair[0] == "string");
var attrnameStr = htmlspecialchars(kvPair[0]);
keyTd.append('' + attrnameStr + '');
// values can be arbitrary objects, so recurse:
renderNestedObject(kvPair[1], valTd);
});
}
}
else if (obj[0] == 'FUNCTION') {
assert(obj.length == 3);
var funcName = htmlspecialchars(obj[1]); // for displaying weird names like ''
var parentFrameID = obj[2]; // optional
if (parentFrameID) {
d3DomElement.append('function ' + funcName + ' [parent=f'+ parentFrameID + ']
');
}
else {
d3DomElement.append('function ' + funcName + '
');
}
}
else {
// render custom data type
assert(obj.length == 2);
var typeName = obj[0];
var strRepr = obj[1];
strRepr = htmlspecialchars(strRepr); // escape strings!
d3DomElement.append('' + typeName + '
');
d3DomElement.append('');
}
}
// Render globals and then stack frames:
// TODO: could convert to using d3 to map globals and stack frames directly into stack frame divs
// (which might make it easier to do smooth transitions)
// However, I need to think carefully about what to use as object keys for stack objects.
// Perhaps a combination of function name and current position index? This might handle
// recursive calls well (i.e., when there are multiple invocations of the same function
// on the stack)
// render all global variables IN THE ORDER they were created by the program,
// in order to ensure continuity:
if (curEntry.ordered_globals.length > 0) {
this.domRoot.find("#stack").append('');
this.domRoot.find("#stack #globals").append('');
var tbl = this.domRoot.find("#global_table");
$.each(curEntry.ordered_globals, function(i, varname) {
var val = curEntry.globals[varname];
// (use '!==' to do an EXACT match against undefined)
if (val !== undefined) { // might not be defined at this line, which is OKAY!
tbl.append('| ' + varname + ' | |
');
var curTr = tbl.find('tr:last');
if (isPrimitiveType(val)) {
renderPrimitiveObject(val, curTr.find("td.stackFrameValue"));
}
else{
// add a stub so that we can connect it with a connector later.
// IE needs this div to be NON-EMPTY in order to properly
// render jsPlumb endpoints, so that's why we add an " "!
// make sure varname doesn't contain any weird
// characters that are illegal for CSS ID's ...
var varDivID = 'global__' + varnameToCssID(varname);
curTr.find("td.stackFrameValue").append('
');
assert(!connectionEndpointIDs.has(varDivID));
connectionEndpointIDs.set(varDivID, 'heap_object_' + getRefID(val));
}
}
});
}
$.each(curEntry.stack_to_render, function(i, e) {
renderStackFrame(e, i, e.is_zombie);
});
function renderStackFrame(frame, ind, is_zombie) {
var funcName = htmlspecialchars(frame.func_name); // might contain '<' or '>' for weird names like
var frameID = frame.frame_id; // optional (btw, this isn't a CSS id)
// optional (btw, this isn't a CSS id)
var parentFrameID = null;
if (frame.parent_frame_id_list.length > 0) {
parentFrameID = frame.parent_frame_id_list[0];
}
var localVars = frame.encoded_locals
// the stackFrame div's id is simply its index ("stack")
var divClass, divID, headerDivID;
if (is_zombie) {
divClass = 'zombieStackFrame';
divID = "zombie_stack" + ind;
headerDivID = "zombie_stack_header" + ind;
}
else {
divClass = 'stackFrame';
divID = "stack" + ind;
headerDivID = "stack_header" + ind;
}
myViz.domRoot.find("#stack").append('');
var headerLabel = funcName + '()';
if (frameID) {
headerLabel = 'f' + frameID + ': ' + headerLabel;
}
if (parentFrameID) {
headerLabel = headerLabel + ' [parent=f' + parentFrameID + ']';
}
myViz.domRoot.find("#stack #" + divID).append('');
if (frame.ordered_varnames.length > 0) {
var tableID = divID + '_table';
myViz.domRoot.find("#stack #" + divID).append('');
var tbl = myViz.domRoot.find("#" + tableID);
$.each(frame.ordered_varnames, function(xxx, varname) {
var val = localVars[varname];
// special treatment for displaying return value and indicating
// that the function is about to return to its caller
//
// DON'T do this for zombie frames
if (varname == '__return__' && !is_zombie) {
assert(curEntry.event == 'return'); // sanity check
tbl.append('| About to return |
');
tbl.append('| Return value: | |
');
}
else {
tbl.append('| ' + varname + ' | |
');
}
var curTr = tbl.find('tr:last');
if (isPrimitiveType(val)) {
renderPrimitiveObject(val, curTr.find("td.stackFrameValue"));
}
else {
// add a stub so that we can connect it with a connector later.
// IE needs this div to be NON-EMPTY in order to properly
// render jsPlumb endpoints, so that's why we add an " "!
// make sure varname doesn't contain any weird
// characters that are illegal for CSS ID's ...
var varDivID = divID + '__' + varnameToCssID(varname);
curTr.find("td.stackFrameValue").append('
');
assert(!connectionEndpointIDs.has(varDivID));
connectionEndpointIDs.set(varDivID, 'heap_object_' + getRefID(val));
}
});
}
}
// finally add all the connectors!
connectionEndpointIDs.forEach(function(varID, valueID) {
jsPlumb.connect({source: varID, target: valueID});
});
function highlight_frame(frameID) {
var allConnections = jsPlumb.getConnections();
for (var i = 0; i < allConnections.length; i++) {
var c = allConnections[i];
// this is VERY VERY fragile code, since it assumes that going up
// five layers of parent() calls will get you from the source end
// of the connector to the enclosing stack frame
var stackFrameDiv = c.source.parent().parent().parent().parent().parent();
// if this connector starts in the selected stack frame ...
if (stackFrameDiv.attr('id') == frameID) {
// then HIGHLIGHT IT!
c.setPaintStyle({lineWidth:1, strokeStyle: darkBlue});
c.endpoints[0].setPaintStyle({fillStyle: darkBlue});
c.endpoints[1].setVisible(false, true, true); // JUST set right endpoint to be invisible
$(c.canvas).css("z-index", 1000); // ... and move it to the VERY FRONT
}
// for heap->heap connectors
else if (heapConnectionEndpointIDs.has(c.endpoints[0].elementId)) {
// then HIGHLIGHT IT!
c.setPaintStyle({lineWidth:1, strokeStyle: darkBlue});
c.endpoints[0].setPaintStyle({fillStyle: darkBlue});
c.endpoints[1].setVisible(false, true, true); // JUST set right endpoint to be invisible
$(c.canvas).css("z-index", 1000); // ... and move it to the VERY FRONT
}
else {
// else unhighlight it
c.setPaintStyle({lineWidth:1, strokeStyle: lightGray});
c.endpoints[0].setPaintStyle({fillStyle: lightGray});
c.endpoints[1].setVisible(false, true, true); // JUST set right endpoint to be invisible
$(c.canvas).css("z-index", 0);
}
}
// clear everything, then just activate this one ...
myViz.domRoot.find(".stackFrame").removeClass("highlightedStackFrame");
myViz.domRoot.find('#' + frameID).addClass("highlightedStackFrame");
}
// highlight the top-most non-zombie stack frame or, if not available, globals
var frame_already_highlighted = false;
$.each(curEntry.stack_to_render, function(i, e) {
if (e.is_highlighted) {
highlight_frame('stack' + i);
frame_already_highlighted = true;
}
});
if (!frame_already_highlighted) {
highlight_frame('globals');
}
}
// Utilities
/* colors - see pytutor.css */
var lightYellow = '#F5F798';
var lightLineColor = '#FFFFCC';
var errorColor = '#F87D76';
var visitedLineColor = '#3D58A2';
var lightGray = "#cccccc";
var darkBlue = "#3D58A2";
var medBlue = "#41507A";
var medLightBlue = "#6F89D1";
var lightBlue = "#899CD1";
var pinkish = "#F15149";
var lightPink = "#F89D99";
var darkRed = "#9D1E18";
var breakpointColor = pinkish;
var hoverBreakpointColor = medLightBlue;
function assert(cond) {
// TODO: add more precision in the error message
if (!cond) {
alert("Error: ASSERTION FAILED!!!");
}
}
// taken from http://www.toao.net/32-my-htmlspecialchars-function-for-javascript
function htmlspecialchars(str) {
if (typeof(str) == "string") {
str = str.replace(/&/g, "&"); /* must do & first */
// ignore these for now ...
//str = str.replace(/"/g, """);
//str = str.replace(/'/g, "'");
str = str.replace(//g, ">");
// replace spaces:
str = str.replace(/ /g, " ");
}
return str;
}
String.prototype.rtrim = function() {
return this.replace(/\s*$/g, "");
}
// make sure varname doesn't contain any weird
// characters that are illegal for CSS ID's ...
//
// I know for a fact that iterator tmp variables named '_[1]'
// are NOT legal names for CSS ID's.
// I also threw in '{', '}', '(', ')', '<', '>' as illegal characters.
//
// TODO: what other characters are illegal???
var lbRE = new RegExp('\\[|{|\\(|<', 'g');
var rbRE = new RegExp('\\]|}|\\)|>', 'g');
function varnameToCssID(varname) {
return varname.replace(lbRE, 'LeftB_').replace(rbRE, '_RightB');
}
// compare two JSON-encoded compound objects for structural equivalence:
function structurallyEquivalent(obj1, obj2) {
// punt if either isn't a compound type
if (isPrimitiveType(obj1) || isPrimitiveType(obj2)) {
return false;
}
// must be the same compound type
if (obj1[0] != obj2[0]) {
return false;
}
// must have the same number of elements or fields
if (obj1.length != obj2.length) {
return false;
}
// for a list or tuple, same size (e.g., a cons cell is a list/tuple of size 2)
if (obj1[0] == 'LIST' || obj1[0] == 'TUPLE') {
return true;
}
else {
var startingInd = -1;
if (obj1[0] == 'DICT') {
startingInd = 2;
}
else if (obj1[0] == 'INSTANCE') {
startingInd = 3;
}
else {
return false;
}
var obj1fields = d3.map();
// for a dict or object instance, same names of fields (ordering doesn't matter)
for (var i = startingInd; i < obj1.length; i++) {
obj1fields.set(obj1[i][0], 1); // use as a set
}
for (var i = startingInd; i < obj2.length; i++) {
if (!obj1fields.has(obj2[i][0])) {
return false;
}
}
return true;
}
}
function isPrimitiveType(obj) {
var typ = typeof obj;
return ((obj == null) || (typ != "object"));
}
function getRefID(obj) {
assert(obj[0] == 'REF');
return obj[1];
}
// global initializers (fortunately, jQuery allows multiple $(document).ready calls!)
$(document).ready(function() {
// set some sensible jsPlumb defaults
jsPlumb.Defaults.Endpoint = ["Dot", {radius:3}];
//jsPlumb.Defaults.Endpoint = ["Rectangle", {width:3, height:3}];
jsPlumb.Defaults.EndpointStyle = {fillStyle: lightGray};
jsPlumb.Defaults.Anchors = ["RightMiddle", "LeftMiddle"]; // for aesthetics!
jsPlumb.Defaults.PaintStyle = {lineWidth:1, strokeStyle: lightGray};
// bezier curve style:
//jsPlumb.Defaults.Connector = [ "Bezier", { curviness:15 }]; /* too much 'curviness' causes lines to run together */
//jsPlumb.Defaults.Overlays = [[ "Arrow", { length: 14, width:10, foldback:0.55, location:0.35 }]]
// state machine curve style:
jsPlumb.Defaults.Connector = [ "StateMachine" ];
jsPlumb.Defaults.Overlays = [[ "Arrow", { length: 10, width:7, foldback:0.55, location:1 }]];
jsPlumb.Defaults.EndpointHoverStyle = {fillStyle: pinkish};
jsPlumb.Defaults.HoverPaintStyle = {lineWidth:2, strokeStyle: pinkish};
});