├── LICENSE ├── README.md ├── index.js ├── package.json └── src ├── ast.js ├── interpreter.js ├── lexer.js ├── parser.js └── token.js /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Tadeu Zagallo 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # lc-js: A λ-calculus interpreter written in JavaScript 2 | 3 | This interpreter was written for [this blog post][post] and was based on the implementation showed in the book [Types and Programming Languages][TAPL]. 4 | 5 | ## Running 6 | 7 | In order to run the interpreter, clone this repo, and run: 8 | 9 | ``` 10 | $ node index.js 11 | ``` 12 | 13 | You can also print the AST for the program by running: 14 | 15 | ``` 16 | $ node index.js --ast 17 | ``` 18 | 19 | [post]: http://tadeuzagallo.com/blog/writing-a-lambda-calculus-interpreter-in-javascript/ 20 | [TAPL]: https://www.cis.upenn.edu/~bcpierce/tapl 21 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const Lexer = require('./src/lexer'); 2 | const Parser = require('./src/parser'); 3 | const Interpreter = require('./src/interpreter'); 4 | 5 | const fs = require('fs'); 6 | const util = require('util'); 7 | 8 | let filename; 9 | let printAST = false; 10 | if (process.argv[2] === '--ast') { 11 | printAST = true; 12 | filename = process.argv[3]; 13 | } else { 14 | filename = process.argv[2]; 15 | } 16 | 17 | const source = fs.readFileSync(filename).toString(); 18 | 19 | const lexer = new Lexer(source); 20 | const parser = new Parser(lexer); 21 | const ast = parser.parse(); 22 | 23 | if (printAST) { 24 | const output = util.inspect(ast, { 25 | depth: null, 26 | colors: true, 27 | }); 28 | console.log(output); 29 | } else { 30 | console.log(Interpreter.eval(ast).toString()); 31 | } 32 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lc-js", 3 | "version": "0.0.1", 4 | "description": "A λ-calculus interpreter written in JavaScript", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/tadeuzagallo/lc-js.git" 12 | }, 13 | "keywords": [ 14 | "lambda-calculus", 15 | "lambda", 16 | "interpreter" 17 | ], 18 | "author": "Tadeu Zagallo ", 19 | "license": "MIT", 20 | "bugs": { 21 | "url": "https://github.com/tadeuzagallo/lc-js/issues" 22 | }, 23 | "homepage": "https://github.com/tadeuzagallo/lc-js#readme" 24 | } 25 | -------------------------------------------------------------------------------- /src/ast.js: -------------------------------------------------------------------------------- 1 | class Abstraction { 2 | /** 3 | * param here is the name of the variable of the abstraction. Body is the 4 | * subtree representing the body of the abstraction. 5 | */ 6 | constructor(param, body) { 7 | this.param = param; 8 | this.body = body; 9 | } 10 | 11 | toString(ctx=[]) { 12 | return `(λ${this.param}. ${this.body.toString([this.param].concat(ctx))})`; 13 | } 14 | } 15 | 16 | class Application { 17 | /** 18 | * (lhs rhs) - left-hand side and right-hand side of an application. 19 | */ 20 | constructor(lhs, rhs) { 21 | this.lhs = lhs; 22 | this.rhs = rhs; 23 | } 24 | 25 | toString(ctx) { 26 | return `${this.lhs.toString(ctx)} ${this.rhs.toString(ctx)}`; 27 | } 28 | } 29 | 30 | class Identifier { 31 | /** 32 | * name is the string matched for this identifier. 33 | */ 34 | constructor(value) { 35 | this.value = value; 36 | } 37 | 38 | toString(ctx) { 39 | return ctx[this.value]; 40 | } 41 | } 42 | 43 | exports.Abstraction = Abstraction; 44 | exports.Application = Application; 45 | exports.Identifier = Identifier; 46 | -------------------------------------------------------------------------------- /src/interpreter.js: -------------------------------------------------------------------------------- 1 | const AST = require('./ast'); 2 | 3 | const isValue = node => node instanceof AST.Abstraction; 4 | 5 | const eval = ast => { 6 | while (true) { 7 | if (ast instanceof AST.Application) { 8 | if (isValue(ast.lhs) && isValue(ast.rhs)) { 9 | /** 10 | * if both sides of the application are values we can proceed and 11 | * substitute the rhs value for the variables that reference the 12 | * abstraction's parameter in the evaluation body and then evaluate the 13 | * abstraction's body 14 | */ 15 | ast = substitute(ast.rhs, ast.lhs.body); 16 | } else if (isValue(ast.lhs)) { 17 | /** 18 | * We should only evaluate rhs once lhs has been reduced to a value 19 | */ 20 | ast.rhs = eval(ast.rhs); 21 | } else { 22 | /** 23 | * Keep reducing lhs until it becomes a value 24 | */ 25 | ast.lhs = eval(ast.lhs); 26 | } 27 | } else if (isValue(ast)) { 28 | /** 29 | * * `ast` is a value, and therefore an abstraction. That means we're done 30 | * reducing it, and this is the result of the current evaluation. 31 | */ 32 | return ast; 33 | } 34 | } 35 | }; 36 | 37 | const traverse = fn => 38 | function(node, ...args) { 39 | const config = fn(...args); 40 | if (node instanceof AST.Application) 41 | return config.Application(node); 42 | else if (node instanceof AST.Abstraction) 43 | return config.Abstraction(node); 44 | else if (node instanceof AST.Identifier) 45 | return config.Identifier(node); 46 | } 47 | 48 | const shift = (by, node) => { 49 | const aux = traverse(from => ({ 50 | Application(app) { 51 | return new AST.Application( 52 | aux(app.lhs, from), 53 | aux(app.rhs, from) 54 | ); 55 | }, 56 | Abstraction(abs) { 57 | return new AST.Abstraction( 58 | abs.param, 59 | aux(abs.body, from + 1) 60 | ); 61 | }, 62 | Identifier(id) { 63 | return new AST.Identifier( 64 | id.value + (id.value >= from ? by : 0) 65 | ); 66 | } 67 | })); 68 | return aux(node, 0); 69 | }; 70 | 71 | const subst = (value, node) => { 72 | const aux = traverse(depth => ({ 73 | Application(app) { 74 | return new AST.Application( 75 | aux(app.lhs, depth), 76 | aux(app.rhs, depth) 77 | ); 78 | }, 79 | Abstraction(abs) { 80 | return new AST.Abstraction( 81 | abs.param, 82 | aux(abs.body, depth + 1) 83 | ); 84 | }, 85 | Identifier(id) { 86 | if (depth === id.value) 87 | return shift(depth, value); 88 | else 89 | return id; 90 | } 91 | })); 92 | return aux(node, 0); 93 | }; 94 | 95 | const substitute = (value, node) => { 96 | return shift(-1, subst(shift(1, value), node)); 97 | }; 98 | 99 | exports.eval = eval; 100 | -------------------------------------------------------------------------------- /src/lexer.js: -------------------------------------------------------------------------------- 1 | const Token = require('./token'); 2 | 3 | class Lexer { 4 | constructor(input) { 5 | this._input = input; 6 | this._index = 0; 7 | this._token = undefined; 8 | this._nextToken(); 9 | } 10 | 11 | /** 12 | * Return the next char of the input or '\0' if we've reached the end 13 | */ 14 | _nextChar() { 15 | if (this._index >= this._input.length) { 16 | return '\0'; 17 | } 18 | 19 | return this._input[this._index++]; 20 | } 21 | 22 | /** 23 | * Set this._token based on the remaining of the input 24 | * 25 | * This method is meant to be private, it doesn't return a token, just sets 26 | * up the state for the helper functions. 27 | */ 28 | _nextToken() { 29 | let c; 30 | do { 31 | c = this._nextChar(); 32 | } while (/\s/.test(c)); 33 | 34 | switch (c) { 35 | case 'λ': 36 | case '\\': 37 | this._token = new Token(Token.LAMBDA); 38 | break; 39 | 40 | case '.': 41 | this._token = new Token(Token.DOT); 42 | break; 43 | 44 | case '(': 45 | this._token = new Token(Token.LPAREN); 46 | break; 47 | 48 | case ')': 49 | this._token = new Token(Token.RPAREN); 50 | break; 51 | 52 | case '\0': 53 | this._token = new Token(Token.EOF); 54 | break; 55 | 56 | default: 57 | if (/[a-z]/.test(c)) { 58 | let str = ''; 59 | do { 60 | str += c; 61 | c = this._nextChar(); 62 | } while (/[a-zA-Z]/.test(c)); 63 | 64 | // put back the last char which is not part of the identifier 65 | this._index--; 66 | 67 | this._token = new Token(Token.LCID, str); 68 | } else { 69 | this.fail(); 70 | } 71 | } 72 | } 73 | 74 | /** 75 | * Assert that the next token has the given type, return it, and skip to the 76 | * next token. 77 | */ 78 | token(type) { 79 | if (!type) { 80 | return this._token.value; 81 | } 82 | 83 | const token = this._token; 84 | this.match(type); 85 | return token.value; 86 | } 87 | 88 | /** 89 | * Throw an unexpected token error - ideally this would print the source 90 | * location 91 | */ 92 | fail() { 93 | throw new Error(`Unexpected token at offset ${this._index}`); 94 | } 95 | 96 | /** 97 | * Returns a boolean indicating whether the next token has the given type. 98 | */ 99 | next(type) { 100 | return this._token.type == type; 101 | } 102 | 103 | /** 104 | * Assert that the next token has the given type and skip it. 105 | */ 106 | match(type) { 107 | if (this.next(type)) { 108 | this._nextToken(); 109 | return; 110 | } 111 | console.error(`${this._index}: Invalid token: Expected '${type}' found '${this._token.type}'`); 112 | throw new Error('Parse Error'); 113 | } 114 | 115 | /** 116 | * Same as `next`, but skips the token if it matches the expected type. 117 | */ 118 | skip(type) { 119 | if (this.next(type)) { 120 | this._nextToken(); 121 | return true; 122 | } 123 | return false; 124 | } 125 | } 126 | 127 | module.exports = Lexer; 128 | -------------------------------------------------------------------------------- /src/parser.js: -------------------------------------------------------------------------------- 1 | const AST = require('./ast'); 2 | const Token = require('./token'); 3 | 4 | class Parser { 5 | constructor(lexer) { 6 | this.lexer = lexer; 7 | } 8 | 9 | parse() { 10 | const result = this.term([]); 11 | // make sure we consumed all the program, otherwise there was a syntax error 12 | this.lexer.match(Token.EOF); 13 | 14 | return result; 15 | } 16 | 17 | // term ::= LAMBDA LCID DOT term 18 | // | application 19 | term(ctx) { 20 | if (this.lexer.skip(Token.LAMBDA)) { 21 | const id = this.lexer.token(Token.LCID); 22 | this.lexer.match(Token.DOT); 23 | const term = this.term([id].concat(ctx)); 24 | return new AST.Abstraction(id, term); 25 | } else { 26 | return this.application(ctx); 27 | } 28 | } 29 | 30 | // application ::= atom application' 31 | application(ctx) { 32 | let lhs = this.atom(ctx); 33 | 34 | // application' ::= atom application' 35 | // | ε 36 | while (true) { 37 | const rhs = this.atom(ctx); 38 | if (!rhs) { 39 | return lhs; 40 | } else { 41 | lhs = new AST.Application(lhs, rhs); 42 | } 43 | } 44 | } 45 | 46 | // atom ::= LPAREN term RPAREN 47 | // | LCID 48 | atom(ctx) { 49 | if (this.lexer.skip(Token.LPAREN)) { 50 | const term = this.term(ctx); 51 | this.lexer.match(Token.RPAREN); 52 | return term; 53 | } else if (this.lexer.next(Token.LCID)) { 54 | const id = this.lexer.token(Token.LCID) 55 | return new AST.Identifier(ctx.indexOf(id)); 56 | } else { 57 | return undefined; 58 | } 59 | } 60 | } 61 | 62 | module.exports = Parser; 63 | -------------------------------------------------------------------------------- /src/token.js: -------------------------------------------------------------------------------- 1 | class Token { 2 | /** 3 | * type should be one of the valid token types list below, and value is an 4 | * optional value that can carry any extra information necessary for a given 5 | * token type. (e.g. the matched string for an identifier) 6 | */ 7 | constructor(type, value) { 8 | this.type = type; 9 | this.value = value; 10 | } 11 | }; 12 | 13 | [ 14 | 'EOF', // we augment the tokens with EOF, to indicate the end of the input. 15 | 'LAMBDA', 16 | 'LPAREN', 17 | 'RPAREN', 18 | 'LCID', 19 | 'DOT', 20 | ].forEach(token => Token[token] = token); 21 | 22 | module.exports = Token; 23 | --------------------------------------------------------------------------------