Skip to content

Commit

Permalink
alright now we have lexical and dynamic scopes
Browse files Browse the repository at this point in the history
  • Loading branch information
keyz committed Apr 13, 2016
1 parent ed8a00d commit 3bbf3df
Show file tree
Hide file tree
Showing 12 changed files with 335 additions and 0 deletions.
4 changes: 4 additions & 0 deletions .babelrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"presets": [ "es2015", "stage-0" ],
"plugins": ["transform-flow-strip-types"],
}
30 changes: 30 additions & 0 deletions .eslintrc
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",
],
}
4 changes: 4 additions & 0 deletions .flowconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
[ignore]

.*/node_modules/.*
./dist/.*
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,8 @@ node_modules

# Optional REPL history
.node_repl_history

.DS_Store
dist

.nyc_output
37 changes: 37 additions & 0 deletions package.json
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"
}
}
3 changes: 3 additions & 0 deletions samples/adder.js
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);
24 changes: 24 additions & 0 deletions src/Closure.js
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,
};
40 changes: 40 additions & 0 deletions src/Environment.js
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,
};
124 changes: 124 additions & 0 deletions src/Interp.js
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));
54 changes: 54 additions & 0 deletions src/Parser.js
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 };
5 changes: 5 additions & 0 deletions src/Reader.js
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 };
5 changes: 5 additions & 0 deletions src/utils.js
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,
};

0 comments on commit 3bbf3df

Please sign in to comment.