/* Online Python Tutor https://github.com/pgbovine/OnlinePythonTutor/ Copyright (C) Philip J. Guo (philip@pgbovine.net) Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /* To import, put this at the top of your HTML page: */ /* Coding gotchas: - NEVER use raw $(__) or d3.select(__) statements to select DOM elements. ALWAYS use myViz.domRoot or myViz.domRootD3 for jQuery and D3, respectively. Otherwise things will break in weird ways when you have more than one visualization embedded within a webpage, due to multiple matches in the global namespace. - always use generateID to generate unique CSS IDs, or else things will break when multiple ExecutionVisualizer instances are displayed on a webpage */ var SVG_ARROW_POLYGON = '0,3 12,3 12,0 18,5 12,10 12,7 0,7'; var SVG_ARROW_HEIGHT = 10; // must match height of SVG_ARROW_POLYGON var curVisualizerID = 1; // global to uniquely identify each ExecutionVisualizer instance // domRootID is the string ID of the root element where to render this instance // dat is data returned by the Python Tutor backend consisting of two fields: // code - string of executed code // trace - a full execution trace // // params is an object containing optional parameters, such as: // jumpToEnd - if non-null, jump to the very end of execution if // there's no error, or if there's an error, jump to the // FIRST ENTRY with an error // startingInstruction - the (zero-indexed) execution point to display upon rendering // if this is set, then it *overrides* jumpToEnd // hideOutput - hide "Program output" display // codeDivHeight - maximum height of #pyCodeOutputDiv (in integer pixels) // codeDivWidth - maximum width of #pyCodeOutputDiv (in integer pixels) // editCodeBaseURL - the base URL to visit when the user clicks 'Edit code' (if null, then 'Edit code' link hidden) // allowEditAnnotations - allow user to edit per-step annotations (default: false) // embeddedMode - shortcut for allowEditAnnotations=false, // codeDivWidth=this.DEFAULT_EMBEDDED_CODE_DIV_WIDTH, // codeDivHeight=this.DEFAULT_EMBEDDED_CODE_DIV_HEIGHT // (and don't activate keyboard shortcuts!) // disableHeapNesting - if true, then render all heap objects at the top level (i.e., no nested objects) // drawParentPointers - if true, then draw environment diagram parent pointers for all frames // WARNING: there are hard-to-debug MEMORY LEAKS associated with activating this option // textualMemoryLabels - render references using textual memory labels rather than as jsPlumb arrows. // this is good for slow browsers or when used with disableHeapNesting // to prevent "arrow overload" // showOnlyOutputs - show only program outputs and NOT internal data structures // updateOutputCallback - function to call (with 'this' as parameter) // whenever this.updateOutput() is called // (BEFORE rendering the output display) // heightChangeCallback - function to call (with 'this' as parameter) // whenever the HEIGHT of #dataViz changes // verticalStack - if true, then stack code display ON TOP of visualization // (else place side-by-side) // visualizerIdOverride - override visualizer ID instead of auto-assigning it // (BE CAREFUL ABOUT NOT HAVING DUPLICATE IDs ON THE SAME PAGE, // OR ELSE ARROWS AND OTHER STUFF WILL GO HAYWIRE!) // executeCodeWithRawInputFunc - function to call when you want to re-execute the given program // with some new user input (somewhat hacky!) // highlightLines - highlight current and previously executed lines (default: false) // arrowLines - draw arrows pointing to current and previously executed lines (default: true) // compactFuncLabels - render functions with a 'func' prefix and no type label // showAllFrameLabels - display frame and parent frame labels for all functions (default: false) // pyCrazyMode - run with Py2crazy, which provides expression-level // granularity instead of line-level granularity (HIGHLY EXPERIMENTAL!) // hideCode - hide the code display and show only the data structure viz // tabularView - render a tabular view of ALL steps at once (EXPERIMENTAL) // lang - to render labels in a style appropriate for other languages, // and to display the proper language in langDisplayDiv: // 'py2' for Python 2, 'py3' for Python 3, 'js' for JavaScript, 'java' for Java, // 'ts' for TypeScript, 'ruby' for Ruby, 'c' for C, 'cpp' for C++ // [default is Python-style labels] // debugMode - some extra debugging printouts function ExecutionVisualizer(domRootID, dat, params) { this.curInputCode = dat.code.rtrim(); // kill trailing spaces this.curTrace = dat.trace; this.DEFAULT_EMBEDDED_CODE_DIV_WIDTH = 350; this.DEFAULT_EMBEDDED_CODE_DIV_HEIGHT = 400; // if the final entry is raw_input or mouse_input, then trim it from the trace and // set a flag to prompt for user input when execution advances to the // end of the trace if (this.curTrace.length > 0) { var lastEntry = this.curTrace[this.curTrace.length - 1]; if (lastEntry.event == 'raw_input') { this.promptForUserInput = true; this.userInputPromptStr = htmlspecialchars(lastEntry.prompt); this.curTrace.pop() // kill last entry so that it doesn't get displayed } else if (lastEntry.event == 'mouse_input') { this.promptForMouseInput = true; this.userInputPromptStr = htmlspecialchars(lastEntry.prompt); this.curTrace.pop() // kill last entry so that it doesn't get displayed } } this.curInstr = 0; this.params = params; if (!this.params) { this.params = {}; // make it an empty object by default } var arrowLinesDef = (this.params.arrowLines !== undefined); var highlightLinesDef = (this.params.highlightLines !== undefined); if (!arrowLinesDef && !highlightLinesDef) { // neither is set this.params.highlightLines = false; this.params.arrowLines = true; } else if (arrowLinesDef && highlightLinesDef) { // both are set, so just use their set values } else if (arrowLinesDef) { // only arrowLines set this.params.highlightLines = !(this.params.arrowLines); } else { // only highlightLines set this.params.arrowLines = !(this.params.highlightLines); } this.compactFuncLabels = this.params.compactFuncLabels; // audible! if (this.params.pyCrazyMode) { this.params.arrowLines = this.params.highlightLines = false; } if (this.params.visualizerIdOverride) { this.visualizerID = this.params.visualizerIdOverride; } else { // needs to be unique! this.visualizerID = curVisualizerID; curVisualizerID++; } this.leftGutterSvgInitialized = false; this.arrowOffsetY = undefined; this.codeRowHeight = undefined; // avoid 'undefined' state this.disableHeapNesting = (this.params.disableHeapNesting == true); this.drawParentPointers = (this.params.drawParentPointers == true); this.textualMemoryLabels = (this.params.textualMemoryLabels == true); this.showOnlyOutputs = (this.params.showOnlyOutputs == true); this.tabularView = (this.params.tabularView == true); this.showAllFrameLabels = (this.params.showAllFrameLabels == true); this.executeCodeWithRawInputFunc = this.params.executeCodeWithRawInputFunc; // cool, we can create a separate jsPlumb instance for each visualization: this.jsPlumbInstance = jsPlumb.getInstance({ Endpoint: ["Dot", {radius:3}], EndpointStyles: [{fillStyle: connectorBaseColor}, {fillstyle: null} /* make right endpoint invisible */], Anchors: ["RightMiddle", "LeftMiddle"], PaintStyle: {lineWidth:1, strokeStyle: connectorBaseColor}, // bezier curve style: //Connector: [ "Bezier", { curviness:15 }], /* too much 'curviness' causes lines to run together */ //Overlays: [[ "Arrow", { length: 14, width:10, foldback:0.55, location:0.35 }]], // state machine curve style: Connector: [ "StateMachine" ], Overlays: [[ "Arrow", { length: 10, width:7, foldback:0.55, location:1 }]], EndpointHoverStyles: [{fillStyle: connectorHighlightColor}, {fillstyle: null} /* make right endpoint invisible */], HoverPaintStyle: {lineWidth: 1, strokeStyle: connectorHighlightColor}, }); // true iff trace ended prematurely since maximum instruction limit has // been reached var instrLimitReached = false; // the root elements for jQuery and D3 selections, respectively. // ALWAYS use these and never use raw $(__) or d3.select(__) this.domRoot = $('#' + domRootID); this.domRoot.data("vis",this); // bnm store a reference to this as div data for use later. this.domRootD3 = d3.select('#' + domRootID); // stick a new div.ExecutionVisualizer within domRoot and make that // the new domRoot: this.domRoot.html('
'); this.domRoot = this.domRoot.find('div.ExecutionVisualizer'); this.domRootD3 = this.domRootD3.select('div.ExecutionVisualizer'); // initialize in renderPyCodeOutput() this.codeOutputLines = null; this.breakpoints = null; // set of execution points to set as breakpoints this.sortedBreakpointsList = []; // sorted and synced with breakpointLines this.classAttrsHidden = {}; // kludgy hack for 'show/hide attributes' for class objects // API for adding a hook, created by David Pritchard this.pytutor_hooks = {}; // keys, hook names; values, list of functions if (this.params.lang === 'java') { this.activateJavaFrontend(); // ohhhh yeah! } // how many lines does curTrace print to stdout max? this.numStdoutLines = 0; // go backwards from the end ... sometimes the final entry doesn't // have an stdout var lastStdout; for (var i = this.curTrace.length-1; i >= 0; i--) { lastStdout = this.curTrace[i].stdout; if (lastStdout) { break; } } if (lastStdout) { this.numStdoutLines = lastStdout.rtrim().split('\n').length; } this.try_hook("end_constructor", {myViz:this}); this.hasRendered = false; this.render(); // go for it! } /* API for adding a hook, created by David Pritchard https://github.com/daveagp [this documentation is a bit deprecated since Philip made try_hook a method of ExecutionVisualizer, but the general ideas remains] An external user should call add_pytutor_hook("hook_name_here", function(args) {...}) args will be a javascript object with several named properties; this is meant to be similar to Python's keyword arguments. The hooked function should return an array whose first element is a boolean: true if it completely handled the situation (no further hooks nor the base function should be called); false otherwise (wasn't handled). If the hook semantically represents a function that returns something, the second value of the returned array is that semantic return value. E.g. for the Java visualizer a simplified version of a hook we use is: add_pytutor_hook( "isPrimitiveType", function(args) { var obj = args.obj; // unpack if (obj instanceof Array && obj[0] == "CHAR-LITERAL") return [true, true]; // yes we handled it, yes it's primitive return [false]; // didn't handle it, let someone else }); Hook callbacks can return false or undefined (i.e. no return value) in lieu of [false]. NB: If multiple functions are added to a hook, the oldest goes first. */ ExecutionVisualizer.prototype.add_pytutor_hook = function(hook_name, func) { if (this.pytutor_hooks[hook_name]) this.pytutor_hooks[hook_name].push(func); else this.pytutor_hooks[hook_name] = [func]; } /* [this documentation is a bit deprecated since Philip made try_hook a method of ExecutionVisualizer, but the general ideas remains] try_hook(hook_name, args): how the internal codebase invokes a hook. args will be a javascript object with several named properties; this is meant to be similar to Python's keyword arguments. E.g., function isPrimitiveType(obj) { var hook_result = try_hook("isPrimitiveType", {obj:obj}); if (hook_result[0]) return hook_result[1]; // go on as normal if the hook didn't handle it Although add_pytutor_hook allows the hooked function to return false or undefined, try_hook will always return something with the strict format [false], [true] or [true, ...]. */ ExecutionVisualizer.prototype.try_hook = function(hook_name, args) { if (this.pytutor_hooks[hook_name]) { for (var i=0; i| \
\
\
\
Frames \
| \
\
\
\
Objects \
| \
| ' + codeDisplayHTML + ' |
| ' + codeVizHTML + ' |
| ' + codeDisplayHTML + ' | ' + codeVizHTML + ' |
next line to execute
'); myViz.domRootD3.select('svg#prevLegendArrowSVG') .append('polygon') .attr('points', SVG_ARROW_POLYGON) .attr('fill', lightArrowColor); myViz.domRootD3.select('svg#curLegendArrowSVG') .append('polygon') .attr('points', SVG_ARROW_POLYGON) .attr('fill', darkArrowColor); } else if (this.params.highlightLines) { myViz.domRoot.find('#legendDiv') .append('line that has just executed ') .append('next line to execute') } else if (this.params.pyCrazyMode) { myViz.domRoot.find('#legendDiv') .append('Py2crazy mode!') .append(' Stepping through (roughly) each executed expression. Color codes:') .append('expression that just executed| elements
// for simplicity, CLEAR this entire div every time, which totally
// gets rid of the incremental benefits of using d3 for, say,
// transitions or efficient updates. but it provides more
// deterministic and predictable output for other functions. sigh, i'm
// not really using d3 to the fullest, but oh wells!
myViz.domRoot.find('#heap')
.empty()
.html(' Objects ');
var heapRows = myViz.domRootD3.select('#heap')
.selectAll('table.heapRow')
.attr('id', function(d, i){ return 'heapRow' + i; }) // add unique ID
.data(curToplevelLayout, function(objLst) {
return objLst[0]; // return first element, which is the row ID tag
});
// insert new heap rows
heapRows.enter().append('table')
//.each(function(objLst, i) {console.log('NEW ROW:', objLst, i);})
.attr('id', function(d, i){ return 'heapRow' + i; }) // add unique ID
.attr('class', 'heapRow');
// delete a heap row
var hrExit = heapRows.exit();
hrExit
.each(function(d, idx) {
//console.log('DEL ROW:', d, idx);
$(this).empty(); // crucial for garbage collecting jsPlumb connectors!
})
.remove();
// update an existing heap row
var toplevelHeapObjects = heapRows
//.each(function(objLst, i) { console.log('UPDATE ROW:', objLst, i); })
.selectAll('td.toplevelHeapObject')
.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 */);
// insert a new toplevelHeapObject
var tlhEnter = toplevelHeapObjects.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 a toplevelHeapObject
toplevelHeapObjects
.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();
if (myViz.isCppMode()) {
// TODO: why might this be undefined?!? because the object
// disappeared from the heap all of a sudden?!?
if (curEntry.heap[objID] !== undefined) {
myViz.renderCompoundObject(objID, myViz.curInstr, $(this), true);
}
} else {
myViz.renderCompoundObject(objID, myViz.curInstr, $(this), true);
}
});
// delete a toplevelHeapObject
var tlhExit = toplevelHeapObjects.exit();
tlhExit
.each(function(d, idx) {
$(this).empty(); // crucial for garbage collecting jsPlumb connectors!
})
.remove();
// Render globals and then stack frames using d3:
// TODO: this sometimes seems buggy on Safari, so nix it for now:
function highlightAliasedConnectors(d, i) {
// if this row contains a stack pointer, then highlight its arrow and
// ALL aliases that also point to the same heap object
var stackPtrId = $(this).find('div.stack_pointer').attr('id');
if (stackPtrId) {
var foundTargetId = null;
myViz.jsPlumbInstance.select({source: stackPtrId}).each(function(c) {foundTargetId = c.targetId;});
// use foundTargetId to highlight ALL ALIASES
myViz.jsPlumbInstance.select().each(function(c) {
if (c.targetId == foundTargetId) {
c.setHover(true);
$(c.canvas).css("z-index", 2000); // ... and move it to the VERY FRONT
}
else {
c.setHover(false);
}
});
}
}
function unhighlightAllConnectors(d, i) {
myViz.jsPlumbInstance.select().each(function(c) {
c.setHover(false);
});
}
// TODO: coalesce code for rendering globals and stack frames,
// since there's so much copy-and-paste grossness right now
// render all global variables IN THE ORDER they were created by the program,
// in order to ensure continuity:
// Derive a list where each element contains varname
// as long as value is NOT undefined.
// (Sometimes entries in curEntry.ordered_globals are undefined,
// so filter those out.)
var realGlobalsLst = [];
$.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!
realGlobalsLst.push(varname);
}
});
var globalsID = myViz.generateID('globals');
var globalTblID = myViz.generateID('global_table');
var globalVarTable = myViz.domRootD3.select('#' + globalTblID)
.selectAll('tr')
.data(realGlobalsLst,
function(d) {return d;} // use variable name as key
);
globalVarTable
.enter()
.append('tr')
.attr('class', 'variableTr')
.attr('id', function(d, i) {
return myViz.generateID(varnameToCssID('global__' + d + '_tr')); // make globally unique (within the page)
});
var globalVarTableCells = globalVarTable
.selectAll('td.stackFrameVar,td.stackFrameValue')
.data(function(d, i){return [d, d];}) /* map varname down both columns */
globalVarTableCells.enter()
.append('td')
.attr('class', function(d, i) {return (i == 0) ? 'stackFrameVar' : 'stackFrameValue';});
// remember that the enter selection is added to the update
// selection so that we can process it later ...
// UPDATE
globalVarTableCells
.order() // VERY IMPORTANT to put in the order corresponding to data elements
.each(function(varname, i) {
if (i == 0) {
$(this).html(varname);
}
else {
// always delete and re-render the global var ...
// NB: trying to cache and compare the old value using,
// say -- $(this).attr('data-curvalue', valStringRepr) -- leads to
// a mysterious and killer memory leak that I can't figure out yet
$(this).empty();
// make sure varname doesn't contain any weird
// characters that are illegal for CSS ID's ...
var varDivID = myViz.generateID('global__' + varnameToCssID(varname));
// need to get rid of the old connector in preparation for rendering a new one:
existingConnectionEndpointIDs.remove(varDivID);
var val = curEntry.globals[varname];
if (myViz.isPrimitiveType(val)) {
myViz.renderPrimitiveObject(val, $(this));
}
else if (val[0] === 'C_STRUCT' || val[0] === 'C_ARRAY') {
// C structs and arrays can be inlined in frames
myViz.renderCStructArray(val, myViz.curInstr, $(this));
}
else {
var heapObjID = myViz.generateHeapObjID(getRefID(val), myViz.curInstr);
if (myViz.textualMemoryLabels) {
var labelID = varDivID + '_text_label';
$(this).append('id' + getRefID(val) + ' ');
$(this).find('div#' + labelID).hover(
function() {
myViz.jsPlumbInstance.connect({source: labelID, target: heapObjID,
scope: 'varValuePointer'});
},
function() {
myViz.jsPlumbInstance.select({source: labelID}).detach();
});
}
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 " "!
$(this).append(''); var headerLabel = funcName; // only display if you're someone's parent (unless showAllFrameLabels) if (frame.is_parent || myViz.showAllFrameLabels) { headerLabel = 'f' + frame.frame_id + ': ' + headerLabel; } // optional (btw, this isn't a CSS id) if (frame.parent_frame_id_list.length > 0) { var parentFrameID = frame.parent_frame_id_list[0]; headerLabel = headerLabel + ' [parent=f' + parentFrameID + ']'; } else if (myViz.showAllFrameLabels) { headerLabel = headerLabel + ' [parent=Global]'; } return headerLabel; }); sfdEnter .append('table') .attr('class', 'stackFrameVarTable'); var stackVarTable = stackFrameDiv .order() // VERY IMPORTANT to put in the order corresponding to data elements .select('table').selectAll('tr') .data(function(frame) { // each list element contains a reference to the entire frame // object as well as the variable name // TODO: look into whether we can use d3 parent nodes to avoid // this hack ... http://bost.ocks.org/mike/nest/ return frame.ordered_varnames.map(function(varname) {return {varname:varname, frame:frame};}); }, function(d) { // TODO: why would d ever be null?!? weird if (d) { return d.varname; // use variable name as key } } ); stackVarTable .enter() .append('tr') .attr('class', 'variableTr') .attr('id', function(d, i) { return myViz.generateID(varnameToCssID(d.frame.unique_hash + '__' + d.varname + '_tr')); // make globally unique (within the page) }); var stackVarTableCells = stackVarTable .selectAll('td.stackFrameVar,td.stackFrameValue') .data(function(d, i) {return [d, d] /* map identical data down both columns */;}); stackVarTableCells.enter() .append('td') .attr('class', function(d, i) {return (i == 0) ? 'stackFrameVar' : 'stackFrameValue';}); stackVarTableCells .order() // VERY IMPORTANT to put in the order corresponding to data elements .each(function(d, i) { var varname = d.varname; var frame = d.frame; if (i == 0) { if (varname == '__return__') $(this).html('Return value'); else $(this).html(varname); } else { // always delete and re-render the stack var ... // NB: trying to cache and compare the old value using, // say -- $(this).attr('data-curvalue', valStringRepr) -- leads to // a mysterious and killer memory leak that I can't figure out yet $(this).empty(); // make sure varname and frame.unique_hash don't contain any weird // characters that are illegal for CSS ID's ... var varDivID = myViz.generateID(varnameToCssID(frame.unique_hash + '__' + varname)); // need to get rid of the old connector in preparation for rendering a new one: existingConnectionEndpointIDs.remove(varDivID); var val = frame.encoded_locals[varname]; if (myViz.isPrimitiveType(val)) { myViz.renderPrimitiveObject(val, $(this)); } else if (val[0] === 'C_STRUCT' || val[0] === 'C_ARRAY') { // C structs and arrays can be inlined in frames myViz.renderCStructArray(val, myViz.curInstr, $(this)); } else { var heapObjID = myViz.generateHeapObjID(getRefID(val), myViz.curInstr); if (myViz.textualMemoryLabels) { var labelID = varDivID + '_text_label'; $(this).append(' id' + getRefID(val) + ' ');
$(this).find('div#' + labelID).hover(
function() {
myViz.jsPlumbInstance.connect({source: labelID, target: heapObjID,
scope: 'varValuePointer'});
},
function() {
myViz.jsPlumbInstance.select({source: labelID}).detach();
});
}
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 " "!
$(this).append('literalStr = literalStr.replace(new RegExp('\n', 'g'), ' '); // replace ALL literalStr = literalStr.replace(new RegExp('\"', 'g'), '\\"'); // replace ALL literalStr = '"' + literalStr + '"'; d3DomElement.append('' + literalStr + ''); } else if (typ == "object") { if (obj[0] == 'C_DATA') { var typeName = obj[2]; // special cases for brevity: if (typeName === 'short int') { typeName = 'short'; } else if (typeName === 'short unsigned int') { typeName = 'unsigned short'; } else if (typeName === 'long int') { typeName = 'long'; } else if (typeName === 'long long int') { typeName = 'long long'; } else if (typeName === 'long unsigned int') { typeName = 'unsigned long'; } else if (typeName === 'long long unsigned int') { typeName = 'unsigned long long int'; } var isValidPtr = ((typeName === 'pointer') && (obj[3] !== ' '; // prepend address } // prefix with 'cdata_' so that we can distinguish this from a // top-level heap ID generated by generateHeapObjID var cdataId = myViz.generateHeapObjID('cdata_' + addr, myViz.curInstr); if (isValidPtr) { // for pointers, put cdataId in the header d3DomElement.append(' ' + leader + typeName + ' ');
var ptrVal = obj[3];
// 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 ptrSrcId = myViz.generateHeapObjID('ptrSrc_' + addr, myViz.curInstr);
var ptrTargetId = myViz.generateHeapObjID('cdata_' + ptrVal, myViz.curInstr); // don't forget cdata_ prefix!
var debugInfo = '';
if (myViz.debugMode) {
debugInfo = ptrTargetId;
}
// make it really narrow so that the div doesn't STRETCH too wide
d3DomElement.append(' ' + debugInfo + ' ');
assert(!myViz.jsPlumbManager.connectionEndpointIDs.has(ptrSrcId));
myViz.jsPlumbManager.connectionEndpointIDs.set(ptrSrcId, ptrTargetId);
//console.log(ptrSrcId, '->', ptrTargetId);
} else {
// for non-pointers, put cdataId on the element itself, so that
// pointers can point directly at the element, not the header
d3DomElement.append('' + leader + typeName + ' ');
var rep = '';
if (typeof obj[3] === 'string') {
var literalStr = obj[3];
if (literalStr === '' + rep + ' ');
}
} else {
assert(obj[0] == 'SPECIAL_FLOAT' || obj[0] == 'JS_SPECIAL_VAL');
d3DomElement.append('' + obj[1] + '');
}
}
else {
assert(false);
}
}
ExecutionVisualizer.prototype.renderNestedObject = function(obj, stepNum, d3DomElement) {
if (this.isPrimitiveType(obj)) {
this.renderPrimitiveObject(obj, d3DomElement);
}
else {
if (obj[0] === 'REF') {
// obj is a ["REF", id' + objID + ' ');
myViz.domRoot.find('div#' + labelID).hover(
function() {
myViz.jsPlumbInstance.connect({source: labelID, target: dstDivID,
scope: 'varValuePointer'});
},
function() {
myViz.jsPlumbInstance.select({source: labelID}).detach();
});
}
else {
// 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 " "!
d3DomElement.append('' + typeLabelPrefix + ' empty ' + myViz.getRealLabel(label) + ' ');
}
else {
d3DomElement.append('' + typeLabelPrefix + myViz.getRealLabel(label) + ' ');
d3DomElement.append('');
headerTr.find('td:last').append(ind - 1);
contentTr.append(' | ');
myViz.renderNestedObject(val, stepNum, 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(' | ');
myViz.renderNestedObject(val, stepNum, curTr.find('td:last'));
});
}
else if (obj[0] == 'DICT') {
$.each(obj, function(ind, kvPair) {
if (ind < 1) return; // skip 'DICT' tag
tbl.append(' | ' + typeLabelPrefix + obj[1] + ' ' + myViz.getRealLabel('instance') + ' ');
}
else {
var superclassStr = '';
if (obj[2].length > 0) {
superclassStr += ('[extends ' + obj[2].join(', ') + '] ');
}
d3DomElement.append('' + typeLabelPrefix + obj[1] + ' class ' + superclassStr +
' ');
}
// right now, let's NOT display class members, since that clutters
// up the display too much. in the future, consider displaying
// class members in a pop-up pane on mouseover or mouseclick
// actually nix what i just said above ...
//if (!isInstance) return;
if (obj.length > headerLength) {
var lab = isInstance ? 'inst' : 'class';
d3DomElement.append('' + 'hide attributes' + ' ' + typeLabelPrefix + obj[1] + ' instance ');
strRepr = htmlspecialchars(obj[2]); // escape strings!
d3DomElement.append('
' + typeLabelPrefix + myViz.getRealLabel('function') + ' ');
}
var funcPrefix = myViz.compactFuncLabels ? 'func' : '';
if (parentFrameID) {
d3DomElement.append('' + funcPrefix + ' ' + funcName + ' [parent=f'+ parentFrameID + '] ');
}
else if (myViz.showAllFrameLabels) {
d3DomElement.append('' + funcPrefix + ' ' + funcName + ' [parent=Global] ');
}
else {
d3DomElement.append('' + funcPrefix + ' ' + funcName + ' ');
}
}
else if (obj[0] == 'JS_FUNCTION') { /* TODO: refactor me */
// JavaScript function
assert(obj.length == 5);
var funcName = htmlspecialchars(obj[1]);
var funcCode = typeLabelPrefix + htmlspecialchars(obj[2]);
var funcProperties = obj[3]; // either null or a non-empty list of key-value pairs
var parentFrameID = obj[4];
if (funcProperties || parentFrameID || myViz.showAllFrameLabels) {
d3DomElement.append('' + funcCode + '' + ' parent | ' + 'f' + parentFrameID + ' | parent | ' + 'global' + ' | ' + funcCode + ''); } } else if (obj[0] == 'HEAP_PRIMITIVE') { assert(obj.length == 3); var typeName = obj[1]; var primitiveVal = obj[2]; // add a bit of padding to heap primitives, for aesthetics d3DomElement.append(''); d3DomElement.find('div.heapPrimitive').append(' ' + typeLabelPrefix + typeName + ' ');
myViz.renderPrimitiveObject(primitiveVal, d3DomElement.find('div.heapPrimitive'));
}
else if (obj[0] == 'C_STRUCT' || obj[0] == 'C_ARRAY') {
myViz.renderCStructArray(obj, stepNum, d3DomElement);
}
else {
// render custom data type
assert(obj.length == 2);
var typeName = obj[0];
var strRepr = obj[1];
strRepr = htmlspecialchars(strRepr); // escape strings!
d3DomElement.append('' + typeLabelPrefix + typeName + ' ');
d3DomElement.append('
'; } if (myViz.params.lang === 'cpp') { // call it 'object' instead of 'struct' d3DomElement.append(' ' + leader + 'object ' + typename + ' ');
} else {
d3DomElement.append('' + leader + 'struct ' + typename + ' ');
}
if (obj.length > 3) {
d3DomElement.append(''; } d3DomElement.append(' ' + leader + 'array ');
d3DomElement.append('');
headerTr.find('td:last').append(ind - 2 /* adjust */);
contentTr.append(' | ');
myViz.renderNestedObject(val, stepNum, contentTr.find('td:last'));
});
}
}
ExecutionVisualizer.prototype.redrawConnectors = function() {
this.jsPlumbInstance.repaintEverything();
}
ExecutionVisualizer.prototype.getRealLabel = function(label) {
if (this.params.lang === 'js' || this.params.lang === 'ts' || this.params.lang === 'ruby') {
if (label === 'list') {
return 'array';
} else if (label === 'instance') {
return 'object';
} else if (label === 'True') {
return 'true';
} else if (label === 'False') {
return 'false';
}
}
if (this.params.lang === 'js') {
if (label === 'dict') {
return 'Map';
} else if (label === 'set') {
return 'Set';
}
}
if (this.params.lang === 'ruby') {
if (label === 'dict') {
return 'hash';
} else if (label === 'set') {
return 'Set'; // the Ruby Set class is capitalized
} else if (label === 'function') {
return 'method';
} else if (label === 'None') {
return 'nil';
} else if (label === 'Global frame') {
return 'Global Object';
}
}
// default fallthrough case
return label;
};
// Utilities
/* colors - see pytutor.css for more colors */
var highlightedLineColor = '#e4faeb';
var highlightedLineBorderColor = '#005583';
var highlightedLineLighterColor = '#e8fff0';
var funcCallLineColor = '#a2eebd';
var brightRed = '#e93f34';
var connectorBaseColor = '#005583';
var connectorHighlightColor = brightRed;
var connectorInactiveColor = '#cccccc';
var errorColor = brightRed;
var breakpointColor = brightRed;
// Unicode arrow types: '\u21d2', '\u21f0', '\u2907'
var darkArrowColor = brightRed;
var lightArrowColor = '#c9e6ca';
function assert(cond) {
if (!cond) {
alert("Assertion Failure (see console log for backtrace)");
throw 'Assertion Failure';
}
}
// 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, " ");
// replace tab as four spaces:
str = str.replace(/\t/g, " ");
}
return str;
}
// same as htmlspecialchars except don't worry about expanding spaces or
// tabs since we want proper word wrapping in divs.
function htmlsanitize(str) {
if (typeof(str) == "string") {
str = str.replace(/&/g, "&"); /* must do & first */
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.
//
// also some variable names are like '.0' (for generator expressions),
// and '.' seems to be illegal.
//
// also '=', '!', and '?' are common in Ruby names, so escape those as well
//
// also spaces are illegal, so convert to '_'
// TODO: what other characters are illegal???
var lbRE = new RegExp('\\[|{|\\(|<', 'g');
var rbRE = new RegExp('\\]|}|\\)|>', 'g');
function varnameToCssID(varname) {
// make sure to REPLACE ALL (using the 'g' option)
// rather than just replacing the first entry
return varname.replace(lbRE, 'LeftB_')
.replace(rbRE, '_RightB')
.replace(/[!]/g, '_BANG_')
.replace(/[?]/g, '_QUES_')
.replace(/[:]/g, '_COLON_')
.replace(/[=]/g, '_EQ_')
.replace(/[.]/g, '_DOT_')
.replace(/ /g, '_');
}
// compare two JSON-encoded compound objects for structural equivalence:
ExecutionVisualizer.prototype.structurallyEquivalent = function(obj1, obj2) {
// punt if either isn't a compound type
if (this.isPrimitiveType(obj1) || this.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; // punt on all other types
}
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;
}
}
ExecutionVisualizer.prototype.isPrimitiveType = function(obj) {
var hook_result = this.try_hook("isPrimitiveType", {obj:obj});
if (hook_result[0]) return hook_result[1];
// null is a primitive
if (obj === null) {
return true;
}
if (typeof obj == "object") {
// kludge: only 'SPECIAL_FLOAT' objects count as primitives
return (obj[0] == 'SPECIAL_FLOAT' || obj[0] == 'JS_SPECIAL_VAL' ||
obj[0] == 'C_DATA' /* TODO: is this right?!? */);
}
else {
// non-objects are primitives
return true;
}
}
function isHeapRef(obj, heap) {
// ordinary REF
if (obj[0] === 'REF') {
return (heap[obj[1]] !== undefined);
} else if (obj[0] === 'C_DATA' && obj[2] === 'pointer') {
// C-style pointer that has a valid value
if (obj[3] != ' | ' + typeLabelPrefix + visibleLabel + ' ');
d3DomElement.append(' | ');
headerTr.find('td:last').append(elide ? "…" : ind);
contentTr.append(''); if (!elide) { myViz.renderNestedObject(val, stepNum, contentTr.find('td:last')); ind++; } else { contentTr.find('td:last').append("…"); ind += val[1]; // val[1] is the number of cells to skip } } } // end of LIST handling // Stack and Queue handling code by Will Gwozdz /* The table produced for stacks and queues is formed slightly differently than the others, missing the header row. Two rows made the dashed border not line up properly */ if (obj[0] == 'STACK') { tbl.append(' |
| '+'↔'+' | '); $.each(obj, function(ind, val) { if (ind < 1) return; // skip type tag and ID entry contentTr.append(''); myViz.renderNestedObject(val, stepNum, contentTr.find('td:last')); }); contentTr.append(' | '+' | '); } if (obj[0] == 'QUEUE') { tbl.append('
| '+'← | '); $.each(obj, function(ind, val) { if (ind < 1) return; // skip type tag and ID entry contentTr.append(''); myViz.renderNestedObject(val, stepNum, contentTr.find('td:last')); }); contentTr.append(' | '+'← | '); } return [true]; // did handle }); this.add_pytutor_hook( "end_renderDataStructures", function(args) { var myViz = args.myViz; myViz.domRoot.find("td.instKey:contains('___NO_LABEL!___')").hide(); myViz.domRoot.find(".typeLabel:contains('dict')").each( function(i) { if ($(this).html()=='dict') $(this).html('symbol table'); if ($(this).html()=='empty dict') $(this).html('empty symbol table'); }); }); // java synthetics cause things which javascript doesn't like in an id // VERY important to bind(this) so that when it's called, 'this' is this current object var old_generateID = ExecutionVisualizer.prototype.generateID.bind(this); this.generateID = function(original_id) { var sanitized = original_id.replace( /[^0-9a-zA-Z_]/g, function(match) {return '-'+match.charCodeAt(0)+'-';} ); return old_generateID(sanitized); } // utility functions var entityMap = { "&": "&", "<": "<", ">": ">", '"': '"', "'": ''', "/": '/' }; var escapeHtml = function(string) { return String(string).replace(/[&<>"'\/]/g, function (s) { return entityMap[s]; }); }; }