├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── browser ├── examples │ ├── generators.js │ └── ycomb.js ├── index.html ├── metajs.coffee ├── style.css └── vendor │ ├── codemirror.css │ ├── codemirror.js │ ├── javascript.js │ └── jquery-1.9.1.min.js ├── lib ├── interpreter.coffee └── util.coffee ├── package.json ├── repl.coffee └── tests ├── control.js ├── es6 ├── for-of-loop.expected ├── for-of-loop.js ├── generators.expected └── generators.js ├── objects.js ├── reflection.js └── scope.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.swp 3 | *.swo 4 | *~ 5 | *.actual 6 | *.expected 7 | browser/*.js 8 | lib/*.js -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2013 int3, omphalos 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | TESTS = $(wildcard tests/*.js) 2 | ES6TESTS = $(wildcard tests/es6/*.js) 3 | LIB_COFFEE = $(wildcard lib/*.coffee) 4 | LIB_JS = $(LIB_COFFEE:.coffee=.js) 5 | BROWSER_COFFEE = $(wildcard browser/*.coffee) 6 | 7 | browser: $(BROWSER_COFFEE:.coffee=.js) browser/bundle.js 8 | 9 | test: $(TESTS:.js=.result) $(ES6TESTS:.js=.result) $(LIB_JS) 10 | echo $(LIB_JS) 11 | 12 | %.actual: %.js $(LIB_JS) repl.js 13 | @echo "testing $<... \c" 14 | @node repl.js $< > $@ 15 | 16 | %.expected: %.js 17 | @node $? > $@ 18 | 19 | %.result: %.actual %.expected 20 | @diff $? 21 | @echo "passed" 22 | 23 | browser/bundle.js: $(LIB_JS) 24 | browserify $^ -o browser/bundle.js --exports require 25 | 26 | %.js: %.coffee 27 | iced -c -I browserify $< 28 | 29 | .SECONDARY: $(LIB_JS) 30 | .PHONY: browser 31 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | metajs 2 | ====== 3 | 4 | A CPS Javascript metacircular interpreter that visualizes script execution. 5 | 6 | Written in [IcedCoffeeScript][2]. Uses [Esprima][1] for the parser and 7 | [CodeMirror][3] for the front-end. 8 | 9 | Setup 10 | ----- 11 | 12 | npm install 13 | npm install -g browserify@1.17.3 iced-coffee-script@1.4.0-c 14 | 15 | Usage 16 | ----- 17 | 18 | To start the REPL: 19 | 20 | ./repl.coffee 21 | 22 | To execute a file: 23 | 24 | ./repl.coffee [filename] 25 | 26 | To run in the browser: 27 | 28 | make browser 29 | cd browser 30 | python -m SimpleHTTPServer 31 | 32 | Then point your browser to http://localhost:8000/. 33 | 34 | Testing 35 | ------- 36 | 37 | make test 38 | 39 | Contributors 40 | ------------ 41 | 42 | * [omphalos](https://github.com/omphalos) 43 | 44 | [1]: http://esprima.org/ 45 | [2]: http://maxtaco.github.com/coffee-script/ 46 | [3]: http://codemirror.net/ 47 | -------------------------------------------------------------------------------- /browser/examples/generators.js: -------------------------------------------------------------------------------- 1 | var gen = function*() { 2 | for (var i = 0; i < 3; i++) { 3 | var inner = innerGen(i + 1); 4 | yield* inner; 5 | } 6 | }; 7 | 8 | var innerGen = function*(j) { 9 | for (var i = 0; i < 3; i++) 10 | yield i * j; 11 | }; 12 | 13 | for (var n of gen()) { 14 | console.log(n); 15 | } 16 | -------------------------------------------------------------------------------- /browser/examples/ycomb.js: -------------------------------------------------------------------------------- 1 | var Y = function (F) { 2 | return (function (x) { 3 | return F(function (y) { return x(x)(y); }); 4 | })(function (x) { 5 | return F(function (y) { return x(x)(y); }); 6 | }); 7 | }; 8 | 9 | var factorial = function (self) { 10 | return function(n) { 11 | return n === 0 ? 1 : n * self(n-1); 12 | }; 13 | }; 14 | 15 | var result; 16 | console.log(result = Y(factorial)(4)); 17 | -------------------------------------------------------------------------------- /browser/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | metajs: visualize javascript AST execution 4 | 5 | 6 | 7 | 8 | 9 | 20 | 21 | 22 | 26 |
27 |
28 | 29 | 33 |
34 |
Expression Stack
35 |
Environment
36 |
37 | 38 |
39 |
40 |
41 |
42 | 43 | 44 | 45 |
46 | 50 |
51 | 52 | 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /browser/metajs.coffee: -------------------------------------------------------------------------------- 1 | esprima = require 'esprima' 2 | {Util} = require './lib/util' 3 | interpreter = require './lib/interpreter' 4 | {Environment} = interpreter 5 | 6 | Message = do -> 7 | messageMap = {} 8 | silence = {} 9 | { 10 | listen: (msg, cb) -> (messageMap[msg] ?= []).push cb 11 | once: (msg, cb) -> 12 | @listen msg, (args) -> 13 | cb args... 14 | for fn, i in messageMap[msg] 15 | if fn is cb 16 | messageMap[msg].splice i, 1 17 | break 18 | squelch: (msg) -> silence[msg] = true 19 | unsquelch: (msg) -> delete silence[msg] 20 | send: (msg, args...) -> 21 | return if msg of silence 22 | for cb in messageMap[msg] 23 | cb args... 24 | } 25 | 26 | activeStates = [] 27 | 28 | await $(document).ready defer() 29 | 30 | Continuers = 31 | toFinish: (cont, v) -> cont v 32 | toNextStep: (cont, v) -> 33 | if cont is Continuations.bottom 34 | cont v 35 | else 36 | Continuations.next = -> cont v 37 | autoStep: (cont, v) -> 38 | await setTimeout defer(), 400 39 | cont v 40 | 41 | interpreter.evaluate = do (original = interpreter.evaluate) -> 42 | interpreter.continuer = -> Continuers.toFinish 43 | (node, env, cont, errCont) -> 44 | Message.send 'interpreter:eval', node, env, cont 45 | newCont = (v) -> 46 | Message.send 'interpreter:continue', node, env, cont, v 47 | await interpreter.continuer defer(w), v 48 | Message.send 'interpreter:call-continue' 49 | cont w 50 | original node, env, newCont, errCont 51 | 52 | Message.listen 'interpreter:eval', (node, env, cont) -> 53 | cont.id = activeStates.length 54 | activeStates.push {node, env} 55 | 56 | Message.listen 'interpreter:continue', (node, env, cont, v) -> 57 | activeStates.length = cont.id + 1 58 | Util.last(activeStates).value = v 59 | Message.send 'state:render' 60 | 61 | Message.listen 'interpreter:call-continue', -> 62 | activeStates.pop() 63 | 64 | Message.listen 'interpreter:done', -> Message.send 'state:render' 65 | 66 | editor = $('.CodeMirror')[0].CodeMirror 67 | 68 | editor.disableEditing = () -> 69 | if not $('.CodeMirror').hasClass('readOnly') 70 | editor.setOption('readOnly', true) 71 | $('.CodeMirror').addClass('readOnly') 72 | 73 | editor.enableEditing = () -> 74 | if $('.CodeMirror').hasClass('readOnly') 75 | editor.setOption('readOnly', false) 76 | $('.CodeMirror').removeClass('readOnly') 77 | 78 | editor.on 'change', -> 79 | activeStates = [] 80 | Message.send 'state:render' 81 | Continuations.bottom() 82 | 83 | editor.on 'focus', -> 84 | mark.clear() if (mark = $('#code').data('mark'))? 85 | 86 | loadFile = -> 87 | url = $('#example-box option:selected')[0].getAttribute('href') 88 | await $.ajax(url:url,dataType:'text').done defer(data) 89 | editor.setValue data 90 | 91 | $('#example-box').change loadFile 92 | 93 | loadFile() 94 | 95 | class Continuations 96 | @bottom: => 97 | Message.send 'interpreter:done' 98 | @next = @top 99 | 100 | @top: => 101 | ast = esprima.parse editor.getValue(), loc: true 102 | interpreter.evaluate ast, new Environment, @bottom, (e) => 103 | console.log "Error: #{e}", @bottom() 104 | 105 | @next: @top 106 | 107 | RenderUtils = 108 | pprintNode: (node) -> 109 | switch node.type 110 | when 'Identifier' 111 | "Identifier '#{node.name}'" 112 | when 'VariableDeclarator' 113 | "VariableDeclarator '#{node.id.name}'" 114 | else 115 | node.type 116 | 117 | pprintValue: (value) -> 118 | if value instanceof interpreter.CPSFunction or typeof value is 'function' 119 | 'function' 120 | else if value is null or value is undefined 121 | $('', text: value + '', class: 'atom-value') 122 | else if typeof value is 'number' 123 | $('', text: value, class: 'number-value') 124 | else if typeof value is 'object' 125 | a = $('', text: value, href: '#', class:'object') 126 | a.data 'value', value 127 | else 128 | value + "" 129 | 130 | htmlifyObject: (obj) -> 131 | rv = $("
") 132 | for k,v of obj 133 | rv.append "#{k}: ", (@pprintValue v), "
" 134 | if rv.html() is '' 135 | rv.append "No enumerable properties found" 136 | else 137 | rv 138 | 139 | selectionFromNode: (node) -> 140 | {start, end} = node.loc 141 | [{line:start.line-1,ch:start.column}, {line:end.line-1,ch:end.column}] 142 | 143 | Message.listen 'state:render', -> 144 | activeStatesDisplay = $('#activeStates > ul') 145 | activeStatesDisplay.html('') 146 | for state in activeStates 147 | content = $('', text: (RenderUtils.pprintNode state.node), class:'node') 148 | content.data 'node', state.node 149 | li = activeStatesDisplay.append $('
  • ', html: content) 150 | content.after " → #{RenderUtils.pprintValue state.value}" if state.value 151 | 152 | envDisplay = $('#currentEnv') 153 | envDisplay.html '' 154 | 155 | unless activeStates.length > 0 156 | editor.setSelection editor.getCursor() # deselect 157 | else 158 | mark.clear() if (mark = $('#code').data('mark'))? 159 | latest = Util.last(activeStates) 160 | $('#code').data 'mark', 161 | editor.markText (RenderUtils.selectionFromNode latest.node)..., className: 'execlight' 162 | for scope in latest.env.scopeChain 163 | envDisplay.append $('
    ', html: ul = $('