├── .gitignore ├── MISSING.md ├── Makefile ├── README.md ├── browser.js ├── demos ├── simple_grammar.jison ├── simple_parser.js ├── simple_parsing.js └── tree_of_nodes.js ├── eval.js ├── grammar.jison ├── lexer.js ├── nodes.js ├── package.json ├── runtime.js ├── samples ├── functions.js ├── hoisting.js ├── inheritance.js ├── objects.js ├── operators.js ├── parameters.js ├── primitives.js ├── prototype.js └── scopes.js ├── scripts └── compare_outputs.sh ├── test ├── interpreter_test.js ├── lexer_test.js ├── mocha.opts ├── parser_test.js └── runtime_test.js ├── tiny.js └── tokens.jisonlex /.gitignore: -------------------------------------------------------------------------------- 1 | parser.js 2 | tiny-bundle.js 3 | bundle.js 4 | docs 5 | node_modules -------------------------------------------------------------------------------- /MISSING.md: -------------------------------------------------------------------------------- 1 | # Things not supported by tiny.js 2 | - Other operators (only * and + are implemented) 3 | - if, while, for 4 | - Named functions 5 | - new on properties, eg.: new object.Constructor() 6 | - Escape sequence in strings 7 | - A bunch more things ... -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Use jison bundled in node_modules. 2 | # If installed globally, you could use simply: jison [args] 3 | JISON = node node_modules/jison/lib/cli.js 4 | MOCHA = node node_modules/mocha/bin/mocha 5 | 6 | all: parser.js demos/simple_parser.js 7 | 8 | browserify: all 9 | browserify browser.js -o bundle.js 10 | 11 | parser.js: grammar.jison tokens.jisonlex 12 | ${JISON} $^ -o $@ 13 | 14 | demos/simple_parser.js: demos/simple_grammar.jison tokens.jisonlex 15 | ${JISON} $^ -o $@ 16 | 17 | test: parser.js 18 | ${MOCHA} 19 | make test-samples 20 | 21 | test-samples: 22 | @for f in samples/*.js; do sh scripts/compare_outputs.sh $$f; done 23 | 24 | test-lexer: parser.js 25 | ${MOCHA} test/lexer_test.js 26 | 27 | test-parser: parser.js 28 | ${MOCHA} test/parser_test.js 29 | 30 | test-runtime: parser.js 31 | ${MOCHA} test/runtime_test.js 32 | 33 | test-interpreter: parser.js 34 | ${MOCHA} test/interpreter_test.js 35 | 36 | .PHONY: browserify test test-samples test-lexer test-parser test-runtime test-interpreter -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Tiny.js - A tiny subset of JavaScript in JavaScript 2 | 3 | See samples/ and MISSING.md for what it can and can't run. 4 | 5 | ### Usage 6 | 7 | $ npm install 8 | $ make 9 | $ node tiny.js 10 | 11 | ### License 12 | 13 | Copyright 2013 Coded Inc. marc@codedinc.com 14 | 15 | You are authorized to use, copy and modify expect for teaching how programming languages work. 16 | -------------------------------------------------------------------------------- /browser.js: -------------------------------------------------------------------------------- 1 | // Expose TinyJs to be used in the browser. 2 | // Bundle requires using: $ browserify browser.js -o bundle.js 3 | 4 | var parser = require('./parser').parser; 5 | var simpleParser = require('./demos/simple_parser').parser; 6 | var lexer = require('./lexer'); 7 | var eval = require('./eval'); 8 | var nodes = require('./nodes'); 9 | var runtime = require('./runtime'); 10 | 11 | window.tinyjs = { 12 | nodes: nodes, 13 | runtime: runtime, 14 | 15 | parser: parser, 16 | simpleParser: simpleParser, 17 | 18 | lexer: lexer, 19 | 20 | eval: function(code) { 21 | node = parser.parse(code); 22 | return node.eval(runtime.root); 23 | } 24 | } -------------------------------------------------------------------------------- /demos/simple_grammar.jison: -------------------------------------------------------------------------------- 1 | %start program // Tell which rule to start with. 2 | 3 | %% 4 | 5 | program: 6 | statements EOF { return $1; } 7 | ; 8 | 9 | statements: 10 | statement { $$ = [ $1 ] } 11 | | statements ';' statement { $1.push($3); // $1 (statements) is the array created on previous line 12 | $$ = $1 } 13 | ; 14 | 15 | statement: 16 | NUMBER { $$ = $1 } 17 | | STRING { $$ = $1 } 18 | ; -------------------------------------------------------------------------------- /demos/simple_parser.js: -------------------------------------------------------------------------------- 1 | /* parser generated by jison 0.4.13 */ 2 | /* 3 | Returns a Parser object of the following structure: 4 | 5 | Parser: { 6 | yy: {} 7 | } 8 | 9 | Parser.prototype: { 10 | yy: {}, 11 | trace: function(), 12 | symbols_: {associative list: name ==> number}, 13 | terminals_: {associative list: number ==> name}, 14 | productions_: [...], 15 | performAction: function anonymous(yytext, yyleng, yylineno, yy, yystate, $$, _$), 16 | table: [...], 17 | defaultActions: {...}, 18 | parseError: function(str, hash), 19 | parse: function(input), 20 | 21 | lexer: { 22 | EOF: 1, 23 | parseError: function(str, hash), 24 | setInput: function(input), 25 | input: function(), 26 | unput: function(str), 27 | more: function(), 28 | less: function(n), 29 | pastInput: function(), 30 | upcomingInput: function(), 31 | showPosition: function(), 32 | test_match: function(regex_match_array, rule_index), 33 | next: function(), 34 | lex: function(), 35 | begin: function(condition), 36 | popState: function(), 37 | _currentRules: function(), 38 | topState: function(), 39 | pushState: function(condition), 40 | 41 | options: { 42 | ranges: boolean (optional: true ==> token location info will include a .range[] member) 43 | flex: boolean (optional: true ==> flex-like lexing behaviour where the rules are tested exhaustively to find the longest match) 44 | backtrack_lexer: boolean (optional: true ==> lexer regexes are tested in order and for each matching regex the action code is invoked; the lexer terminates the scan when a token is returned by the action code) 45 | }, 46 | 47 | performAction: function(yy, yy_, $avoiding_name_collisions, YY_START), 48 | rules: [...], 49 | conditions: {associative list: name ==> set}, 50 | } 51 | } 52 | 53 | 54 | token location info (@$, _$, etc.): { 55 | first_line: n, 56 | last_line: n, 57 | first_column: n, 58 | last_column: n, 59 | range: [start_number, end_number] (where the numbers are indexes into the input string, regular zero-based) 60 | } 61 | 62 | 63 | the parseError function receives a 'hash' object with these members for lexer and parser errors: { 64 | text: (matched text) 65 | token: (the produced terminal token, if any) 66 | line: (yylineno) 67 | } 68 | while parser (grammar) errors will also provide these members, i.e. parser errors deliver a superset of attributes: { 69 | loc: (yylloc) 70 | expected: (string describing the set of expected tokens) 71 | recoverable: (boolean: TRUE when the parser has a error recovery rule available for this particular error) 72 | } 73 | */ 74 | var simple_parser = (function(){ 75 | var parser = {trace: function trace() { }, 76 | yy: {}, 77 | symbols_: {"error":2,"program":3,"statements":4,"EOF":5,"statement":6,";":7,"NUMBER":8,"STRING":9,"$accept":0,"$end":1}, 78 | terminals_: {2:"error",5:"EOF",7:";",8:"NUMBER",9:"STRING"}, 79 | productions_: [0,[3,2],[4,1],[4,3],[6,1],[6,1]], 80 | performAction: function anonymous(yytext, yyleng, yylineno, yy, yystate /* action[1] */, $$ /* vstack */, _$ /* lstack */) { 81 | /* this == yyval */ 82 | 83 | var $0 = $$.length - 1; 84 | switch (yystate) { 85 | case 1: return $$[$0-1]; 86 | break; 87 | case 2: this.$ = [ $$[$0] ] 88 | break; 89 | case 3: $$[$0-2].push($$[$0]); // $$[$0-2] (statements) is the array created on previous line 90 | this.$ = $$[$0-2] 91 | break; 92 | case 4: this.$ = $$[$0] 93 | break; 94 | case 5: this.$ = $$[$0] 95 | break; 96 | } 97 | }, 98 | table: [{3:1,4:2,6:3,8:[1,4],9:[1,5]},{1:[3]},{5:[1,6],7:[1,7]},{5:[2,2],7:[2,2]},{5:[2,4],7:[2,4]},{5:[2,5],7:[2,5]},{1:[2,1]},{6:8,8:[1,4],9:[1,5]},{5:[2,3],7:[2,3]}], 99 | defaultActions: {6:[2,1]}, 100 | parseError: function parseError(str, hash) { 101 | if (hash.recoverable) { 102 | this.trace(str); 103 | } else { 104 | throw new Error(str); 105 | } 106 | }, 107 | parse: function parse(input) { 108 | var self = this, stack = [0], vstack = [null], lstack = [], table = this.table, yytext = '', yylineno = 0, yyleng = 0, recovering = 0, TERROR = 2, EOF = 1; 109 | var args = lstack.slice.call(arguments, 1); 110 | this.lexer.setInput(input); 111 | this.lexer.yy = this.yy; 112 | this.yy.lexer = this.lexer; 113 | this.yy.parser = this; 114 | if (typeof this.lexer.yylloc == 'undefined') { 115 | this.lexer.yylloc = {}; 116 | } 117 | var yyloc = this.lexer.yylloc; 118 | lstack.push(yyloc); 119 | var ranges = this.lexer.options && this.lexer.options.ranges; 120 | if (typeof this.yy.parseError === 'function') { 121 | this.parseError = this.yy.parseError; 122 | } else { 123 | this.parseError = Object.getPrototypeOf(this).parseError; 124 | } 125 | function popStack(n) { 126 | stack.length = stack.length - 2 * n; 127 | vstack.length = vstack.length - n; 128 | lstack.length = lstack.length - n; 129 | } 130 | function lex() { 131 | var token; 132 | token = self.lexer.lex() || EOF; 133 | if (typeof token !== 'number') { 134 | token = self.symbols_[token] || token; 135 | } 136 | return token; 137 | } 138 | var symbol, preErrorSymbol, state, action, a, r, yyval = {}, p, len, newState, expected; 139 | while (true) { 140 | state = stack[stack.length - 1]; 141 | if (this.defaultActions[state]) { 142 | action = this.defaultActions[state]; 143 | } else { 144 | if (symbol === null || typeof symbol == 'undefined') { 145 | symbol = lex(); 146 | } 147 | action = table[state] && table[state][symbol]; 148 | } 149 | if (typeof action === 'undefined' || !action.length || !action[0]) { 150 | var errStr = ''; 151 | expected = []; 152 | for (p in table[state]) { 153 | if (this.terminals_[p] && p > TERROR) { 154 | expected.push('\'' + this.terminals_[p] + '\''); 155 | } 156 | } 157 | if (this.lexer.showPosition) { 158 | errStr = 'Parse error on line ' + (yylineno + 1) + ':\n' + this.lexer.showPosition() + '\nExpecting ' + expected.join(', ') + ', got \'' + (this.terminals_[symbol] || symbol) + '\''; 159 | } else { 160 | errStr = 'Parse error on line ' + (yylineno + 1) + ': Unexpected ' + (symbol == EOF ? 'end of input' : '\'' + (this.terminals_[symbol] || symbol) + '\''); 161 | } 162 | this.parseError(errStr, { 163 | text: this.lexer.match, 164 | token: this.terminals_[symbol] || symbol, 165 | line: this.lexer.yylineno, 166 | loc: yyloc, 167 | expected: expected 168 | }); 169 | } 170 | if (action[0] instanceof Array && action.length > 1) { 171 | throw new Error('Parse Error: multiple actions possible at state: ' + state + ', token: ' + symbol); 172 | } 173 | switch (action[0]) { 174 | case 1: 175 | stack.push(symbol); 176 | vstack.push(this.lexer.yytext); 177 | lstack.push(this.lexer.yylloc); 178 | stack.push(action[1]); 179 | symbol = null; 180 | if (!preErrorSymbol) { 181 | yyleng = this.lexer.yyleng; 182 | yytext = this.lexer.yytext; 183 | yylineno = this.lexer.yylineno; 184 | yyloc = this.lexer.yylloc; 185 | if (recovering > 0) { 186 | recovering--; 187 | } 188 | } else { 189 | symbol = preErrorSymbol; 190 | preErrorSymbol = null; 191 | } 192 | break; 193 | case 2: 194 | len = this.productions_[action[1]][1]; 195 | yyval.$ = vstack[vstack.length - len]; 196 | yyval._$ = { 197 | first_line: lstack[lstack.length - (len || 1)].first_line, 198 | last_line: lstack[lstack.length - 1].last_line, 199 | first_column: lstack[lstack.length - (len || 1)].first_column, 200 | last_column: lstack[lstack.length - 1].last_column 201 | }; 202 | if (ranges) { 203 | yyval._$.range = [ 204 | lstack[lstack.length - (len || 1)].range[0], 205 | lstack[lstack.length - 1].range[1] 206 | ]; 207 | } 208 | r = this.performAction.apply(yyval, [ 209 | yytext, 210 | yyleng, 211 | yylineno, 212 | this.yy, 213 | action[1], 214 | vstack, 215 | lstack 216 | ].concat(args)); 217 | if (typeof r !== 'undefined') { 218 | return r; 219 | } 220 | if (len) { 221 | stack = stack.slice(0, -1 * len * 2); 222 | vstack = vstack.slice(0, -1 * len); 223 | lstack = lstack.slice(0, -1 * len); 224 | } 225 | stack.push(this.productions_[action[1]][0]); 226 | vstack.push(yyval.$); 227 | lstack.push(yyval._$); 228 | newState = table[stack[stack.length - 2]][stack[stack.length - 1]]; 229 | stack.push(newState); 230 | break; 231 | case 3: 232 | return true; 233 | } 234 | } 235 | return true; 236 | }}; 237 | /* generated by jison-lex 0.2.1 */ 238 | var lexer = (function(){ 239 | var lexer = { 240 | 241 | EOF:1, 242 | 243 | parseError:function parseError(str, hash) { 244 | if (this.yy.parser) { 245 | this.yy.parser.parseError(str, hash); 246 | } else { 247 | throw new Error(str); 248 | } 249 | }, 250 | 251 | // resets the lexer, sets new input 252 | setInput:function (input) { 253 | this._input = input; 254 | this._more = this._backtrack = this.done = false; 255 | this.yylineno = this.yyleng = 0; 256 | this.yytext = this.matched = this.match = ''; 257 | this.conditionStack = ['INITIAL']; 258 | this.yylloc = { 259 | first_line: 1, 260 | first_column: 0, 261 | last_line: 1, 262 | last_column: 0 263 | }; 264 | if (this.options.ranges) { 265 | this.yylloc.range = [0,0]; 266 | } 267 | this.offset = 0; 268 | return this; 269 | }, 270 | 271 | // consumes and returns one char from the input 272 | input:function () { 273 | var ch = this._input[0]; 274 | this.yytext += ch; 275 | this.yyleng++; 276 | this.offset++; 277 | this.match += ch; 278 | this.matched += ch; 279 | var lines = ch.match(/(?:\r\n?|\n).*/g); 280 | if (lines) { 281 | this.yylineno++; 282 | this.yylloc.last_line++; 283 | } else { 284 | this.yylloc.last_column++; 285 | } 286 | if (this.options.ranges) { 287 | this.yylloc.range[1]++; 288 | } 289 | 290 | this._input = this._input.slice(1); 291 | return ch; 292 | }, 293 | 294 | // unshifts one char (or a string) into the input 295 | unput:function (ch) { 296 | var len = ch.length; 297 | var lines = ch.split(/(?:\r\n?|\n)/g); 298 | 299 | this._input = ch + this._input; 300 | this.yytext = this.yytext.substr(0, this.yytext.length - len - 1); 301 | //this.yyleng -= len; 302 | this.offset -= len; 303 | var oldLines = this.match.split(/(?:\r\n?|\n)/g); 304 | this.match = this.match.substr(0, this.match.length - 1); 305 | this.matched = this.matched.substr(0, this.matched.length - 1); 306 | 307 | if (lines.length - 1) { 308 | this.yylineno -= lines.length - 1; 309 | } 310 | var r = this.yylloc.range; 311 | 312 | this.yylloc = { 313 | first_line: this.yylloc.first_line, 314 | last_line: this.yylineno + 1, 315 | first_column: this.yylloc.first_column, 316 | last_column: lines ? 317 | (lines.length === oldLines.length ? this.yylloc.first_column : 0) 318 | + oldLines[oldLines.length - lines.length].length - lines[0].length : 319 | this.yylloc.first_column - len 320 | }; 321 | 322 | if (this.options.ranges) { 323 | this.yylloc.range = [r[0], r[0] + this.yyleng - len]; 324 | } 325 | this.yyleng = this.yytext.length; 326 | return this; 327 | }, 328 | 329 | // When called from action, caches matched text and appends it on next action 330 | more:function () { 331 | this._more = true; 332 | return this; 333 | }, 334 | 335 | // When called from action, signals the lexer that this rule fails to match the input, so the next matching rule (regex) should be tested instead. 336 | reject:function () { 337 | if (this.options.backtrack_lexer) { 338 | this._backtrack = true; 339 | } else { 340 | return this.parseError('Lexical error on line ' + (this.yylineno + 1) + '. You can only invoke reject() in the lexer when the lexer is of the backtracking persuasion (options.backtrack_lexer = true).\n' + this.showPosition(), { 341 | text: "", 342 | token: null, 343 | line: this.yylineno 344 | }); 345 | 346 | } 347 | return this; 348 | }, 349 | 350 | // retain first n characters of the match 351 | less:function (n) { 352 | this.unput(this.match.slice(n)); 353 | }, 354 | 355 | // displays already matched input, i.e. for error messages 356 | pastInput:function () { 357 | var past = this.matched.substr(0, this.matched.length - this.match.length); 358 | return (past.length > 20 ? '...':'') + past.substr(-20).replace(/\n/g, ""); 359 | }, 360 | 361 | // displays upcoming input, i.e. for error messages 362 | upcomingInput:function () { 363 | var next = this.match; 364 | if (next.length < 20) { 365 | next += this._input.substr(0, 20-next.length); 366 | } 367 | return (next.substr(0,20) + (next.length > 20 ? '...' : '')).replace(/\n/g, ""); 368 | }, 369 | 370 | // displays the character position where the lexing error occurred, i.e. for error messages 371 | showPosition:function () { 372 | var pre = this.pastInput(); 373 | var c = new Array(pre.length + 1).join("-"); 374 | return pre + this.upcomingInput() + "\n" + c + "^"; 375 | }, 376 | 377 | // test the lexed token: return FALSE when not a match, otherwise return token 378 | test_match:function (match, indexed_rule) { 379 | var token, 380 | lines, 381 | backup; 382 | 383 | if (this.options.backtrack_lexer) { 384 | // save context 385 | backup = { 386 | yylineno: this.yylineno, 387 | yylloc: { 388 | first_line: this.yylloc.first_line, 389 | last_line: this.last_line, 390 | first_column: this.yylloc.first_column, 391 | last_column: this.yylloc.last_column 392 | }, 393 | yytext: this.yytext, 394 | match: this.match, 395 | matches: this.matches, 396 | matched: this.matched, 397 | yyleng: this.yyleng, 398 | offset: this.offset, 399 | _more: this._more, 400 | _input: this._input, 401 | yy: this.yy, 402 | conditionStack: this.conditionStack.slice(0), 403 | done: this.done 404 | }; 405 | if (this.options.ranges) { 406 | backup.yylloc.range = this.yylloc.range.slice(0); 407 | } 408 | } 409 | 410 | lines = match[0].match(/(?:\r\n?|\n).*/g); 411 | if (lines) { 412 | this.yylineno += lines.length; 413 | } 414 | this.yylloc = { 415 | first_line: this.yylloc.last_line, 416 | last_line: this.yylineno + 1, 417 | first_column: this.yylloc.last_column, 418 | last_column: lines ? 419 | lines[lines.length - 1].length - lines[lines.length - 1].match(/\r?\n?/)[0].length : 420 | this.yylloc.last_column + match[0].length 421 | }; 422 | this.yytext += match[0]; 423 | this.match += match[0]; 424 | this.matches = match; 425 | this.yyleng = this.yytext.length; 426 | if (this.options.ranges) { 427 | this.yylloc.range = [this.offset, this.offset += this.yyleng]; 428 | } 429 | this._more = false; 430 | this._backtrack = false; 431 | this._input = this._input.slice(match[0].length); 432 | this.matched += match[0]; 433 | token = this.performAction.call(this, this.yy, this, indexed_rule, this.conditionStack[this.conditionStack.length - 1]); 434 | if (this.done && this._input) { 435 | this.done = false; 436 | } 437 | if (token) { 438 | return token; 439 | } else if (this._backtrack) { 440 | // recover context 441 | for (var k in backup) { 442 | this[k] = backup[k]; 443 | } 444 | return false; // rule action called reject() implying the next rule should be tested instead. 445 | } 446 | return false; 447 | }, 448 | 449 | // return next match in input 450 | next:function () { 451 | if (this.done) { 452 | return this.EOF; 453 | } 454 | if (!this._input) { 455 | this.done = true; 456 | } 457 | 458 | var token, 459 | match, 460 | tempMatch, 461 | index; 462 | if (!this._more) { 463 | this.yytext = ''; 464 | this.match = ''; 465 | } 466 | var rules = this._currentRules(); 467 | for (var i = 0; i < rules.length; i++) { 468 | tempMatch = this._input.match(this.rules[rules[i]]); 469 | if (tempMatch && (!match || tempMatch[0].length > match[0].length)) { 470 | match = tempMatch; 471 | index = i; 472 | if (this.options.backtrack_lexer) { 473 | token = this.test_match(tempMatch, rules[i]); 474 | if (token !== false) { 475 | return token; 476 | } else if (this._backtrack) { 477 | match = false; 478 | continue; // rule action called reject() implying a rule MISmatch. 479 | } else { 480 | // else: this is a lexer rule which consumes input without producing a token (e.g. whitespace) 481 | return false; 482 | } 483 | } else if (!this.options.flex) { 484 | break; 485 | } 486 | } 487 | } 488 | if (match) { 489 | token = this.test_match(match, rules[index]); 490 | if (token !== false) { 491 | return token; 492 | } 493 | // else: this is a lexer rule which consumes input without producing a token (e.g. whitespace) 494 | return false; 495 | } 496 | if (this._input === "") { 497 | return this.EOF; 498 | } else { 499 | return this.parseError('Lexical error on line ' + (this.yylineno + 1) + '. Unrecognized text.\n' + this.showPosition(), { 500 | text: "", 501 | token: null, 502 | line: this.yylineno 503 | }); 504 | } 505 | }, 506 | 507 | // return next match that has a token 508 | lex:function lex() { 509 | var r = this.next(); 510 | if (r) { 511 | return r; 512 | } else { 513 | return this.lex(); 514 | } 515 | }, 516 | 517 | // activates a new lexer condition state (pushes the new lexer condition state onto the condition stack) 518 | begin:function begin(condition) { 519 | this.conditionStack.push(condition); 520 | }, 521 | 522 | // pop the previously active lexer condition state off the condition stack 523 | popState:function popState() { 524 | var n = this.conditionStack.length - 1; 525 | if (n > 0) { 526 | return this.conditionStack.pop(); 527 | } else { 528 | return this.conditionStack[0]; 529 | } 530 | }, 531 | 532 | // produce the lexer rule set which is active for the currently active lexer condition state 533 | _currentRules:function _currentRules() { 534 | if (this.conditionStack.length && this.conditionStack[this.conditionStack.length - 1]) { 535 | return this.conditions[this.conditionStack[this.conditionStack.length - 1]].rules; 536 | } else { 537 | return this.conditions["INITIAL"].rules; 538 | } 539 | }, 540 | 541 | // return the currently active lexer condition state; when an index argument is provided it produces the N-th previous condition state, if available 542 | topState:function topState(n) { 543 | n = this.conditionStack.length - 1 - Math.abs(n || 0); 544 | if (n >= 0) { 545 | return this.conditionStack[n]; 546 | } else { 547 | return "INITIAL"; 548 | } 549 | }, 550 | 551 | // alias for begin(condition) 552 | pushState:function pushState(condition) { 553 | this.begin(condition); 554 | }, 555 | 556 | // return the number of states currently on the stack 557 | stateStackSize:function stateStackSize() { 558 | return this.conditionStack.length; 559 | }, 560 | options: {}, 561 | performAction: function anonymous(yy,yy_,$avoiding_name_collisions,YY_START) { 562 | 563 | var YYSTATE=YY_START; 564 | switch($avoiding_name_collisions) { 565 | case 0:// ignore comment 566 | break; 567 | case 1:return 'NEWLINE'; 568 | break; 569 | case 2:// skip other whitespace 570 | break; 571 | case 3:return 8; 572 | break; 573 | case 4:return 9; 574 | break; 575 | case 5:return 9; 576 | break; 577 | case 6:return 'TRUE'; 578 | break; 579 | case 7:return 'FALSE'; 580 | break; 581 | case 8:return 'NULL'; 582 | break; 583 | case 9:return 'UNDEFINED'; 584 | break; 585 | case 10:return 'THIS'; 586 | break; 587 | case 11:return 'FUNCTION'; 588 | break; 589 | case 12:return 'RETURN'; 590 | break; 591 | case 13:return 'VAR' 592 | break; 593 | case 14:return 'NEW'; 594 | break; 595 | case 15:return 'IDENTIFIER'; 596 | break; 597 | case 16:return '==='; 598 | break; 599 | case 17:return '!=='; 600 | break; 601 | case 18:return '&&'; 602 | break; 603 | case 19:return '||'; 604 | break; 605 | case 20:return yy_.yytext; 606 | break; 607 | case 21:return 5; 608 | break; 609 | } 610 | }, 611 | rules: [/^(?:\/\/.*)/,/^(?:\n+)/,/^(?:\s+)/,/^(?:[0-9]+\b)/,/^(?:"[^"]*")/,/^(?:'[^']*')/,/^(?:true\b)/,/^(?:false\b)/,/^(?:null\b)/,/^(?:undefined\b)/,/^(?:this\b)/,/^(?:function\b)/,/^(?:return\b)/,/^(?:var\b)/,/^(?:new\b)/,/^(?:[a-zA-Z_]\w*)/,/^(?:===)/,/^(?:!==)/,/^(?:&&)/,/^(?:\|\|)/,/^(?:.)/,/^(?:$)/], 612 | conditions: {"INITIAL":{"rules":[0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21],"inclusive":true}} 613 | }; 614 | return lexer; 615 | })(); 616 | parser.lexer = lexer; 617 | function Parser () { 618 | this.yy = {}; 619 | } 620 | Parser.prototype = parser;parser.Parser = Parser; 621 | return new Parser; 622 | })(); 623 | 624 | 625 | if (typeof require !== 'undefined' && typeof exports !== 'undefined') { 626 | exports.parser = simple_parser; 627 | exports.Parser = simple_parser.Parser; 628 | exports.parse = function () { return simple_parser.parse.apply(simple_parser, arguments); }; 629 | exports.main = function commonjsMain(args) { 630 | if (!args[1]) { 631 | console.log('Usage: '+args[0]+' FILE'); 632 | process.exit(1); 633 | } 634 | var source = require('fs').readFileSync(require('path').normalize(args[1]), "utf8"); 635 | return exports.parser.parse(source); 636 | }; 637 | if (typeof module !== 'undefined' && require.main === module) { 638 | exports.main(process.argv.slice(1)); 639 | } 640 | } -------------------------------------------------------------------------------- /demos/simple_parsing.js: -------------------------------------------------------------------------------- 1 | var parser = require('./simple_parser').parser; 2 | 3 | console.log(parser.parse('1')) 4 | console.log(parser.parse('2; "hi"')) -------------------------------------------------------------------------------- /demos/tree_of_nodes.js: -------------------------------------------------------------------------------- 1 | var nodes = require('../nodes'), 2 | runtime = require('../runtime'), 3 | eval = require('../eval'); 4 | 5 | // The following is the three of nodes for `var s = "hi!"; console.log(s);`. 6 | // This is what the parser does, it reads your program and spits out a tree of nodes. 7 | treeOfNodes = new nodes.BlockNode([ 8 | // `var s = "hi";` 9 | new nodes.DeclareVariableNode( 10 | "s", // variable name 11 | new nodes.StringNode("hi!") // value 12 | ), 13 | // `console.log(s);` 14 | new nodes.CallNode( 15 | new nodes.GetVariableNode("console"), // receiving object of function call 16 | "log", // function name 17 | [ // arguments 18 | new nodes.GetVariableNode("s") 19 | ] 20 | ) 21 | ]); 22 | 23 | // This tree of node is the internal representation of our program. 24 | // To execute it, we must evaluate it. That's the job of the interpreter. 25 | 26 | var scope = runtime.root; 27 | treeOfNodes.eval(scope); 28 | -------------------------------------------------------------------------------- /eval.js: -------------------------------------------------------------------------------- 1 | // # The Interpreter 2 | // 3 | // The interpreter part of our language is where we'll evaluate the nodes to execute the program. 4 | // Thus the name `eval` for the function we'll be defining here. 5 | // 6 | // We'll add an `eval` function to each node produced by the parser. Each node will know how to 7 | // evaluate itself. For example, a `StringNode` will know how to turn itself into a real string 8 | // inside our runtime. 9 | 10 | var nodes = require('./nodes'); 11 | var runtime = require('./runtime'); 12 | 13 | // The top node of a tree will always be of type `BlockNode`. Its job is to spread the call to 14 | // `eval` to each of its children. 15 | 16 | nodes.BlockNode.prototype.eval = function(scope) { 17 | try { 18 | // Hoist declarations 19 | this.nodes.forEach(function(node) { if (node.declare) node.declare(scope) }); 20 | // Eval after 21 | this.nodes.forEach(function(node) { node.eval(scope) }); 22 | } catch (e) { 23 | if (e instanceof Return) { 24 | return e.value; 25 | } else { 26 | throw e; 27 | } 28 | } 29 | } 30 | 31 | function Return(value) { this.value = value; } 32 | 33 | nodes.ReturnNode.prototype.eval = function(scope) { 34 | throw new Return(this.valueNode ? this.valueNode.eval(scope) : runtime.undefined); 35 | } 36 | 37 | // Literals are pretty easy to eval. Simply return the runtime value. 38 | 39 | nodes.ThisNode.prototype.eval = function(scope) { return scope.this; } 40 | nodes.TrueNode.prototype.eval = function(scope) { return runtime.true; } 41 | nodes.FalseNode.prototype.eval = function(scope) { return runtime.false; } 42 | nodes.NullNode.prototype.eval = function(scope) { return runtime.null; } 43 | nodes.UndefinedNode.prototype.eval = function(scope) { return runtime.undefined; } 44 | 45 | // Creating various objects is done by instantiating `JsObject`. 46 | 47 | nodes.ObjectNode.prototype.eval = function(scope) { return new runtime.JsObject(); } 48 | nodes.StringNode.prototype.eval = 49 | nodes.NumberNode.prototype.eval = function(scope) { return new runtime.JsObject(this.value); } 50 | 51 | 52 | // Variables are stored in the current scope. All we need to do to interpret the variable nodes is 53 | // get and set values in the scope. 54 | 55 | nodes.DeclareVariableNode.prototype.declare = function(scope) { 56 | scope.locals[this.name] = runtime.undefined; 57 | } 58 | nodes.DeclareVariableNode.prototype.eval = function(scope) { 59 | if (this.valueNode) scope.locals[this.name] = this.valueNode.eval(scope); 60 | } 61 | 62 | nodes.GetVariableNode.prototype.eval = function(scope) { 63 | return scope.get(this.name); 64 | } 65 | 66 | nodes.SetVariableNode.prototype.eval = function(scope) { 67 | return scope.set(this.name, this.valueNode.eval(scope)); 68 | } 69 | 70 | 71 | // Getting and setting properties is handled by the two following nodes. 72 | // One gotcha to note here. We want to make sure to not inject real JavaScript values, 73 | // such as a string, number, `true`, `null` or `undefined` inside the runtime. 74 | // Instead, we want to always return values created for our runtime. For example here, 75 | // we make sure to return `runtime.undefined` and not `undefined` if the property is 76 | // not set. 77 | 78 | nodes.GetPropertyNode.prototype.eval = function(scope) { 79 | return this.objectNode.eval(scope).get(this.name) || runtime.undefined; 80 | } 81 | 82 | nodes.SetPropertyNode.prototype.eval = function(scope) { 83 | return this.objectNode.eval(scope).set(this.name, this.valueNode.eval(scope)); 84 | } 85 | 86 | 87 | // Creating a function is just a matter of instantiating `JsFunction`. 88 | 89 | nodes.FunctionNode.prototype.eval = function(scope) { 90 | return new runtime.JsFunction(this.parameters, this.bodyNode); 91 | } 92 | 93 | // Calling a function can take two forms: 94 | // 95 | // 1. On an object: `object.name(...)`. `this` will be set to `object`. 96 | // 2. On a variable: `name(...)`. `this` will be set to the `root` object. 97 | 98 | nodes.CallNode.prototype.eval = function(scope) { 99 | if (this.objectNode) { // object.name(...) 100 | var object = this.objectNode.eval(scope); 101 | var theFunction = object.get(this.name); 102 | } else { // name() 103 | var object = runtime.root; 104 | var theFunction = scope.get(this.name); 105 | } 106 | 107 | var args = this.argumentNodes.map(function(arg) { return arg.eval(scope) }); 108 | 109 | return theFunction.call(object, scope, args); 110 | } 111 | 112 | // Creating an instance is done by looking up the constructor function in the current scope 113 | // and calling it. 114 | 115 | nodes.NewNode.prototype.eval = function(scope) { 116 | var constructor = scope.get(this.name); 117 | var args = this.argumentNodes.map(function(arg) { return arg.eval(scope) }); 118 | 119 | return constructor.new(scope, args); 120 | } 121 | 122 | 123 | // Operators 124 | 125 | nodes.AddNode.prototype.eval = function(scope) { 126 | return new runtime.JsObject(this.node1.eval(scope).value + this.node2.eval(scope).value); 127 | } 128 | nodes.MultiplyNode.prototype.eval = function(scope) { 129 | return new runtime.JsObject(this.node1.eval(scope).value * this.node2.eval(scope).value); 130 | } 131 | -------------------------------------------------------------------------------- /grammar.jison: -------------------------------------------------------------------------------- 1 | // # The Parser's Grammar 2 | // 3 | // This grammar will be used to generate the parser (parser.js). It will turn the stream of 4 | // tokens (defined in tokens.jisonlex) into a tree of nodes (defined in nodes.js). 5 | 6 | %{ 7 | var nodes = require('./nodes'); 8 | %} 9 | 10 | // Based on [MDN Operator Precedence table](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Operator_Precedence). 11 | %left ',' 12 | %right '=' 13 | %left '||' 14 | %left '&&' 15 | %left '==' '!=' '===' '!==' 16 | %left '>' '>=' '<' '<=' 17 | %left '+' '-' 18 | %left '*' '/' 19 | %right '!' 20 | %right NEW 21 | %right '.' 22 | 23 | %start program // Tell which rule to start with. 24 | 25 | %% 26 | 27 | // A JavaScript program is composed of statements. 28 | program: 29 | statements EOF { return $1; } 30 | ; 31 | 32 | statements: 33 | statement { $$ = new nodes.BlockNode([ $1 ]); } 34 | | statements terminator statement { $1.push($3); // statements ($1) is a BlockNode 35 | $$ = $1; } 36 | | statements terminator { $$ = $1; } 37 | | { $$ = new nodes.BlockNode([]); } 38 | ; 39 | 40 | terminator: 41 | ";" 42 | | NEWLINE 43 | ; 44 | 45 | statement: 46 | expression 47 | | variableDeclaration 48 | | return 49 | ; 50 | 51 | // Expressions, as opposed to statements, return a value and can be nested inside 52 | // other expressions or statements. 53 | expression: 54 | literal 55 | | variable 56 | | property 57 | | call 58 | | operator 59 | | function 60 | | new 61 | | '(' expression ')' { $$ = $2; } 62 | ; 63 | 64 | // Literals are the hard-coded values in our program. 65 | literal: 66 | NUMBER { $$ = new nodes.NumberNode(parseInt($1)); } 67 | | STRING { $$ = new nodes.StringNode($1.substring(1, $1.length-1)); } 68 | | THIS { $$ = new nodes.ThisNode(); } 69 | | TRUE { $$ = new nodes.TrueNode(); } 70 | | FALSE { $$ = new nodes.FalseNode(); } 71 | | NULL { $$ = new nodes.NullNode(); } 72 | | UNDEFINED { $$ = new nodes.UndefinedNode(); } 73 | | "{" "}" { $$ = new nodes.ObjectNode(); } 74 | ; 75 | 76 | // And on and on we define the rest of our language ... 77 | // Keep in mind this grammar only handles a subset of the JavaScript language and is 78 | // intended to be as simple as possible and easy to understand. 79 | 80 | variableDeclaration: 81 | VAR IDENTIFIER "=" expression { $$ = new nodes.DeclareVariableNode($2, $4); } 82 | | VAR IDENTIFIER { $$ = new nodes.DeclareVariableNode($2); } 83 | ; 84 | 85 | variable: 86 | IDENTIFIER { $$ = new nodes.GetVariableNode($1); } 87 | | IDENTIFIER "=" expression { $$ = new nodes.SetVariableNode($1, $3); } 88 | ; 89 | 90 | property: 91 | expression "." IDENTIFIER { $$ = new nodes.GetPropertyNode($1, $3); } 92 | | expression "." IDENTIFIER 93 | "=" expression { $$ = new nodes.SetPropertyNode($1, $3, $5); } 94 | ; 95 | 96 | call: 97 | IDENTIFIER "(" arguments ")" { $$ = new nodes.CallNode(null, $1, $3); } 98 | | expression "." IDENTIFIER "(" arguments ")" { $$ = new nodes.CallNode($1, $3, $5); } 99 | ; 100 | 101 | arguments: 102 | expression { $$ = [ $1 ]; } 103 | | arguments "," expression { $1.push($3); $$ = $1 } 104 | | { $$ = []; } 105 | ; 106 | 107 | operator: 108 | expression '+' expression { $$ = new nodes.AddNode($1, $3) } 109 | | expression '*' expression { $$ = new nodes.MultiplyNode($1, $3) } 110 | ; 111 | 112 | function: 113 | FUNCTION "(" parameters ")" "{" statements "}" 114 | { $$ = new nodes.FunctionNode($3, $6) } 115 | ; 116 | 117 | parameters: 118 | IDENTIFIER { $$ = [ $1 ]; } 119 | | parameters "," IDENTIFIER { $1.push($3); $$ = $1 } 120 | | { $$ = []; } 121 | ; 122 | 123 | new: 124 | NEW IDENTIFIER "(" arguments ")" { $$ = new nodes.NewNode($2, $4); } 125 | ; 126 | 127 | return: 128 | RETURN { $$ = new nodes.ReturnNode() } 129 | | RETURN expression { $$ = new nodes.ReturnNode($2) } 130 | ; 131 | 132 | -------------------------------------------------------------------------------- /lexer.js: -------------------------------------------------------------------------------- 1 | // THIS IS NOT THE LEXER. The lexer's code is in parser.js. 2 | // This file only contains functions to help test the lexer. 3 | 4 | var parser = require('./parser').parser; 5 | 6 | exports.lex = function(code) { 7 | var lexer = parser.lexer; 8 | var terminals = parser.terminals_; 9 | var token, tokens = []; 10 | 11 | lexer.setInput(code); 12 | 13 | while ((token = lexer.lex()) !== 1) { 14 | tokens.push([terminals[token] || token, lexer.yytext]); 15 | } 16 | 17 | return tokens; 18 | } 19 | -------------------------------------------------------------------------------- /nodes.js: -------------------------------------------------------------------------------- 1 | // Define all the nodes produced by the parser. 2 | 3 | exports.BlockNode = function BlockNode(nodes) { 4 | this.nodes = nodes; 5 | } 6 | 7 | exports.BlockNode.prototype.push = function(node) { 8 | this.nodes.push(node); 9 | } 10 | 11 | exports.NumberNode = function NumberNode(value) { this.value = value; } 12 | exports.StringNode = function StringNode(value) { this.value = value; } 13 | 14 | exports.ThisNode = function ThisNode() {} 15 | exports.TrueNode = function TrueNode() {} 16 | exports.FalseNode = function FalseNode() {} 17 | exports.NullNode = function NullNode() {} 18 | exports.UndefinedNode = function UndefinedNode() {} 19 | exports.ObjectNode = function ObjectNode() {} 20 | 21 | exports.GetVariableNode = function GetVariableNode(name) { this.name = name; } 22 | exports.SetVariableNode = function SetVariableNode(name, valueNode) { 23 | this.name = name; 24 | this.valueNode = valueNode; 25 | } 26 | exports.DeclareVariableNode = function DeclareVariableNode(name, valueNode) { 27 | this.name = name; 28 | this.valueNode = valueNode; 29 | } 30 | 31 | exports.GetPropertyNode = function GetPropertyNode(objectNode, name) { 32 | this.objectNode = objectNode; 33 | this.name = name; 34 | } 35 | exports.SetPropertyNode = function SetPropertyNode(objectNode, name, valueNode) { 36 | this.objectNode = objectNode; 37 | this.name = name; 38 | this.valueNode = valueNode; 39 | } 40 | 41 | exports.CallNode = function CallNode(objectNode, name, argumentNodes) { 42 | this.objectNode = objectNode; 43 | this.name = name; 44 | this.argumentNodes = argumentNodes; 45 | } 46 | 47 | exports.FunctionNode = function FunctionNode(parameters, bodyNode) { 48 | this.parameters = parameters; 49 | this.bodyNode = bodyNode; 50 | } 51 | 52 | exports.ReturnNode = function ReturnNode(valueNode) { 53 | this.valueNode = valueNode; 54 | } 55 | 56 | exports.NewNode = function NewNode(name, argumentNodes) { 57 | this.name = name; 58 | this.argumentNodes = argumentNodes; 59 | } 60 | 61 | exports.AddNode = function AddNode(node1, node2) { 62 | this.node1 = node1; 63 | this.node2 = node2; 64 | } 65 | 66 | exports.MultiplyNode = function MultiplyNode(node1, node2) { 67 | this.node1 = node1; 68 | this.node2 = node2; 69 | } 70 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tinyjs", 3 | "version": "0.0.1", 4 | "description": "A tiny JavaScript in JavaScript", 5 | "main": "tiny.js", 6 | "scripts": { 7 | "test": "make test" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git://github.com/codedinc/tinyjs.git" 12 | }, 13 | "keywords": [ 14 | "interpreter", 15 | "parser" 16 | ], 17 | "author": "Marc-Andre Cournoyer", 18 | "bugs": { 19 | "url": "https://github.com/codedinc/tinyjs/issues" 20 | }, 21 | "dependencies": { 22 | "jison": "0.4.x", 23 | "mocha": "1.x" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /runtime.js: -------------------------------------------------------------------------------- 1 | // # The Runtime 2 | // 3 | // The runtime is the environment in which the language executes. Think of it as a box with which 4 | // we'll interact using a specific API. In this file, we're defining this API to build and interact 5 | // with the runtime. 6 | // 7 | // We need to build representations for everything we'll have access to inside the language. 8 | // The first one being: objects! 9 | 10 | var util = require("util"); 11 | 12 | // ## Object 13 | // Objects have properties. One missing piece here is the prototype. To keep things simple, 14 | // we will not have any form of inheritance. All our objects will be able to do is get and set 15 | // the values stored in its properties. 16 | // 17 | // It will also be able to wrap a real JavaScipt value, like a string or a number. We'll use this 18 | // to represent strings and numbers inside our runtime. Every object living inside our program, 19 | // will be an instance of `JsObject`. 20 | // 21 | // If we want to create a number in our runtime, we do: `new JsObject(4)`. 22 | 23 | function JsObject(value) { 24 | this.properties = {}; 25 | this.value = value; // A JavaScipt string or number. 26 | } 27 | exports.JsObject = JsObject; 28 | 29 | JsObject.prototype.hasProperty = function(name) { 30 | return this.properties.hasOwnProperty(name); 31 | } 32 | 33 | JsObject.prototype.get = function(name) { 34 | if (this.hasProperty(name)) return this.properties[name]; 35 | if (this.hasProperty('__tinyProto__')) return this.properties['__tinyProto__'].get(name); 36 | } 37 | JsObject.prototype.set = function(name, value) { 38 | return this.properties[name] = value; 39 | } 40 | 41 | 42 | // ## Scopes 43 | // 44 | // The most confusing part of JavaScript is the way it handles the scope of variables. 45 | // When is it defined as a global variable? What's the value of `this`? 46 | // All of this can be implemented in a very simple and straightforward fashion. 47 | // 48 | // A scope encapsulates the context of execution, the local variables and the value of `this`, 49 | // inside a function or at the root of your program. 50 | // 51 | // Scopes also have a parent scope. The chain of parents will go down to the root scope, 52 | // where you define your global variables. 53 | 54 | function JsScope(_this, parent) { 55 | this.locals = {}; // local variables 56 | this.this = _this; // value of `this` 57 | this.parent = parent; // parent scope 58 | this.root = !parent; // is it the root/global scope? 59 | } 60 | exports.JsScope = JsScope; 61 | 62 | JsScope.prototype.hasLocal = function(name) { 63 | return this.locals.hasOwnProperty(name); 64 | } 65 | 66 | // Getting the value in a variable is done by looking first in the current scope, 67 | // then recursively going in the parent until we reach the root scope. 68 | // This is how you get access to variables defined in parent functions, 69 | // also why defining a variable in a function will override the variables of parent 70 | // functions and global variables. 71 | 72 | JsScope.prototype.get = function(name) { 73 | if (this.hasLocal(name)) return this.locals[name]; // Look in current scope 74 | if (this.parent) return this.parent.get(name); // Look in parent scope 75 | throw this.name + " is not defined"; 76 | } 77 | 78 | // Setting the value of a variables follows the same logic as when getting it's value. 79 | // We search where the variable is defined, and change its value. If the variable 80 | // was not defined in any parent scope, we'll end up in the root scope, which will have 81 | // the effect of declaring it as a global variable. 82 | // 83 | // This is why, in JavaScript, if you assign a value to a variable without declaring it first 84 | // (using `var`), it will search in parent scopes until it reaches the root scope and 85 | // declare it there, thus declaring it as a global variable. 86 | 87 | JsScope.prototype.set = function(name, value) { 88 | if (this.root || this.hasLocal(name)) return this.locals[name] = value; 89 | return this.parent.set(name, value); 90 | } 91 | 92 | 93 | // ## Functions 94 | // 95 | // Functions encapsulate a body (block of code) that we can execute (eval), and also parameters: 96 | // `function (parameters) { body }`. 97 | 98 | function JsFunction(parameters, body) { 99 | JsObject.call(this); 100 | this.body = body; 101 | this.parameters = parameters; 102 | this.properties['prototype'] = new JsObject(); 103 | } 104 | util.inherits(JsFunction, JsObject); // Functions are objects. 105 | exports.JsFunction = JsFunction; 106 | 107 | // When the function is called, a new scope is created so that the function will have its 108 | // own set of local variables and its own value for `this`. 109 | // 110 | // The function's body is a tree of nodes. 111 | // - nodes.js defines those nodes. 112 | // - eval.js defines how each node is evaluated. 113 | // 114 | // To execute the function, we `eval` its body. 115 | 116 | JsFunction.prototype.call = function(object, scope, args) { 117 | var functionScope = new JsScope(object, scope); // this = object, parent scope = scope 118 | 119 | // We assign arguments to local variables. That's how you get access to them. 120 | for (var i = 0; i < this.parameters.length; i++) { 121 | functionScope.locals[this.parameters[i]] = args[i]; 122 | } 123 | 124 | return this.body.eval(functionScope); 125 | } 126 | 127 | // Creating an instance 128 | 129 | JsFunction.prototype.new = function(scope, args) { 130 | var object = new JsObject(); 131 | 132 | // We can't use __proto__ to not override the real JavaScript property. 133 | object.properties['__tinyProto__'] = this.properties['prototype']; 134 | 135 | // Call the constructor 136 | this.call(object, scope, args); 137 | 138 | return object; 139 | } 140 | 141 | 142 | // ## Primitives 143 | // 144 | // We map the primitives to their JavaScript counterparts (in their `value` property). 145 | // Note that `true` and `false` are objects, but `null` and `undefined` are not.. 146 | 147 | exports.true = new JsObject(true); 148 | exports.false = new JsObject(false); 149 | exports.null = { value: null }; 150 | exports.undefined = { value: undefined }; 151 | 152 | 153 | // ## The root object 154 | // 155 | // The only missing piece of the runtime at this point is the root (global) object. 156 | // 157 | // It is the scope of execution at the root of your program and also the `root` or `window` 158 | // object that you get access to. 159 | // 160 | // Thus, we create it as a scope that also acts as an object (has properties). 161 | 162 | var root = exports.root = new JsObject(); 163 | root.this = root; // this == root when inside the root scope. 164 | 165 | // Properties of the root/global scope are also the local variables. That's why when you 166 | // use `var a = 1;` in the root scope, it will also assign the value to `root.a`. 167 | 168 | root.locals = root.properties; 169 | 170 | // Here we'd normaly define all the fancy things, like global funtions and objects, that you 171 | // have access to inside your JavaScript programs. But we're keeping it simple and only define 172 | // `root`, the `console.log` and `alert` functions. 173 | 174 | root.locals['root'] = root; 175 | root.locals['console'] = new JsObject(); 176 | 177 | // We can pass real JavaScript functions in our runtime objects since they have a `call` 178 | // property too, like `JsFunction.prototype.call`. So they will behave much like `JsFunction`, but 179 | // without the need to create a scope. 180 | 181 | root.locals['console'].properties['log'] = function(scope, args) { 182 | console.log.apply(console, args.map(function(arg) { return arg.value })); 183 | return exports.undefined; 184 | } 185 | 186 | root.locals['alert'] = function(scope, args) { 187 | alert.apply(null, args.map(function(arg) { return arg.value })); 188 | return exports.undefined; 189 | } 190 | 191 | root.locals['potato'] = new JsObject(); 192 | -------------------------------------------------------------------------------- /samples/functions.js: -------------------------------------------------------------------------------- 1 | // Create an object 2 | a = {}; 3 | a.x = "object"; 4 | x = "global"; 5 | 6 | // Create a function referencing property `x`. 7 | var f = function() { 8 | return this.x; 9 | console.log("unreachable"); 10 | } 11 | a.f = f; 12 | 13 | console.log(a.f()); // => "object" 14 | console.log(f()); // => "global" 15 | -------------------------------------------------------------------------------- /samples/hoisting.js: -------------------------------------------------------------------------------- 1 | var f = function() { 2 | console.log(a); 3 | var a = 'a'; 4 | console.log(a); 5 | } 6 | 7 | f(); -------------------------------------------------------------------------------- /samples/inheritance.js: -------------------------------------------------------------------------------- 1 | var Parent = function() {} 2 | var Child = function() {} 3 | 4 | // Inherit from Parent 5 | Child.prototype = new Parent(); 6 | 7 | Parent.prototype.parentProperty = "in parent"; 8 | Child.prototype.childProperty = "in child"; 9 | 10 | var child = new Child(); 11 | 12 | console.log(child.childProperty); 13 | console.log(child.parentProperty); 14 | -------------------------------------------------------------------------------- /samples/objects.js: -------------------------------------------------------------------------------- 1 | var object = {}; 2 | 3 | var Constructor = function() {}; 4 | var object = new Constructor(); 5 | 6 | var string = "potato"; 7 | var number = 32; 8 | -------------------------------------------------------------------------------- /samples/operators.js: -------------------------------------------------------------------------------- 1 | var x = 1 + 2 * 3; 2 | var y = (1 + 2) * 3; 3 | 4 | console.log(x); -------------------------------------------------------------------------------- /samples/parameters.js: -------------------------------------------------------------------------------- 1 | f = function(x, y) { console.log(x, y); } 2 | 3 | f("ok", 1); // => "ok" -------------------------------------------------------------------------------- /samples/primitives.js: -------------------------------------------------------------------------------- 1 | console.log(true); 2 | console.log(false); 3 | console.log(null); 4 | console.log(undefined); 5 | console.log("hi"); 6 | console.log(1); 7 | -------------------------------------------------------------------------------- /samples/prototype.js: -------------------------------------------------------------------------------- 1 | var A = function() { 2 | this.x = 'x'; 3 | } 4 | 5 | A.prototype.y = 'y'; 6 | 7 | var a = new A(); 8 | 9 | console.log(a.x); 10 | console.log(a.y); -------------------------------------------------------------------------------- /samples/scopes.js: -------------------------------------------------------------------------------- 1 | var top = "top"; 2 | 3 | f1 = function(x) { 4 | var f1Var = "f1Var"; 5 | 6 | var f2 = function() { 7 | var y = "y"; 8 | 9 | console.log(x); 10 | console.log(y); 11 | console.log(top); 12 | console.log(f1Var); 13 | 14 | top = "top overriden from function"; 15 | console.log(top); 16 | global = "global defined from function"; 17 | 18 | f1Var = "f1Var modified from f2"; 19 | } 20 | 21 | f2(); 22 | console.log(f1Var); 23 | } 24 | 25 | f1("x"); 26 | console.log(global); 27 | console.log(root.f1Var); -------------------------------------------------------------------------------- /scripts/compare_outputs.sh: -------------------------------------------------------------------------------- 1 | # Run a .js file both on tiny.js and node.js and diff the output. 2 | 3 | # Run on tiny.js 4 | node tiny.js $1 > __tiny_output.txt 5 | 6 | # Run on node and replace some property names. 7 | node $1 > __node_output.txt 8 | 9 | echo "$1:" 10 | diff __tiny_output.txt __node_output.txt && echo "ok" 11 | 12 | rm __tiny_output.txt __node_output.txt -------------------------------------------------------------------------------- /test/interpreter_test.js: -------------------------------------------------------------------------------- 1 | var assert = require("assert"); 2 | var runtime = require('../runtime'); 3 | var parser = require('../parser').parser; 4 | require('../eval'); 5 | 6 | describe('Interpreter', function() { 7 | it('returns', function() { 8 | var nodes = parser.parse('return true; undefined') 9 | 10 | assert.equal(runtime.true, nodes.eval(runtime.root)) 11 | }) 12 | 13 | it('hoist variable declaration', function() { 14 | var nodes = parser.parse('return a; var a = 1') 15 | 16 | assert.equal(runtime.undefined, nodes.eval(runtime.root)) 17 | }) 18 | 19 | it('set properties', function() { 20 | var nodes = parser.parse('potato.x = true; return potato.x') 21 | 22 | assert.equal(runtime.true, nodes.eval(runtime.root)) 23 | }) 24 | 25 | it('define function', function() { 26 | var nodes = parser.parse('potato.f = function() { return true }; return potato.f()') 27 | 28 | assert.equal(runtime.true, nodes.eval(runtime.root)) 29 | }) 30 | 31 | it('call object method', function() { 32 | parser.parse('potato.x = true; potato.f = function() { return this.x }').eval(runtime.root) 33 | 34 | var nodes = parser.parse('return potato.f()') 35 | 36 | assert.equal(runtime.true, nodes.eval(runtime.root)) 37 | }) 38 | 39 | it('call function', function() { 40 | parser.parse('var x = true; var f = function() { return this.x }').eval(runtime.root) 41 | 42 | var nodes = parser.parse('return f()') 43 | 44 | assert.equal(runtime.true, nodes.eval(runtime.root)) 45 | }) 46 | 47 | it('create instances on new', function() { 48 | parser.parse('var F = function(x) { this.x = x }').eval(runtime.root) 49 | parser.parse('F.prototype.y = true').eval(runtime.root) 50 | 51 | var nodes = parser.parse('return new F(true)') 52 | var object = nodes.eval(runtime.root) 53 | 54 | assert.equal(runtime.true, object.get('x')) 55 | assert.equal(runtime.true, object.get('y')) 56 | }) 57 | }) -------------------------------------------------------------------------------- /test/lexer_test.js: -------------------------------------------------------------------------------- 1 | var assert = require("assert"); 2 | var lexer = require('../lexer'); 3 | 4 | describe('Lexer', function() { 5 | it('lex new lines', function() { 6 | assert.deepEqual([["NEWLINE","\n"],["EOF",""]], lexer.lex("\n")) 7 | }) 8 | 9 | it('discards comments', function() { 10 | assert.deepEqual([["EOF",""]], lexer.lex("// hi")) 11 | }) 12 | 13 | it('lex numbers', function() { 14 | assert.deepEqual([["NUMBER","1"],["EOF",""]], lexer.lex('1')) 15 | }) 16 | 17 | it('lex strings', function() { 18 | assert.deepEqual([["STRING",'"hi"'],["EOF",""]], lexer.lex('"hi"')) 19 | }) 20 | 21 | it('lex true', function() { 22 | assert.deepEqual([["TRUE",'true'],["EOF",""]], lexer.lex('true')) 23 | }) 24 | 25 | // Exercise: make this pass 26 | it('lex new', function() { 27 | assert.deepEqual([["NEW",'new'],["EOF",""]], lexer.lex('new')) 28 | }) 29 | 30 | it('catch all one character', function() { 31 | assert.deepEqual([["+",'+'],[".",'.'],["(",'('],["EOF",""]], lexer.lex('+ . (')) 32 | }) 33 | }) 34 | -------------------------------------------------------------------------------- /test/mocha.opts: -------------------------------------------------------------------------------- 1 | --reporter spec -------------------------------------------------------------------------------- /test/parser_test.js: -------------------------------------------------------------------------------- 1 | var assert = require("assert"); 2 | var parser = require('../parser').parser; 3 | var nodes = require('../nodes'); 4 | 5 | describe('Parser', function() { 6 | it('parses numbers', function() { 7 | assert.deepEqual( 8 | new nodes.BlockNode([ 9 | new nodes.NumberNode(1) 10 | ]) 11 | , parser.parse("1")) 12 | }) 13 | 14 | it('parses statements', function() { 15 | assert.deepEqual( 16 | new nodes.BlockNode([ 17 | new nodes.NumberNode(1), 18 | new nodes.NumberNode(2), 19 | ]) 20 | , parser.parse("1; 2")) 21 | }) 22 | 23 | it('parses variable declaration', function() { 24 | assert.deepEqual( 25 | new nodes.BlockNode([ 26 | new nodes.DeclareVariableNode("a") 27 | ]) 28 | , parser.parse("var a")) 29 | }) 30 | 31 | it('parses variable assignment', function() { 32 | assert.deepEqual( 33 | new nodes.BlockNode([ 34 | new nodes.SetVariableNode("a", new nodes.NumberNode(1)) 35 | ]) 36 | , parser.parse("a = 1")) 37 | }) 38 | 39 | it('parses property', function() { 40 | assert.deepEqual( 41 | new nodes.BlockNode([ 42 | new nodes.SetPropertyNode( 43 | new nodes.ThisNode(), 44 | "a", 45 | new nodes.GetPropertyNode( 46 | new nodes.GetPropertyNode( 47 | new nodes.GetVariableNode("b"), 48 | "x" 49 | ), 50 | "y" 51 | ) 52 | ) 53 | ]) 54 | , parser.parse("this.a = b.x.y")) 55 | }) 56 | 57 | it('parses call', function() { 58 | assert.deepEqual( 59 | new nodes.BlockNode([ 60 | new nodes.CallNode(new nodes.ThisNode, "a", [new nodes.NumberNode(1), new nodes.NumberNode(2)]) 61 | ]) 62 | , parser.parse("this.a(1, 2)")) 63 | }) 64 | 65 | it('parses operators respecting precedence', function() { 66 | assert.deepEqual( 67 | new nodes.BlockNode([ 68 | new nodes.AddNode(new nodes.NumberNode(1), 69 | new nodes.MultiplyNode(new nodes.NumberNode(2), new nodes.NumberNode(3)) 70 | ) 71 | ]) 72 | , parser.parse("1 + 2 * 3")) 73 | }) 74 | 75 | it('parses function', function() { 76 | assert.deepEqual( 77 | new nodes.BlockNode([ 78 | new nodes.FunctionNode(["a", "b"], new nodes.BlockNode([ new nodes.ReturnNode ])) 79 | ]) 80 | , parser.parse("function(a, b) { return }")) 81 | }) 82 | 83 | // Exercise: make this pass 84 | it('parses new', function() { 85 | assert.deepEqual( 86 | new nodes.BlockNode([ 87 | new nodes.NewNode("A", [new nodes.NumberNode(1), new nodes.NumberNode(2)]) 88 | ]) 89 | , parser.parse("new A(1, 2)")) 90 | }) 91 | 92 | }) -------------------------------------------------------------------------------- /test/runtime_test.js: -------------------------------------------------------------------------------- 1 | var assert = require("assert"); 2 | var runtime = require('../runtime'); 3 | var parser = require('../parser').parser; 4 | require('../eval'); 5 | 6 | describe('Runtime', function() { 7 | it('get property', function() { 8 | var object = new runtime.JsObject() 9 | object.set('x', runtime.true) 10 | 11 | assert.equal(runtime.true, object.get('x')) 12 | }) 13 | 14 | it('get property from prototype', function() { 15 | var object = new runtime.JsObject() 16 | var proto = new runtime.JsObject() 17 | object.set('__tinyProto__', proto) 18 | proto.set('x', runtime.true) 19 | 20 | assert.equal(runtime.true, object.get('x')) 21 | }) 22 | 23 | it('creates global when setting undeclared local', function() { 24 | var global = new runtime.JsScope() 25 | var scope = new runtime.JsScope(runtime.root, global) 26 | scope.set('x', runtime.true) 27 | 28 | assert.equal(runtime.true, global.get('x')) 29 | }) 30 | 31 | it('call functions', function() { 32 | var func = new runtime.JsFunction(['a'], parser.parse('return a')) 33 | 34 | assert.equal(runtime.true, func.call(null, runtime.root, [runtime.true])) 35 | }) 36 | 37 | describe('new Function', function() { 38 | 39 | it('passes arguments', function() { 40 | var constructor = new runtime.JsFunction(['a'], parser.parse('this.a = a')) 41 | var object = constructor.new(runtime.root, [runtime.true]); 42 | 43 | assert.equal(runtime.true, object.get('a')) 44 | }) 45 | 46 | it('assigns prototype', function() { 47 | var constructor = new runtime.JsFunction([], parser.parse('')) 48 | constructor.get('prototype').set('x', runtime.true) 49 | var object = constructor.new(runtime.root, []); 50 | 51 | assert.equal(runtime.true, object.get('x')) 52 | }) 53 | 54 | }) 55 | 56 | }) -------------------------------------------------------------------------------- /tiny.js: -------------------------------------------------------------------------------- 1 | // # Putting it all together 2 | // 3 | // All the pieces of our language are put together here. 4 | // 5 | // usage: node tiny.js samples/functions.js 6 | 7 | var parser = require('./parser').parser; 8 | var eval = require('./eval'); 9 | var runtime = require('./runtime'); 10 | var fs = require('fs'); 11 | 12 | // We first read the file passed as an argument to the process. 13 | var file = process.argv[2]; 14 | var code = fs.readFileSync(file, "utf8"); 15 | 16 | // We then feed the code to the parser. Which will turn our code into 17 | // a tree of nodes. 18 | var node = parser.parse(code); 19 | 20 | // Finally, start the evaluation of our program on the top of the tree, 21 | // passing the root (global) object as the scope in which to start its execution. 22 | node.eval(runtime.root); -------------------------------------------------------------------------------- /tokens.jisonlex: -------------------------------------------------------------------------------- 1 | // # The Tokens 2 | // The tokens are the atomic units of our programs. We tag each one with a type. 3 | // This stream of tokens will then be fed to the parser. 4 | // 5 | // Note that the rules are applied from top to bottom, first one to match. 6 | 7 | %% 8 | 9 | "//".* // ignore comment 10 | 11 | \n+ return 'NEWLINE'; 12 | \s+ // skip other whitespace 13 | 14 | // Literals: the hard-coded values in your programs. 15 | [0-9]+\b return 'NUMBER'; 16 | \"[^"]*\" return 'STRING'; 17 | \'[^']*\' return 'STRING'; 18 | "true" return 'TRUE'; 19 | "false" return 'FALSE'; 20 | "null" return 'NULL'; 21 | "undefined" return 'UNDEFINED'; 22 | 23 | // Keywords 24 | "this" return 'THIS'; 25 | "function" return 'FUNCTION'; 26 | "return" return 'RETURN'; 27 | "var" return 'VAR' 28 | "new" return 'NEW'; 29 | 30 | // Identifiers are names: variable and function names. 31 | [a-zA-Z_]\w* return 'IDENTIFIER'; 32 | 33 | // Operators 34 | "===" return '==='; 35 | "!==" return '!=='; 36 | "&&" return '&&'; 37 | "||" return '||'; 38 | 39 | // We end with a catch all rule. Any one single character that has not been matched 40 | // will be handled here. A few examples: `.`, `+`, `(` and `)`. 41 | . return yytext; 42 | 43 | <> return 'EOF'; 44 | --------------------------------------------------------------------------------