diff --git a/.gitignore b/.gitignore index 4dbc4d1..9c44e71 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ *.log tmp/ *report* +node_modules diff --git a/README.md b/README.md new file mode 100644 index 0000000..e4fa352 --- /dev/null +++ b/README.md @@ -0,0 +1,13 @@ +# evil-lisp + +A lisp intepreter/transpiler built on top of Javascript + +### Features + +* Transpiles to Javascript +* Close interop with Javascript primitives and libraries + * Can directly interact with JS methods: `(.log console "hello")` + * Access to `window` and `global` objects +* Dynamic binding +* Macro system +* REPL diff --git a/compiler.js b/compiler.js deleted file mode 100644 index 88566b3..0000000 --- a/compiler.js +++ /dev/null @@ -1,112 +0,0 @@ -const { format } = require('./naming') -const { unread } = require('./reader') -const { parse } = require('./parser') -const { safeLookup } = require('./environment') -const { evaluateMacroDefinition, evaluate, expandMacro, isMacro } = require('./evaluator') -const primitives = require('./primitives') - -//@TODO unuglify js -//@TODO objects - -function compile(node, env){ - switch(node.type){ - case "num": - case "str": - case "bool": return JSON.stringify(node.value, env) - case "quote": return compileQuote(node, env) - case "var": return node.value - case "assignment": return compileAssignment(node, env) - case "definition": return compileDefinition(node, env) - case "if": return compileIf(node, env) - case "let": return compileLet(node, env) - case "lambda": return compileLambda(node, env) - case "progn": return compileProgn(node, env) - case "macrodefinition": return compileMacroDefinition(node, env) - case "macroexpansion": return compileMacroExpansion(node, env) - case "application": return compileApplication(node, env) - } -} - -function compileQuote(node){ - return JSON.stringify(node.value)//`"${node.value}"` -} - -function compileDefinition(node, env){ - return `(${node.variable} = ${compile(node.value, env)})` -} - -function compileAssignment(node, env){ - return `${node.variable} = ${compile(node.value, env)}` -} - -function compileIf(node, env){ - let js = `(${compile(node.condition, env)} ? ` - js += `${compile(node.body, env)} : ` - if(node.elseBody){ - js+= `${compile(node.elseBody, env)})` - } else { - js+= `undefined )` - } - return js -} - -function compileLet(node, env){ - const values = node.values.map(n => compile(n, env)) - let js = `(function ` - js += `(${node.vars.join(',')})` - js += `{return ${compile(node.body, env)}})` - js += `(${values.join(',')})` - return js -} - -function compileLambda(node, env){ - let js = `(function ${node.name ? node.name : ''}` - js += `(${node.params.join(',')})` - js += `{return ${compile(node.body, env)}})` - return js -} - -function compileProgn(node, env){ - const sequence = node.nodes.map(n => compile(n, env)) - return `(${sequence.join(', ')})` -} - -function compileMacroDefinition(node, env){ - evaluateMacroDefinition(node, env) - return '"[macrodefined]"' -} - -function compileMacroExpansion(node, env){ - const macro = evaluate(node.operator, env) - const [expanded] = expandMacro(macro, node.args, env) - return `'${unread(expanded)}'` -} - -function compileApplication(node, env){ - // @TODO test macro inside macro - const operatorName = compile(node.operator, env) - const operator = safeLookup(operatorName, env) - if(operator && isMacro(operator)){ - const args = node._exp.slice(1) //@TODO abstract - const [expanded, scope] = expandMacro(operator, args, env) - return compile(parse(expanded, scope), scope) - } - let operands = node.operands.map(o => compile(o, env)) - operands = operands.map(o => typeof o === 'object' ? JSON.stringify(o) : o) - return `${format(operatorName)}(${operands.join(',')})` -} - -// ======================================================= -function compilePrimitive(primitive){ - return primitive[1].toString() -} - -function createCompilationPrefix(){ - let output = '' - for(let primitive of primitives){ - output += compilePrimitive(primitive) + ';\n' - } - return output -} - -module.exports = { compile, createCompilationPrefix } diff --git a/evaluator.js b/evaluator.js deleted file mode 100644 index e507b0d..0000000 --- a/evaluator.js +++ /dev/null @@ -1,167 +0,0 @@ -const { isAtom, isUnquoted, isUnquoteSliced, parse } = require('./parser') -const { unread } = require('./reader') -const { lookupVariable, - defineVariable, - defineVariables, - setVariable, - extendEnvironment } = require('./environment') - -function evaluate(node, env){ - switch (node.type){ - case "num": - case "str": - case "bool": return node.value - case "quote": return node.value - case "syntaxquote": return evaluateSyntaxQuote(node, env) - case "var": return evaluateVariable(node, env) - case "assignment": return evaluateAssignment(node, env) - case "definition": return evaluateDefinition(node, env) - case "if": return evaluateIf(node, env) - case "let": return evaluateLet(node, env) - case "lambda": return evaluateLambda(node, env) - case "progn": return evaluateProgn(node, env) - case "macrodefinition": return evaluateMacroDefinition(node, env) - case "macroexpansion": return evaluateMacroExpansion(node, env) - case "application": return evaluateApplication(node, env) - } -} - -function evaluateVariable(node, env){ - return lookupVariable(node.value, env) -} - -function evaluateAssignment(node, env){ - const value = evaluate(node.value, env) - return setVariable(node.variable, value, env) -} - -function evaluateDefinition(node, env){ - const value = evaluate(node.value, env) - return defineVariable(node.variable, value, env) -} - -function evaluateIf(node, env){ - if(evaluate(node.condition, env)){ - return evaluate(node.body, env) - } else { - if(node.elseBody) return evaluate(node.elseBody, env) - } -} - -function evaluateLet(node, env){ - const scope = extendEnvironment(env) - for(let binding of node.bindings){ - evaluate({type: 'definition', variable: binding[0], value: binding[1]}, scope) - } - return evaluate(node.body, scope) -} - -function evaluateProgn(node, env){ - return node.nodes.map(n => evaluate(n, env)).slice(-1)[0] -} - -function evaluateLambda(node, env){ - const body = node.body - const params = node.params - return (...args) => { - const scope = createScope(params, args, env) - const result = evaluate(body, scope) - return result - } -} - -function evaluateApplication(node, env){ - const operator = evaluate(node.operator, env) - if(isMacro(operator)){ - const macro = evaluate(node.operator, env) - const args = node._exp.slice(1) //@TODO abstract - const [expanded, scope] = expandMacro(macro, args, env) - return evaluate(parse(expanded), scope) - } - - const args = node.operands.map(operand => evaluate(operand, env)) - return operator(...args) -} - -function evaluateMacroDefinition(node, env){ - const macro = makeMacro(node.params, node.body) - return defineVariable(node.name, macro, env) -} - -function evaluateMacroExpansion(node, env){ - const macro = evaluate(node.operator, env) - const [expanded] = expandMacro(macro, node.args, env) - return unread(expanded) -} - -function evaluateSyntaxQuote(node, env){ - const result = recursiveSyntaxUnquote(node.value, env) //@TODO if is atom... - return result -} - -// Helpers -// =============================================================== -function expandMacro(macro, args, env){ - const body = getMacroBody(macro) - const params = getMacroParams(macro) - const scope = createScope(params, args, env) - const expanded = body.map(n => evaluate(n, scope)).slice(-1)[0] - return [expanded, scope] -} - -function makeMacro(parameters, body){ - return [ 'macro', parameters, body] -} - -function getMacroBody(exp){ - return exp[2] -} - -function getMacroParams(exp){ - return exp[1] -} - -function isMacro(exp){ - return exp[0] === 'macro' -} - -function syntaxUnquote(exp, env){ - return evaluate(parse(exp[1]), env) -} - -function recursiveSyntaxUnquote(exps, env){ - let output = [] - for(let exp of exps){ - if(!isAtom(exp)) { - if(isUnquoted(exp)){ - output.push(syntaxUnquote(exp, env)) - } else if(isUnquoteSliced(exp)){ - output.push(...syntaxUnquote(exp, env)) - } else { - output.push(recursiveSyntaxUnquote(exp, env)) - } - } - else output.push(exp) - } - return output -} - -function createScope(params, args, env){ - ;[params, args] = destructureArgs(params, args) - const scope = extendEnvironment(env) - defineVariables(params, args, scope) - return scope -} - -function destructureArgs(params, args){ - const restIndex = params.indexOf('&') - if(restIndex >= 0){ - const restArgs = args.slice(restIndex) - outputParams = [...params.slice(0, restIndex), params[restIndex + 1]] - outputArgs = [...args.slice(0, restIndex), restArgs] - return [outputParams, outputArgs] - } - return [params, args] -} - -module.exports = { evaluate, evaluateMacroDefinition, expandMacro, isMacro } diff --git a/evil.js b/evil.js deleted file mode 100644 index e98df13..0000000 --- a/evil.js +++ /dev/null @@ -1,27 +0,0 @@ -const { createEnvironment, defineVariable } = require('./environment') -const primitives = require('./primitives') -const { parse } = require('./parser') -const { evaluate } = require('./evaluator') -const { read, InputStream } = require('./reader') -const fs = require('fs') - -function createGlobalEnvironment(){ - const env = createEnvironment() - for(let primitive of primitives){ - defineVariable(primitive[0], primitive[1], env) - } - defineVariable('console', console, env) - return env -} - -const path = process.argv[2] -let content = fs.readFileSync(path).toString() -content = content.replace(/;.*/g, "") -content = content.replace(/\r\n/g, " ") -content = content.replace(/\n/g, " ") -const input = `(progn ${content})` - -const env = createGlobalEnvironment() - -const ast = parse(read(InputStream(input)), env) -evaluate(ast, env) diff --git a/package.json b/package.json new file mode 100644 index 0000000..4510543 --- /dev/null +++ b/package.json @@ -0,0 +1,5 @@ +{ + "dependencies": { + "yargs": "^17.7.2" + } +} diff --git a/primitives.js b/primitives.js deleted file mode 100644 index 2a6c147..0000000 --- a/primitives.js +++ /dev/null @@ -1,106 +0,0 @@ -const { executeProcedure } = require('./interpreter') - -// @TODO: optimize, hardcode parameters until 5 or 6, if more use reduce -const primitive = (key, func) => ([key, func]) -function add(...args){ - return args.reduce((acc, x) => acc + x, 0) -} - -function sub(...args){ - return args.reduce((acc, x) => acc - x, args[0] * 2) -} - -function lt(a, b){ - return a < b -} - -function gt(a, b){ - return a > b -} - -function list(...args){ - return args -} - -// const mul = (args) => args.reduce((acc, x) => acc * x, 1) -// const div = (args) => args.reduce((acc, x) => acc / x, args[0] * args[0]) -// const mod = ([a, b]) => a % b -// const gt = ([a, b]) => a > b -// const lt = ([a, b]) => a < b -// const gte = ([a, b]) => a >= b -// const lte = ([a, b]) => a <= b -// const not = ([a]) => !a -function not(a){ - return !a -} -// const eq = (args) => args.reduce((acc, x) => acc === x, args[0]) -// const and = (args) => args.reduce((acc, x) => acc && x, true) -// const or = (args) => args.reduce((acc, x) => acc || x, false) -// const inc = ([a]) => a + 1 -function print (...args) { - return console.log(...args) -} - -function get(seq, accessor){ - return seq[accessor] -} -// const pprint = (args) => console.log(JSON.stringify(args)) -// const assoc = ([coll, k, v]) => {coll[k] = v; return coll} -// const obj = () => ({}) -// const list = (args) => args -function dot(key, coll, ...args){ - return coll[key](...args) -} - -function map(func, coll){ - return coll.map(func) -} -// const filter = ([proc, coll]) => coll.filter(arg => executeProcedure(proc, [arg])) -// const reduce = ([proc, coll, initial]) => coll.reduce((acc, x) => executeProcedure(proc, [acc, x]), initial) - -// const each = ([coll, proc, step = 1, initial = 0]) => { -// for(let i = initial; i < coll.length; i = i + step){ -// executeProcedure(proc, [coll[i], i]) -// } -// } - -function partition (coll, size) { - coll = [...coll] - const output = [] - while(coll.length > 0){ - output.push(coll.splice(0, size)) - } - return output -} - -const primitives = [ - primitive('+', add), - primitive('-', sub), - // primitive('*', mul), - // primitive('/', div), - // primitive('%', mod), - primitive('>', gt), - primitive('<', lt), - // primitive('>=', gte), - // primitive('<=', lte), - // primitive('eq', eq), - primitive('not', not), - // primitive('and', and), - // primitive('or', or), - // primitive('inc', inc), - primitive('print', print), - // primitive('pprint', pprint), - primitive('get', get), - // primitive('assoc', assoc), - // primitive('obj', obj), - primitive('list', list), - primitive('.', dot), - primitive('map', map), - // primitive('filter', filter), - // primitive('reduce', reduce), - // primitive('each', each), - primitive('partition', partition) -] - - -module.exports = primitives diff --git a/repl.js b/repl.js deleted file mode 100644 index c1e5340..0000000 --- a/repl.js +++ /dev/null @@ -1,91 +0,0 @@ -const { createEnvironment, defineVariable } = require('./environment') -const primitives = require('./primitives') -// const { evaluate } = require('./interpreter') -const { format } = require('./naming') -const { evaluate } = require('./evaluator') -const { compile } = require('./compiler') -const { parse } = require('./parser') -const { read, InputStream } = require('./reader') -const readline = require("readline") - -let quit = false -const rl = readline.createInterface({ - input: process.stdin, - output: process.stdout -}) - -function pprint(msg){ - console.log(JSON.stringify(msg, null, 2)) -} - -function prompt(question){ - return new Promise((resolve, reject) => { - rl.question(question, input => { - if(input === 'q') quit = true - resolve(input) - }) - }) -} - -function createGlobalEnvironment(){ - const env = createEnvironment() - for(let primitive of primitives){ - defineVariable(primitive[0], primitive[1], env) - } - defineVariable('console', console, env) - return env -} - -function compilePrimitive(primitive){ - return primitive[1].toString() -} - -function createCompilationPrefix(){ - let output = '' - for(let primitive of primitives){ - output += compilePrimitive(primitive) + ';\n' - } - return output -} - -async function launch(){ - const env = createGlobalEnvironment() - const prefix = createCompilationPrefix() - - while(!quit){ - try{ - const input = await prompt('> ') - if(quit) break - const ast = parse(read(InputStream(input)), env) - console.log("") - console.log("") - console.log("") - console.log("AST") - console.log("================================") - pprint(ast) - - console.log("") - console.log("COMPILATION") - console.log("================================") - const result = prefix + '\n' + compile(ast) - console.log(result) - - // const result = evaluate(ast, env) - console.log("") - console.log("EVALUATION") - console.log("================================") - console.log(evaluate(ast, env)) - - console.log("") - console.log("JS EVAL") - console.log("================================") - console.log(eval(result)) - - } catch(err){ - console.log(err) - } - } - rl.close() -} - -launch() diff --git a/run.js b/run.js deleted file mode 100644 index f086789..0000000 --- a/run.js +++ /dev/null @@ -1,49 +0,0 @@ -const { createEnvironment, defineVariable } = require('./environment') -const primitives = require('./primitives') -const { compile, createCompilationPrefix } = require('./compiler') -const { parse } = require('./parser') -const { read, InputStream } = require('./reader') -const fs = require('fs') - -// function pprint(msg){ -// console.log(JSON.stringify(msg, null, 2)) -// } - -const path = process.argv[2] -let content = fs.readFileSync(path).toString() -content = content.replace(/;.*/g, "") -content = content.replace(/\r\n/g, " ") -content = content.replace(/\n/g, " ") -const input = `(progn ${content})` - -//@ TODO createCompilationEnvironment instead -function createGlobalEnvironment(){ - const env = createEnvironment() - for(let primitive of primitives){ - defineVariable(primitive[0], primitive[1], env) - } - defineVariable('console', console, env) - return env -} - -console.time('read') -const env = createGlobalEnvironment() -const r = read(InputStream(input)) -console.timeEnd('read') -console.time('parse') -const ast = parse(r) -console.timeEnd('parse') -console.time('compile') -const compilation = createCompilationPrefix() + compile(ast, env) -console.timeEnd('compile') -console.log('') -console.log('COMPILATION OUTPUT') -console.log('=================================') -console.log(`(function(){${compilation}}())`) - -console.log('') -console.log('JS EVAL') -console.log('=================================') -console.time('eval') -eval(`(function(){${compilation}}())`) //@TODO where to put this -console.timeEnd('eval') diff --git a/src/compiler.js b/src/compiler.js new file mode 100644 index 0000000..a3e8431 --- /dev/null +++ b/src/compiler.js @@ -0,0 +1,145 @@ +const { format } = require("./naming"); +const { unread } = require("./reader"); +const { parse } = require("./parser"); +const { attempt } = require("./utils/utils"); +const { safeLookup, createEnvironment, defineVariable } = require("./environment"); +const { + evaluateMacroDefinition, + evaluate, + expandMacro, + isMacro, +} = require("./evaluator"); +const primitives = require("./primitives"); + +//@TODO unuglify js +//@TODO objects + +function compile(node, env) { + switch (node.type) { + case "num": + case "str": + case "bool": + return JSON.stringify(node.value, env); + case "quote": + return compileQuote(node, env); + case "var": + return node.value; + case "assignment": + return compileAssignment(node, env); + case "definition": + return compileDefinition(node, env); + case "if": + return compileIf(node, env); + case "let": + return compileLet(node, env); + case "lambda": + return compileLambda(node, env); + case "progn": + return compileProgn(node, env); + case "macrodefinition": + return compileMacroDefinition(node, env); + case "macroexpansion": + return compileMacroExpansion(node, env); + case "application": + return compileApplication(node, env); + } +} + +function compileQuote(node) { + return JSON.stringify(node.value); //`"${node.value}"` +} + +function compileDefinition(node, env) { + return `(${node.variable} = ${compile(node.value, env)})`; +} + +function compileAssignment(node, env) { + return `${node.variable} = ${compile(node.value, env)}`; +} + +function compileIf(node, env) { + let js = `(${compile(node.condition, env)} ? `; + js += `${compile(node.body, env)} : `; + if (node.elseBody) { + js += `${compile(node.elseBody, env)})`; + } else { + js += `undefined )`; + } + return js; +} + +function compileLet(node, env) { + const values = node.values.map((n) => compile(n, env)); + let js = `(function `; + js += `(${node.vars.join(",")})`; + js += `{return ${compile(node.body, env)}})`; + js += `(${values.join(",")})`; + return js; +} + +function compileLambda(node, env) { + let js = `(function ${node.name ? node.name : ""}`; + js += `(${node.params.join(",")})`; + js += `{return ${compile(node.body, env)}})`; + return js; +} + +function compileProgn(node, env) { + const sequence = node.nodes.map((n) => compile(n, env)); + return `(${sequence.join(", ")})`; +} + +function compileMacroDefinition(node, env) { + evaluateMacroDefinition(node, env); + return '"[macrodefined]"'; +} + +function compileMacroExpansion(node, env) { + const macro = evaluate(node.operator, env); + const [expanded] = expandMacro(macro, node.args, env); + return `'${unread(expanded)}'`; +} + +function compileApplication(node, env) { + // @TODO test macro inside macro + const operatorName = compile(node.operator, env); + const operator = safeLookup(operatorName, env); + if (operator && isMacro(operator)) { + const args = node._exp.slice(1); //@TODO abstract + const [expanded, scope] = expandMacro(operator, args, env); + return compile(parse(expanded, scope), scope); + } + let operands = node.operands.map((o) => compile(o, env)); + operands = operands.map((o) => + typeof o === "object" ? JSON.stringify(o) : o + ); + return `${format(operatorName)}(${operands.join(",")})`; +} + +// ======================================================= + +function compilePrimitive(primitive) { + return primitive[1].toString(); +} + +function createCompilationPrefix() { + let output = ""; + for (let primitive of primitives) { + output += compilePrimitive(primitive) + ";\n"; + } + return output; +} + +function createCompilationEnvironment(){ + + const env = createEnvironment(); + for (let primitive of primitives) { + defineVariable(primitive[0], primitive[1], env); + } + defineVariable("console", console, env); + attempt(() => defineVariable("window", window, env)); + attempt(() => defineVariable("global", global, env)); + return env; +} + +module.exports = { compile, createCompilationPrefix, createCompilationEnvironment }; diff --git a/environment.js b/src/environment.js similarity index 100% rename from environment.js rename to src/environment.js diff --git a/src/evaluator.js b/src/evaluator.js new file mode 100644 index 0000000..930dc59 --- /dev/null +++ b/src/evaluator.js @@ -0,0 +1,183 @@ +const { isAtom, isUnquoted, isUnquoteSliced, parse } = require("./parser"); +const { unread } = require("./reader"); +const { + lookupVariable, + defineVariable, + defineVariables, + setVariable, + extendEnvironment, +} = require("./environment"); + +function evaluate(node, env) { + switch (node.type) { + case "num": + case "str": + case "bool": + case "quote": + return node.value; + case "syntaxquote": + return evaluateSyntaxQuote(node, env); + case "var": + return evaluateVariable(node, env); + case "assignment": + return evaluateAssignment(node, env); + case "definition": + return evaluateDefinition(node, env); + case "if": + return evaluateIf(node, env); + case "let": + return evaluateLet(node, env); + case "lambda": + return evaluateLambda(node, env); + case "progn": + return evaluateProgn(node, env); + case "macrodefinition": + return evaluateMacroDefinition(node, env); + case "macroexpansion": + return evaluateMacroExpansion(node, env); + case "application": + return evaluateApplication(node, env); + } +} + +function evaluateVariable(node, env) { + return lookupVariable(node.value, env); +} + +function evaluateAssignment(node, env) { + const value = evaluate(node.value, env); + return setVariable(node.variable, value, env); +} + +function evaluateDefinition(node, env) { + const value = evaluate(node.value, env); + return defineVariable(node.variable, value, env); +} + +function evaluateIf(node, env) { + if (evaluate(node.condition, env)) { + return evaluate(node.body, env); + } else { + if (node.elseBody) return evaluate(node.elseBody, env); + } +} + +function evaluateLet(node, env) { + const scope = extendEnvironment(env); + for (let binding of node.bindings) { + evaluate( + { type: "definition", variable: binding[0], value: binding[1] }, + scope + ); + } + return evaluate(node.body, scope); +} + +function evaluateProgn(node, env) { + return node.nodes.map((n) => evaluate(n, env)).slice(-1)[0]; +} + +function evaluateLambda(node, env) { + const body = node.body; + const params = node.params; + return (...args) => { + const scope = createScope(params, args, env); + const result = evaluate(body, scope); + return result; + }; +} + +function evaluateApplication(node, env) { + const operator = evaluate(node.operator, env); + if (isMacro(operator)) { + const macro = evaluate(node.operator, env); + const args = node._exp.slice(1); //@TODO abstract + const [expanded, scope] = expandMacro(macro, args, env); + return evaluate(parse(expanded), scope); + } + + const args = node.operands.map((operand) => evaluate(operand, env)); + return operator(...args); +} + +function evaluateMacroDefinition(node, env) { + const macro = makeMacro(node.params, node.body); + return defineVariable(node.name, macro, env); +} + +function evaluateMacroExpansion(node, env) { + const macro = evaluate(node.operator, env); + const [expanded] = expandMacro(macro, node.args, env); + return unread(expanded); +} + +function evaluateSyntaxQuote(node, env) { + const result = recursiveSyntaxUnquote(node.value, env); //@TODO if is atom... + return result; +} + +// Helpers +// =============================================================== +function expandMacro(macro, args, env) { + const body = getMacroBody(macro); + const params = getMacroParams(macro); + const scope = createScope(params, args, env); + const expanded = body.map((n) => evaluate(n, scope)).slice(-1)[0]; + return [expanded, scope]; +} + +function makeMacro(parameters, body) { + return ["macro", parameters, body]; +} + +function getMacroBody(exp) { + return exp[2]; +} + +function getMacroParams(exp) { + return exp[1]; +} + +function isMacro(exp) { + return exp[0] === "macro"; +} + +function syntaxUnquote(exp, env) { + return evaluate(parse(exp[1]), env); +} + +function recursiveSyntaxUnquote(exps, env) { + let output = []; + for (let exp of exps) { + if (!isAtom(exp)) { + if (isUnquoted(exp)) { + output.push(syntaxUnquote(exp, env)); + } else if (isUnquoteSliced(exp)) { + output.push(...syntaxUnquote(exp, env)); + } else { + output.push(recursiveSyntaxUnquote(exp, env)); + } + } else output.push(exp); + } + return output; +} + +function createScope(params, args, env) { + [params, args] = destructureArgs(params, args); + const scope = extendEnvironment(env); + defineVariables(params, args, scope); + return scope; +} + +function destructureArgs(params, args) { + const restIndex = params.indexOf("&"); + if (restIndex >= 0) { + const restArgs = args.slice(restIndex); + outputParams = [...params.slice(0, restIndex), params[restIndex + 1]]; + outputArgs = [...args.slice(0, restIndex), restArgs]; + return [outputParams, outputArgs]; + } + return [params, args]; +} + +module.exports = { evaluate, evaluateMacroDefinition, expandMacro, isMacro }; diff --git a/src/evil.js b/src/evil.js new file mode 100644 index 0000000..ed9a449 --- /dev/null +++ b/src/evil.js @@ -0,0 +1,42 @@ +const { createEnvironment, defineVariable } = require("./environment"); +const primitives = require("./primitives"); +const { parse } = require("./parser"); +const { evaluate } = require("./evaluator"); +const { repl } = require("./repl"); +const { read, InputStream } = require("./reader"); +const { hideBin } = require("yargs/helpers"); +const { createCompilationEnvironment } = require("./compiler"); +const yargs = require("yargs/yargs"); +const fs = require("fs"); + +process.argv = yargs(hideBin(process.argv)).argv; +const sourceFile = process.argv._[0]; +const debugMode = process.argv.debug; +const compiledEval = process.argv.compiledEval; +const env = createCompilationEnvironment(); + +if(sourceFile) { + evalFile(sourceFile) +} else { + repl(env); +} + +function preprocessFile(file){ + let content = fs.readFileSync(file).toString(); + content = content.replace(/;.*/g, ""); + content = content.replace(/\r\n/g, " "); + content = content.replace(/\n/g, " "); + return `(progn ${content})`; +} + +function evalFile(sourceFile) { + const input = preprocessFile(sourceFile); + const ast = parse(read(InputStream(input)), env); + + if (compiledEval) { + const compilation = createCompilationPrefix() + compile(ast, env); + eval(`(function(){${compilation}}())`); + } else { + evaluate(ast, env); + } +} diff --git a/naming.js b/src/naming.js similarity index 100% rename from naming.js rename to src/naming.js diff --git a/parser.js b/src/parser.js similarity index 99% rename from parser.js rename to src/parser.js index 46b9749..085f078 100644 --- a/parser.js +++ b/src/parser.js @@ -1,4 +1,4 @@ -const { isSurroundedBy } = require('./utils') +const { isSurroundedBy } = require('./utils/utils') // DOMAIN NOTES // ================================ diff --git a/src/primitives.js b/src/primitives.js new file mode 100644 index 0000000..5c253b6 --- /dev/null +++ b/src/primitives.js @@ -0,0 +1,103 @@ +function add(...args) { + return args.reduce((acc, x) => acc + x, 0); +} + +function sub(...args) { + return args.reduce((acc, x) => acc - x, args[0] * 2); +} + +function lt(a, b) { + return a < b; +} + +function gt(a, b) { + return a > b; +} + +function list(...args) { + return args; +} + +// const mul = (args) => args.reduce((acc, x) => acc * x, 1) +// const div = (args) => args.reduce((acc, x) => acc / x, args[0] * args[0]) +// const mod = ([a, b]) => a % b +// const gt = ([a, b]) => a > b +// const lt = ([a, b]) => a < b +// const gte = ([a, b]) => a >= b +// const lte = ([a, b]) => a <= b +// const not = ([a]) => !a +function not(a) { + return !a; +} +// const eq = (args) => args.reduce((acc, x) => acc === x, args[0]) +// const and = (args) => args.reduce((acc, x) => acc && x, true) +// const or = (args) => args.reduce((acc, x) => acc || x, false) +// const inc = ([a]) => a + 1 +function print(...args) { + return console.log(...args); +} + +function get(seq, accessor) { + return seq[accessor]; +} +// const pprint = (args) => console.log(JSON.stringify(args)) +// const assoc = ([coll, k, v]) => {coll[k] = v; return coll} +// const obj = () => ({}) +// const list = (args) => args +function dot(key, coll, ...args) { + return coll[key](...args); +} + +function map(func, coll) { + return coll.map(func); +} +// const filter = ([proc, coll]) => coll.filter(arg => executeProcedure(proc, [arg])) +// const reduce = ([proc, coll, initial]) => coll.reduce((acc, x) => executeProcedure(proc, [acc, x]), initial) + +// const each = ([coll, proc, step = 1, initial = 0]) => { +// for(let i = initial; i < coll.length; i = i + step){ +// executeProcedure(proc, [coll[i], i]) +// } +// } + +function partition(coll, size) { + coll = [...coll]; + const output = []; + while (coll.length > 0) { + output.push(coll.splice(0, size)); + } + return output; +} + +const primitive = (key, func) => [key, func]; + +const primitives = [ + primitive("+", add), + primitive("-", sub), + // primitive('*', mul), + // primitive('/', div), + // primitive('%', mod), + primitive(">", gt), + primitive("<", lt), + // primitive('>=', gte), + // primitive('<=', lte), + // primitive('eq', eq), + primitive("not", not), + // primitive('and', and), + // primitive('or', or), + // primitive('inc', inc), + primitive("print", print), + // primitive('pprint', pprint), + primitive("get", get), + // primitive('assoc', assoc), + // primitive('obj', obj), + primitive("list", list), + primitive(".", dot), + primitive("map", map), + // primitive('filter', filter), + // primitive('reduce', reduce), + // primitive('each', each), + primitive("partition", partition), +]; + +module.exports = primitives; diff --git a/reader.js b/src/reader.js similarity index 97% rename from reader.js rename to src/reader.js index a0a1048..d9e018f 100644 --- a/reader.js +++ b/src/reader.js @@ -1,4 +1,4 @@ -const { recursiveMap } = require('./utils') +const { recursiveMap } = require('./utils/utils') let listOverridesPending = 0 @@ -14,7 +14,6 @@ const macroCharacters = { let token = readWhile(input, char => !isWhitespace(char) && !isTerminatingMacro(char)) // (. log console "foo") - console.log('token:', token) if(token === ""){ return '.' } @@ -127,6 +126,8 @@ function readList(input){ } function InputStream(input){ + input = input.replace(/\[/g, '('); + input = input.replace(/\]/g, ')'); const delimited = {} // number of open delimited expressions (eg: (), "", []) let pos = 0 const next = () => input.charAt(pos++) diff --git a/src/repl.js b/src/repl.js new file mode 100644 index 0000000..e76d2f9 --- /dev/null +++ b/src/repl.js @@ -0,0 +1,59 @@ +const { createEnvironment, defineVariable } = require("./environment"); +const primitives = require("./primitives"); +const { format } = require("./naming"); +const { evaluate } = require("./evaluator"); +const { compile, createCompilationEnvironment, createCompilationPrefix } = require("./compiler"); +const { parse } = require("./parser"); +const { read, InputStream } = require("./reader"); +const readline = require("readline"); + +let quit = false; +let rl + +function pprint(msg) { + console.log(JSON.stringify(msg, null, 2)); +} + +function prompt(question) { + return new Promise((resolve, reject) => { + rl.question(question, (input) => { + if (input === "q") quit = true; + resolve(input); + }); + }); +} + +function compileAndEval(ast, env, prefix){ + const compilation = prefix + "\n" + compile(ast, env); + if (process.argv.debug) { + return verboseCompiledEvaluation(ast, compilation, () => eval(compilation)); + } else { + return eval(compilation); + } +} + +async function repl(env, debug = false) { + rl = readline.createInterface({ + input: process.stdin, + output: process.stdout, + }); + + const prefix = createCompilationPrefix(); + + while (!quit) { + try { + const input = await prompt("> "); + if (quit) break; + const ast = parse(read(InputStream(input)), env); + const result = compileAndEval(ast, env, prefix) + console.log(result) + } catch (err) { + console.log(err); + } + } + rl.close(); +} + +module.exports = { + repl +} diff --git a/src/utils/debug.js b/src/utils/debug.js new file mode 100644 index 0000000..3677957 --- /dev/null +++ b/src/utils/debug.js @@ -0,0 +1,17 @@ +function verboseCompiledEvaluation(ast, compilation, evaluationFunc) { + console.log("\n\n\nAST"); + console.log("================================"); + pprint(ast); + + console.log("\nCOMPILATION"); + console.log("================================"); + console.log(compilation); + + console.log("\nJS EVAL"); + console.log("================================"); + console.log(evaluationFunc()); +} + +module.exports = { + verboseCompiledEvaluation: verboseCompiledEvaluation +} diff --git a/utils.js b/src/utils/utils.js similarity index 71% rename from utils.js rename to src/utils/utils.js index 93ad8b7..5e9df75 100644 --- a/utils.js +++ b/src/utils/utils.js @@ -11,4 +11,10 @@ function recursiveMap(func, array){ return output } -module.exports = { isSurroundedBy, recursiveMap } +function attempt(func){ + try { + func() + } catch (err) {} +} + +module.exports = { isSurroundedBy, recursiveMap, attempt } diff --git a/test.lisp b/test.lisp index 6b20d59..5e2365f 100644 --- a/test.lisp +++ b/test.lisp @@ -33,6 +33,6 @@ (print "let works" a) (print "for multiple parameters" b)) ;3 - (. :log console "interop works") +(.log console "interop works") (print '1) diff --git a/yarn.lock b/yarn.lock new file mode 100644 index 0000000..28f8bd2 --- /dev/null +++ b/yarn.lock @@ -0,0 +1,109 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +ansi-regex@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" + integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== + +ansi-styles@^4.0.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" + integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== + dependencies: + color-convert "^2.0.1" + +cliui@^8.0.1: + version "8.0.1" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-8.0.1.tgz#0c04b075db02cbfe60dc8e6cf2f5486b1a3608aa" + integrity sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ== + dependencies: + string-width "^4.2.0" + strip-ansi "^6.0.1" + wrap-ansi "^7.0.0" + +color-convert@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" + integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== + dependencies: + color-name "~1.1.4" + +color-name@~1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" + integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== + +emoji-regex@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" + integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== + +escalade@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" + integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw== + +get-caller-file@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" + integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== + +is-fullwidth-code-point@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" + integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== + +require-directory@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" + integrity sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q== + +string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +strip-ansi@^6.0.0, strip-ansi@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + +y18n@^5.0.5: + version "5.0.8" + resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55" + integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA== + +yargs-parser@^21.1.1: + version "21.1.1" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35" + integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw== + +yargs@^17.7.2: + version "17.7.2" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.7.2.tgz#991df39aca675a192b816e1e0363f9d75d2aa269" + integrity sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w== + dependencies: + cliui "^8.0.1" + escalade "^3.1.1" + get-caller-file "^2.0.5" + require-directory "^2.1.1" + string-width "^4.2.3" + y18n "^5.0.5" + yargs-parser "^21.1.1"