\
223 | |
\
224 | |
\
225 | ');
226 | }
227 | else {
228 | this.domRoot.html('');
232 | }
233 |
234 | // create a container for a resizable slider to encompass
235 | // both CodeDisplay and NavigationController
236 | this.domRoot.find('#vizLayoutTdFirst').append('');
237 | var base = this.domRoot.find('#vizLayoutTdFirst #codAndNav');
238 | var baseD3 = this.domRootD3.select('#vizLayoutTdFirst #codAndNav');
239 |
240 | this.codDisplay = new CodeDisplay(this, base, baseD3,
241 | this.curInputCode, this.params.lang, this.params.editCodeBaseURL);
242 | this.navControls = new NavigationController(this, base, baseD3, this.curTrace.length);
243 |
244 | if (this.params.embeddedMode) {
245 | // don't override if they've already been set!
246 | if (this.params.codeDivWidth === undefined) {
247 | this.params.codeDivWidth = DEFAULT_EMBEDDED_CODE_DIV_WIDTH;
248 | }
249 |
250 | if (this.params.codeDivHeight === undefined) {
251 | this.params.codeDivHeight = DEFAULT_EMBEDDED_CODE_DIV_HEIGHT;
252 | }
253 |
254 | // add an extra label to link back to the main site, so that viewers
255 | // on the embedded page know that they're seeing an OPT visualization
256 | // base.append('');
257 | base.find('#codeFooterDocs').hide(); // cut out extraneous docs
258 | }
259 |
260 | // not enough room for these extra buttons ...
261 | if (this.params.codeDivWidth &&
262 | this.params.codeDivWidth < 470) {
263 | this.domRoot.find('#jmpFirstInstr').hide();
264 | this.domRoot.find('#jmpLastInstr').hide();
265 | }
266 |
267 | if (this.params.codeDivWidth) {
268 | this.domRoot.find('#codAndNav').width(this.params.codeDivWidth);
269 | }
270 |
271 | if (this.params.codeDivHeight) {
272 | this.domRoot.find('#pyCodeOutputDiv')
273 | .css('max-height', this.params.codeDivHeight + 'px');
274 | }
275 |
276 |
277 | // enable left-right draggable pane resizer (originally from David Pritchard)
278 | base.resizable({
279 | handles: "e", // "east" (i.e., right)
280 | minWidth: 100, //otherwise looks really goofy
281 | resize: (event, ui) => {
282 |
283 | this.updateOutput(true);
284 |
285 | this.domRoot.find("#codeDisplayDiv").css("height", "auto"); // redetermine height if necessary
286 | this.navControls.renderSliderBreakpoints(this.sortedBreakpointsList); // update breakpoint display accordingly on resize
287 | if (this.params.updateOutputCallback) // report size change
288 | this.params.updateOutputCallback(this);
289 | }});
290 |
291 | this.outputBox = new ProgramOutputBox(this, this.domRoot.find('#vizLayoutTdSecond'),
292 | this.params.embeddedMode ? '45px' : null);
293 | this.dataViz = new DataVisualizer(this,
294 | this.domRoot.find('#vizLayoutTdSecond'),
295 | this.domRootD3.select('#vizLayoutTdSecond'));
296 |
297 | myViz.navControls.showError(this.instrLimitReachedWarningMsg);
298 | myViz.navControls.setupSlider(this.curTrace.length - 1);
299 |
300 | if (this.params.startingInstruction) {
301 | this.params.jumpToEnd = false; // override! make sure to handle FIRST
302 |
303 | // weird special case for something like:
304 | // e=raw_input(raw_input("Enter something:"))
305 | if (this.params.startingInstruction == this.curTrace.length) {
306 | this.params.startingInstruction--;
307 | }
308 |
309 | // fail-soft with out-of-bounds startingInstruction values:
310 | if (this.params.startingInstruction < 0) {
311 | this.params.startingInstruction = 0;
312 | }
313 | if (this.params.startingInstruction >= this.curTrace.length) {
314 | this.params.startingInstruction = this.curTrace.length - 1;
315 | }
316 |
317 | assert(0 <= this.params.startingInstruction &&
318 | this.params.startingInstruction < this.curTrace.length);
319 | this.curInstr = this.params.startingInstruction;
320 | }
321 |
322 | if (this.params.jumpToEnd) {
323 | var firstErrorStep = -1;
324 | for (var i = 0; i < this.curTrace.length; i++) {
325 | var e = this.curTrace[i];
326 | if (e.event == 'exception' || e.event == 'uncaught_exception') {
327 | firstErrorStep = i;
328 | break;
329 | }
330 | }
331 |
332 | // set to first error step if relevant since that's more informative
333 | // than simply jumping to the very end
334 | if (firstErrorStep >= 0) {
335 | this.curInstr = firstErrorStep;
336 | } else {
337 | this.curInstr = this.curTrace.length - 1;
338 | }
339 | }
340 |
341 | if (this.params.hideCode) {
342 | this.domRoot.find('#vizLayoutTdFirst').hide(); // gigantic hack!
343 | }
344 |
345 | this.dataViz.precomputeCurTraceLayouts();
346 |
347 | if (!this.params.hideCode) {
348 | this.codDisplay.renderPyCodeOutput();
349 | }
350 |
351 | this.updateOutput();
352 | this.hasRendered = true;
353 | this.try_hook("end_render", {myViz:this});
354 | }
355 |
356 | _getSortedBreakpointsList() {
357 | var ret = [];
358 | this.breakpoints.forEach(function(k, v) {
359 | ret.push(Number(k)); // these should be NUMBERS, not strings
360 | });
361 | ret.sort(function(x,y){return x-y}); // WTF, javascript sort is lexicographic by default!
362 | return ret;
363 | }
364 |
365 | addToBreakpoints(executionPoints) {
366 | $.each(executionPoints, (i, ep) => {
367 | this.breakpoints.set(ep, 1);
368 | });
369 | this.sortedBreakpointsList = this._getSortedBreakpointsList(); // keep synced!
370 | }
371 |
372 | removeFromBreakpoints(executionPoints) {
373 | $.each(executionPoints, (i, ep) => {
374 | this.breakpoints.remove(ep);
375 | });
376 | this.sortedBreakpointsList = this._getSortedBreakpointsList(); // keep synced!
377 | }
378 |
379 | setBreakpoint(d) {
380 | this.addToBreakpoints(d.executionPoints);
381 | this.navControls.renderSliderBreakpoints(this.sortedBreakpointsList);
382 | }
383 |
384 | unsetBreakpoint(d) {
385 | this.removeFromBreakpoints(d.executionPoints);
386 | this.navControls.renderSliderBreakpoints(this.sortedBreakpointsList);
387 | }
388 |
389 | // find the previous/next breakpoint to c or return -1 if it doesn't exist
390 | findPrevBreakpoint() {
391 | var c = this.curInstr;
392 |
393 | if (this.sortedBreakpointsList.length == 0) {
394 | return -1;
395 | }
396 | else {
397 | for (var i = 1; i < this.sortedBreakpointsList.length; i++) {
398 | var prev = this.sortedBreakpointsList[i-1];
399 | var cur = this.sortedBreakpointsList[i];
400 | if (c <= prev)
401 | return -1;
402 | if (cur >= c)
403 | return prev;
404 | }
405 |
406 | // final edge case:
407 | var lastElt = this.sortedBreakpointsList[this.sortedBreakpointsList.length - 1];
408 | return (lastElt < c) ? lastElt : -1;
409 | }
410 | }
411 |
412 | findNextBreakpoint() {
413 | var c = this.curInstr;
414 |
415 | if (this.sortedBreakpointsList.length == 0) {
416 | return -1;
417 | }
418 | // usability hack: if you're currently on a breakpoint, then
419 | // single-step forward to the next execution point, NOT the next
420 | // breakpoint. it's often useful to see what happens when the line
421 | // at a breakpoint executes.
422 | else if ($.inArray(c, this.sortedBreakpointsList) >= 0) {
423 | return c + 1;
424 | }
425 | else {
426 | for (var i = 0; i < this.sortedBreakpointsList.length - 1; i++) {
427 | var cur = this.sortedBreakpointsList[i];
428 | var next = this.sortedBreakpointsList[i+1];
429 | if (c < cur)
430 | return cur;
431 | if (cur <= c && c < next) // subtle
432 | return next;
433 | }
434 |
435 | // final edge case:
436 | var lastElt = this.sortedBreakpointsList[this.sortedBreakpointsList.length - 1];
437 | return (lastElt > c) ? lastElt : -1;
438 | }
439 | }
440 |
441 | // returns true if action successfully taken
442 | stepForward() {
443 | var myViz = this;
444 |
445 | if (myViz.curInstr < myViz.curTrace.length - 1) {
446 | // if there is a next breakpoint, then jump to it ...
447 | if (myViz.sortedBreakpointsList.length > 0) {
448 | var nextBreakpoint = myViz.findNextBreakpoint();
449 | if (nextBreakpoint != -1)
450 | myViz.curInstr = nextBreakpoint;
451 | else
452 | myViz.curInstr += 1; // prevent "getting stuck" on a solitary breakpoint
453 | }
454 | else {
455 | myViz.curInstr += 1;
456 | }
457 | myViz.updateOutput(true);
458 | return true;
459 | }
460 |
461 | return false;
462 | }
463 |
464 | // returns true if action successfully taken
465 | stepBack() {
466 | var myViz = this;
467 |
468 | if (myViz.curInstr > 0) {
469 | // if there is a prev breakpoint, then jump to it ...
470 | if (myViz.sortedBreakpointsList.length > 0) {
471 | var prevBreakpoint = myViz.findPrevBreakpoint();
472 | if (prevBreakpoint != -1)
473 | myViz.curInstr = prevBreakpoint;
474 | else
475 | myViz.curInstr -= 1; // prevent "getting stuck" on a solitary breakpoint
476 | }
477 | else {
478 | myViz.curInstr -= 1;
479 | }
480 | myViz.updateOutput();
481 | return true;
482 | }
483 |
484 | return false;
485 | }
486 |
487 | // This function is called every time the display needs to be updated
488 | updateOutput(smoothTransition=false) {
489 | if (this.params.hideCode) {
490 | this.updateOutputMini();
491 | }
492 | else {
493 | this.updateOutputFull(smoothTransition);
494 | }
495 | this.outputBox.renderOutput(this.curTrace[this.curInstr].stdout);
496 | this.try_hook("end_updateOutput", {myViz:this});
497 | }
498 |
499 | // does a LOT of stuff, called by updateOutput
500 | updateOutputFull(smoothTransition) {
501 | assert(this.curTrace);
502 | assert(!this.params.hideCode);
503 |
504 | var myViz = this; // to prevent confusion of 'this' inside of nested functions
505 |
506 | // there's no point in re-rendering if this pane isn't even visible in the first place!
507 | if (!myViz.domRoot.is(':visible')) {
508 | return;
509 | }
510 |
511 | myViz.updateLineAndExceptionInfo(); // very important to call this before rendering code (argh order dependency)
512 |
513 | var prevDataVizHeight = myViz.dataViz.height();
514 |
515 | this.codDisplay.updateCodOutput(smoothTransition);
516 |
517 | // call the callback if necessary (BEFORE rendering)
518 | if (this.params.updateOutputCallback) {
519 | this.params.updateOutputCallback(this);
520 | }
521 |
522 | var totalInstrs = this.curTrace.length;
523 | var isFirstInstr = (this.curInstr == 0);
524 | var isLastInstr = (this.curInstr == (totalInstrs-1));
525 | var msg = "Step " + String(this.curInstr + 1) + " of " + String(totalInstrs-1);
526 | if (isLastInstr) {
527 | if (this.promptForUserInput || this.promptForMouseInput) {
528 | msg = 'Enter user input below:';
529 | } else if (this.instrLimitReached) {
530 | msg = "Instruction limit reached";
531 | } else {
532 | msg = "Program terminated";
533 | }
534 | }
535 |
536 | this.navControls.setVcrControls(msg, isFirstInstr, isLastInstr);
537 | this.navControls.setSliderVal(this.curInstr);
538 |
539 | // render error (if applicable):
540 | if (myViz.curLineExceptionMsg) {
541 | if (myViz.curLineExceptionMsg === "Unknown error") {
542 | myViz.navControls.showError('Unknown error: Please email a bug report to philip@pgbovine.net');
543 | } else {
544 | myViz.navControls.showError(myViz.curLineExceptionMsg);
545 | }
546 | } else if (!this.instrLimitReached) { // ugly, I know :/
547 | myViz.navControls.showError(null);
548 | }
549 |
550 | // finally, render all of the data structures
551 | this.dataViz.renderDataStructures(this.curInstr);
552 |
553 | // call the callback if necessary (AFTER rendering)
554 | if (myViz.dataViz.height() != prevDataVizHeight) {
555 | if (this.params.heightChangeCallback) {
556 | this.params.heightChangeCallback(this);
557 | }
558 | }
559 |
560 | if (isLastInstr &&
561 | myViz.params.executeCodeWithRawInputFunc &&
562 | myViz.promptForUserInput) {
563 | this.navControls.showUserInputDiv();
564 | } else {
565 | this.navControls.hideUserInputDiv();
566 | }
567 | } // end of updateOutputFull
568 |
569 | updateOutputMini() {
570 | assert(this.params.hideCode);
571 | this.dataViz.renderDataStructures(this.curInstr);
572 | }
573 |
574 | renderStep(step) {
575 | assert(0 <= step);
576 | assert(step < this.curTrace.length);
577 |
578 | // ignore redundant calls
579 | if (this.curInstr == step) {
580 | return;
581 | }
582 |
583 | this.curInstr = step;
584 | this.updateOutput();
585 | }
586 |
587 | redrawConnectors() {
588 | this.dataViz.redrawConnectors();
589 | }
590 |
591 | // All of the Java frontend code in this function was written by David
592 | // Pritchard and Will Gwozdz, and integrated into pytutor.js by Philip Guo
593 | activateJavaFrontend() {
594 | var prevLine = null;
595 | this.curTrace.forEach((e, i) => {
596 | // ugh the Java backend doesn't attach line numbers to exception
597 | // events, so just take the previous line number as our best guess
598 | if (e.event === 'exception' && !e.line) {
599 | e.line = prevLine;
600 | }
601 |
602 | // super hack by Philip that reverses the direction of the stack so
603 | // that it grows DOWN and renders the same way as the Python and JS
604 | // visualizer stacks
605 | if (e.stack_to_render !== undefined) {
606 | e.stack_to_render.reverse();
607 | }
608 |
609 | prevLine = e.line;
610 | });
611 |
612 | this.add_pytutor_hook(
613 | "renderPrimitiveObject",
614 | function(args) {
615 | var obj = args.obj, d3DomElement = args.d3DomElement;
616 | var typ = typeof obj;
617 | if (obj instanceof Array && obj[0] == "VOID") {
618 | d3DomElement.append('void');
619 | }
620 | else if (obj instanceof Array && obj[0] == "NUMBER-LITERAL") {
621 | // actually transmitted as a string
622 | d3DomElement.append('' + obj[1] + '');
623 | }
624 | else if (obj instanceof Array && obj[0] == "CHAR-LITERAL") {
625 | var asc = obj[1].charCodeAt(0);
626 | var ch = obj[1];
627 |
628 | // default
629 | var show = asc.toString(16);
630 | while (show.length < 4) show = "0" + show;
631 | show = "\\u" + show;
632 |
633 | if (ch == "\n") show = "\\n";
634 | else if (ch == "\r") show = "\\r";
635 | else if (ch == "\t") show = "\\t";
636 | else if (ch == "\b") show = "\\b";
637 | else if (ch == "\f") show = "\\f";
638 | else if (ch == "\'") show = "\\\'";
639 | else if (ch == "\"") show = "\\\"";
640 | else if (ch == "\\") show = "\\\\";
641 | else if (asc >= 32) show = ch;
642 |
643 | // stringObj to make monospace
644 | d3DomElement.append('\'' + show + '\'');
645 | }
646 | else
647 | return [false]; // we didn't handle it
648 | return [true]; // we handled it
649 | });
650 |
651 | this.add_pytutor_hook(
652 | "isPrimitiveType",
653 | function(args) {
654 | var obj = args.obj;
655 | if ((obj instanceof Array && obj[0] == "VOID")
656 | || (obj instanceof Array && obj[0] == "NUMBER-LITERAL")
657 | || (obj instanceof Array && obj[0] == "CHAR-LITERAL")
658 | || (obj instanceof Array && obj[0] == "ELIDE"))
659 | return [true, true]; // we handled it, it's primitive
660 | return [false]; // didn't handle it
661 | });
662 |
663 | this.add_pytutor_hook(
664 | "end_updateOutput",
665 | function(args) {
666 | var myViz = args.myViz;
667 | var curEntry = myViz.curTrace[myViz.curInstr];
668 | if (myViz.params.stdin && myViz.params.stdin != "") {
669 | var stdinPosition = curEntry.stdinPosition || 0;
670 | var stdinContent =
671 | ''+
672 | escapeHtml(myViz.params.stdin.substr(0, stdinPosition))+
673 | ''+
674 | escapeHtml(myViz.params.stdin.substr(stdinPosition));
675 | myViz.domRoot.find('#stdinShow').html(stdinContent);
676 | }
677 | return [false];
678 | });
679 |
680 | this.add_pytutor_hook(
681 | "end_render",
682 | function(args) {
683 | var myViz = args.myViz;
684 |
685 | if (myViz.params.stdin && myViz.params.stdin != "") {
686 | var stdinHTML = '';
687 | myViz.domRoot.find('#dataViz').append(stdinHTML); // TODO: leaky abstraction with #dataViz
688 | }
689 |
690 | myViz.domRoot.find('#'+myViz.generateID('globals_header')).html("Static fields");
691 | });
692 |
693 | this.add_pytutor_hook(
694 | "isLinearObject",
695 | function(args) {
696 | var heapObj = args.heapObj;
697 | if (heapObj[0]=='STACK' || heapObj[0]=='QUEUE')
698 | return ['true', 'true'];
699 | return ['false'];
700 | });
701 |
702 | this.add_pytutor_hook(
703 | "renderCompoundObject",
704 | function(args) {
705 | var objID = args.objID;
706 | var d3DomElement = args.d3DomElement;
707 | var obj = args.obj;
708 | var typeLabelPrefix = args.typeLabelPrefix;
709 | var myViz = args.myViz;
710 | var stepNum = args.stepNum;
711 |
712 | if (!(obj[0] == 'LIST' || obj[0] == 'QUEUE' || obj[0] == 'STACK'))
713 | return [false]; // didn't handle
714 |
715 | var label = obj[0].toLowerCase();
716 | var visibleLabel = {list:'array', queue:'queue', stack:'stack'}[label];
717 |
718 | if (obj.length == 1) {
719 | d3DomElement.append('' + typeLabelPrefix + 'empty ' + visibleLabel + '
');
720 | return [true]; //handled
721 | }
722 |
723 | d3DomElement.append('' + typeLabelPrefix + visibleLabel + '
');
724 | d3DomElement.append('');
725 | var tbl = d3DomElement.children('table');
726 |
727 | if (obj[0] == 'LIST') {
728 | tbl.append('|
');
729 | var headerTr = tbl.find('tr:first');
730 | var contentTr = tbl.find('tr:last');
731 |
732 | // i: actual index in json object; ind: apparent index
733 | for (var i=1, ind=0; i