/* Online Python Tutor Copyright (C) 2010 Philip J. Guo (philip@pgbovine.net) https://github.com/pgbovine/OnlinePythonTutor/ This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ // The Online Python Tutor front-end, which calls the cgi-bin/web_exec.py // back-end with a string representing the user's script POST['user_script'] // and receives a complete execution trace, which it parses and displays to HTML. var PythonTutor; if (! PythonTutor) { PythonTutor = {}; } (function () { function Visualizer(source_code, trace_data, divid) { this.stdOutElement = "#pyStdout_" + divid; this.warnOutElement = "#warningOutput_" + divid; this.errorOutputElement = "#errorOutput_" + divid; this.vcrControlsDiv = "#vcrControls_" + divid; this.dataVisElement = "#dataViz_" + divid; this.outputPaneTable = "#pyOutputPane_" + divid; this.codeOutputTable = "#pyCodeOutput_" + divid; this.curTrace = null; this.curInstr = 0; this.divid = divid; this.jmpFirstInstr = $("#jmpFirstInstr_"+divid); this.jmpLastInstr = $("#jmpLastInstr_"+divid); this.jmpStepBack = $("#jmpStepBack_"+divid); this.jmpStepFwd = $("#jmpStepFwd_"+divid); this.renderPyCodeOutput(source_code); this.processTrace(trace_data); $(this.outputPaneTable).show(); var myvis = this; this.jmpFirstInstr.click(function() { myvis.curInstr = 0; myvis.updateOutput(); logBookEvent({'event':'codelens', 'act': 'first', 'div_id':myvis.divid}); // Log the run event }); this.jmpLastInstr.click(function() { myvis.curInstr = myvis.curTrace.length - 1; myvis.updateOutput(); logBookEvent({'event':'codelens', 'act':'last', 'div_id': myvis.divid}); // Log the run event }); this.jmpStepBack.click(function() { if (myvis.curInstr > 0) { myvis.curInstr -= 1; myvis.updateOutput(); logBookEvent({'event':'codelens', 'act': 'back', 'div_id': myvis.divid}); // Log the run event } }); this.jmpStepFwd.click(function() { if (myvis.curInstr < myvis.curTrace.length - 1) { myvis.curInstr += 1; myvis.updateOutput(); logBookEvent({'event':'codelens', 'act':'fwd', 'div_id':myvis.divid}); // Log the run event } }); } Visualizer.prototype.processTrace = function(traceData) { this.curTrace = traceData; this.curInstr = 0; // delete all stale output $(this.warnOutElement).html(''); $(this.stdOutElement).val(''); if (this.curTrace.length > 0) { var lastEntry = this.curTrace[this.curTrace.length - 1]; // if there is some sort of error, then JUMP to it so that we can // immediately alert the user: // (cgi-bin/pg_logger.py ensures that if there is an uncaught // exception, then that exception event will be the FINAL // entry in this.curTrace. a caught exception will appear somewhere in // the MIDDLE of this.curTrace) // // on second thought, let's hold off on that for now /* if (lastEntry.event == 'exception' || lastEntry.event == 'uncaught_exception') { // updateOutput should take care of the rest ... curInstr = this.curTrace.length - 1; } */ if (lastEntry.event == 'instruction_limit_reached') { this.curTrace.pop() // kill last entry var warningMsg = lastEntry.exception_msg; $(this.warnOutElement).html(htmlspecialchars(warningMsg)); } // as imran suggests, for a (non-error) one-liner, SNIP off the // first instruction so that we start after the FIRST instruction // has been executed ... else if (this.curTrace.length == 2) { this.curTrace.shift(); } } this.updateOutput(); } Visualizer.prototype.highlightCodeLine = function(curLine, visitedLinesSet, hasError) { var tbl = $(this.codeOutputTable); /* colors - see edu-python.css */ var lightYellow = '#F5F798'; var lightLineColor = '#FFFFCC'; var errorColor = '#F87D76'; var visitedLineColor = '#3D58A2'; // reset then set: tbl.find('td.lineNo').css('color', ''); tbl.find('td.lineNo').css('font-weight', ''); $.each(visitedLinesSet, function(k, v) { tbl.find('td.lineNo:eq(' + (k - 1) + ')').css('color', visitedLineColor); tbl.find('td.lineNo:eq(' + (k - 1) + ')').css('font-weight', 'bold'); }); var lineBgCol = lightLineColor; if (hasError) { lineBgCol = errorColor; } tbl.find('td.cod').css('border-bottom', '1px solid #ffffff'); if (!hasError) { tbl.find('td.cod:eq(' + (curLine - 1) + ')').css('border-bottom', '1px solid #F87D76') } tbl.find('td.cod').css('background-color', ''); tbl.find('td.cod:eq(' + (curLine - 1) + ')').css('background-color', lineBgCol); } // relies on this.curTrace and curInstr globals Visualizer.prototype.updateOutput = function() { var curEntry = this.curTrace[this.curInstr]; var hasError = false; // render VCR controls: var totalInstrs = this.curTrace.length var vcr = $(this.vcrControlsDiv); vcr.find("#curInstr_"+this.divid).html(this.curInstr + 1); vcr.find("#totalInstrs_"+this.divid).html(totalInstrs); this.jmpFirstInstr.attr("disabled", false); this.jmpStepBack.attr("disabled", false); this.jmpStepFwd.attr("disabled", false); this.jmpLastInstr.attr("disabled", false); if (this.curInstr == 0) { this.jmpFirstInstr.attr("disabled", true); this.jmpStepBack.attr("disabled", true); } if (this.curInstr == (totalInstrs - 1)) { this.jmpLastInstr.attr("disabled", true); this.jmpStepFwd.attr("disabled", true); } // render error (if applicable): if (curEntry.event == 'exception' || curEntry.event == 'uncaught_exception') { assert(curEntry.exception_msg); if (curEntry.exception_msg == "Unknown error") { $(this.errorOutputElement).html('Unknown error: view code and please
email as a bug report to philip@pgbovine.net'); } else { $(this.errorOutputElement).html(htmlspecialchars(curEntry.exception_msg)); } $(this.errorOutputElement).show(); hasError = true; } else { $(this.errorOutputElement).hide(); } // render code output: if (curEntry.line) { // calculate all lines that have been 'visited' // by execution up to (but NOT INCLUDING) curInstr: var visitedLinesSet = {} for (var i = 0; i < this.curInstr; i++) { if (this.curTrace[i].line) { visitedLinesSet[this.curTrace[i].line] = true; } } this.highlightCodeLine(curEntry.line, visitedLinesSet, hasError); } // render stdout: // keep original horizontal scroll level: // var oldLeft = $(this.stdOutElement).scrollLeft(); // $(this.stdOutElement).val(curEntry.stdout); // $(this.stdOutElement).scrollLeft(oldLeft); // // scroll to bottom, tho: // $(this.stdOutElement).scrollTop($(this.stdOutElement).attr('scrollHeight')); $(this.stdOutElement).text(curEntry.stdout); // render data structures: $(this.dataVisElement).html(''); // CLEAR IT! // render locals on stack: if (curEntry.stack_locals != undefined) { var self = this; $.each(curEntry.stack_locals, function (i, frame) { var funcName = htmlspecialchars(frame[0]); // might contain '<' or '>' for weird names like var localVars = frame[1]; $(self.dataVisElement).append('
Local variables for ' + funcName + ':
'); // render locals in alphabetical order for tidiness: var orderedVarnames = []; $.each(localVars, function(varname, val) { orderedVarnames.push(varname); }); orderedVarnames.sort(); if (orderedVarnames.length > 0) { $(self.dataVisElement + " .vizFrame:last").append('
'); var tbl = $(self.outputPaneTable + " table:last"); $.each(orderedVarnames, function(i, varname) { var val = localVars[varname]; tbl.append(''); var curTr = tbl.find('tr:last'); if (varname == '__return__') { curTr.find("td.varname").html('return value'); } else { curTr.find("td.varname").html(varname); } self.renderData(val, curTr.find("td.val")); }); tbl.find("tr:last").find("td.varname").css('border-bottom', '0px'); tbl.find("tr:last").find("td.val").css('border-bottom', '0px'); } else { $(this.dataVisElement + " .vizFrame:last").append(' none'); } }); } // render globals LAST: $(this.dataVisElement).append('
Global variables:
'); var nonEmptyGlobals = false; var curGlobalFields = {}; if (curEntry.globals != undefined) { $.each(curEntry.globals, function(varname, val) { curGlobalFields[varname] = true; nonEmptyGlobals = true; }); } if (nonEmptyGlobals) { $(this.dataVisElement + " .vizFrame:last").append('
'); // render all global variables IN THE ORDER they were created by the program, // in order to ensure continuity: var orderedGlobals = [] // iterating over ALL instructions (could be SLOW if not for our optimization below) for (var i = 0; i <= this.curInstr; i++) { // some entries (like for exceptions) don't have GLOBALS if (this.curTrace[i].globals == undefined) continue; $.each(this.curTrace[i].globals, function(varname, val) { // eliminate duplicates (act as an ordered set) if ($.inArray(varname, orderedGlobals) == -1) { orderedGlobals.push(varname); curGlobalFields[varname] = undefined; // 'unset it' } }); var earlyStop = true; // as an optimization, STOP as soon as you've found everything in curGlobalFields: for (o in curGlobalFields) { if (curGlobalFields[o] != undefined) { earlyStop = false; break; } } if (earlyStop) { break; } } var tbl = $(this.outputPaneTable + " table:last"); var self = this; // iterate IN ORDER (it's possible that not all vars are in curEntry.globals) $.each(orderedGlobals, function(i, varname) { var val = curEntry.globals[varname]; if (val != undefined) { // might not be defined at this line, which is OKAY! tbl.append(''); var curTr = tbl.find('tr:last'); curTr.find("td.varname").html(varname); self.renderData(val, curTr.find("td.val")); } }); tbl.find("tr:last").find("td.varname").css('border-bottom', '0px'); tbl.find("tr:last").find("td.val").css('border-bottom', '0px'); } else { $(this.dataVisElement + " .vizFrame:last").append(' none'); } } // render the JS data object obj inside of jDomElt, // which is a jQuery wrapped DOM object // (obj is in a format encoded by cgi-bin/pg_encoder.py) Visualizer.prototype.renderData = function(obj, jDomElt) { // dispatch on types: var typ = typeof obj; if (obj == null) { jDomElt.append('None'); } else if (typ == "number") { jDomElt.append('' + obj + ''); } else if (typ == "boolean") { if (obj) { jDomElt.append('True'); } else { jDomElt.append('False'); } } else if (typ == "string") { // escape using htmlspecialchars to prevent HTML/script injection // print as a JSON literal var literalStr = htmlspecialchars(obj); literalStr = literalStr.replace('\"', '\\"'); literalStr = '"' + literalStr + '"'; jDomElt.append('' + literalStr + ''); } else if (typ == "object") { assert($.isArray(obj)); if (obj[0] == 'LIST') { assert(obj.length >= 2); if (obj.length == 2) { jDomElt.append('
empty list (id=' + obj[1] + ')
'); } else { jDomElt.append('
list (id=' + obj[1] + '):
'); jDomElt.append('
'); var tbl = jDomElt.children('table'); var headerTr = tbl.find('tr:first'); var contentTr = tbl.find('tr:last'); var self = this; jQuery.each(obj, function(ind, val) { if (ind < 2) return; // skip 'LIST' tag and ID entry // add a new column and then pass in that newly-added column // as jDomElt to the recursive call to child: headerTr.append(''); headerTr.find('td:last').append(ind - 2); contentTr.append(''); self.renderData(val, contentTr.find('td:last')); }); } } else if (obj[0] == 'TUPLE') { assert(obj.length >= 2); if (obj.length == 2) { jDomElt.append('
empty tuple (id=' + obj[1] + ')
'); } else { jDomElt.append('
tuple (id=' + obj[1] + '):
'); jDomElt.append('
'); var tbl = jDomElt.children('table'); var headerTr = tbl.find('tr:first'); var contentTr = tbl.find('tr:last'); var self = this; jQuery.each(obj, function(ind, val) { if (ind < 2) return; // skip 'TUPLE' tag and ID entry // add a new column and then pass in that newly-added column // as jDomElt to the recursive call to child: headerTr.append(''); headerTr.find('td:last').append(ind - 2); contentTr.append(''); self.renderData(val, contentTr.find('td:last')); }); } } else if (obj[0] == 'SET') { assert(obj.length >= 2); if (obj.length == 2) { jDomElt.append('
empty set (id=' + obj[1] + ')
'); } else { jDomElt.append('
set (id=' + obj[1] + '):
'); jDomElt.append('
'); var tbl = jDomElt.children('table'); // create an R x C matrix: var numElts = obj.length - 2; // 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; } var self = this; jQuery.each(obj, function(ind, val) { if (ind < 2) return; // skip 'SET' tag and ID entry if (((ind - 2) % numCols) == 0) { tbl.append(''); } var curTr = tbl.find('tr:last'); curTr.append(''); self.renderData(val, curTr.find('td:last')); }); } } else if (obj[0] == 'DICT') { assert(obj.length >= 2); if (obj.length == 2) { jDomElt.append('
empty dict (id=' + obj[1] + ')
'); } else { jDomElt.append('
dict (id=' + obj[1] + '):
'); jDomElt.append('
'); var tbl = jDomElt.children('table'); var self = this; $.each(obj, function(ind, kvPair) { if (ind < 2) return; // skip 'DICT' tag and ID entry tbl.append(''); var newRow = tbl.find('tr:last'); var keyTd = newRow.find('td:first'); var valTd = newRow.find('td:last'); self.renderData(kvPair[0], keyTd); self.renderData(kvPair[1], valTd); }); } } else if (obj[0] == 'INSTANCE') { assert(obj.length >= 3); jDomElt.append('
' + obj[1] + ' instance (id=' + obj[2] + ')
'); if (obj.length > 3) { jDomElt.append('
'); var tbl = jDomElt.children('table'); var self = this; $.each(obj, function(ind, kvPair) { if (ind < 3) return; // skip type tag, class name, and ID entry 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: self.renderData(kvPair[1], valTd); }); } } else if (obj[0] == 'CLASS') { assert(obj.length >= 4); var superclassStr = ''; if (obj[3].length > 0) { superclassStr += ('[extends ' + obj[3].join(',') + '] '); } jDomElt.append('
' + obj[1] + ' class ' + superclassStr + '(id=' + obj[2] + ')
'); if (obj.length > 4) { jDomElt.append('
'); var tbl = jDomElt.children('table'); var self = this; $.each(obj, function(ind, kvPair) { if (ind < 4) return; // skip type tag, class name, ID, and superclasses entries 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: self.renderData(kvPair[1], valTd); }); } } else if (obj[0] == 'CIRCULAR_REF') { assert(obj.length == 2); jDomElt.append('
circular reference to id=' + obj[1] + '
'); } else { // render custom data type assert(obj.length == 3); typeName = obj[0]; id = obj[1]; strRepr = obj[2]; // if obj[2] is like ' at 0x84760>', // then display an abbreviated version rather than the gory details noStrReprRE = /<.* at 0x.*>/; if (noStrReprRE.test(strRepr)) { jDomElt.append('' + typeName + ' (id=' + id + ')'); } else { strRepr = htmlspecialchars(strRepr); // escape strings! // warning: we're overloading tuple elts for custom data types jDomElt.append('
' + typeName + ' (id=' + id + '):
'); jDomElt.append('
' + strRepr + '
'); } } } else { alert("Error: renderData FAIL!"); } } Visualizer.prototype.renderPyCodeOutput = function(codeStr) { var tbl = $(this.codeOutputTable); tbl.html(''); var lines = codeStr.rtrim().split('\n'); $.each(lines, function(i, cod) { var lineNo = i + 1; var htmlCod = htmlspecialchars(cod); tbl.append(''); var curRow = tbl.find('tr:last'); curRow.find('td.lineNo').html(lineNo); curRow.find('td.cod').html(htmlCod); }); } PythonTutor.Visualizer = Visualizer; })(); String.prototype.rtrim = function() { return this.replace(/\s*$/g, ""); } function assert(cond) { 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; }