-
Notifications
You must be signed in to change notification settings - Fork 14
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
alright now we have lexical and dynamic scopes
- Loading branch information
Showing
12 changed files
with
335 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
{ | ||
"presets": [ "es2015", "stage-0" ], | ||
"plugins": ["transform-flow-strip-types"], | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
{ | ||
"extends": "airbnb/base", | ||
"env": { | ||
"node": true, | ||
"es6": true, | ||
"mocha": true, | ||
}, | ||
"ecmaFeatures": { | ||
"generators": true, | ||
}, | ||
"globals": { | ||
"__DEV__": true, | ||
}, | ||
"parser": "babel-eslint", | ||
"rules": { | ||
"babel/generator-star-spacing": 1, | ||
"babel/new-cap": 1, | ||
"babel/object-shorthand": 1, | ||
"babel/arrow-parens": 1, | ||
"babel/no-await-in-loop": 1, | ||
"array-bracket-spacing": [1, "always", { | ||
"singleValue": true, | ||
"objectsInArrays": false, | ||
"arraysInArrays": true, | ||
}], | ||
}, | ||
"plugins": [ | ||
"babel", | ||
], | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
[ignore] | ||
|
||
.*/node_modules/.* | ||
./dist/.* |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -31,3 +31,8 @@ node_modules | |
|
||
# Optional REPL history | ||
.node_repl_history | ||
|
||
.DS_Store | ||
dist | ||
|
||
.nyc_output |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
{ | ||
"name": "trivial-js-interpreter", | ||
"version": "1.0.0", | ||
"description": "", | ||
"main": "index.js", | ||
"scripts": { | ||
"test": "echo \"Error: no test specified\" && exit 1" | ||
}, | ||
"repository": { | ||
"type": "git", | ||
"url": "git+https://github.com/keyanzhang/trivial-js-interpreter.git" | ||
}, | ||
"keywords": [], | ||
"author": "Keyan Zhang <[email protected]> (http:https://keya.nz)", | ||
"license": "ISC", | ||
"bugs": { | ||
"url": "https://github.com/keyanzhang/trivial-js-interpreter/issues" | ||
}, | ||
"homepage": "https://github.com/keyanzhang/trivial-js-interpreter#readme", | ||
"devDependencies": { | ||
"babel-core": "^6.7.6", | ||
"babel-eslint": "^6.0.2", | ||
"babel-plugin-transform-flow-strip-types": "^6.7.0", | ||
"babel-preset-es2015": "^6.6.0", | ||
"babel-preset-stage-0": "^6.5.0", | ||
"eslint": "^2.7.0", | ||
"eslint-config-airbnb": "^7.0.0", | ||
"eslint-plugin-babel": "^3.2.0", | ||
"expect": "^1.16.0", | ||
"jest": "^0.1.40", | ||
"rimraf": "^2.5.2" | ||
}, | ||
"dependencies": { | ||
"babylon": "^6.7.0", | ||
"immutable": "^3.7.6" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
const adder = (x) => (y) => x + y; | ||
const add3 = adder(3); | ||
add3(39); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
import { batchExtendEnv } from './Environment'; | ||
|
||
const makeClosure = (args, body, defineTimeEnv) => ({ | ||
args, | ||
body, | ||
env: defineTimeEnv, | ||
}); | ||
|
||
const applyClosure = (interp, closure, vals, callTimeEnv, isLexical = true) => { | ||
const { args, body, env: defineTimeEnv } = closure; | ||
|
||
if (isLexical) { | ||
const newEnv = batchExtendEnv(args, vals, defineTimeEnv); | ||
return interp(body, newEnv); | ||
} | ||
|
||
const newEnv = batchExtendEnv(args, vals, callTimeEnv); | ||
return interp(body, newEnv); | ||
}; | ||
|
||
export { | ||
makeClosure, | ||
applyClosure, | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
/* | ||
* An environment is just a mapping from names to values. | ||
* For instance, after we evaluate `const foo = 12;`, | ||
* the environment contains a mapping that looks like `{ foo: 12 }`. | ||
* Here we use the immutable map from Immutable.js (mostly because its api is pretty nice). | ||
*/ | ||
import { Map as iMap } from 'immutable'; | ||
|
||
const emptyEnv = iMap(); | ||
|
||
const lookupEnv = (name, env) => { | ||
if (!env.has(name)) { | ||
throw new Error(`unbound variable ${name}`); | ||
} | ||
|
||
return env.get(name); | ||
}; | ||
|
||
const extendEnv = (name, val, env) => env.set(name, val); | ||
|
||
const batchExtendEnv = (names, vals, env) => { | ||
if (names.length !== vals.length) { | ||
throw new Error(`unmatched parameter vs. argument count: ${names}, ${vals}`); | ||
} | ||
|
||
return names.reduce( | ||
(newEnv, name, idx) => { | ||
const val = vals[idx]; | ||
return extendEnv(name, val, newEnv); | ||
}, | ||
env, | ||
); | ||
}; | ||
|
||
export { | ||
emptyEnv, | ||
lookupEnv, | ||
extendEnv, | ||
batchExtendEnv, | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,124 @@ | ||
/* | ||
Welcome to the interpreter! | ||
It only supports the following expressions and statements: | ||
- numbers | ||
- booleans | ||
- variables | ||
- if statements | ||
- function expression | ||
- function calls | ||
- +, -, *, / | ||
*/ | ||
|
||
import { parse } from './Parser'; | ||
import { readFile } from './Reader'; | ||
import { | ||
emptyEnv, | ||
lookupEnv, | ||
extendEnv, | ||
} from './Environment'; | ||
|
||
import { | ||
makeClosure, | ||
applyClosure, | ||
} from './Closure'; | ||
|
||
const code = readFile('../samples/adder.js'); | ||
const ast = parse(code); | ||
|
||
const expInterp = (exp, env) => { | ||
switch (exp.type) { | ||
case 'ArrowFunctionExpression': { | ||
const { expression, params, body } = exp; | ||
if (expression) { // () => exp | ||
const names = params.map((obj) => obj.name); | ||
const val = makeClosure(names, body, env); | ||
return { val, env }; | ||
} | ||
// () => { BlockStatement stuff } | ||
// @TODO | ||
break; | ||
} | ||
case 'CallExpression': { | ||
const { callee, arguments: args } = exp; | ||
const { val: closure } = expInterp(callee, env); | ||
const vals = args.map((obj) => expInterp(obj, env).val); | ||
const { val } = applyClosure(expInterp, closure, vals, env); | ||
return { val, env }; | ||
} | ||
case 'NumericLiteral': { | ||
const { value: val } = exp; | ||
return { val, env }; | ||
} | ||
case 'Identifier': { | ||
const { name } = exp; | ||
const val = lookupEnv(name, env); | ||
return { val, env }; | ||
} | ||
case 'BinaryExpression': { | ||
const { left, operator, right } = exp; | ||
const { val: leftVal } = expInterp(left, env); | ||
const { val: rightVal } = expInterp(right, env); | ||
switch (operator) { | ||
case '+': { | ||
return { val: leftVal + rightVal, env }; | ||
} | ||
case '-': { | ||
return { val: leftVal - rightVal, env }; | ||
} | ||
case '*': { | ||
return { val: leftVal * rightVal, env }; | ||
} | ||
case '/': { | ||
return { val: leftVal / rightVal, env }; | ||
} | ||
default: { | ||
throw new Error(`unsupported binary operator ${operator}`); | ||
} | ||
} | ||
} | ||
default: { | ||
throw new Error(`unsupported type ${exp.type}`); | ||
} | ||
} | ||
}; | ||
|
||
const blockStatementInterp = (exp, env) => { | ||
switch (exp.type) { | ||
case 'VariableDeclaration': { | ||
const { id, init } = exp.declarations[0]; | ||
|
||
const { name } = id; | ||
const { val: bindingVal } = expInterp(init, env); | ||
|
||
const newEnv = extendEnv(name, bindingVal, env); | ||
|
||
return { val: undefined, env: newEnv }; | ||
} | ||
case 'ExpressionStatement': { | ||
const { expression } = exp; | ||
const { val } = expInterp(expression, env); | ||
return { val, env }; | ||
} | ||
default: { | ||
throw new Error(`unsupported type ${exp.type}`); | ||
} | ||
} | ||
}; | ||
|
||
const programInterp = (exp, env) => { | ||
switch (exp.type) { | ||
case 'Program': { | ||
const { val } = exp.body.reduce( | ||
({ env: lastEnv }, newTarget) => blockStatementInterp(newTarget, lastEnv), | ||
{ val: undefined, env }, | ||
); | ||
return val; | ||
} | ||
default: { | ||
throw new Error('top level program not found'); | ||
} | ||
} | ||
}; | ||
|
||
console.log(programInterp(ast, emptyEnv)); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
/* | ||
* Writing a parser is way beyond the scope of this interpreter. | ||
* Let's just borrow it from babel. | ||
*/ | ||
import { parse as bParse } from 'babylon'; | ||
import { isObject } from './utils'; | ||
|
||
const defaultOptions = { | ||
sourceType: 'script', | ||
}; | ||
|
||
const astStripList = [ | ||
'start', | ||
'end', | ||
'loc', | ||
'comments', | ||
'tokens', | ||
'extra', | ||
'directives', | ||
'generator', | ||
]; | ||
|
||
/* | ||
* We don't care about line numbers and source locations for now -- let's clean them up. | ||
* The correct way to implement this AST traversal is to use the visitor pattern. | ||
* See https://github.com/thejameskyle/babel-handbook/blob/master/translations/en/plugin-handbook.md#traversal | ||
*/ | ||
const cleanupAst = (target) => { | ||
if (isObject(target)) { | ||
const fields = Object.keys(target); | ||
|
||
return fields.reduce( | ||
(res, fieldName) => { | ||
if (astStripList.indexOf(fieldName) === -1) { | ||
res[fieldName] = cleanupAst(target[fieldName]); // eslint-disable-line no-param-reassign | ||
} | ||
|
||
return res; | ||
}, | ||
{}, | ||
); | ||
} else if (Array.isArray(target)) { | ||
return target.map(cleanupAst); | ||
} | ||
|
||
return target; | ||
}; | ||
|
||
const parse = (code, options = defaultOptions) => { | ||
const originalAst = bParse(code, options); | ||
return cleanupAst(originalAst).program; // we don't care about `File` type, too | ||
}; | ||
|
||
export { parse }; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
import fs from 'fs'; | ||
|
||
const readFile = (filename: string) => fs.readFileSync(filename, 'utf8'); | ||
|
||
export { readFile }; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
const isObject = (x) => x && typeof x === 'object' && !Array.isArray(x); | ||
|
||
export { | ||
isObject, | ||
}; |