Skip to content

Commit

Permalink
letrec fixed yo
Browse files Browse the repository at this point in the history
  • Loading branch information
keyz committed Apr 17, 2016
1 parent 506470e commit 03763c4
Show file tree
Hide file tree
Showing 5 changed files with 82 additions and 38 deletions.
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
# trivial-js-interpreter
# trivial-js-interpreter
Let's explain what a **closure** is by writing a JavaScript interpreter in JavaScript.
33 changes: 27 additions & 6 deletions src/Closure.js
Original file line number Diff line number Diff line change
@@ -1,20 +1,40 @@
import invariant from 'fbjs/lib/invariant';

import { batchExtendEnv } from './Environment';
import { batchExtendEnv, extendEnv } from './Environment';
const CLOSURE_TYPE_FLAG = Symbol('CLOSURE_TYPE_FLAG');

/*
* As we've said,
* a closure is just a combination of a function definition and an environment snapshot.
* Here we represent a closure simply as a plain object.
*/
const makeClosure = (args, body, defineTimeEnv) => ({
const makeClosure = (params, body, defineTimeEnv) => ({
type: CLOSURE_TYPE_FLAG,
args,
params,
body,
env: defineTimeEnv,
});

/*
* This is a little bit tricky and please feel free to ignore this part.
*
* Our arrow functions can be recursively defined. For instance,
* in `const fact = (x) => (x < 2 ? 1 : x * fact(x - 1));`:
* we need to reference `fact` in the body of `fact` itself.
*
* If we create a "normal" closure for the function above, the `fact` in the body
* will be unbound.
*
* A quick fix is to update the environment with a reference to the closure itself.
* For more information, check http:https://www.cs.indiana.edu/~dyb/papers/fixing-letrec.pdf
*/
const makeRecClosure = (id, params, body, defineTimeEnv) => {
const closure = makeClosure(params, body, defineTimeEnv);
const updatedEnvWithSelfRef = extendEnv(id, closure, defineTimeEnv);
closure.env = updatedEnvWithSelfRef;
return closure;
};

/*
* `applyClosure` is where the function invocation (computation) happens.
* For demonstration purposes, we pass an additional `callTimeEnv` and an `isLexical` flag
Expand All @@ -29,22 +49,23 @@ const makeClosure = (args, body, defineTimeEnv) => ({
const applyClosure = (evaluator, closure, vals, callTimeEnv, isLexical = true) => {
invariant(closure.type === CLOSURE_TYPE_FLAG, `${closure} is not a closure`);

const { args, body, env: defineTimeEnv } = closure;
const { params, body, env: defineTimeEnv } = closure;

if (!isLexical) {
// Dynamic scope.
// `callTimeEnv` is the latest binding information.
const envForTheEvaluator = batchExtendEnv(args, vals, callTimeEnv);
const envForTheEvaluator = batchExtendEnv(params, vals, callTimeEnv);
return evaluator(body, envForTheEvaluator);
}

// Lexical closure yo.
// `defineTimeEnv` is the one that got extracted from the closure.
const envForTheEvaluator = batchExtendEnv(args, vals, defineTimeEnv);
const envForTheEvaluator = batchExtendEnv(params, vals, defineTimeEnv);
return evaluator(body, envForTheEvaluator);
};

export {
makeClosure,
makeRecClosure,
applyClosure,
};
18 changes: 9 additions & 9 deletions src/Interp/ExpressionInterp.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {

import {
makeClosure,
makeRecClosure,
applyClosure,
} from '../Closure';

Expand All @@ -33,15 +34,15 @@ const interp = (exp, env) => {
}

case 'ArrowFunctionExpression': {
/*
* @TODO double check the logic,
* but for now we don't need to dispatch based on `expression` here
*/
const { body, params } = exp;

const names = params.map((obj) => obj.name);
const val = makeClosure(names, body, env);
return val;

if (exp.extra && exp.extra.isLambda) {
const { name: selfId } = exp.extra;
return makeRecClosure(selfId, names, body, env);
}

return makeClosure(names, body, env);
}

case 'CallExpression': {
Expand All @@ -50,8 +51,7 @@ const interp = (exp, env) => {
const closure = interp(callee, env);
const vals = rawArgs.map((obj) => interp(obj, env));

const val = applyClosure(interp, closure, vals, env);
return val;
return applyClosure(interp, closure, vals, env);
}

case 'UnaryExpression': {
Expand Down
41 changes: 33 additions & 8 deletions src/Interp/StatementInterp.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@ const interp = (exp, env) => {
switch (exp.type) {
case 'BlockStatement': {
let currentEnv = env;

for (let i = 0; i < exp.body.length; i++) {
const currentExp = exp.body[i];

switch (currentExp.type) {
case 'ExpressionStatement': {
expInterp(currentExp.expression, currentEnv);
Expand All @@ -21,26 +23,48 @@ const interp = (exp, env) => {
case 'ReturnStatement': {
const { argument } = currentExp;

return expInterp(argument, currentEnv); // return!
return expInterp(argument, currentEnv); // early return!
}

case 'VariableDeclaration': {
const { kind, declarations } = currentExp;

invariant(
kind === 'const',
`unsupported VariableDeclaration kind ${kind}`,
);

for (let j = 0; j < declarations.length; j++) {
const { id, init } = declarations[j];
const { name } = id;
const val = expInterp(init, currentEnv);
currentEnv = extendEnv(name, val, currentEnv);
invariant(
declarations.length === 1,
`unsupported multiple (${declarations.length}) VariableDeclarations`,
);

const { id, init } = declarations[0];
const { name } = id;

if (init.type === 'ArrowFunctionExpression') {
/*
* TL;DR: it could be a letrec!
*
* A better way is to do a static analysis and to see whether the RHS
* actually contains recursive definitions.
* However, for the sake of simplicity,
* we treat all RHS lambdas as potential self-referencing definitions,
* a.k.a., `letrec`s.
*
* For more information, check the comments and definitions in `Closure.js`
* and http:https://www.cs.indiana.edu/~dyb/papers/fixing-letrec.pdf
*/
init.extra = { isLambda: true, name };
}

const val = expInterp(init, currentEnv);
currentEnv = extendEnv(name, val, currentEnv);

continue;
}

// @TODO need to return return in ifs
// @TODO if statements
// case 'IfStatement': {
// const { alternate, consequent, test } = currentExp;
// const testVal = expInterp(test, currentEnv);
Expand All @@ -56,8 +80,9 @@ const interp = (exp, env) => {
}
}

return undefined;
return undefined; // `return` hasn't been called so we return `undefined`
}

default: {
throw new Error(`unsupported statement type ${exp.type}`);
}
Expand Down
25 changes: 11 additions & 14 deletions src/Interp/__tests__/ExpressionInterp-test.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
jest.unmock('../ExpressionInterp').unmock('../StatementInterp')
.unmock('../../Parser')
jest.unmock('../../Parser')
.unmock('../ExpressionInterp').unmock('../StatementInterp')
.unmock('../../Environment').unmock('../../Closure');

const { interp } = require('../ExpressionInterp');
const { parse } = require('../../Parser');
const {
emptyEnv,
// lookupEnv,
extendEnv,
// batchExtendEnv,
} = require('../../Environment');

import { makeClosure } from '../../Closure';
Expand Down Expand Up @@ -163,16 +161,15 @@ describe('Interp', () => {
})());
});

// it('should support basic recursion', () => {
// expect(interpExp(`(() => {
// const fact = (x) => (x === 1 ? 1 : x * fact(x - 1));
// // return fact(5);
// })()`)).toBe((() => {
// const fact = (x) => (x === 1 ? 1 : x * fact(x - 1));
// // return fact(5);
// })());
// });

it('should support recursions (letrec)', () => {
expect(interpExp(`(() => {
const fact = (x) => (x < 2 ? 1 : x * fact(x - 1));
return fact(5);
})()`)).toBe((() => {
const fact = (x) => (x < 2 ? 1 : x * fact(x - 1));
return fact(5);
})());
});

// it('IfStatement', () => {
// expect(interpExp(`(() => {
Expand Down

0 comments on commit 03763c4

Please sign in to comment.