summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorElizabeth Hunt <elizabeth.hunt@simponic.xyz>2024-02-13 19:57:27 -0700
committerElizabeth Hunt <elizabeth.hunt@simponic.xyz>2024-02-13 19:57:27 -0700
commit36834bb8f64de5e6e4d16553172ef7c75fc5fc4c (patch)
treec5f0baf754ecfbac342d3657716f709c363c0413
downloadlambda-calculus-interpreter-36834bb8f64de5e6e4d16553172ef7c75fc5fc4c.tar.gz
lambda-calculus-interpreter-36834bb8f64de5e6e4d16553172ef7c75fc5fc4c.zip
interpreter
-rw-r--r--README.md1
-rw-r--r--grammar.peggy14
-rw-r--r--parser.js629
-rw-r--r--repl.ts106
4 files changed, 750 insertions, 0 deletions
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..79361d7
--- /dev/null
+++ b/README.md
@@ -0,0 +1 @@
+usage: `bun repl.ts`
diff --git a/grammar.peggy b/grammar.peggy
new file mode 100644
index 0000000..c2dd580
--- /dev/null
+++ b/grammar.peggy
@@ -0,0 +1,14 @@
+LambdaTerm = Application / Abstraction / Variable
+
+Application = LPAREN _ left:LambdaTerm _ right:LambdaTerm RPAREN { return { application: { left, right } }; }
+
+Abstraction = LPAREN _ LAMBDA _ param:Variable _ DOT _ body:LambdaTerm RPAREN { return { abstraction: { param, body } }; }
+
+Variable = param:[a-zA-Z0-9]+ { return param.join(""); }
+
+LPAREN = "("
+RPAREN = ")"
+DOT = "."
+LAMBDA = "λ" / "\\"
+
+_ = ("\n" / " " / "\t" / "\r\n")*
diff --git a/parser.js b/parser.js
new file mode 100644
index 0000000..b57a127
--- /dev/null
+++ b/parser.js
@@ -0,0 +1,629 @@
+module.exports = // @generated by Peggy 4.0.0.
+//
+// https://peggyjs.org/
+(function() {
+ "use strict";
+
+function peg$subclass(child, parent) {
+ function C() { this.constructor = child; }
+ C.prototype = parent.prototype;
+ child.prototype = new C();
+}
+
+function peg$SyntaxError(message, expected, found, location) {
+ var self = Error.call(this, message);
+ // istanbul ignore next Check is a necessary evil to support older environments
+ if (Object.setPrototypeOf) {
+ Object.setPrototypeOf(self, peg$SyntaxError.prototype);
+ }
+ self.expected = expected;
+ self.found = found;
+ self.location = location;
+ self.name = "SyntaxError";
+ return self;
+}
+
+peg$subclass(peg$SyntaxError, Error);
+
+function peg$padEnd(str, targetLength, padString) {
+ padString = padString || " ";
+ if (str.length > targetLength) { return str; }
+ targetLength -= str.length;
+ padString += padString.repeat(targetLength);
+ return str + padString.slice(0, targetLength);
+}
+
+peg$SyntaxError.prototype.format = function(sources) {
+ var str = "Error: " + this.message;
+ if (this.location) {
+ var src = null;
+ var k;
+ for (k = 0; k < sources.length; k++) {
+ if (sources[k].source === this.location.source) {
+ src = sources[k].text.split(/\r\n|\n|\r/g);
+ break;
+ }
+ }
+ var s = this.location.start;
+ var offset_s = (this.location.source && (typeof this.location.source.offset === "function"))
+ ? this.location.source.offset(s)
+ : s;
+ var loc = this.location.source + ":" + offset_s.line + ":" + offset_s.column;
+ if (src) {
+ var e = this.location.end;
+ var filler = peg$padEnd("", offset_s.line.toString().length, ' ');
+ var line = src[s.line - 1];
+ var last = s.line === e.line ? e.column : line.length + 1;
+ var hatLen = (last - s.column) || 1;
+ str += "\n --> " + loc + "\n"
+ + filler + " |\n"
+ + offset_s.line + " | " + line + "\n"
+ + filler + " | " + peg$padEnd("", s.column - 1, ' ')
+ + peg$padEnd("", hatLen, "^");
+ } else {
+ str += "\n at " + loc;
+ }
+ }
+ return str;
+};
+
+peg$SyntaxError.buildMessage = function(expected, found) {
+ var DESCRIBE_EXPECTATION_FNS = {
+ literal: function(expectation) {
+ return "\"" + literalEscape(expectation.text) + "\"";
+ },
+
+ class: function(expectation) {
+ var escapedParts = expectation.parts.map(function(part) {
+ return Array.isArray(part)
+ ? classEscape(part[0]) + "-" + classEscape(part[1])
+ : classEscape(part);
+ });
+
+ return "[" + (expectation.inverted ? "^" : "") + escapedParts.join("") + "]";
+ },
+
+ any: function() {
+ return "any character";
+ },
+
+ end: function() {
+ return "end of input";
+ },
+
+ other: function(expectation) {
+ return expectation.description;
+ }
+ };
+
+ function hex(ch) {
+ return ch.charCodeAt(0).toString(16).toUpperCase();
+ }
+
+ function literalEscape(s) {
+ return s
+ .replace(/\\/g, "\\\\")
+ .replace(/"/g, "\\\"")
+ .replace(/\0/g, "\\0")
+ .replace(/\t/g, "\\t")
+ .replace(/\n/g, "\\n")
+ .replace(/\r/g, "\\r")
+ .replace(/[\x00-\x0F]/g, function(ch) { return "\\x0" + hex(ch); })
+ .replace(/[\x10-\x1F\x7F-\x9F]/g, function(ch) { return "\\x" + hex(ch); });
+ }
+
+ function classEscape(s) {
+ return s
+ .replace(/\\/g, "\\\\")
+ .replace(/\]/g, "\\]")
+ .replace(/\^/g, "\\^")
+ .replace(/-/g, "\\-")
+ .replace(/\0/g, "\\0")
+ .replace(/\t/g, "\\t")
+ .replace(/\n/g, "\\n")
+ .replace(/\r/g, "\\r")
+ .replace(/[\x00-\x0F]/g, function(ch) { return "\\x0" + hex(ch); })
+ .replace(/[\x10-\x1F\x7F-\x9F]/g, function(ch) { return "\\x" + hex(ch); });
+ }
+
+ function describeExpectation(expectation) {
+ return DESCRIBE_EXPECTATION_FNS[expectation.type](expectation);
+ }
+
+ function describeExpected(expected) {
+ var descriptions = expected.map(describeExpectation);
+ var i, j;
+
+ descriptions.sort();
+
+ if (descriptions.length > 0) {
+ for (i = 1, j = 1; i < descriptions.length; i++) {
+ if (descriptions[i - 1] !== descriptions[i]) {
+ descriptions[j] = descriptions[i];
+ j++;
+ }
+ }
+ descriptions.length = j;
+ }
+
+ switch (descriptions.length) {
+ case 1:
+ return descriptions[0];
+
+ case 2:
+ return descriptions[0] + " or " + descriptions[1];
+
+ default:
+ return descriptions.slice(0, -1).join(", ")
+ + ", or "
+ + descriptions[descriptions.length - 1];
+ }
+ }
+
+ function describeFound(found) {
+ return found ? "\"" + literalEscape(found) + "\"" : "end of input";
+ }
+
+ return "Expected " + describeExpected(expected) + " but " + describeFound(found) + " found.";
+};
+
+function peg$parse(input, options) {
+ options = options !== undefined ? options : {};
+
+ var peg$FAILED = {};
+ var peg$source = options.grammarSource;
+
+ var peg$startRuleFunctions = { LambdaTerm: peg$parseLambdaTerm };
+ var peg$startRuleFunction = peg$parseLambdaTerm;
+
+ var peg$c0 = "(";
+ var peg$c1 = ")";
+ var peg$c2 = ".";
+ var peg$c3 = "\r\n";
+
+ var peg$r0 = /^[a-zA-Z0-9]/;
+ var peg$r1 = /^[\\\u03BB]/;
+ var peg$r2 = /^[\t-\n ]/;
+
+ var peg$e0 = peg$classExpectation([["a", "z"], ["A", "Z"], ["0", "9"]], false, false);
+ var peg$e1 = peg$literalExpectation("(", false);
+ var peg$e2 = peg$literalExpectation(")", false);
+ var peg$e3 = peg$literalExpectation(".", false);
+ var peg$e4 = peg$classExpectation(["\\", "\u03BB"], false, false);
+ var peg$e5 = peg$classExpectation([["\t", "\n"], " "], false, false);
+ var peg$e6 = peg$literalExpectation("\r\n", false);
+
+ var peg$f0 = function(left, right) { return { application: { left, right } }; };
+ var peg$f1 = function(param, body) { return { abstraction: { param, body } }; };
+ var peg$f2 = function(param) { return param.join(""); };
+ var peg$currPos = options.peg$currPos | 0;
+ var peg$savedPos = peg$currPos;
+ var peg$posDetailsCache = [{ line: 1, column: 1 }];
+ var peg$maxFailPos = peg$currPos;
+ var peg$maxFailExpected = options.peg$maxFailExpected || [];
+ var peg$silentFails = options.peg$silentFails | 0;
+
+ var peg$result;
+
+ if (options.startRule) {
+ if (!(options.startRule in peg$startRuleFunctions)) {
+ throw new Error("Can't start parsing from rule \"" + options.startRule + "\".");
+ }
+
+ peg$startRuleFunction = peg$startRuleFunctions[options.startRule];
+ }
+
+ function text() {
+ return input.substring(peg$savedPos, peg$currPos);
+ }
+
+ function offset() {
+ return peg$savedPos;
+ }
+
+ function range() {
+ return {
+ source: peg$source,
+ start: peg$savedPos,
+ end: peg$currPos
+ };
+ }
+
+ function location() {
+ return peg$computeLocation(peg$savedPos, peg$currPos);
+ }
+
+ function expected(description, location) {
+ location = location !== undefined
+ ? location
+ : peg$computeLocation(peg$savedPos, peg$currPos);
+
+ throw peg$buildStructuredError(
+ [peg$otherExpectation(description)],
+ input.substring(peg$savedPos, peg$currPos),
+ location
+ );
+ }
+
+ function error(message, location) {
+ location = location !== undefined
+ ? location
+ : peg$computeLocation(peg$savedPos, peg$currPos);
+
+ throw peg$buildSimpleError(message, location);
+ }
+
+ function peg$literalExpectation(text, ignoreCase) {
+ return { type: "literal", text: text, ignoreCase: ignoreCase };
+ }
+
+ function peg$classExpectation(parts, inverted, ignoreCase) {
+ return { type: "class", parts: parts, inverted: inverted, ignoreCase: ignoreCase };
+ }
+
+ function peg$anyExpectation() {
+ return { type: "any" };
+ }
+
+ function peg$endExpectation() {
+ return { type: "end" };
+ }
+
+ function peg$otherExpectation(description) {
+ return { type: "other", description: description };
+ }
+
+ function peg$computePosDetails(pos) {
+ var details = peg$posDetailsCache[pos];
+ var p;
+
+ if (details) {
+ return details;
+ } else {
+ if (pos >= peg$posDetailsCache.length) {
+ p = peg$posDetailsCache.length - 1;
+ } else {
+ p = pos;
+ while (!peg$posDetailsCache[--p]) {}
+ }
+
+ details = peg$posDetailsCache[p];
+ details = {
+ line: details.line,
+ column: details.column
+ };
+
+ while (p < pos) {
+ if (input.charCodeAt(p) === 10) {
+ details.line++;
+ details.column = 1;
+ } else {
+ details.column++;
+ }
+
+ p++;
+ }
+
+ peg$posDetailsCache[pos] = details;
+
+ return details;
+ }
+ }
+
+ function peg$computeLocation(startPos, endPos, offset) {
+ var startPosDetails = peg$computePosDetails(startPos);
+ var endPosDetails = peg$computePosDetails(endPos);
+
+ var res = {
+ source: peg$source,
+ start: {
+ offset: startPos,
+ line: startPosDetails.line,
+ column: startPosDetails.column
+ },
+ end: {
+ offset: endPos,
+ line: endPosDetails.line,
+ column: endPosDetails.column
+ }
+ };
+ if (offset && peg$source && (typeof peg$source.offset === "function")) {
+ res.start = peg$source.offset(res.start);
+ res.end = peg$source.offset(res.end);
+ }
+ return res;
+ }
+
+ function peg$fail(expected) {
+ if (peg$currPos < peg$maxFailPos) { return; }
+
+ if (peg$currPos > peg$maxFailPos) {
+ peg$maxFailPos = peg$currPos;
+ peg$maxFailExpected = [];
+ }
+
+ peg$maxFailExpected.push(expected);
+ }
+
+ function peg$buildSimpleError(message, location) {
+ return new peg$SyntaxError(message, null, null, location);
+ }
+
+ function peg$buildStructuredError(expected, found, location) {
+ return new peg$SyntaxError(
+ peg$SyntaxError.buildMessage(expected, found),
+ expected,
+ found,
+ location
+ );
+ }
+
+ function peg$parseLambdaTerm() {
+ var s0;
+
+ s0 = peg$parseApplication();
+ if (s0 === peg$FAILED) {
+ s0 = peg$parseAbstraction();
+ if (s0 === peg$FAILED) {
+ s0 = peg$parseVariable();
+ }
+ }
+
+ return s0;
+ }
+
+ function peg$parseApplication() {
+ var s0, s1, s2, s3, s4, s5, s6;
+
+ s0 = peg$currPos;
+ s1 = peg$parseLPAREN();
+ if (s1 !== peg$FAILED) {
+ s2 = peg$parse_();
+ s3 = peg$parseLambdaTerm();
+ if (s3 !== peg$FAILED) {
+ s4 = peg$parse_();
+ s5 = peg$parseLambdaTerm();
+ if (s5 !== peg$FAILED) {
+ s6 = peg$parseRPAREN();
+ if (s6 !== peg$FAILED) {
+ peg$savedPos = s0;
+ s0 = peg$f0(s3, s5);
+ } else {
+ peg$currPos = s0;
+ s0 = peg$FAILED;
+ }
+ } else {
+ peg$currPos = s0;
+ s0 = peg$FAILED;
+ }
+ } else {
+ peg$currPos = s0;
+ s0 = peg$FAILED;
+ }
+ } else {
+ peg$currPos = s0;
+ s0 = peg$FAILED;
+ }
+
+ return s0;
+ }
+
+ function peg$parseAbstraction() {
+ var s0, s1, s2, s3, s4, s5, s6, s7, s8, s9, s10;
+
+ s0 = peg$currPos;
+ s1 = peg$parseLPAREN();
+ if (s1 !== peg$FAILED) {
+ s2 = peg$parse_();
+ s3 = peg$parseLAMBDA();
+ if (s3 !== peg$FAILED) {
+ s4 = peg$parse_();
+ s5 = peg$parseVariable();
+ if (s5 !== peg$FAILED) {
+ s6 = peg$parse_();
+ s7 = peg$parseDOT();
+ if (s7 !== peg$FAILED) {
+ s8 = peg$parse_();
+ s9 = peg$parseLambdaTerm();
+ if (s9 !== peg$FAILED) {
+ s10 = peg$parseRPAREN();
+ if (s10 !== peg$FAILED) {
+ peg$savedPos = s0;
+ s0 = peg$f1(s5, s9);
+ } else {
+ peg$currPos = s0;
+ s0 = peg$FAILED;
+ }
+ } else {
+ peg$currPos = s0;
+ s0 = peg$FAILED;
+ }
+ } else {
+ peg$currPos = s0;
+ s0 = peg$FAILED;
+ }
+ } else {
+ peg$currPos = s0;
+ s0 = peg$FAILED;
+ }
+ } else {
+ peg$currPos = s0;
+ s0 = peg$FAILED;
+ }
+ } else {
+ peg$currPos = s0;
+ s0 = peg$FAILED;
+ }
+
+ return s0;
+ }
+
+ function peg$parseVariable() {
+ var s0, s1, s2;
+
+ s0 = peg$currPos;
+ s1 = [];
+ s2 = input.charAt(peg$currPos);
+ if (peg$r0.test(s2)) {
+ peg$currPos++;
+ } else {
+ s2 = peg$FAILED;
+ if (peg$silentFails === 0) { peg$fail(peg$e0); }
+ }
+ if (s2 !== peg$FAILED) {
+ while (s2 !== peg$FAILED) {
+ s1.push(s2);
+ s2 = input.charAt(peg$currPos);
+ if (peg$r0.test(s2)) {
+ peg$currPos++;
+ } else {
+ s2 = peg$FAILED;
+ if (peg$silentFails === 0) { peg$fail(peg$e0); }
+ }
+ }
+ } else {
+ s1 = peg$FAILED;
+ }
+ if (s1 !== peg$FAILED) {
+ peg$savedPos = s0;
+ s1 = peg$f2(s1);
+ }
+ s0 = s1;
+
+ return s0;
+ }
+
+ function peg$parseLPAREN() {
+ var s0;
+
+ if (input.charCodeAt(peg$currPos) === 40) {
+ s0 = peg$c0;
+ peg$currPos++;
+ } else {
+ s0 = peg$FAILED;
+ if (peg$silentFails === 0) { peg$fail(peg$e1); }
+ }
+
+ return s0;
+ }
+
+ function peg$parseRPAREN() {
+ var s0;
+
+ if (input.charCodeAt(peg$currPos) === 41) {
+ s0 = peg$c1;
+ peg$currPos++;
+ } else {
+ s0 = peg$FAILED;
+ if (peg$silentFails === 0) { peg$fail(peg$e2); }
+ }
+
+ return s0;
+ }
+
+ function peg$parseDOT() {
+ var s0;
+
+ if (input.charCodeAt(peg$currPos) === 46) {
+ s0 = peg$c2;
+ peg$currPos++;
+ } else {
+ s0 = peg$FAILED;
+ if (peg$silentFails === 0) { peg$fail(peg$e3); }
+ }
+
+ return s0;
+ }
+
+ function peg$parseLAMBDA() {
+ var s0;
+
+ s0 = input.charAt(peg$currPos);
+ if (peg$r1.test(s0)) {
+ peg$currPos++;
+ } else {
+ s0 = peg$FAILED;
+ if (peg$silentFails === 0) { peg$fail(peg$e4); }
+ }
+
+ return s0;
+ }
+
+ function peg$parse_() {
+ var s0, s1;
+
+ s0 = [];
+ s1 = input.charAt(peg$currPos);
+ if (peg$r2.test(s1)) {
+ peg$currPos++;
+ } else {
+ s1 = peg$FAILED;
+ if (peg$silentFails === 0) { peg$fail(peg$e5); }
+ }
+ if (s1 === peg$FAILED) {
+ if (input.substr(peg$currPos, 2) === peg$c3) {
+ s1 = peg$c3;
+ peg$currPos += 2;
+ } else {
+ s1 = peg$FAILED;
+ if (peg$silentFails === 0) { peg$fail(peg$e6); }
+ }
+ }
+ while (s1 !== peg$FAILED) {
+ s0.push(s1);
+ s1 = input.charAt(peg$currPos);
+ if (peg$r2.test(s1)) {
+ peg$currPos++;
+ } else {
+ s1 = peg$FAILED;
+ if (peg$silentFails === 0) { peg$fail(peg$e5); }
+ }
+ if (s1 === peg$FAILED) {
+ if (input.substr(peg$currPos, 2) === peg$c3) {
+ s1 = peg$c3;
+ peg$currPos += 2;
+ } else {
+ s1 = peg$FAILED;
+ if (peg$silentFails === 0) { peg$fail(peg$e6); }
+ }
+ }
+ }
+
+ return s0;
+ }
+
+ peg$result = peg$startRuleFunction();
+
+ if (options.peg$library) {
+ return /** @type {any} */ ({
+ peg$result,
+ peg$currPos,
+ peg$FAILED,
+ peg$maxFailExpected,
+ peg$maxFailPos
+ });
+ }
+ if (peg$result !== peg$FAILED && peg$currPos === input.length) {
+ return peg$result;
+ } else {
+ if (peg$result !== peg$FAILED && peg$currPos < input.length) {
+ peg$fail(peg$endExpectation());
+ }
+
+ throw peg$buildStructuredError(
+ peg$maxFailExpected,
+ peg$maxFailPos < input.length ? input.charAt(peg$maxFailPos) : null,
+ peg$maxFailPos < input.length
+ ? peg$computeLocation(peg$maxFailPos, peg$maxFailPos + 1)
+ : peg$computeLocation(peg$maxFailPos, peg$maxFailPos)
+ );
+ }
+}
+
+ return {
+ StartRules: ["LambdaTerm"],
+ SyntaxError: peg$SyntaxError,
+ parse: peg$parse
+ };
+})()
+;
diff --git a/repl.ts b/repl.ts
new file mode 100644
index 0000000..6a67e50
--- /dev/null
+++ b/repl.ts
@@ -0,0 +1,106 @@
+const peggy = require("./parser.js");
+
+const isVariable = (ast: any) => typeof ast === "string";
+const isApplication = (ast: any) =>
+ typeof ast === "object" && "application" in ast;
+const isAbstraction = (ast: any) =>
+ typeof ast === "object" && "abstraction" in ast;
+
+const emitCode = (ast: any) => {
+ if (isVariable(ast)) {
+ return ast;
+ }
+
+ if (isApplication(ast)) {
+ const { application } = ast;
+ const { left, right } = application;
+ return `( ${emitCode(left)} ${emitCode(right)} )`;
+ }
+
+ const { abstraction } = ast;
+ const { param, body } = abstraction;
+
+ return `(λ ${emitCode(param)} . ${emitCode(body)})`;
+};
+
+const substitute = (param: string, term: any, result: any) => {
+ if (isVariable(term)) {
+ if (term === param) {
+ return result;
+ }
+ return term;
+ }
+
+ if (isApplication(term)) {
+ const { application } = term;
+ const { left, right } = application;
+
+ return {
+ application: {
+ left: substitute(param, left, result),
+ right: substitute(param, right, result),
+ },
+ };
+ }
+
+ const { abstraction } = term;
+ const { body } = abstraction;
+ return {
+ abstraction: {
+ param: abstraction.param,
+ body: substitute(param, body, result),
+ },
+ };
+};
+
+const betaReduce = (ast: any) => {
+ if (isVariable(ast)) {
+ return ast;
+ }
+
+ if (isApplication(ast)) {
+ const { application } = ast;
+ const { left, right } = application;
+
+ if (isAbstraction(left)) {
+ const { abstraction } = left;
+ const { param } = abstraction;
+
+ return substitute(param, right);
+ }
+
+ return {
+ application: {
+ left: betaReduce(left),
+ right: betaReduce(right),
+ },
+ };
+ }
+
+ // it's an abstraction
+ const { abstraction } = ast;
+ const { param, body } = abstraction;
+ return { param: betaReduce(param), body: betaReduce(body) };
+};
+
+const evaluate = (lambdaTerm: string) => {
+ const ast = peggy.parse(lambdaTerm);
+
+ const fullBetaReduction = betaReduce(ast);
+ return emitCode(fullBetaReduction);
+};
+
+const doRepl = async () => {
+ const prompt = ">>> ";
+ process.stdout.write(prompt);
+
+ for await (const line of console) {
+ const result = evaluate(line);
+ console.log(result);
+ break;
+ }
+
+ await doRepl();
+};
+
+await doRepl();