├── .gitignore ├── LICENSE ├── README.md ├── bundle.js ├── data ├── README.md ├── examples │ ├── accumulate_example.json │ ├── g_example.json │ ├── product_example.json │ └── repeated_example.json ├── generated │ ├── accumulate.json │ ├── g.json │ ├── product.json │ └── repeated.json ├── get_trace.py ├── parse.py ├── pg_encoder.py ├── pg_logger.py ├── raw │ ├── accumulate_all_attempts.json │ ├── g_all_attempts.json │ ├── product_all_attempts.json │ └── repeated_all_attempts.json ├── test │ ├── test.js │ └── test.json └── trace.js ├── index.html ├── package-lock.json ├── package.json ├── resources ├── abstract.png ├── compare.png ├── concept.png ├── demo.gif ├── filter.png └── trace.png ├── src ├── components │ ├── App.js │ ├── ControlPanel.js │ ├── PythonTutor.js │ ├── PythonTutor │ │ ├── CodeDisplay.js │ │ ├── DataVisualizer.js │ │ ├── ExecutionVisualizer.js │ │ ├── Ladder.js │ │ ├── NavigationController.js │ │ └── ProgramOutputBox.js │ └── Trace │ │ ├── Record.js │ │ ├── Stream.js │ │ └── Tree.js ├── index.js ├── redux │ ├── actions.js │ ├── reducer.js │ └── store.js └── style │ ├── codemirror.less │ ├── main.less │ └── pytutor.less └── webpack.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional REPL history 37 | .node_repl_history 38 | .DS_Store 39 | .pyc 40 | 41 | 42 | # Byte-compiled / optimized / DLL files 43 | __pycache__/ 44 | *.py[cod] 45 | *$py.class 46 | 47 | # C extensions 48 | *.so 49 | 50 | # Distribution / packaging 51 | .Python 52 | build/ 53 | develop-eggs/ 54 | dist/ 55 | downloads/ 56 | eggs/ 57 | .eggs/ 58 | lib/ 59 | lib64/ 60 | parts/ 61 | sdist/ 62 | var/ 63 | wheels/ 64 | *.egg-info/ 65 | .installed.cfg 66 | *.egg 67 | 68 | # PyInstaller 69 | # Usually these files are written by a python script from a template 70 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 71 | *.manifest 72 | *.spec 73 | 74 | # Installer logs 75 | pip-log.txt 76 | pip-delete-this-directory.txt 77 | 78 | # Unit test / coverage reports 79 | htmlcov/ 80 | .tox/ 81 | .coverage 82 | .coverage.* 83 | .cache 84 | nosetests.xml 85 | coverage.xml 86 | *.cover 87 | .hypothesis/ 88 | 89 | # Translations 90 | *.mo 91 | *.pot 92 | 93 | # Django stuff: 94 | *.log 95 | local_settings.py 96 | 97 | # Flask stuff: 98 | instance/ 99 | .webassets-cache 100 | 101 | # Scrapy stuff: 102 | .scrapy 103 | 104 | # Sphinx documentation 105 | docs/_build/ 106 | 107 | # PyBuilder 108 | target/ 109 | 110 | # Jupyter Notebook 111 | .ipynb_checkpoints 112 | 113 | # pyenv 114 | .python-version 115 | 116 | # celery beat schedule file 117 | celerybeat-schedule 118 | 119 | # SageMath parsed files 120 | *.sage.py 121 | 122 | # Environments 123 | .env 124 | .venv 125 | env/ 126 | venv/ 127 | ENV/ 128 | 129 | # Spyder project settings 130 | .spyderproject 131 | .spyproject 132 | 133 | # Rope project settings 134 | .ropeproject 135 | 136 | # mkdocs documentation 137 | /site 138 | 139 | # mypy 140 | .mypy_cache/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Ryo Suzuki 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TraceDiff 2 | 3 | TraceDiff: Debugging Unexpected Code Behavior Using Trace Divergences [VL/HCC 2017] 4 | 5 | [[PDF]](http://ryosuzuki.org/publications/vlhcc-2017-tracediff.pdf) 6 | 7 | [![npm](https://img.shields.io/npm/v/npm.svg)]() 8 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) 9 | 10 | ## Installation 11 | 12 | ```shell 13 | npm install 14 | npm start 15 | ``` 16 | 17 | 18 | ## [Online Demo](https://ryosuzuki.github.io/trace-diff) 19 | 20 | ![](https://github.com/ryosuzuki/trace-diff/raw/master/resources/demo.gif) 21 | 22 | 23 | ## Abstract 24 | 25 | Recent advances in program synthesis offer means to automatically debug student submissions and generate personalized feedback in massive programming classrooms. When automatically generating feedback for programming assignments, a key challenge is designing pedagogically-useful hints that are as effective as the manual feedback given by teachers. Through an analysis of teachers' hint-giving practices in 132 online Q&A posts, we establish three design guidelines that an effective feedback design should follow. Based on these guidelines, we develop a feedback system that leverages both program synthesis and visualization techniques. Our system compares the dynamic code execution of both incorrect and fixed code and highlights how the error leads to a difference in behavior and where the incorrect code trace diverges from the expected solution. Results from our study suggest that our system enables students to detect and fix bugs that are not caught by students using another existing visual debugging tool. 26 | 27 | 28 | 29 | 30 | ## Features 31 | 32 | ### Filter 33 | Focus attention by extracting important steps: It extracts important control flow steps by identifying a list of variables and function calls that take on different values. 34 | 35 | 36 | 37 | 38 | ### Highlight 39 | Highlight behavior that diverges from the nearest solution: It compares the code execution of incorrect code with the fixed code and highlights a point when the control flow diverges. 40 | 41 | 42 | 43 | 44 | ### Explore 45 | Explore behavior through interactive program visualization: Integrate Python Tutor to allow effective and interactive exploration of collected code traces. 46 | 47 | 48 | 49 | 50 | ### Abstraction 51 | Map the error to the cause by abstracting expressions: It enables the student to interactively map a concrete value (e.g., `sum = 3` and `return 11`) back to the expressions that computed these values, such as variables and function calls (e.g., `sum = add(1, 2)` and `return total`) to help locate the cause of the bug. 52 | 53 | 54 | 55 | 56 | 57 | ## Citation 58 | 59 | ``` 60 | @inproceedings{suzuki2017tracediff, 61 | title={TraceDiff: Debugging unexpected code behavior using trace divergences}, 62 | author={Suzuki, Ryo and Soares, Gustavo and Head, Andrew and Glassman, Elena and Reis, Ruan and Mongiovi, Melina and Antoni, Loris D'and Hartman, Bj\"{o}rn}, 63 | booktitle={Visual Languages and Human-Centric Computing (VL/HCC), 2017 IEEE Symposium on}, 64 | year={2017}, 65 | organization={IEEE} 66 | } 67 | ``` 68 | 69 | ## Acknowledgements 70 | 71 | This is a joint work between the University of Colorado Boulder, UC Berkeley, Federal University of Campina Grande, and University of Wisconsin-Madison. 72 | This research was supported by the NSF Expeditions in Computing award CCF 1138996, NSF CAREER award IIS 1149799, CAPES 8114/15-3, an NDSEG fellowship, a Google CS Capacity Award, and the Nakajima Foundation. 73 | 74 | 75 | ## License 76 | MIT 77 | -------------------------------------------------------------------------------- /data/README.md: -------------------------------------------------------------------------------- 1 | 2 | # Generate Input Dataset 3 | 4 | Run the following command to generate dataset which includes code traces and abstract syntax tree information. 5 | 6 | ``` 7 | node trace.js accumulate 8 | ``` 9 | 10 | It generates 11 | ``` 12 | ./generated/accumulate.json 13 | ./examples/accumulate_example.json 14 | ``` 15 | from `./raw/accumulate_all_attempts.json` 16 | 17 | `accumulate_example.json` is just a subset of `accumulate.json`. 18 | 19 | 20 | Other commands: 21 | ``` 22 | node trace.js g 23 | node trace.js product 24 | node trace.js repeated 25 | ``` 26 | 27 | 28 | # Example Data Structure 29 | 30 | This is an example input data for TraceDiff 31 | 32 | ```json 33 | { 34 | "diffs": "...", 35 | "code": "...", 36 | "beforeCode": "...", 37 | "afterCode": "...", 38 | "log": "...", 39 | "expected": 25, 40 | "result": 135, 41 | "beforeAst": [ 42 | "...", 43 | ], 44 | "afterAst": [ 45 | "...", 46 | ], 47 | "beforeTraces": [ 48 | { 49 | "ordered_globals": [ "accumulate" ], 50 | "stdout": "", 51 | "func_name": "", 52 | "stack_to_render": [], 53 | "globals": { 54 | "accumulate": [ "REF", 1 ] 55 | }, 56 | "heap": { 57 | "1": [ "FUNCTION", "accumulate(combiner, base, n, term)", null ] 58 | }, 59 | "stack_locals": [], 60 | "line": 7, 61 | "event": "step_line" 62 | } 63 | ], 64 | "afterTraces": [ 65 | { 66 | "ordered_globals": [ "accumulate" ], 67 | "stdout": "", 68 | "func_name": "", 69 | "stack_to_render": [], 70 | "globals": { 71 | "accumulate": [ "REF", 1 ] 72 | }, 73 | "heap": { 74 | "1": [ "FUNCTION", "accumulate(combiner, base, n, term)", null ] 75 | }, 76 | "stack_locals": [], 77 | "line": 7, 78 | "event": "step_line" 79 | } 80 | ], 81 | } 82 | ``` 83 | 84 | 85 | 86 | 87 | # Example Problems 88 | 89 | ## Question: Accumulate 90 | Show that both summation and product are instances of a more general function, called accumulate, with the following signature: 91 | 92 | ```python 93 | def accumulate(combiner, base, n, term): 94 | """Return the result of combining the first N terms in a sequence. The 95 | terms to be combined are TERM(1), TERM(2), ..., TERM(N). COMBINER is a 96 | two-argument function. Treating COMBINER as if it were a binary operator, 97 | the return value is 98 | BASE COMBINER TERM(1) COMBINER TERM(2) ... COMBINER TERM(N) 99 | 100 | >>> accumulate(add, 0, 5, identity) # 0 + 1 + 2 + 3 + 4 + 5 101 | 15 102 | >>> accumulate(add, 11, 5, identity) # 11 + 1 + 2 + 3 + 4 + 5 103 | 26 104 | >>> accumulate(add, 11, 0, identity) # 11 105 | 11 106 | >>> accumulate(add, 11, 3, square) # 11 + 1^2 + 2^2 + 3^2 107 | 25 108 | >>> accumulate(mul, 2, 3, square) # 2 * 1^2 * 2^2 * 3^2 109 | 72 110 | """ 111 | "*** YOUR CODE HERE ***" 112 | ``` 113 | 114 | accumulate(combiner, base, n, term) takes the following arguments: 115 | 116 | - term and n: the same arguments as in summation and product 117 | - combiner: a two-argument function that specifies how the current term combined with the previously accumulated terms. 118 | - base: value that specifies what value to use to start the accumulation. 119 | 120 | For example, accumulate(add, 11, 3, square) is 121 | ```python 122 | 11 + square(1) + square(2) + square(3) 123 | ``` 124 | 125 | 126 | 127 | ## Question: G function 128 | A mathematical function G on positive integers is defined by two cases: 129 | ```python 130 | G(n) = n, if n <= 3 131 | G(n) = G(n - 1) + 2 * G(n - 2) + 3 * G(n - 3), if n > 3 132 | ``` 133 | Write a recursive function g that computes G(n). Then, write an iterative function g_iter that also computes G(n): 134 | 135 | ```python 136 | def g(n): 137 | """Return the value of G(n), computed recursively. 138 | 139 | >>> g(1) 140 | 1 141 | >>> g(2) 142 | 2 143 | >>> g(3) 144 | 3 145 | >>> g(4) 146 | 10 147 | >>> g(5) 148 | 22 149 | >>> from construct_check import check 150 | >>> check(HW_SOURCE_FILE, 'g', ['While', 'For']) 151 | True 152 | """ 153 | "*** YOUR CODE HERE ***" 154 | ``` 155 | 156 | 157 | 158 | ## Question: Product 159 | The summation(term, n) function from lecture adds up `term(1) + ... + term(n)` Write a similar product(n, term) function that returns `term(1) * ... * term(n)`. Show how to define the factorial function in terms of product. Hint: try using the identity function for factorial. 160 | 161 | ```python 162 | def product(n, term): 163 | """Return the product of the first n terms in a sequence. 164 | 165 | n -- a positive integer 166 | term -- a function that takes one argument 167 | 168 | >>> product(3, identity) # 1 * 2 * 3 169 | 6 170 | >>> product(5, identity) # 1 * 2 * 3 * 4 * 5 171 | 120 172 | >>> product(3, square) # 1^2 * 2^2 * 3^2 173 | 36 174 | >>> product(5, square) # 1^2 * 2^2 * 3^2 * 4^2 * 5^2 175 | 14400 176 | """ 177 | "*** YOUR CODE HERE ***" 178 | ``` 179 | 180 | 181 | 182 | ## Question: Repeated 183 | Implement repeated(f, n): 184 | 185 | - f is a one-argument function that takes a number and returns another number. 186 | - n is a non-negative integer 187 | 188 | repeated returns another function that, when given an argument x, will compute f(f(....(f(x))....)) (apply f a total n times). For example, repeated(square, 3)(42) evaluates to square(square(square(42))). Yes, it makes sense to apply the function zero times! See if you can figure out a reasonable function to return for that case. 189 | 190 | ```python 191 | def repeated(f, n): 192 | """Return the function that computes the nth application of f. 193 | 194 | >>> add_three = repeated(increment, 3) 195 | >>> add_three(5) 196 | 8 197 | >>> repeated(triple, 5)(1) # 3 * 3 * 3 * 3 * 3 * 1 198 | 243 199 | >>> repeated(square, 2)(5) # square(square(5)) 200 | 625 201 | >>> repeated(square, 4)(5) # square(square(square(square(5)))) 202 | 152587890625 203 | >>> repeated(square, 0)(5) 204 | 5 205 | """ 206 | "*** YOUR CODE HERE ***" 207 | 208 | Hint: You may find it convenient to use compose1 from the textbook: 209 | 210 | def compose1(f, g): 211 | """Return a function h, such that h(x) = f(g(x)).""" 212 | def h(x): 213 | return f(g(x)) 214 | return h 215 | ``` 216 | -------------------------------------------------------------------------------- /data/get_trace.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import json 3 | import re 4 | import pg_logger 5 | import json 6 | import parse 7 | 8 | question_type = sys.argv[1] 9 | path = './generated/' + question_type + '.json' 10 | 11 | items = [] 12 | with open(path) as file: 13 | items = json.load(file) 14 | 15 | i = 0 16 | for item in items: 17 | before = item['before'] 18 | after = item['after'] 19 | test = item['test'] 20 | 21 | before_ast = [] 22 | after_ast = [] 23 | 24 | for line in before.splitlines(): 25 | line = line.strip() 26 | try: 27 | ast = parse.make_ast(line) 28 | except Exception as err: 29 | ast = { 'error': True } 30 | before_ast.append(ast) 31 | 32 | for line in after.splitlines(): 33 | line = line.strip() 34 | try: 35 | ast = parse.make_ast(line) 36 | except Exception as err: 37 | ast = { 'error': True } 38 | after_ast.append(ast) 39 | 40 | item['beforeAst'] = before_ast 41 | item['afterAst'] = after_ast 42 | 43 | keywords = re.findall(r"[\w]+", test) 44 | combiner = None 45 | term = None 46 | combiner_func = '' 47 | term_func = '' 48 | test = test.split('#')[0] 49 | 50 | before = before.replace(' ', ' ') 51 | after = after.replace(' ', ' ') 52 | 53 | # e.g. 54 | # product(3, identity) 55 | # >>> ['product', '3', 'identity'] 56 | # 57 | # accumulate(add, 11, 3, square) 58 | # >>> ['accumulate', 'add', '11', '3', 'square'] 59 | # 60 | # add_three = repeated(increment, 3) 61 | # >>> ['add_three', 'repeated', 'increment', '3'] 62 | # 63 | # add_three(5) 64 | # >>> ['add_three', '5'] 65 | # 66 | # repeated(square, 4)(5) 67 | # >>> ['repeated', 'square', '4', '5'] 68 | # 69 | 70 | if not keywords: 71 | continue 72 | key = keywords[0] 73 | 74 | if key == 'product': 75 | term = keywords[2] 76 | elif key == 'accumulate': 77 | combiner = keywords[1] 78 | term = keywords[4] 79 | elif key == 'repeated': 80 | term = keywords[1] 81 | elif key == 'add_three': 82 | term = "increment" 83 | if keywords[1] == '5': 84 | test = "repeated(increment, 3)(5)" 85 | item['test'] = test 86 | 87 | if combiner == 'add': 88 | combiner_func = "def add(a, b):\n return a + b" 89 | elif combiner == 'mul': 90 | combiner_func = "def mul(a, b):\n return a * b" 91 | 92 | if term == 'identity': 93 | term_func = "def identity(x):\n return x" 94 | elif term == 'square': 95 | term_func = "def square(x):\n return x * x" 96 | elif term == 'increment': 97 | term_func = "def increment(x):\n return x + 1" 98 | elif term == 'triple': 99 | term_func = "def triple(x):\n return 3 * x" 100 | 101 | if combiner_func is not '': 102 | before += '\n\n' 103 | before += combiner_func 104 | 105 | if key == 'add_three' or key == 'repeated': 106 | before += '\n\n' 107 | before += "def identity(x):\n return x" 108 | 109 | if term_func is not '': 110 | before += '\n\n' 111 | before += term_func 112 | 113 | before += '\n\n' 114 | before += test 115 | 116 | if combiner_func is not '': 117 | after += '\n\n' 118 | after += combiner_func 119 | 120 | if key == 'add_three' or key == 'repeated': 121 | after += '\n\n' 122 | after += "def identity(x):\n return x" 123 | 124 | if term_func is not '': 125 | after += '\n\n' 126 | after += term_func 127 | 128 | after += '\n\n' 129 | after += test 130 | 131 | beforeTraces = pg_logger.exec_script_str(before).trace 132 | afterTraces = pg_logger.exec_script_str(after).trace 133 | item['beforeCode'] = before 134 | item['afterCode'] = after 135 | item['beforeTraces'] = beforeTraces 136 | item['afterTraces'] = afterTraces 137 | 138 | 139 | with open('./examples/' + question_type + '_example.json', 'w') as file: 140 | json.dump([items[0]], file, indent = 2) 141 | 142 | with open(path, 'w') as file: 143 | json.dump(items, file) 144 | -------------------------------------------------------------------------------- /data/parse.py: -------------------------------------------------------------------------------- 1 | import ast 2 | import json 3 | 4 | def classname(cls): 5 | return cls.__class__.__name__ 6 | 7 | def jsonify_ast(node, level=0): 8 | fields = {} 9 | for k in node._fields: 10 | fields[k] = '...' 11 | v = getattr(node, k) 12 | if isinstance(v, ast.AST): 13 | if v._fields: 14 | fields[k] = jsonify_ast(v) 15 | else: 16 | fields[k] = classname(v) 17 | 18 | elif isinstance(v, list): 19 | fields[k] = [] 20 | for e in v: 21 | fields[k].append(jsonify_ast(e)) 22 | 23 | elif isinstance(v, str): 24 | fields[k] = v 25 | 26 | elif isinstance(v, int) or isinstance(v, float): 27 | fields[k] = v 28 | 29 | elif v is None: 30 | fields[k] = None 31 | 32 | else: 33 | fields[k] = 'unrecognized' 34 | 35 | ret = { classname(node): fields } 36 | return ret 37 | 38 | def make_ast(code): 39 | tree = ast.parse(code) 40 | return jsonify_ast(tree) 41 | 42 | 43 | -------------------------------------------------------------------------------- /data/pg_encoder.py: -------------------------------------------------------------------------------- 1 | # Online Python Tutor 2 | # https://github.com/pgbovine/OnlinePythonTutor/ 3 | # 4 | # Copyright (C) Philip J. Guo (philip@pgbovine.net) 5 | # 6 | # Permission is hereby granted, free of charge, to any person obtaining a 7 | # copy of this software and associated documentation files (the 8 | # "Software"), to deal in the Software without restriction, including 9 | # without limitation the rights to use, copy, modify, merge, publish, 10 | # distribute, sublicense, and/or sell copies of the Software, and to 11 | # permit persons to whom the Software is furnished to do so, subject to 12 | # the following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be included 15 | # in all copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 18 | # OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 19 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 20 | # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 21 | # CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 22 | # TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 23 | # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 24 | 25 | # Thanks to John DeNero for making the encoder work on both Python 2 and 3 26 | 27 | 28 | # Given an arbitrary piece of Python data, encode it in such a manner 29 | # that it can be later encoded into JSON. 30 | # http://json.org/ 31 | # 32 | # We use this function to encode run-time traces of data structures 33 | # to send to the front-end. 34 | # 35 | # Format: 36 | # Primitives: 37 | # * None, int, long, float, str, bool - unchanged 38 | # (json.dumps encodes these fine verbatim, except for inf, -inf, and nan) 39 | # 40 | # exceptions: float('inf') -> ['SPECIAL_FLOAT', 'Infinity'] 41 | # float('-inf') -> ['SPECIAL_FLOAT', '-Infinity'] 42 | # float('nan') -> ['SPECIAL_FLOAT', 'NaN'] 43 | # x == int(x) -> ['SPECIAL_FLOAT', '%.1f' % x] 44 | # (this way, 3.0 prints as '3.0' and not as 3, which looks like an int) 45 | # 46 | # If render_heap_primitives is True, then primitive values are rendered 47 | # on the heap as ['HEAP_PRIMITIVE', , ] 48 | # 49 | # (for SPECIAL_FLOAT values, is a list like ['SPECIAL_FLOAT', 'Infinity']) 50 | # 51 | # Compound objects: 52 | # * list - ['LIST', elt1, elt2, elt3, ..., eltN] 53 | # * tuple - ['TUPLE', elt1, elt2, elt3, ..., eltN] 54 | # * set - ['SET', elt1, elt2, elt3, ..., eltN] 55 | # * dict - ['DICT', [key1, value1], [key2, value2], ..., [keyN, valueN]] 56 | # * instance - ['INSTANCE', class name, [attr1, value1], [attr2, value2], ..., [attrN, valueN]] 57 | # * instance with __str__ defined - ['INSTANCE_PPRINT', class name, <__str__ value>] 58 | # * class - ['CLASS', class name, [list of superclass names], [attr1, value1], [attr2, value2], ..., [attrN, valueN]] 59 | # * function - ['FUNCTION', function name, parent frame ID (for nested functions)] 60 | # * module - ['module', module name] 61 | # * other - [, string representation of object] 62 | # * compound object reference - ['REF', target object's unique_id] 63 | # 64 | # the unique_id is derived from id(), which allows us to capture aliasing 65 | 66 | 67 | # number of significant digits for floats 68 | FLOAT_PRECISION = 4 69 | 70 | 71 | from collections import defaultdict 72 | import re, types 73 | import sys 74 | import math 75 | typeRE = re.compile("") 76 | classRE = re.compile("") 77 | 78 | import inspect 79 | 80 | # TODO: maybe use the 'six' library to smooth over Py2 and Py3 incompatibilities? 81 | is_python3 = (sys.version_info[0] == 3) 82 | if is_python3: 83 | # avoid name errors (GROSS!) 84 | long = int 85 | unicode = str 86 | 87 | 88 | def is_class(dat): 89 | """Return whether dat is a class.""" 90 | if is_python3: 91 | return isinstance(dat, type) 92 | else: 93 | return type(dat) in (types.ClassType, types.TypeType) 94 | 95 | 96 | def is_instance(dat): 97 | """Return whether dat is an instance of a class.""" 98 | if is_python3: 99 | return type(dat) not in PRIMITIVE_TYPES and \ 100 | isinstance(type(dat), type) and \ 101 | not isinstance(dat, type) 102 | else: 103 | # ugh, classRE match is a bit of a hack :( 104 | return type(dat) == types.InstanceType or classRE.match(str(type(dat))) 105 | 106 | 107 | def get_name(obj): 108 | """Return the name of an object.""" 109 | return obj.__name__ if hasattr(obj, '__name__') else get_name(type(obj)) 110 | 111 | 112 | PRIMITIVE_TYPES = (int, long, float, str, unicode, bool, type(None)) 113 | 114 | def encode_primitive(dat): 115 | t = type(dat) 116 | if t is float: 117 | if math.isinf(dat): 118 | if dat > 0: 119 | return ['SPECIAL_FLOAT', 'Infinity'] 120 | else: 121 | return ['SPECIAL_FLOAT', '-Infinity'] 122 | elif math.isnan(dat): 123 | return ['SPECIAL_FLOAT', 'NaN'] 124 | else: 125 | # render floats like 3.0 as '3.0' and not as 3 126 | if dat == int(dat): 127 | return ['SPECIAL_FLOAT', '%.1f' % dat] 128 | else: 129 | return round(dat, FLOAT_PRECISION) 130 | elif t is str and (not is_python3): 131 | # hack only for Python 2 strings ... always turn into unicode 132 | # and display '?' when it's not valid unicode 133 | return dat.decode('utf-8', 'replace') 134 | else: 135 | # return all other primitives verbatim 136 | return dat 137 | 138 | 139 | # grab a line number like ' ' or ' ' 140 | def create_lambda_line_number(codeobj, line_to_lambda_code): 141 | try: 142 | lambda_lineno = codeobj.co_firstlineno 143 | lst = line_to_lambda_code[lambda_lineno] 144 | ind = lst.index(codeobj) 145 | # add a suffix for all subsequent lambdas on a line beyond the first 146 | # (nix this for now because order isn't guaranteed when you have 147 | # multiple lambdas on the same line) 148 | ''' 149 | if ind > 0: 150 | lineno_str = str(lambda_lineno) + chr(ord('a') + ind) 151 | else: 152 | lineno_str = str(lambda_lineno) 153 | ''' 154 | lineno_str = str(lambda_lineno) 155 | return ' ' 156 | except: 157 | return '' 158 | 159 | 160 | # Note that this might BLOAT MEMORY CONSUMPTION since we're holding on 161 | # to every reference ever created by the program without ever releasing 162 | # anything! 163 | class ObjectEncoder: 164 | def __init__(self, render_heap_primitives): 165 | # Key: canonicalized small ID 166 | # Value: encoded (compound) heap object 167 | self.encoded_heap_objects = {} 168 | 169 | self.render_heap_primitives = render_heap_primitives 170 | 171 | self.id_to_small_IDs = {} 172 | self.cur_small_ID = 1 173 | 174 | # wow, creating unique identifiers for lambdas is quite annoying, 175 | # especially if we want to properly differentiate: 176 | # 1.) multiple lambdas defined on the same line, and 177 | # 2.) the same lambda code defined multiple times on different lines 178 | # 179 | # However, it gets confused when there are multiple identical 180 | # lambdas on the same line, like: 181 | # f(lambda x:x*x, lambda y:y*y, lambda x:x*x) 182 | 183 | # (assumes everything is in one file) 184 | # Key: line number 185 | # Value: list of the code objects of lambdas defined 186 | # on that line in the order they were defined 187 | self.line_to_lambda_code = defaultdict(list) 188 | 189 | 190 | def get_heap(self): 191 | return self.encoded_heap_objects 192 | 193 | 194 | def reset_heap(self): 195 | # VERY IMPORTANT to reassign to an empty dict rather than just 196 | # clearing the existing dict, since get_heap() could have been 197 | # called earlier to return a reference to a previous heap state 198 | self.encoded_heap_objects = {} 199 | 200 | def set_function_parent_frame_ID(self, ref_obj, enclosing_frame_id): 201 | assert ref_obj[0] == 'REF' 202 | func_obj = self.encoded_heap_objects[ref_obj[1]] 203 | assert func_obj[0] == 'FUNCTION' 204 | func_obj[-1] = enclosing_frame_id 205 | 206 | 207 | # return either a primitive object or an object reference; 208 | # and as a side effect, update encoded_heap_objects 209 | def encode(self, dat, get_parent): 210 | """Encode a data value DAT using the GET_PARENT function for parent ids.""" 211 | # primitive type 212 | if not self.render_heap_primitives and type(dat) in PRIMITIVE_TYPES: 213 | return encode_primitive(dat) 214 | # compound type - return an object reference and update encoded_heap_objects 215 | else: 216 | my_id = id(dat) 217 | 218 | try: 219 | my_small_id = self.id_to_small_IDs[my_id] 220 | except KeyError: 221 | my_small_id = self.cur_small_ID 222 | self.id_to_small_IDs[my_id] = self.cur_small_ID 223 | self.cur_small_ID += 1 224 | 225 | del my_id # to prevent bugs later in this function 226 | 227 | ret = ['REF', my_small_id] 228 | 229 | # punt early if you've already encoded this object 230 | if my_small_id in self.encoded_heap_objects: 231 | return ret 232 | 233 | 234 | # major side-effect! 235 | new_obj = [] 236 | self.encoded_heap_objects[my_small_id] = new_obj 237 | 238 | typ = type(dat) 239 | 240 | if typ == list: 241 | new_obj.append('LIST') 242 | for e in dat: 243 | new_obj.append(self.encode(e, get_parent)) 244 | elif typ == tuple: 245 | new_obj.append('TUPLE') 246 | for e in dat: 247 | new_obj.append(self.encode(e, get_parent)) 248 | elif typ == set: 249 | new_obj.append('SET') 250 | for e in dat: 251 | new_obj.append(self.encode(e, get_parent)) 252 | elif typ == dict: 253 | new_obj.append('DICT') 254 | for (k, v) in dat.items(): 255 | # don't display some built-in locals ... 256 | if k not in ('__module__', '__return__', '__locals__'): 257 | new_obj.append([self.encode(k, get_parent), self.encode(v, get_parent)]) 258 | elif typ in (types.FunctionType, types.MethodType): 259 | if is_python3: 260 | argspec = inspect.getfullargspec(dat) 261 | else: 262 | argspec = inspect.getargspec(dat) 263 | 264 | printed_args = [e for e in argspec.args] 265 | if argspec.varargs: 266 | printed_args.append('*' + argspec.varargs) 267 | 268 | if is_python3: 269 | if argspec.varkw: 270 | printed_args.append('**' + argspec.varkw) 271 | if argspec.kwonlyargs: 272 | printed_args.extend(argspec.kwonlyargs) 273 | else: 274 | if argspec.keywords: 275 | printed_args.append('**' + argspec.keywords) 276 | 277 | func_name = get_name(dat) 278 | 279 | pretty_name = func_name 280 | 281 | # sometimes might fail for, say, , so just ignore 282 | # failures for now ... 283 | try: 284 | pretty_name += '(' + ', '.join(printed_args) + ')' 285 | except TypeError: 286 | pass 287 | 288 | # put a line number suffix on lambdas to more uniquely identify 289 | # them, since they don't have names 290 | if func_name == '': 291 | cod = (dat.__code__ if is_python3 else dat.func_code) # ugh! 292 | lst = self.line_to_lambda_code[cod.co_firstlineno] 293 | if cod not in lst: 294 | lst.append(cod) 295 | pretty_name += create_lambda_line_number(cod, 296 | self.line_to_lambda_code) 297 | 298 | encoded_val = ['FUNCTION', pretty_name, None] 299 | if get_parent: 300 | enclosing_frame_id = get_parent(dat) 301 | encoded_val[2] = enclosing_frame_id 302 | new_obj.extend(encoded_val) 303 | elif typ is types.BuiltinFunctionType: 304 | pretty_name = get_name(dat) + '(...)' 305 | new_obj.extend(['FUNCTION', pretty_name, None]) 306 | elif is_class(dat) or is_instance(dat): 307 | self.encode_class_or_instance(dat, new_obj) 308 | elif typ is types.ModuleType: 309 | new_obj.extend(['module', dat.__name__]) 310 | elif typ in PRIMITIVE_TYPES: 311 | assert self.render_heap_primitives 312 | new_obj.extend(['HEAP_PRIMITIVE', type(dat).__name__, encode_primitive(dat)]) 313 | else: 314 | typeStr = str(typ) 315 | m = typeRE.match(typeStr) 316 | 317 | if not m: 318 | m = classRE.match(typeStr) 319 | 320 | assert m, typ 321 | 322 | if is_python3: 323 | encoded_dat = str(dat) 324 | else: 325 | # ugh, for bytearray() in Python 2, str() returns 326 | # non-JSON-serializable characters, so need to decode: 327 | encoded_dat = str(dat).decode('utf-8', 'replace') 328 | new_obj.extend([m.group(1), encoded_dat]) 329 | 330 | return ret 331 | 332 | 333 | def encode_class_or_instance(self, dat, new_obj): 334 | """Encode dat as a class or instance.""" 335 | if is_instance(dat): 336 | if hasattr(dat, '__class__'): 337 | # common case ... 338 | class_name = get_name(dat.__class__) 339 | else: 340 | # super special case for something like 341 | # "from datetime import datetime_CAPI" in Python 3.2, 342 | # which is some weird 'PyCapsule' type ... 343 | # http://docs.python.org/release/3.1.5/c-api/capsule.html 344 | class_name = get_name(type(dat)) 345 | 346 | if hasattr(dat, '__str__') and \ 347 | (not dat.__class__.__str__ is object.__str__): # make sure it's not the lame default __str__ 348 | # N.B.: when objects are being constructed, this call 349 | # might fail since not all fields have yet been populated 350 | try: 351 | pprint_str = str(dat) 352 | except: 353 | pprint_str = '' 354 | 355 | new_obj.extend(['INSTANCE_PPRINT', class_name, pprint_str]) 356 | return # bail early 357 | else: 358 | new_obj.extend(['INSTANCE', class_name]) 359 | # don't traverse inside modules, or else risk EXPLODING the visualization 360 | if class_name == 'module': 361 | return 362 | else: 363 | superclass_names = [e.__name__ for e in dat.__bases__ if e is not object] 364 | new_obj.extend(['CLASS', get_name(dat), superclass_names]) 365 | 366 | # traverse inside of its __dict__ to grab attributes 367 | # (filter out useless-seeming ones, based on anecdotal observation): 368 | hidden = ('__doc__', '__module__', '__return__', '__dict__', 369 | '__locals__', '__weakref__', '__qualname__') 370 | if hasattr(dat, '__dict__'): 371 | user_attrs = sorted([e for e in dat.__dict__ if e not in hidden]) 372 | else: 373 | user_attrs = [] 374 | 375 | for attr in user_attrs: 376 | new_obj.append([self.encode(attr, None), self.encode(dat.__dict__[attr], None)]) 377 | -------------------------------------------------------------------------------- /data/test/test.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const PythonShell = require('python-shell') 3 | 4 | const test = [{ 5 | "before": "def accumulate(combiner, base, n, term):\n previous = term(base)\n for i in range(1, n+1):\n previous = combiner(previous, term(i))\n return previous", 6 | "after": "def accumulate(combiner, base, n, term):\n previous = base\n for i in range(1, n+1):\n previous = combiner(previous, term(i))\n return previous", 7 | "test": "accumulate(add, 11, 3, square)", 8 | }] 9 | 10 | const path = 'test.json' 11 | 12 | fs.writeFileSync(path, JSON.stringify(test, null, 2)) 13 | 14 | console.log('write finish') 15 | 16 | PythonShell.run('../get_trace.py', { args: [path] }, (err) => { 17 | if (err) throw err 18 | console.log('generate finish') 19 | }) 20 | 21 | -------------------------------------------------------------------------------- /data/test/test.json: -------------------------------------------------------------------------------- 1 | [{ 2 | "before": "def foo():\n return 1 + 1\nfoo()", 3 | "after": "def foo():\n return 1 + 2\nfoo()" 4 | }] -------------------------------------------------------------------------------- /data/trace.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const jsdiff = require('diff') 3 | const PythonShell = require('python-shell') 4 | 5 | const main = () => { 6 | const args = process.argv.slice(2) 7 | const type = args[0] 8 | const types = ['accumulate', 'g', 'product', 'repeated'] 9 | if (types.includes(type)) { 10 | console.log(type) 11 | const trace = new Trace() 12 | trace.open(type) 13 | trace.generate(type) 14 | } else { 15 | console.log(`${type} is not available`) 16 | console.log(`available args:`) 17 | console.log(types) 18 | } 19 | } 20 | 21 | class Trace { 22 | constructor() { 23 | this.items = [] 24 | this.results = [] 25 | } 26 | 27 | open(type) { 28 | const path = `./raw/${type}_all_attempts.json` 29 | console.log(`parse ${path}`) 30 | const json = fs.readFileSync(path, 'utf8') 31 | this.items = JSON.parse(json) 32 | } 33 | 34 | generate(type) { 35 | const path = `./generated/${type}.json` 36 | console.log(`generating ${path}`) 37 | 38 | let results = [] 39 | let id = 0 40 | for (let item of this.items) { 41 | item = new Item(item, id++) 42 | item.generate() 43 | this.results.push(item) 44 | } 45 | fs.writeFileSync(path, JSON.stringify(this.results, null, 2)) 46 | console.log('write finish') 47 | console.log('analyzing behavior...') 48 | PythonShell.run('get_trace.py', { args: [type] }, (err) => { 49 | if (err) throw err 50 | console.log('generate finish') 51 | }) 52 | } 53 | } 54 | 55 | class Item { 56 | constructor(item, id) { 57 | this.item = item 58 | this.id = id 59 | this.studentId = this.item.studentId 60 | this.rule = this.item.UsedFix 61 | this.before = this.item.before.substr(2) 62 | this.after = this.item.SynthesizedAfter.substr(2) 63 | this.code = '' 64 | this.diffs = [] 65 | this.added = [] 66 | this.removed = [] 67 | } 68 | 69 | generate() { 70 | this.getDiff() 71 | this.getTest() 72 | delete this.item 73 | } 74 | 75 | getDiff() { 76 | if (this.before.includes('from operator import')) { 77 | let lines = [] 78 | for (let line of this.before.split('\r\n')) { 79 | if (line.includes('from operator import')) continue 80 | lines.push(line) 81 | } 82 | this.before = lines.join('\n') 83 | } 84 | this.before = this.before.replace(/\r\n/g, '\n'); 85 | this.after = this.after.replace(/\r\n/g, '\n'); 86 | 87 | 88 | let diffs = jsdiff.diffJson(this.before, this.after) 89 | let line = -1 90 | let code = '' 91 | let added = [] 92 | let removed = [] 93 | let addedLine = [] 94 | let removedLine = [] 95 | for (let diff of diffs) { 96 | let lines = diff.value.split('\n') 97 | for (let i = 0; i < diff.count; i++) { 98 | code += lines[i] 99 | code += '\n' 100 | line++ 101 | if (diff.added) { 102 | added.push(line) 103 | addedLine.push({ line: line, code: lines[i] }) 104 | } 105 | if (diff.removed) { 106 | removed.push(line) 107 | removedLine.push({ line: line, code: lines[i] }) 108 | } 109 | } 110 | } 111 | this.code = code 112 | this.diffs = diffs 113 | this.added = added 114 | this.removed = removed 115 | this.addedLine = addedLine 116 | this.removedLine = removedLine 117 | } 118 | 119 | getTest() { 120 | let i = 0 121 | let testIndex = 0 122 | let errorIndex = 0 123 | for (let text of this.item.failed) { 124 | if (text.includes('>>> ')) testIndex = i 125 | if (text.includes('# Error: expected')) errorIndex = i 126 | i++ 127 | } 128 | let pass = parseInt(this.item.failed[this.item.failed.length-2]) 129 | let test = this.item.failed[testIndex] 130 | test = test.substr(4) 131 | // test = test.substr(0, test.indexOf(')') + 1) 132 | let expected = this.item.failed[errorIndex+1] 133 | expected = parseInt(expected.substr(1)) 134 | let result = this.item.failed[errorIndex+3] 135 | result = result.substr(6) 136 | if (!isNaN(parseInt(result))) { 137 | result = parseInt(result) 138 | } 139 | let log = this.item.failed.slice(testIndex, errorIndex+4).join('\n') 140 | 141 | this.test = test 142 | this.expected = expected 143 | this.result = result 144 | this.log = log 145 | } 146 | } 147 | 148 | main() 149 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | TraceDiff 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 | 28 | 29 | Fork me on GitHub 30 | 31 | 32 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "trace-diff", 3 | "version": "1.0.0", 4 | "description": "TraceDiff: Debugging Unexpected Code Behavior Using Trace Divergences", 5 | "main": "src/index.js ", 6 | "scripts": { 7 | "start": "webpack-dev-server --hot --inline", 8 | "build": "webpack --color --progress", 9 | "test": "echo \"Error: no test specified\" && exit 1" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/ryosuzuki/trace-diff.git" 14 | }, 15 | "author": "Ryo Suzuki (http://ryosuzuki.org/)", 16 | "license": "MIT", 17 | "bugs": { 18 | "url": "https://github.com/ryosuzuki/trace-diff/issues" 19 | }, 20 | "homepage": "https://github.com/ryosuzuki/trace-diff#readme", 21 | "dependencies": { 22 | "async": "^2.1.4", 23 | "body-parser": "^1.15.2", 24 | "classnames": "^2.2.5", 25 | "consolidate": "^0.14.5", 26 | "d3": "^2", 27 | "diff": "^3.2.0", 28 | "ejs": "^2.5.5", 29 | "express": "^4.14.0", 30 | "font-awesome": "^4.7.0", 31 | "fs-extra": "^1.0.0", 32 | "glob": "^7.1.1", 33 | "highlight.js": "^9.9.0", 34 | "jquery": "^3.1.1", 35 | "jquery-ui": "^1.12.1", 36 | "jquery-ui-bundle": "^1.12.1", 37 | "lodash": "^4.17.4", 38 | "marked": "^0.3.6", 39 | "moment": "^2.17.1", 40 | "mousetrap": "^1.6.0", 41 | "nedb": "^1.8.0", 42 | "nedb-promise": "^2.0.1", 43 | "python-shell": "^0.4.0", 44 | "radium": "^0.18.1", 45 | "rc-slider": "^6.0.1", 46 | "rc-tooltip": "^3.4.2", 47 | "react": "^15.4.2", 48 | "react-commits-graph": "^0.2.0", 49 | "react-dom": "^15.4.2", 50 | "react-highlight": "^0.9.0", 51 | "react-hot-loader": "^1.3.1", 52 | "react-marked-markdown": "^1.4.2", 53 | "react-redux": "^5.0.1", 54 | "react-simplemde-editor": "^3.6.8", 55 | "redux": "^3.6.0", 56 | "redux-logger": "^2.7.4", 57 | "redux-thunk": "^2.1.0", 58 | "redux-watch": "^1.1.1", 59 | "semantic-ui-css": "^2.2.4" 60 | }, 61 | "devDependencies": { 62 | "babel-cli": "^6.18.0", 63 | "babel-core": "^6.21.0", 64 | "babel-eslint": "^7.1.1", 65 | "babel-loader": "^6.2.10", 66 | "babel-plugin-css-modules-transform": "^1.1.0", 67 | "babel-plugin-transform-async-to-generator": "^6.22.0", 68 | "babel-plugin-webpack-loaders": "^0.8.0", 69 | "babel-polyfill": "^6.22.0", 70 | "babel-preset-es2015": "^6.18.0", 71 | "babel-preset-react": "^6.16.0", 72 | "babel-preset-react-hmre": "^1.1.1", 73 | "babel-preset-stage-0": "^6.16.0", 74 | "babel-preset-stage-3": "^6.22.0", 75 | "bower-webpack-plugin": "^0.1.9", 76 | "css-loader": "^0.26.1", 77 | "file-loader": "^0.9.0", 78 | "html-loader": "^0.4.4", 79 | "jsx-control-statements": "^3.1.5", 80 | "less": "^2.7.2", 81 | "less-loader": "^2.2.3", 82 | "react-codemirror": "^0.3.0", 83 | "react-hot-loader": "^1.3.1", 84 | "react-scripts": "^0.8.4", 85 | "static-loader": "^0.1.8", 86 | "style-loader": "^0.13.1", 87 | "svg-inline-loader": "^0.7.1", 88 | "url-loader": "^0.5.7", 89 | "webpack": "^2.2.1", 90 | "webpack-dev-middleware": "^1.10.0", 91 | "webpack-dev-server": "^2.3.0", 92 | "webpack-hot-middleware": "^2.16.1" 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /resources/abstract.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryosuzuki/trace-diff/11f24e0d4c28a859df8f86017de255565a9a3677/resources/abstract.png -------------------------------------------------------------------------------- /resources/compare.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryosuzuki/trace-diff/11f24e0d4c28a859df8f86017de255565a9a3677/resources/compare.png -------------------------------------------------------------------------------- /resources/concept.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryosuzuki/trace-diff/11f24e0d4c28a859df8f86017de255565a9a3677/resources/concept.png -------------------------------------------------------------------------------- /resources/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryosuzuki/trace-diff/11f24e0d4c28a859df8f86017de255565a9a3677/resources/demo.gif -------------------------------------------------------------------------------- /resources/filter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryosuzuki/trace-diff/11f24e0d4c28a859df8f86017de255565a9a3677/resources/filter.png -------------------------------------------------------------------------------- /resources/trace.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryosuzuki/trace-diff/11f24e0d4c28a859df8f86017de255565a9a3677/resources/trace.png -------------------------------------------------------------------------------- /src/components/App.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { connect } from 'react-redux' 3 | import { bindActionCreators } from 'redux' 4 | import actions from '../redux/actions' 5 | import Mousetrap from 'mousetrap' 6 | import Datastore from 'nedb' 7 | 8 | import PythonTutor from './PythonTutor' 9 | import ControlPanel from './ControlPanel' 10 | import Stream from './Trace/Stream' 11 | import Record from './Trace/Record' 12 | 13 | const db = new Datastore() 14 | 15 | class App extends Component { 16 | constructor(props) { 17 | super(props) 18 | this.state = {} 19 | window.app = this 20 | window.db = db 21 | 22 | window.quizes = [] 23 | } 24 | 25 | componentDidMount() { 26 | const href = window.location.href 27 | const params = {} 28 | href.replace( 29 | new RegExp( "([^?=&]+)(=([^&]*))?", "g" ), 30 | function( $0, $1, $2, $3 ){ 31 | params[ $1 ] = $3; 32 | } 33 | ) 34 | 35 | const types = ['accumulate', 'product', 'g', 'repeated'] 36 | if (!params.type && params.id) { 37 | window.location.href = `/?type=accumulate&id=${params.id}` 38 | return false 39 | } 40 | if (!params.type || !params.id || !types.includes(params.type)) { 41 | window.location.href = `${window.location.pathname}?type=accumulate&id=0` 42 | return false 43 | } 44 | 45 | $.ajax({ 46 | method: 'GET', 47 | url: `${window.location.pathname}data/generated/${params.type}.json` 48 | }) 49 | .then((items) => { 50 | console.log('start') 51 | window.type = params.type 52 | 53 | $(`#type-links #${params.type}`).addClass('primary') 54 | const id = Number(params.id) 55 | this.updateState({ items: items }) 56 | this.setCurrent(id) 57 | 58 | items = items.map((item) => { 59 | return { 60 | id: item.id, 61 | test: item.test, 62 | expected: item.expected, 63 | result: item.result 64 | } 65 | }) 66 | db.insert(items, (err) => { 67 | console.log('finish') 68 | let item = items[id] 69 | db.find({ test: item.test, result: item.result }, function(err, items) { 70 | this.updateState({ relatedItems: items }) 71 | }.bind(this)) 72 | 73 | }) 74 | }) 75 | 76 | Mousetrap.bind('left', () => { 77 | this.setCurrent(this.props.id - 1) 78 | }) 79 | Mousetrap.bind('right', () => { 80 | this.setCurrent(this.props.id + 1) 81 | }) 82 | } 83 | 84 | 85 | setCurrent(id) { 86 | console.log('set current') 87 | let item = this.props.items[id] 88 | 89 | let stream = new Stream() 90 | stream.generate(item.beforeTraces, item.beforeCode, 'before') 91 | stream.generate(item.afterTraces, item.afterCode, 'after') 92 | stream.check() 93 | 94 | let record = new Record() 95 | record.generate(stream.beforeTraces, 'before') 96 | record.generate(stream.afterTraces, 'after') 97 | record.check() 98 | 99 | let state = Object.assign(item, { 100 | id: id, 101 | beforeTraces: stream.beforeTraces, 102 | afterTraces: stream.afterTraces, 103 | traces: stream.traces, 104 | currentCode: item.beforeCode, 105 | step: 0, 106 | stop: false, 107 | beforeHistory: record.beforeHistory, 108 | afterHistory: record.afterHistory, 109 | beforeTicks: record.beforeTicks, 110 | afterTicks: record.afterTicks, 111 | commonKeys: record.commonKeys, 112 | focusKeys: record.focusKeys, 113 | beforeEvents: record.beforeEvents, 114 | afterEvents: record.afterEvents, 115 | }) 116 | this.updateState(state) 117 | window.history.pushState(null, null, `?type=${window.type}&id=${id}`) 118 | 119 | window.pythonTutor.init() 120 | 121 | setTimeout(() => { 122 | console.log('call init') 123 | // window.pythonTutor.init() 124 | }, 500) 125 | 126 | db.find({ test: item.test, result: item.result }, function(err, items) { 127 | this.updateState({ relatedItems: items }) 128 | }.bind(this)) 129 | } 130 | 131 | updateState(state) { 132 | this.props.store.dispatch(actions.updateState(state)) 133 | } 134 | 135 | render() { 136 | const options = { 137 | mode: 'python', 138 | theme: 'base16-light', 139 | lineNumbers: true 140 | } 141 | 142 | return ( 143 |
144 |
145 | 151 |
152 | 156 |
157 |
158 | 201 |
202 | ) 203 | } 204 | } 205 | 206 | function mapStateToProps(state) { 207 | return state 208 | } 209 | 210 | function mapDispatchToProps(dispatch) { 211 | return { 212 | actions: bindActionCreators(actions, dispatch) 213 | } 214 | } 215 | 216 | export default connect(mapStateToProps, mapDispatchToProps)(App) 217 | -------------------------------------------------------------------------------- /src/components/ControlPanel.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import Slider from 'rc-slider' 3 | import Tooltip from 'rc-tooltip' 4 | 5 | class ControlPanel extends Component { 6 | constructor(props) { 7 | super(props) 8 | window.controlPanel = this 9 | } 10 | 11 | onChange(value) { 12 | if (value.target) value = value.target.value 13 | const id = Math.floor(value) 14 | window.app.setCurrent(id) 15 | } 16 | 17 | render() { 18 | return ( 19 |
20 |
21 | 26 |
27 | 35 |
36 | ) 37 | } 38 | } 39 | 40 | export default ControlPanel 41 | 42 | 43 | const Handle = Slider.Handle; 44 | const handle = (props) => { 45 | const { value, dragging, index, ...restProps } = props; 46 | return ( 47 | 53 | 54 | 55 | ); 56 | }; -------------------------------------------------------------------------------- /src/components/PythonTutor.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import CodeMirror from 'react-codemirror' 3 | import 'codemirror/mode/python/python' 4 | import _ from 'lodash' 5 | 6 | import ExecutionVisualizer from './PythonTutor/ExecutionVisualizer' 7 | import Ladder from './PythonTutor/Ladder' 8 | import Tree from './Trace/Tree' 9 | 10 | class PythonTutor extends Component { 11 | constructor(props) { 12 | super(props) 13 | this.state = { 14 | beforeHistory: {}, 15 | afterHistory: {}, 16 | } 17 | window.pythonTutor = this 18 | } 19 | 20 | componentDidMount() { 21 | } 22 | 23 | init() { 24 | let data = _.clone(this.props) 25 | let options = { 26 | embeddedMode: true, 27 | lang: 'py2', 28 | startingInstruction: 0, 29 | editCodeBaseURL: 'visualize.html', 30 | } 31 | window.viz = new ExecutionVisualizer('viz', data, options); 32 | } 33 | 34 | generate(type, level = 0) { 35 | let events, asts, key 36 | if (type === 'before') { 37 | events = this.props.beforeEvents 38 | asts = this.props.beforeAst 39 | key = 'beforeEvents' 40 | } else { 41 | events = this.props.afterEvents 42 | asts = this.props.afterAst 43 | key = 'afterEvents' 44 | } 45 | 46 | const isEqual = (before, after) => { 47 | let bool = true 48 | if (!before || !after) return false 49 | for (let key of ['value', 'key', 'type', 'history']) { 50 | if (!_.isEqual(before[key], after[key])) bool = false 51 | } 52 | return bool 53 | } 54 | 55 | let focusKeys = _.union(Object.keys(this.props.beforeHistory), Object.keys(this.props.afterHistory)).map((key) => { 56 | if (isEqual(this.props.beforeHistory[key], this.props.afterHistory[key]) && this.props.beforeHistory[key].type !== 'call') return false 57 | return key 58 | }).filter(key => key) 59 | 60 | let indent = 0 61 | events = events.map((event) => { 62 | if (!focusKeys.includes(event.key)) return false 63 | if (event.builtin) return false 64 | // if (event.type === 'call' && event.children.length === 0) return false 65 | 66 | let trimmedEvents = events.slice(0, event.id) 67 | let history = {} 68 | for (let e of trimmedEvents) { 69 | history[e.key] = e 70 | } 71 | 72 | let ast = asts[event.line-1] 73 | let tree = new Tree() 74 | 75 | try { 76 | tree.history = history 77 | tree.analyze(ast) 78 | event.updates = tree.updates 79 | 80 | if (event.type !== tree.type && event.value !== '') { 81 | event.updates = [event.value] 82 | } 83 | 84 | return event 85 | } catch (err) { 86 | event.updates = [] 87 | return event 88 | } 89 | }).filter(event => event) 90 | 91 | let max = 0 92 | events = events.map((event) => { 93 | let updates = _.uniq(event.updates).reverse() 94 | let value = updates[level] 95 | if (value === undefined) value = _.last(updates) 96 | if (value === undefined) value = event.value 97 | 98 | max = Math.max(max, updates.length - 1) 99 | 100 | switch (event.type) { 101 | case 'call': 102 | // event.call = event.children[0] 103 | event.html = [ 104 | { className: 'normal', text: 'call ' }, 105 | { className: 'keyword', text: event.key }, 106 | ] 107 | indent++ 108 | event.indent = indent 109 | break 110 | case 'return': 111 | event.html = [ 112 | { className: 'keyword', text: event.key }, 113 | { className: 'normal', text: ' returns ' }, 114 | { className: 'number', text: value }, 115 | ] 116 | event.indent = indent 117 | indent-- 118 | break 119 | default: 120 | event.html = [ 121 | { className: 'keyword', text: event.key }, 122 | { className: 'normal', text: ' = ' }, 123 | { className: 'number', text: value }, 124 | ] 125 | event.indent = indent 126 | break 127 | } 128 | return event 129 | }) 130 | 131 | return { events: events, focusKeys: focusKeys, max: max } 132 | } 133 | 134 | 135 | 136 | render() { 137 | return ( 138 |
139 |
140 |
141 | ) 142 | } 143 | } 144 | 145 | export default PythonTutor 146 | -------------------------------------------------------------------------------- /src/components/PythonTutor/CodeDisplay.js: -------------------------------------------------------------------------------- 1 | 2 | const SVG_ARROW_POLYGON = '0,3 12,3 12,0 18,5 12,10 12,7 0,7'; 3 | const SVG_ARROW_HEIGHT = 10; // must match height of SVG_ARROW_POLYGON 4 | 5 | 6 | var brightRed = '#e93f34'; 7 | var darkArrowColor = brightRed; 8 | var lightArrowColor = '#c9e6ca'; 9 | // Unicode arrow types: '\u21d2', '\u21f0', '\u2907' 10 | 11 | var heapPtrSrcRE = /__heap_pointer_src_/; 12 | var rightwardNudgeHack = true; // suggested by John DeNero, toggle with global 13 | 14 | 15 | class CodeDisplay { 16 | 17 | constructor(owner, domRoot, domRootD3, 18 | codToDisplay: string, lang: string, editCodeBaseURL: string) { 19 | 20 | this.leftGutterSvgInitialized = false; 21 | 22 | this.owner = owner; 23 | this.domRoot = domRoot; 24 | this.domRootD3 = domRootD3; 25 | this.codToDisplay = codToDisplay; 26 | 27 | /* 28 | var codeDisplayHTML = 29 | '
\ 30 |
\ 31 |
\ 32 | \ 35 |
\ 36 |
Click a line of code to set a breakpoint; use the Back and Forward buttons to jump there.
\ 37 |
'; 38 | */ 39 | 40 | var codeDisplayHTML = 41 | '
\ 42 |
\ 43 |
\ 44 |
\ 45 |
'; 46 | 47 | this.domRoot.append(codeDisplayHTML); 48 | if (this.owner.params.embeddedMode) { 49 | this.domRoot.find('#editCodeLinkDiv').css('font-size', '10pt'); 50 | } 51 | this.domRoot.find('#legendDiv') 52 | .append(' line that has just executed') 53 | .append('

next line to execute

'); 54 | this.domRootD3.select('svg#prevLegendArrowSVG') 55 | .append('polygon') 56 | .attr('points', SVG_ARROW_POLYGON) 57 | .attr('fill', lightArrowColor); 58 | this.domRootD3.select('svg#curLegendArrowSVG') 59 | .append('polygon') 60 | .attr('points', SVG_ARROW_POLYGON) 61 | .attr('fill', darkArrowColor); 62 | 63 | if (editCodeBaseURL) { 64 | // kinda kludgy 65 | var pyVer = '2'; // default 66 | if (lang === 'js') { 67 | pyVer = 'js'; 68 | } else if (lang === 'ts') { 69 | pyVer = 'ts'; 70 | } else if (lang === 'java') { 71 | pyVer = 'java'; 72 | } else if (lang === 'py3') { 73 | pyVer = '3'; 74 | } else if (lang === 'c') { 75 | pyVer = 'c'; 76 | } else if (lang === 'cpp') { 77 | pyVer = 'cpp'; 78 | } 79 | 80 | var urlStr = '' 81 | // $.param.fragment(editCodeBaseURL, 82 | // {code: this.codToDisplay, py: pyVer}, 83 | // 2); 84 | this.domRoot.find('#editBtn').attr('href', urlStr); 85 | } 86 | else { 87 | this.domRoot.find('#editCodeLinkDiv').hide(); // just hide for simplicity! 88 | this.domRoot.find('#editBtn').attr('href', "#"); 89 | this.domRoot.find('#editBtn').click(function(){return false;}); // DISABLE the link! 90 | } 91 | 92 | if (lang !== undefined) { 93 | if (lang === 'js') { 94 | this.domRoot.find('#langDisplayDiv').html('JavaScript'); 95 | } else if (lang === 'ts') { 96 | this.domRoot.find('#langDisplayDiv').html('TypeScript'); 97 | } else if (lang === 'ruby') { 98 | this.domRoot.find('#langDisplayDiv').html('Ruby'); 99 | } else if (lang === 'java') { 100 | this.domRoot.find('#langDisplayDiv').html('Java'); 101 | } else if (lang === 'py2') { 102 | this.domRoot.find('#langDisplayDiv').html('Python 2.7'); 103 | } else if (lang === 'py3') { 104 | this.domRoot.find('#langDisplayDiv').html('Python 3.6'); 105 | } else if (lang === 'c') { 106 | if (this.owner.params.embeddedMode) { 107 | this.domRoot.find('#langDisplayDiv').html('C (gcc 4.8, C11)'); 108 | } else { 109 | this.domRoot.find('#langDisplayDiv').html('C (gcc 4.8, C11) EXPERIMENTAL!
see known bugs and report to philip@pgbovine.net'); 110 | } 111 | } else if (lang === 'cpp') { 112 | if (this.owner.params.embeddedMode) { 113 | this.domRoot.find('#langDisplayDiv').html('C++ (gcc 4.8, C++11)'); 114 | } else { 115 | this.domRoot.find('#langDisplayDiv').html('C++ (gcc 4.8, C++11) EXPERIMENTAL!
see known bugs and report to philip@pgbovine.net'); 116 | } 117 | } else { 118 | this.domRoot.find('#langDisplayDiv').hide(); 119 | } 120 | } 121 | 122 | } 123 | 124 | renderPyCodeOutput() { 125 | var myCodOutput = this; // capture 126 | this.domRoot.find('#pyCodeOutputDiv').empty(); 127 | 128 | var target = document.getElementById('pyCodeOutputDiv') 129 | var myCodeMirror = CodeMirror(target, { 130 | value: this.owner.props.beforeCode, 131 | mode: 'python', 132 | theme: 'base16-light', 133 | lineNumbers: true, 134 | }); 135 | 136 | this.owner.cm = myCodeMirror 137 | 138 | // create a left-most gutter td that spans ALL rows ... 139 | // (NB: valign="top" is CRUCIAL for this to work in IE) 140 | this.domRoot.find('#pyCodeOutputDiv .CodeMirror-gutters') 141 | .prepend('
'); 142 | 143 | // create prevLineArrow and curLineArrow 144 | this.domRootD3.select('svg#leftCodeGutterSVG') 145 | .append('polygon') 146 | .attr('id', 'prevLineArrow') 147 | .attr('points', SVG_ARROW_POLYGON) 148 | .attr('fill', lightArrowColor); 149 | 150 | this.domRootD3.select('svg#leftCodeGutterSVG') 151 | .append('polygon') 152 | .attr('id', 'curLineArrow') 153 | .attr('points', SVG_ARROW_POLYGON) 154 | .attr('fill', darkArrowColor); 155 | 156 | /* 157 | // maps codeOutputLines down both table columns 158 | // TODO: get rid of pesky owner dependency 159 | var codeOutputD3 = this.domRootD3.select('#pyCodeOutputDiv') 160 | .append('table') 161 | .attr('id', 'pyCodeOutput') 162 | .selectAll('tr') 163 | .data(this.owner.codeOutputLines) 164 | .enter().append('tr') 165 | .selectAll('td') 166 | .data(function(d, i){return [d, d] ;}) 167 | .enter().append('td') 168 | .attr('class', function(d, i) { 169 | // add the togetherjsCloneClick class on here so that we can 170 | // sync clicks via TogetherJS for setting breakpoints in shared 171 | // sessions (kinda leaky abstraction since pytutor.ts shouldn't 172 | // need to know about TogetherJS, but oh wells) 173 | if (i == 0) { 174 | return 'lineNo togetherjsCloneClick'; 175 | } 176 | else { 177 | return 'cod togetherjsCloneClick'; 178 | } 179 | }) 180 | .attr('id', (d, i) => { 181 | if (i == 0) { 182 | return 'lineNo' + d.lineNumber; 183 | } 184 | else { 185 | return this.owner.generateID('cod' + d.lineNumber); // make globally unique (within the page) 186 | } 187 | }) 188 | .html(function(d, i) { 189 | if (i == 0) { 190 | return d.lineNumber; 191 | } 192 | else { 193 | return htmlspecialchars(d.text); 194 | } 195 | }); 196 | 197 | // 2012-09-05: Disable breakpoints for now to simplify UX 198 | // 2016-05-01: Revive breakpoint functionality 199 | codeOutputD3 200 | .style('cursor', function(d, i) { 201 | // don't do anything if exePts empty (i.e., this line was never executed) 202 | var exePts = d.executionPoints; 203 | if (!exePts || exePts.length == 0) { 204 | return; 205 | } else { 206 | return 'pointer' 207 | } 208 | }) 209 | .on('click', function(d, i) { 210 | // don't do anything if exePts empty (i.e., this line was never executed) 211 | var exePts = d.executionPoints; 212 | if (!exePts || exePts.length == 0) { 213 | return; 214 | } 215 | 216 | d.breakpointHere = !d.breakpointHere; // toggle 217 | if (d.breakpointHere) { 218 | myCodOutput.owner.setBreakpoint(d); 219 | d3.select(this.parentNode).select('td.lineNo').style('color', breakpointColor); 220 | d3.select(this.parentNode).select('td.lineNo').style('font-weight', 'bold'); 221 | d3.select(this.parentNode).select('td.cod').style('color', breakpointColor); 222 | } 223 | else { 224 | myCodOutput.owner.unsetBreakpoint(d); 225 | d3.select(this.parentNode).select('td.lineNo').style('color', ''); 226 | d3.select(this.parentNode).select('td.lineNo').style('font-weight', ''); 227 | d3.select(this.parentNode).select('td.cod').style('color', ''); 228 | } 229 | }); 230 | */ 231 | 232 | } 233 | 234 | updateCodOutput(smoothTransition=false) { 235 | 236 | var gutterTD = this.domRoot.find('#gutterTD') 237 | var gutterSVG = this.domRoot.find('svg#leftCodeGutterSVG'); 238 | 239 | const outerHeight = this.domRoot.find('.CodeMirror-lines').height() 240 | gutterTD.height(outerHeight); 241 | gutterSVG.height(outerHeight); 242 | 243 | 244 | // one-time initialization of the left gutter 245 | // (we often can't do this earlier since the entire pane 246 | // might be invisible and hence returns a height of zero or NaN 247 | // -- the exact format depends on browser) 248 | if (!this.leftGutterSvgInitialized) { 249 | // set the gutter's height to match that of its parent 250 | 251 | // gutterTD.height(gutterTD.parent().height()); 252 | // gutterSVG.height(gutterSVG.parent().height()); 253 | 254 | var firstRowOffsetY = this.domRoot.find('#pyCodeOutputDiv .CodeMirror-gutters').offset().top; 255 | 256 | // first take care of edge case when there's only one line ... 257 | this.codeRowHeight = this.domRoot.find('#pyCodeOutputDiv .CodeMirror-line:first').height(); 258 | 259 | // ... then handle the (much more common) multi-line case ... 260 | // this weird contortion is necessary to get the accurate row height on Internet Explorer 261 | // (simpler methods work on all other major browsers, erghhhhhh!!!) 262 | 263 | /* 264 | if (this.owner.codeOutputLines.length > 1) { 265 | var secondRowOffsetY = this.domRoot.find('#pyCodeOutputDiv .CodeMirror-line:nth-child(2)').offset().top; 266 | this.codeRowHeight = secondRowOffsetY - firstRowOffsetY; 267 | } 268 | */ 269 | 270 | // assert(this.codeRowHeight > 0); 271 | 272 | var gutterOffsetY = gutterSVG.offset().top; 273 | var teenyAdjustment = gutterOffsetY - firstRowOffsetY; 274 | 275 | teenyAdjustment = -4 276 | 277 | // super-picky detail to adjust the vertical alignment of arrows so that they line up 278 | // well with the pointed-to code text ... 279 | // (if you want to manually adjust tableTop, then ~5 is a reasonable number) 280 | this.arrowOffsetY = Math.floor((this.codeRowHeight / 2) - (SVG_ARROW_HEIGHT / 2)) - teenyAdjustment; 281 | 282 | this.leftGutterSvgInitialized = true; 283 | } 284 | 285 | // assert(this.arrowOffsetY !== undefined); 286 | // assert(this.codeRowHeight !== undefined); 287 | // assert(0 <= this.arrowOffsetY && this.arrowOffsetY <= this.codeRowHeight); 288 | 289 | // assumes that this.owner.updateLineAndExceptionInfo has already 290 | // been run, so line number info is up-to-date! 291 | 292 | // TODO: get rid of this pesky 'owner' dependency 293 | var myViz = this.owner; 294 | var isLastInstr = (myViz.curInstr === (myViz.curTrace.length-1)); 295 | var curEntry = myViz.curTrace[myViz.curInstr]; 296 | var hasError = (curEntry.event === 'exception' || curEntry.event === 'uncaught_exception'); 297 | var isTerminated = (!myViz.instrLimitReached && isLastInstr); 298 | var pcod = this.domRoot.find('#pyCodeOutputDiv'); 299 | 300 | var prevVerticalNudge = myViz.prevLineIsReturn ? Math.floor(this.codeRowHeight / 3) : 0; 301 | var curVerticalNudge = myViz.curLineIsReturn ? Math.floor(this.codeRowHeight / 3) : 0; 302 | 303 | // ugly edge case for the final instruction :0 304 | if (isTerminated && !hasError) { 305 | if (myViz.prevLineNumber !== myViz.curLineNumber) { 306 | curVerticalNudge = curVerticalNudge - 2; 307 | } 308 | } 309 | 310 | if (myViz.prevLineNumber) { 311 | var pla = this.domRootD3.select('#prevLineArrow'); 312 | var translatePrevCmd = 'translate(0, ' + (((myViz.prevLineNumber - 1) * this.codeRowHeight) + this.arrowOffsetY + prevVerticalNudge) + ')'; 313 | if (smoothTransition) { 314 | pla 315 | .transition() 316 | .duration(200) 317 | .attr('fill', 'white') 318 | .each('end', function() { 319 | pla 320 | .attr('transform', translatePrevCmd) 321 | .attr('fill', lightArrowColor); 322 | gutterSVG.find('#prevLineArrow').show(); // show at the end to avoid flickering 323 | }); 324 | } 325 | else { 326 | pla.attr('transform', translatePrevCmd) 327 | gutterSVG.find('#prevLineArrow').show(); 328 | } 329 | } else { 330 | gutterSVG.find('#prevLineArrow').hide(); 331 | } 332 | 333 | if (myViz.curLineNumber) { 334 | var cla = this.domRootD3.select('#curLineArrow'); 335 | var translateCurCmd = 'translate(0, ' + (((myViz.curLineNumber - 1) * this.codeRowHeight) + this.arrowOffsetY + curVerticalNudge) + ')'; 336 | if (smoothTransition) { 337 | cla 338 | .transition() 339 | .delay(200) 340 | .duration(250) 341 | .attr('transform', translateCurCmd); 342 | } else { 343 | cla.attr('transform', translateCurCmd); 344 | } 345 | gutterSVG.find('#curLineArrow').show(); 346 | } 347 | else { 348 | gutterSVG.find('#curLineArrow').hide(); 349 | } 350 | 351 | this.domRootD3.selectAll('#pyCodeOutputDiv .CodeMirror-line ') 352 | .style('border-top', function(d) { 353 | if (hasError && (d.lineNumber == curEntry.line)) { 354 | return '1px solid ' + errorColor; 355 | } 356 | else { 357 | return ''; 358 | } 359 | }) 360 | .style('border-bottom', function(d) { 361 | // COPY AND PASTE ALERT! 362 | if (hasError && (d.lineNumber == curEntry.line)) { 363 | return '1px solid ' + errorColor; 364 | } 365 | else { 366 | return ''; 367 | } 368 | }); 369 | 370 | // returns True iff lineNo is visible in pyCodeOutputDiv 371 | var isOutputLineVisible = (lineNo) => { 372 | return true 373 | /* 374 | var lineNoTd = this.domRoot.find('#lineNo' + lineNo); 375 | var LO = lineNoTd.offset().top; 376 | 377 | var PO = pcod.offset().top; 378 | var ST = pcod.scrollTop(); 379 | var H = pcod.height(); 380 | 381 | // add a few pixels of fudge factor on the bottom end due to bottom scrollbar 382 | return (PO <= LO) && (LO < (PO + H - 30)); 383 | */ 384 | } 385 | 386 | // smoothly scroll pyCodeOutputDiv so that the given line is at the center 387 | var scrollCodeOutputToLine = (lineNo) => { 388 | var lineNoTd = this.domRoot.find('#lineNo' + lineNo); 389 | var LO = lineNoTd.offset().top; 390 | 391 | var PO = pcod.offset().top; 392 | var ST = pcod.scrollTop(); 393 | var H = pcod.height(); 394 | 395 | pcod.stop(); // first stop all previously-queued animations 396 | pcod.animate({scrollTop: (ST + (LO - PO - (Math.round(H / 2))))}, 300); 397 | } 398 | 399 | // smoothly scroll code display 400 | if (!isOutputLineVisible(curEntry.line)) { 401 | scrollCodeOutputToLine(curEntry.line); 402 | } 403 | } 404 | 405 | } // END class CodeDisplay 406 | 407 | export default CodeDisplay 408 | 409 | 410 | function assert(cond) { 411 | if (!cond) { 412 | console.trace(); 413 | alert("Assertion Failure (see console log for backtrace)"); 414 | throw 'Assertion Failure'; 415 | } 416 | } 417 | 418 | function htmlspecialchars(str) { 419 | if (typeof(str) == "string") { 420 | str = str.replace(/&/g, "&"); /* must do & first */ 421 | 422 | // ignore these for now ... 423 | //str = str.replace(/"/g, """); 424 | //str = str.replace(/'/g, "'"); 425 | 426 | str = str.replace(//g, ">"); 428 | 429 | // replace spaces: 430 | str = str.replace(/ /g, " "); 431 | 432 | // replace tab as four spaces: 433 | str = str.replace(/\t/g, "    "); 434 | } 435 | return str; 436 | } 437 | -------------------------------------------------------------------------------- /src/components/PythonTutor/ExecutionVisualizer.js: -------------------------------------------------------------------------------- 1 | 2 | import DataVisualizer from './DataVisualizer' 3 | import NavigationController from './NavigationController' 4 | import ProgramOutputBox from './ProgramOutputBox' 5 | import CodeDisplay from './CodeDisplay' 6 | 7 | let curVisualizerID = 1; 8 | let DEFAULT_EMBEDDED_CODE_DIV_WIDTH = 380; 9 | let DEFAULT_EMBEDDED_CODE_DIV_HEIGHT = 400; 10 | 11 | class ExecutionVisualizer { 12 | 13 | constructor(domRootID, data, params) { 14 | this.params = {}; 15 | this.curInputCode = ''; 16 | this.curTrace = []; 17 | this.codeOutputLines = [] // {text: string, lineNumber: number, executionPoints: number[], this.breakpointHere = boolean}[] = []; 18 | this.promptForUserInput // boolean; 19 | this.userInputPromptStr // string; 20 | this.promptForMouseInput // boolean; 21 | this.curInstr = 0; 22 | this.updataeHistory = []; 23 | this.creationTime // number; 24 | this.pytutor_hooks = {} // {string?: any[]} = {}; 25 | this.prevLineIsReturn // boolean; 26 | this.curLineIsReturn // boolean; 27 | this.prevLineNumber // number; 28 | this.curLineNumber // number; 29 | this.curLineExceptionMsg // string; 30 | this.instrLimitReached = false; 31 | this.instrLimitReachedWarningMsg // string; 32 | this.hasRendered = false; 33 | this.visualizerID // number; 34 | this.breakpoints = d3.map(); // d3.Map<{}> set of execution points to set as breakpoints 35 | this.sortedBreakpointsList = []; // sorted and synced with breakpoints 36 | 37 | 38 | this.curInputCode = data.beforeCode.replace(/\s*$/g, ""); // kill trailing spaces 39 | this.params = params; 40 | 41 | this.props = data 42 | this.curTrace = data.beforeTraces; 43 | 44 | // postprocess the trace 45 | if (this.curTrace.length > 0) { 46 | var lastEntry = this.curTrace[this.curTrace.length - 1]; 47 | 48 | // if the final entry is raw_input or mouse_input, then trim it from the trace and 49 | // set a flag to prompt for user input when execution advances to the 50 | // end of the trace 51 | if (lastEntry.event === 'raw_input') { 52 | this.promptForUserInput = true; 53 | this.userInputPromptStr = htmlspecialchars(lastEntry.prompt); 54 | this.curTrace.pop() // kill last entry so that it doesn't get displayed 55 | } 56 | else if (lastEntry.event === 'mouse_input') { 57 | this.promptForMouseInput = true; 58 | this.userInputPromptStr = htmlspecialchars(lastEntry.prompt); 59 | this.curTrace.pop() // kill last entry so that it doesn't get displayed 60 | } 61 | else if (lastEntry.event === 'instruction_limit_reached') { 62 | this.instrLimitReached = true; 63 | this.instrLimitReachedWarningMsg = lastEntry.exception_msg; 64 | this.curTrace.pop() // postprocess to kill last entry 65 | } 66 | } 67 | 68 | // if you have multiple ExecutionVisualizer on a page, their IDs 69 | // better be unique or else you'll run into rendering problems 70 | if (this.params.visualizerIdOverride) { 71 | this.visualizerID = this.params.visualizerIdOverride; 72 | } else { 73 | this.visualizerID = curVisualizerID; 74 | assert(this.visualizerID > 0); 75 | curVisualizerID++; 76 | } 77 | 78 | // sanitize to avoid nasty 'undefined' states 79 | this.params.disableHeapNesting = (this.params.disableHeapNesting === true); 80 | this.params.drawParentPointers = (this.params.drawParentPointers === true); 81 | this.params.textualMemoryLabels = (this.params.textualMemoryLabels === true); 82 | this.params.showAllFrameLabels = (this.params.showAllFrameLabels === true); 83 | 84 | // insert ExecutionVisualizer into domRootID in the DOM 85 | var tmpRoot = $('#' + domRootID); 86 | var tmpRootD3 = d3.select('#' + domRootID); 87 | tmpRoot.html('
'); 88 | 89 | // the root elements for jQuery and D3 selections, respectively. 90 | // ALWAYS use these and never use raw $(__) or d3.select(__) 91 | this.domRoot = tmpRoot.find('div.ExecutionVisualizer'); 92 | this.domRootD3 = tmpRootD3.select('div.ExecutionVisualizer'); 93 | 94 | if (this.params.lang === 'java') { 95 | this.activateJavaFrontend(); // ohhhh yeah! do this before initializing codeOutputLines (ugh order dependency) 96 | } 97 | 98 | 99 | var lines = this.curInputCode.split('\n'); 100 | for (var i = 0; i < lines.length; i++) { 101 | var cod = lines[i]; 102 | var n: {text: string, lineNumber: number, executionPoints: number[], breakpointHere: boolean} = { 103 | text: cod, 104 | lineNumber: i + 1, 105 | executionPoints: [], 106 | breakpointHere: false, 107 | }; 108 | 109 | $.each(this.curTrace, function(j, elt) { 110 | if (elt.line === n.lineNumber) { 111 | n.executionPoints.push(j); 112 | } 113 | }); 114 | 115 | // if there is a comment containing 'breakpoint' and this line was actually executed, 116 | // then set a breakpoint on this line 117 | var breakpointInComment = false; 118 | var toks = cod.split('#'); 119 | for (var j = 1 /* start at index 1, not 0 */; j < toks.length; j++) { 120 | if (toks[j].indexOf('breakpoint') != -1) { 121 | breakpointInComment = true; 122 | } 123 | } 124 | 125 | if (breakpointInComment && n.executionPoints.length > 0) { 126 | n.breakpointHere = true; 127 | this.addToBreakpoints(n.executionPoints); 128 | } 129 | 130 | this.codeOutputLines.push(n); 131 | } 132 | 133 | this.try_hook("end_constructor", {myViz:this}); 134 | this.render(); // go for it! 135 | } 136 | 137 | /* API for adding a hook, created by David Pritchard 138 | https://github.com/daveagp 139 | 140 | [this documentation is a bit deprecated since Philip made try_hook a 141 | method of ExecutionVisualizer, but the general ideas remain] 142 | 143 | An external user should call 144 | add_pytutor_hook("hook_name_here", function(args) {...}) 145 | args will be a javascript object with several named properties; 146 | this is meant to be similar to Python's keyword arguments. 147 | 148 | The hooked function should return an array whose first element is a boolean: 149 | true if it completely handled the situation (no further hooks 150 | nor the base function should be called); false otherwise (wasn't handled). 151 | If the hook semantically represents a function that returns something, 152 | the second value of the returned array is that semantic return value. 153 | 154 | E.g. for the Java visualizer a simplified version of a hook we use is: 155 | 156 | add_pytutor_hook( 157 | "isPrimitiveType", 158 | function(args) { 159 | var obj = args.obj; // unpack 160 | if (obj instanceof Array && obj[0] == "CHAR-LITERAL") 161 | return [true, true]; // yes we handled it, yes it's primitive 162 | return [false]; // didn't handle it, let someone else 163 | }); 164 | 165 | Hook callbacks can return false or undefined (i.e. no return 166 | value) in lieu of [false]. 167 | 168 | NB: If multiple functions are added to a hook, the oldest goes first. */ 169 | add_pytutor_hook(hook_name, func) { 170 | if (this.pytutor_hooks[hook_name]) 171 | this.pytutor_hooks[hook_name].push(func); 172 | else 173 | this.pytutor_hooks[hook_name] = [func]; 174 | } 175 | 176 | /* [this documentation is a bit deprecated since Philip made try_hook a 177 | method of ExecutionVisualizer, but the general ideas remain] 178 | 179 | try_hook(hook_name, args): how the internal codebase invokes a hook. 180 | 181 | args will be a javascript object with several named properties; 182 | this is meant to be similar to Python's keyword arguments. E.g., 183 | 184 | function isPrimitiveType(obj) { 185 | var hook_result = try_hook("isPrimitiveType", {obj:obj}); 186 | if (hook_result[0]) return hook_result[1]; 187 | // go on as normal if the hook didn't handle it 188 | 189 | Although add_pytutor_hook allows the hooked function to 190 | return false or undefined, try_hook will always return 191 | something with the strict format [false], [true] or [true, ...]. */ 192 | try_hook(hook_name, args) { 193 | if (this.pytutor_hooks[hook_name]) { 194 | for (var i=0; i\ 223 | \ 224 | \ 225 | '); 226 | } 227 | else { 228 | this.domRoot.html('\ 229 | \ 230 | \ 231 |
'); 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('
Visualized using Python Tutor by Philip Guo
'); 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 = '
stdin:
'; 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'); 740 | headerTr.find('td:last').append(elide ? "…" : ind); 741 | 742 | contentTr.append(''); 743 | if (!elide) { 744 | myViz.renderNestedObject(val, stepNum, contentTr.find('td:last')); 745 | ind++; 746 | } 747 | else { 748 | contentTr.find('td:last').append("…"); 749 | ind += val[1]; // val[1] is the number of cells to skip 750 | } 751 | } 752 | } // end of LIST handling 753 | 754 | // Stack and Queue handling code by Will Gwozdz 755 | /* The table produced for stacks and queues is formed slightly differently than the others, 756 | missing the header row. Two rows made the dashed border not line up properly */ 757 | if (obj[0] == 'STACK') { 758 | tbl.append(''); 759 | var contentTr = tbl.find('tr:last'); 760 | contentTr.append(''+''+''); 761 | $.each(obj, function(ind, val) { 762 | if (ind < 1) return; // skip type tag and ID entry 763 | contentTr.append(''); 764 | myViz.renderNestedObject(val, stepNum, contentTr.find('td:last')); 765 | }); 766 | contentTr.append(''+''); 767 | } 768 | 769 | if (obj[0] == 'QUEUE') { 770 | tbl.append(''); 771 | var contentTr = tbl.find('tr:last'); 772 | // Add arrows showing in/out direction 773 | contentTr.append(''+''); 774 | $.each(obj, function(ind, val) { 775 | if (ind < 1) return; // skip type tag and ID entry 776 | contentTr.append(''); 777 | myViz.renderNestedObject(val, stepNum, contentTr.find('td:last')); 778 | }); 779 | contentTr.append(''+''); 780 | } 781 | 782 | return [true]; // did handle 783 | }); 784 | 785 | this.add_pytutor_hook( 786 | "end_renderDataStructures", 787 | function(args) { 788 | var myViz = args.myViz; 789 | myViz.domRoot.find("td.instKey:contains('___NO_LABEL!___')").hide(); 790 | myViz.domRoot.find(".typeLabel:contains('dict')").each( 791 | function(i) { 792 | if ($(this).html()=='dict') 793 | $(this).html('symbol table'); 794 | if ($(this).html()=='empty dict') 795 | $(this).html('empty symbol table'); 796 | }); 797 | }); 798 | 799 | // java synthetics cause things which javascript doesn't like in an id 800 | 801 | // VERY important to bind(this) so that when it's called, 'this' is this current object 802 | var old_generateID = ExecutionVisualizer.prototype.generateID.bind(this); 803 | this.generateID = function(original_id) { 804 | var sanitized = original_id.replace( 805 | /[^0-9a-zA-Z_]/g, 806 | function(match) {return '-'+match.charCodeAt(0)+'-';} 807 | ); 808 | return old_generateID(sanitized); 809 | } 810 | 811 | // utility functions 812 | var entityMap = { 813 | "&": "&", 814 | "<": "<", 815 | ">": ">", 816 | '"': '"', 817 | "'": ''', 818 | "/": '/' 819 | }; 820 | 821 | var escapeHtml = function(string) { 822 | return String(string).replace(/[&<>"'\/]/g, function (s) { 823 | return entityMap[s]; 824 | }); 825 | }; 826 | } 827 | 828 | // update fields corresponding to the current and previously executed lines 829 | // in the trace so that they can be properly highlighted; must call before 830 | // rendering code output (NB: argh pesky ordering dependency) 831 | updateLineAndExceptionInfo() { 832 | var myViz = this; 833 | var totalInstrs = myViz.curTrace.length; 834 | var isLastInstr = myViz.curInstr === (totalInstrs-1); 835 | 836 | myViz.curLineNumber = undefined; 837 | myViz.prevLineNumber = undefined; 838 | myViz.curLineIsReturn = undefined; 839 | myViz.prevLineIsReturn = undefined; 840 | myViz.curLineExceptionMsg = undefined; 841 | 842 | /* if instrLimitReached, then treat like a normal non-terminating line */ 843 | var isTerminated = (!myViz.instrLimitReached && isLastInstr); 844 | 845 | var curLineNumber = null; 846 | var prevLineNumber = null; 847 | 848 | var curEntry = myViz.curTrace[myViz.curInstr]; 849 | 850 | var curIsReturn = (curEntry.event == 'return'); 851 | var prevIsReturn = false; 852 | 853 | if (myViz.curInstr > 0) { 854 | prevLineNumber = myViz.curTrace[myViz.curInstr - 1].line; 855 | prevIsReturn = (myViz.curTrace[myViz.curInstr - 1].event == 'return'); 856 | 857 | /* kinda nutsy hack: if the previous line is a return line, don't 858 | highlight it. instead, highlight the line in the enclosing 859 | function that called this one (i.e., the call site). e.g.,: 860 | 861 | 1. def foo(lst): 862 | 2. return len(lst) 863 | 3. 864 | 4. y = foo([1,2,3]) 865 | 5. print y 866 | 867 | If prevLineNumber is 2 and prevIsReturn, then curLineNumber is 868 | 5, since that's the line that executes right after line 2 869 | finishes. However, this looks confusing to the user since what 870 | actually happened here was that the return value of foo was 871 | assigned to y on line 4. I want to have prevLineNumber be line 872 | 4 so that it gets highlighted. There's no ideal solution, but I 873 | think that looks more sensible, since line 4 was the previous 874 | line that executed *in this function's frame*. 875 | */ 876 | if (prevIsReturn) { 877 | var idx = myViz.curInstr - 1; 878 | var retStack = myViz.curTrace[idx].stack_to_render; 879 | assert(retStack.length > 0); 880 | var retFrameId = retStack[retStack.length - 1].frame_id; 881 | 882 | // now go backwards until we find a 'call' to this frame 883 | while (idx >= 0) { 884 | var entry = myViz.curTrace[idx]; 885 | if (entry.event == 'call' && entry.stack_to_render) { 886 | var topFrame = entry.stack_to_render[entry.stack_to_render.length - 1]; 887 | if (topFrame.frame_id == retFrameId) { 888 | break; // DONE, we found the call that corresponds to this return 889 | } 890 | } 891 | idx--; 892 | } 893 | 894 | // now idx is the index of the 'call' entry. we need to find the 895 | // entry before that, which is the instruction before the call. 896 | // THAT's the line of the call site. 897 | if (idx > 0) { 898 | var callingEntry = myViz.curTrace[idx - 1]; 899 | prevLineNumber = callingEntry.line; // WOOHOO!!! 900 | prevIsReturn = false; // this is now a call site, not a return 901 | } 902 | } 903 | } 904 | 905 | var hasError = false; 906 | if (curEntry.event === 'exception' || curEntry.event === 'uncaught_exception') { 907 | assert(curEntry.exception_msg); 908 | hasError = true; 909 | myViz.curLineExceptionMsg = curEntry.exception_msg; 910 | } 911 | 912 | curLineNumber = curEntry.line; 913 | 914 | // edge case for the final instruction :0 915 | if (isTerminated && !hasError) { 916 | // don't show redundant arrows on the same line when terminated ... 917 | if (prevLineNumber == curLineNumber) { 918 | curLineNumber = null; 919 | } 920 | } 921 | 922 | // add these fields to myViz, which is the point of this function! 923 | myViz.curLineNumber = curLineNumber; 924 | myViz.prevLineNumber = prevLineNumber; 925 | myViz.curLineIsReturn = curIsReturn; 926 | myViz.prevLineIsReturn = prevIsReturn; 927 | } 928 | 929 | isOutputLineVisibleForBubbles(lineDivID) { 930 | var pcod = this.domRoot.find('#pyCodeOutputDiv'); 931 | 932 | var lineNoTd = $('#' + lineDivID); 933 | var LO = lineNoTd.offset().top; 934 | 935 | var PO = pcod.offset().top; 936 | var ST = pcod.scrollTop(); 937 | var H = pcod.height(); 938 | 939 | // add a few pixels of fudge factor on the bottom end due to bottom scrollbar 940 | return (PO <= LO) && (LO < (PO + H - 25)); 941 | } 942 | 943 | } // END class ExecutionVisualizer 944 | 945 | 946 | function assert(cond) { 947 | if (!cond) { 948 | console.trace(); 949 | alert("Assertion Failure (see console log for backtrace)"); 950 | throw 'Assertion Failure'; 951 | } 952 | } 953 | 954 | // taken from http://www.toao.net/32-my-htmlspecialchars-function-for-javascript 955 | function htmlspecialchars(str) { 956 | if (typeof(str) == "string") { 957 | str = str.replace(/&/g, "&"); /* must do & first */ 958 | 959 | // ignore these for now ... 960 | //str = str.replace(/"/g, """); 961 | //str = str.replace(/'/g, "'"); 962 | 963 | str = str.replace(//g, ">"); 965 | 966 | // replace spaces: 967 | str = str.replace(/ /g, " "); 968 | 969 | // replace tab as four spaces: 970 | str = str.replace(/\t/g, "    "); 971 | } 972 | return str; 973 | } 974 | 975 | 976 | // same as htmlspecialchars except don't worry about expanding spaces or 977 | // tabs since we want proper word wrapping in divs. 978 | function htmlsanitize(str) { 979 | if (typeof(str) == "string") { 980 | str = str.replace(/&/g, "&"); /* must do & first */ 981 | 982 | str = str.replace(//g, ">"); 984 | } 985 | return str; 986 | } 987 | 988 | 989 | 990 | // make sure varname doesn't contain any weird 991 | // characters that are illegal for CSS ID's ... 992 | // 993 | // I know for a fact that iterator tmp variables named '_[1]' 994 | // are NOT legal names for CSS ID's. 995 | // I also threw in '{', '}', '(', ')', '<', '>' as illegal characters. 996 | // 997 | // also some variable names are like '.0' (for generator expressions), 998 | // and '.' seems to be illegal. 999 | // 1000 | // also '=', '!', and '?' are common in Ruby names, so escape those as well 1001 | // 1002 | // also spaces are illegal, so convert to '_' 1003 | // TODO: what other characters are illegal??? 1004 | 1005 | function isHeapRef(obj, heap) { 1006 | // ordinary REF 1007 | if (obj[0] === 'REF') { 1008 | return (heap[obj[1]] !== undefined); 1009 | } else if (obj[0] === 'C_DATA' && obj[2] === 'pointer') { 1010 | // C-style pointer that has a valid value 1011 | if (obj[3] != '' && obj[3] != '') { 1012 | return (heap[obj[3]] !== undefined); 1013 | } 1014 | } 1015 | 1016 | return false; 1017 | } 1018 | 1019 | 1020 | export default ExecutionVisualizer 1021 | 1022 | 1023 | 1024 | 1025 | // Constructor with an ever-growing feature-crepped list of options :) 1026 | // domRootID is the string ID of the root element where to render this instance 1027 | // 1028 | // dat is data returned by the Python Tutor backend consisting of two fields: 1029 | // code - string of executed code 1030 | // trace - a full execution trace 1031 | // 1032 | // params is an object containing optional parameters, such as: 1033 | // jumpToEnd - if non-null, jump to the very end of execution if 1034 | // there's no error, or if there's an error, jump to the 1035 | // FIRST ENTRY with an error 1036 | // startingInstruction - the (zero-indexed) execution point to display upon rendering 1037 | // if this is set, then it *overrides* jumpToEnd 1038 | // codeDivHeight - maximum height of #pyCodeOutputDiv (in integer pixels) 1039 | // codeDivWidth - maximum width of #pyCodeOutputDiv (in integer pixels) 1040 | // editCodeBaseURL - the base URL to visit when the user clicks 'Edit code' (if null, then 'Edit code' link hidden) 1041 | // embeddedMode - shortcut for codeDivWidth=DEFAULT_EMBEDDED_CODE_DIV_WIDTH, 1042 | // codeDivHeight=DEFAULT_EMBEDDED_CODE_DIV_HEIGHT 1043 | // (and hide a bunch of other stuff & don't activate keyboard shortcuts!) 1044 | // disableHeapNesting - if true, then render all heap objects at the top level (i.e., no nested objects) 1045 | // drawParentPointers - if true, then draw environment diagram parent pointers for all frames 1046 | // WARNING: there are hard-to-debug MEMORY LEAKS associated with activating this option 1047 | // textualMemoryLabels - render references using textual memory labels rather than as jsPlumb arrows. 1048 | // this is good for slow browsers or when used with disableHeapNesting 1049 | // to prevent "arrow overload" 1050 | // updateOutputCallback - function to call (with 'this' as parameter) 1051 | // whenever this.updateOutput() is called 1052 | // (BEFORE rendering the output display) 1053 | // heightChangeCallback - function to call (with 'this' as parameter) 1054 | // whenever the HEIGHT of #dataViz changes 1055 | // verticalStack - if true, then stack code display ON TOP of visualization 1056 | // (else place side-by-side) 1057 | // visualizerIdOverride - override visualizer ID instead of auto-assigning it 1058 | // (BE CAREFUL ABOUT NOT HAVING DUPLICATE IDs ON THE SAME PAGE, 1059 | // OR ELSE ARROWS AND OTHER STUFF WILL GO HAYWIRE!) 1060 | // executeCodeWithRawInputFunc - function to call when you want to re-execute the given program 1061 | // with some new user input (somewhat hacky!) 1062 | // compactFuncLabels - render functions with a 'func' prefix and no type label 1063 | // showAllFrameLabels - display frame and parent frame labels for all functions (default: false) 1064 | // hideCode - hide the code display and show only the data structure viz 1065 | // lang - to render labels in a style appropriate for other languages, 1066 | // and to display the proper language in langDisplayDiv: 1067 | // 'py2' for Python 2, 'py3' for Python 3, 'js' for JavaScript, 'java' for Java, 1068 | // 'ts' for TypeScript, 'ruby' for Ruby, 'c' for C, 'cpp' for C++ 1069 | // [default is Python-style labels] 1070 | 1071 | 1072 | 1073 | /* 1074 | params: any = {}; 1075 | curInputCode: string; 1076 | curTrace: any[]; 1077 | 1078 | // an array of objects with the following fields: 1079 | // 'text' - the text of the line of code 1080 | // 'lineNumber' - one-indexed (always the array index + 1) 1081 | // 'executionPoints' - an ordered array of zero-indexed execution points where this line was executed 1082 | // 'breakpointHere' - has a breakpoint been set here? 1083 | codeOutputLines: {text: string, lineNumber: number, executionPoints: number[], breakpointHere: boolean}[] = []; 1084 | 1085 | promptForUserInput: boolean; 1086 | userInputPromptStr: string; 1087 | promptForMouseInput: boolean; 1088 | 1089 | codDisplay: CodeDisplay; 1090 | navControls: NavigationController; 1091 | outputBox: ProgramOutputBox; 1092 | dataViz: DataVisualizer; 1093 | 1094 | domRoot: any; 1095 | domRootD3: any; 1096 | 1097 | curInstr: number = 0; 1098 | 1099 | updateHistory: any[]; 1100 | creationTime: number; 1101 | 1102 | // API for adding a hook, created by David Pritchard 1103 | // keys, hook names; values, list of functions 1104 | pytutor_hooks: {string?: any[]} = {}; 1105 | 1106 | // represent the current state of the visualizer object; i.e., which 1107 | // step is it currently visualizing? 1108 | prevLineIsReturn: boolean; 1109 | curLineIsReturn: boolean; 1110 | prevLineNumber: number; 1111 | curLineNumber: number; 1112 | curLineExceptionMsg: string; 1113 | 1114 | // true iff trace ended prematurely since maximum instruction limit has 1115 | // been reached 1116 | instrLimitReached: boolean = false; 1117 | instrLimitReachedWarningMsg: string; 1118 | 1119 | hasRendered: boolean = false; 1120 | 1121 | visualizerID: number; 1122 | 1123 | breakpoints: d3.Map<{}> = d3.map(); // set of execution points to set as breakpoints 1124 | sortedBreakpointsList: any[] = []; // sorted and synced with breakpoints 1125 | */ 1126 | -------------------------------------------------------------------------------- /src/components/PythonTutor/Ladder.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import Highlight from 'react-highlight' 3 | import CodeMirror from 'react-codemirror' 4 | import 'codemirror/mode/python/python' 5 | import Tree from '../Trace/Tree' 6 | import Slider from 'rc-slider' 7 | import Tooltip from 'rc-tooltip' 8 | 9 | 10 | class Ladder extends Component { 11 | constructor(props) { 12 | super(props) 13 | this.state = { 14 | text: '', 15 | level: 0, 16 | max: 0, 17 | marks: {}, 18 | beforeEvents: [], 19 | afterEvents: [], 20 | clicked: false, 21 | events: [], 22 | quizIndex: null, 23 | currentLine: null, 24 | } 25 | window.ladder = this 26 | } 27 | 28 | componentDidMount() { 29 | setTimeout(() => { 30 | this.init() 31 | }, 1500); 32 | } 33 | 34 | init() { 35 | this.generate('before') 36 | this.generate('after') 37 | } 38 | 39 | 40 | onChange(value) { 41 | this.setState({ level: value }, () => { 42 | this.init() 43 | }) 44 | } 45 | 46 | onClick(index, line, event) { 47 | $(event.target).removeClass('primary') 48 | setTimeout(() => { 49 | let target = $(`#hoge .CodeMirror`) 50 | target.popup('toggle') 51 | 52 | this.setState({ quizIndex: index, currentLine: line }) 53 | let top = 75 + parseInt(line)*24 54 | $('.inline-hint').css('top',`${top}px`) 55 | window.cm.addLineClass(line-1, '', 'current-line') 56 | }, 100) 57 | } 58 | 59 | onClose() { 60 | let popup = $('.popup') 61 | if (popup.hasClass('visible')) { 62 | popup.removeClass('visible') 63 | popup.addClass('hidden') 64 | } 65 | window.cm.removeLineClass(this.state.currentLine-1, '', 'current-line') 66 | this.setState({ quizIndex: null, currentLine: null }) 67 | } 68 | 69 | 70 | onMouseOver(line) { 71 | return false 72 | let popup = $('.popup') 73 | if (!popup.hasClass('visible')) { 74 | window.cm.addLineClass(line-1, '', 'current-line') 75 | } 76 | } 77 | 78 | onMouseOut(line) { 79 | return false 80 | let popup = $('.popup') 81 | if (!popup.hasClass('visible')) { 82 | window.cm.removeLineClass(line-1, '', 'current-line') 83 | } 84 | } 85 | 86 | generate(type) { 87 | let events, asts, key 88 | if (type === 'before') { 89 | events = this.props.beforeEvents 90 | asts = this.props.beforeAst 91 | key = 'beforeEvents' 92 | } else { 93 | events = this.props.afterEvents 94 | asts = this.props.afterAst 95 | key = 'afterEvents' 96 | } 97 | 98 | 99 | let indent = 0 100 | events = events.map((event) => { 101 | let focusKeys = _.union(Object.keys(this.props.beforeHistory), Object.keys(this.props.afterHistory)).map((key) => { 102 | if (_.isEqual(this.props.beforeHistory[key], this.props.afterHistory[key])) return false 103 | return key 104 | }).filter(key => key) 105 | if (!focusKeys.includes(event.key)) return false 106 | if (event.builtin) return false 107 | if (event.type === 'call' && event.children.length === 0) return false 108 | 109 | let trimmedEvents = events.slice(0, event.id) 110 | let history = {} 111 | for (let e of trimmedEvents) { 112 | history[e.key] = e 113 | } 114 | 115 | let ast = asts[event.line-1] 116 | let tree = new Tree() 117 | 118 | try { 119 | tree.history = history 120 | tree.analyze(ast) 121 | event.updates = tree.updates 122 | return event 123 | } catch (err) { 124 | event.updates = [] 125 | return event 126 | } 127 | }).filter(event => event) 128 | 129 | let max = this.state.max 130 | events = events.map((event) => { 131 | let updates = _.uniq(event.updates).reverse() 132 | let value = updates[this.state.level] 133 | if (value === undefined) value = _.last(updates) 134 | if (value === undefined) value = event.value 135 | 136 | max = Math.max(max, updates.length - 1) 137 | 138 | switch (event.type) { 139 | case 'call': 140 | event.call = event.children[0] 141 | event.html = [ 142 | { className: 'normal', text: 'call ' }, 143 | { className: 'keyword', text: event.call }, 144 | ] 145 | indent++ 146 | event.indent = indent 147 | break 148 | case 'return': 149 | event.html = [ 150 | { className: 'keyword', text: event.key }, 151 | { className: 'normal', text: ' returns ' }, 152 | { className: 'number', text: value }, 153 | ] 154 | event.indent = indent 155 | indent-- 156 | break 157 | default: 158 | event.html = [ 159 | { className: 'keyword', text: event.key }, 160 | { className: 'normal', text: ' = ' }, 161 | { className: 'number', text: value }, 162 | ] 163 | event.indent = indent 164 | break 165 | } 166 | return event 167 | }) 168 | 169 | let state = {} 170 | let marks = {} 171 | marks[0] = 'concrete' 172 | marks[max] = 'abstract' 173 | state['max'] = max 174 | state['marks'] = marks 175 | state[key] = events 176 | this.setState(state) 177 | } 178 | 179 | 180 | translate(event, index) { 181 | return ( 182 |
183 |

187 | { event.html.map((html, index) => { 188 | return { html.text } 189 | }) } 190 | {/* 191 |   192 | why ? 193 | */} 194 | 195 |

196 |
197 | ) 198 | } 199 | 200 | render() { 201 | /* 202 | $('#hoge .CodeMirror').popup({ 203 | target: $('#hoge .CodeMirror'), 204 | position: 'bottom center', 205 | inline: true, 206 | popup : $(`.inline-hint`), 207 | on: 'manual', 208 | }) 209 | */ 210 | 211 | return ( 212 |
213 |
214 |
215 |

Result

216 |

217 |               { this.state.beforeEvents.map((event, index) => {
218 |                 return this.translate(event, index)
219 |               }) }
220 |             
221 |
222 |
223 |

Expected

224 |

225 |               { this.state.afterEvents.map((event, index) => {
226 |                 return this.translate(event, index)
227 |               }) }
228 |             
229 |
230 | 231 |
232 |
233 | 242 |
243 |
244 |
245 | 246 | {/* 247 |
248 | { this.state.beforeEvents.map((event, index) => { 249 | let question = '' 250 | question += 'Q. Why ' 251 | question += event.key 252 | if (event.type === 'return') { 253 | question += ' returns ' 254 | } 255 | if (event.type === 'assign') { 256 | if (event.index === 0) { 257 | question += ' is initialized with ' 258 | } else { 259 | question += ' is updated to ' 260 | } 261 | } 262 | question += event.value 263 | question += ' ?' 264 | let events = this.props.beforeEvents.slice(0, event.id) 265 | let history = {} 266 | for (let e of events) { 267 | history[e.key] = e 268 | } 269 | 270 | return ( 271 |
272 |

{ question }

273 | 283 |
284 | ) 285 | }) } 286 | 287 |
288 | */} 289 | 290 | 291 |
292 | ) 293 | } 294 | } 295 | 296 | export default Ladder 297 | 298 | 299 | const Handle = Slider.Handle; 300 | const handle = (props) => { 301 | const { value, dragging, index, ...restProps } = props; 302 | return ( 303 | 309 | 310 | 311 | ); 312 | }; 313 | -------------------------------------------------------------------------------- /src/components/PythonTutor/NavigationController.js: -------------------------------------------------------------------------------- 1 | 2 | class NavigationController { 3 | 4 | constructor(owner, domRoot, domRootD3, nSteps) { 5 | this.owner = owner; 6 | this.domRoot = domRoot; 7 | this.domRootD3 = domRootD3; 8 | this.nSteps = nSteps; 9 | 10 | var navHTML = '