diff --git a/README.md b/README.md index a007166..1a56d60 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,3 @@ # sans-bot -small and nearly silent IRC bot \ No newline at end of file +small and nearly silent IRC bot diff --git a/bot.js b/bot.js index 38032cb..3ed0ca3 100644 --- a/bot.js +++ b/bot.js @@ -1,34 +1,33 @@ -// includes +// includes const irc = require("irc"); const lisp = require("./lisp").lisp; // create the configuration const config = { - channels: ["##qrxdkw", "##webpals"], - server: "irc.libera.chat", - port: 6667, - botName: "blotbotboot" + channels: ["##qrxdkw", "##webpals"], + server: "irc.libera.chat", + port: 6667, + botName: "blotbotboot", }; // create the bot name let bot = new irc.Client(config.server, config.botName, { - channels: config.channels + channels: config.channels, }); // listen for commands -bot.addListener("message", function(from, to, text, message) { - if (text.startsWith(",echo")) { - const content = text.substring(6); // where 6 is the length of the trigger command, !echo + 1 space - bot.say(to, content); // to ensures that the response goes to the channel the message was sent on - } else if (text.startsWith(",pm")) { - bot.say(from, "yes?"); // from makes this response a private message to the sender - } else if (text.startsWith(",lisp")) { - const code = text.substring(6); // FIXME: if lisp crashes so does the bot - const ret = lisp.interpret(lisp.parse(code)); - bot.say(to, ret); - } +bot.addListener("message", function (from, to, text, message) { + if (text.startsWith(",echo")) { + const content = text.substring(6); // where 6 is the length of the trigger command, !echo + 1 space + bot.say(to, content); // to ensures that the response goes to the channel the message was sent on + } else if (text.startsWith(",pm")) { + bot.say(from, "yes?"); // from makes this response a private message to the sender + } else if (text.startsWith(",lisp")) { + const code = text.substring(6); // FIXME: if lisp crashes so does the bot + const ret = lisp.interpret(lisp.parse(code)); + bot.say(to, ret); + } }); - -// run w/ -// $ node bot.js \ No newline at end of file +// run w/ +// $ node bot.js diff --git a/lisp.js b/lisp.js index d5e293c..8f7d3c1 100644 --- a/lisp.js +++ b/lisp.js @@ -1,413 +1,424 @@ // lifted from https://github.com/maryrosecook/littlelisp // extended by eli -(function(exports) { - const library = { +(function (exports) { + const library = { + print: (x) => { + console.log(x); + return x; + }, - print: (x) => { - console.log(x); - return x; - }, + display: (x) => { + console.log(x); + return x; + }, - display: (x) => { - console.log(x); - return x; - }, + concat: (...items) => { + return items.reduce((acc, item) => { + return `${acc}${item}`; + }, ""); + }, - concat: (...items) => { - return items.reduce((acc, item) => { - return `${acc}${item}` - }, '') - }, + // math + add: (...args) => { + return args.reduce((sum, val) => sum + val); + }, - // math - add: (...args) => { - return args.reduce((sum, val) => sum + val) - }, + sub: (...args) => { // Subtracts values. + return args.reduce((sum, val) => sum - val); + }, - sub: (...args) => { // Subtracts values. - return args.reduce((sum, val) => sum - val) - }, + mul: (...args) => { // Multiplies values. + return args.reduce((sum, val) => sum * val); + }, - mul: (...args) => { // Multiplies values. - return args.reduce((sum, val) => sum * val) - }, + div: (...args) => { // Divides values. + return args.reduce((sum, val) => sum / val); + }, - div: (...args) => { // Divides values. - return args.reduce((sum, val) => sum / val) - }, + mod: (a, b) => { // Returns the modulo of a and b. + return a % b; + }, - mod: (a, b) => { // Returns the modulo of a and b. - return a % b - }, + clamp: (val, min, max) => { // Clamps a value between min and max. + return Math.min(max, Math.max(min, val)); + }, - clamp: (val, min, max) => { // Clamps a value between min and max. - return Math.min(max, Math.max(min, val)) - }, + step: (val, step) => { + return Math.round(val / step) * step; + }, - step: (val, step) => { - return Math.round(val / step) * step - }, + min: Math.min, + max: Math.max, + ceil: Math.ceil, + floor: Math.floor, // round down to the nearest integer. + sin: Math.sin, + cos: Math.cos, + log: Math.log, // calculates on the base of e. - min: Math.min, - max: Math.max, - ceil: Math.ceil, - floor: Math.floor, // round down to the nearest integer. - sin: Math.sin, - cos: Math.cos, - log: Math.log, // calculates on the base of e. + pow: (a, b) => { // calculates a^b. + return Math.pow(a, b); + }, - pow: (a, b) => { // calculates a^b. - return Math.pow(a, b) - }, + sqrt: Math.sqrt, // calculate the square root. - sqrt: Math.sqrt, // calculate the square root. + sq: (a) => { // calculate the square. + return a * a; + }, - sq: (a) => { // calculate the square. - return a * a - }, + PI: Math.PI, + TWO_PI: Math.PI * 2, - PI: Math.PI, - TWO_PI: Math.PI * 2, + random: (...args) => { + if (args.length >= 2) { + // (random start end) + return args[0] + Math.random() * (args[1] - args[0]); + } else if (args.length === 1) { + // (random max) + return Math.random() * args[0]; + } + return Math.random(); + }, - random: (...args) => { - if (args.length >= 2) { - // (random start end) - return args[0] + Math.random() * (args[1] - args[0]) - } else if (args.length === 1) { - // (random max) - return Math.random() * args[0] - } - return Math.random() - }, + // logic + gt: (a, b) => { // Returns true if a is greater than b, else false. + return a > b; + }, - // logic - gt: (a, b) => { // Returns true if a is greater than b, else false. - return a > b - }, + lt: (a, b) => { // Returns true if a is less than b, else false. + return a < b; + }, - lt: (a, b) => { // Returns true if a is less than b, else false. - return a < b - }, + eq: (a, b) => { // Returns true if a is equal to b, else false. + return a === b; + }, - eq: (a, b) => { // Returns true if a is equal to b, else false. - return a === b - }, - - and: (a, b, ...rest) => { // Returns true if all conditions are true. - const args = [a, b].concat(rest) - for (let i = 0; i < args.length; i++) { - if (!args[i]) { - return args[i] - } - } - return args[args.length - 1] - }, - - or: (a, b, ...rest) => { // Returns true if at least one condition is true. - const args = [a, b].concat(rest) - for (let i = 0; i < args.length; i++) { - if (args[i]) { - return args[i] - } - } - return args[args.length - 1] - }, - - // arrays - map: async(fn, arr) => { - let res = []; - for (let i = 0; i < arr.length; i++) { - const arg = arr[i] - res.push(await fn(arr[i], i)); - } - return res; - }, - - filter: (fn, arr) => { - const list = Array.from(arr) - return Promise.all(list.map((element, index) => fn(element, index, list))) - .then(result => { - return list.filter((_, index) => { - return result[index] - }) - }) - }, - - reduce: async(fn, arr, acc) => { - const length = arr.length - let result = acc === undefined ? subject[0] : acc - for (let i = acc === undefined ? 1 : 0; i < length; i++) { - result = await fn(result, arr[i], i, arr) - } - return result - }, - - len: (item) => { // returns the length of a list. - return item.length - }, - - first: (arr) => { // returns the first item of a list. - return arr[0] - }, - - car: (arr) => { // returns the first item of a list. - return arr[0] - }, - - last: (arr) => { // returns the last - return arr[arr.length - 1] - }, - - rest: ([_, ...arr]) => { - return arr - }, - - cdr: ([_, ...arr]) => { - return arr - }, - - range: (start, end, step = 1) => { - const arr = [] - if (step > 0) { - for (let i = start; i <= end; i += step) { - arr.push(i) - } - } else { - for (let i = start; i >= end; i += step) { - arr.push(i) - } - } - return arr - }, - - // objects - get: (item, key) => { // gets an object's parameter with name. - return item[key] - }, - - set: (item, ...args) => { // sets an object's parameter with name as value. - for (let i = 0; i < args.length; i += 2) { - const key = args[i] - const val = args[i + 1] - item[key] = val - } - return item - }, - - of: (h, ...keys) => { // gets object parameters with names. - return keys.reduce((acc, key) => { - return acc[key] - }, h) - }, - - keys: (item) => { // returns a list of the object's keys - return Object.keys(item) - }, - - values: (item) => { // returns a list of the object's values - return Object.values(item) - }, - - time: (rate = 1) => { // returns timestamp in milliseconds. - return (Date.now() * rate) - }, - - js: () => { // Javascript interop. - return window // note, this only works in the browser - }, - - test: (name, a, b) => { - if (`${a}` !== `${b}`) { - console.warn('failed ' + name, a, b) - } else { - console.log('passed ' + name, a) - } - return a === b - }, - - benchmark: async(fn) => { // logs time taken to execute a function. - const start = Date.now() - const result = await fn() - console.log(`time taken: ${Date.now() - start}ms`) - return result + and: (a, b, ...rest) => { // Returns true if all conditions are true. + const args = [a, b].concat(rest); + for (let i = 0; i < args.length; i++) { + if (!args[i]) { + return args[i]; } + } + return args[args.length - 1]; + }, + or: (a, b, ...rest) => { // Returns true if at least one condition is true. + const args = [a, b].concat(rest); + for (let i = 0; i < args.length; i++) { + if (args[i]) { + return args[i]; + } + } + return args[args.length - 1]; + }, + + // arrays + map: async (fn, arr) => { + let res = []; + for (let i = 0; i < arr.length; i++) { + const arg = arr[i]; + res.push(await fn(arr[i], i)); + } + return res; + }, + + filter: (fn, arr) => { + const list = Array.from(arr); + return Promise.all(list.map((element, index) => fn(element, index, list))) + .then((result) => { + return list.filter((_, index) => { + return result[index]; + }); + }); + }, + + reduce: async (fn, arr, acc) => { + const length = arr.length; + let result = acc === undefined ? subject[0] : acc; + for (let i = acc === undefined ? 1 : 0; i < length; i++) { + result = await fn(result, arr[i], i, arr); + } + return result; + }, + + len: (item) => { // returns the length of a list. + return item.length; + }, + + first: (arr) => { // returns the first item of a list. + return arr[0]; + }, + + car: (arr) => { // returns the first item of a list. + return arr[0]; + }, + + last: (arr) => { // returns the last + return arr[arr.length - 1]; + }, + + rest: ([_, ...arr]) => { + return arr; + }, + + cdr: ([_, ...arr]) => { + return arr; + }, + + range: (start, end, step = 1) => { + const arr = []; + if (step > 0) { + for (let i = start; i <= end; i += step) { + arr.push(i); + } + } else { + for (let i = start; i >= end; i += step) { + arr.push(i); + } + } + return arr; + }, + + // objects + get: (item, key) => { // gets an object's parameter with name. + return item[key]; + }, + + set: (item, ...args) => { // sets an object's parameter with name as value. + for (let i = 0; i < args.length; i += 2) { + const key = args[i]; + const val = args[i + 1]; + item[key] = val; + } + return item; + }, + + of: (h, ...keys) => { // gets object parameters with names. + return keys.reduce((acc, key) => { + return acc[key]; + }, h); + }, + + keys: (item) => { // returns a list of the object's keys + return Object.keys(item); + }, + + values: (item) => { // returns a list of the object's values + return Object.values(item); + }, + + time: (rate = 1) => { // returns timestamp in milliseconds. + return (Date.now() * rate); + }, + + js: () => { // Javascript interop. + return window; // note, this only works in the browser + }, + + test: (name, a, b) => { + if (`${a}` !== `${b}`) { + console.warn("failed " + name, a, b); + } else { + console.log("passed " + name, a); + } + return a === b; + }, + + benchmark: async (fn) => { // logs time taken to execute a function. + const start = Date.now(); + const result = await fn(); + console.log(`time taken: ${Date.now() - start}ms`); + return result; + }, + }; + + const TYPES = { + identifier: 0, + number: 1, + string: 2, + bool: 3, + }; + + const Context = function (scope, parent) { + this.scope = scope; + this.parent = parent; + this.get = function (identifier) { + if (identifier in this.scope) { + return this.scope[identifier]; + } else if (this.parent !== undefined) { + return this.parent.get(identifier); + } }; + }; - const TYPES = { - identifier: 0, - number: 1, - string: 2, - bool: 3 + const special = { + let: function (input, context) { + const letContext = input[1].reduce(function (acc, x) { + acc.scope[x[0].value] = interpret(x[1], context); + return acc; + }, new Context({}, context)); + return interpret(input[2], letContext); + }, + def: function (input, context) { + const identifier = input[1].value; + const value = input[2].type === TYPES.string && input[3] + ? input[3] + : input[2]; + context.scope[identifier] = interpret(value, context); + return value; + }, + defn: function (input, context) { + const fnName = input[1].value; + const fnParams = input[2].type === TYPES.string && input[3] + ? input[3] + : input[2]; + const fnBody = input[2].type === TYPES.string && input[4] + ? input[4] + : input[3]; + context.scope[fnName] = async function () { + const lambdaArguments = arguments; + const lambdaScope = fnParams.reduce(function (acc, x, i) { + acc[x.value] = lambdaArguments[i]; + return acc; + }, {}); + return interpret(fnBody, new Context(lambdaScope, context)); + }; + }, + lambda: function (input, context) { + return async function () { + const lambdaArguments = arguments; + const lambdaScope = input[1].reduce(function (acc, x, i) { + acc[x.value] = lambdaArguments[i]; + return acc; + }, {}); + return interpret(input[2], new Context(lambdaScope, context)); + }; + }, + if: async function (input, context) { + if (await interpret(input[1], context)) { + return interpret(input[2], context); + } + return input[3] ? interpret(input[3], context) : []; + }, + __fn: function (input, context) { + return async function () { + const lambdaArguments = arguments; + const keys = [ + ...new Set( + input.slice(2).flat(100).filter((i) => + i.type === TYPES.identifier && + i.value[0] === "%" + ).map((x) => x.value).sort(), + ), + ]; + const lambdaScope = keys.reduce(function (acc, x, i) { + acc[x] = lambdaArguments[i]; + return acc; + }, {}); + return interpret(input.slice(1), new Context(lambdaScope, context)); + }; + }, + __obj: async function (input, context) { + const obj = {}; + for (let i = 1; i < input.length; i += 2) { + obj[await interpret(input[i], context)] = await interpret( + input[i + 1], + context, + ); + } + return obj; + }, + }; + + const interpretList = function (input, context) { + if (input.length > 0 && input[0].value in special) { + return special[input[0].value](input, context); + } else { + var list = input.map(function (x) { + return interpret(x, context); + }); + if (list[0] instanceof Function) { + return list[0].apply(undefined, list.slice(1)); + } else { + return list; + } } + }; - const Context = function(scope, parent) { - this.scope = scope - this.parent = parent - this.get = function(identifier) { - if (identifier in this.scope) { - return this.scope[identifier] - } else if (this.parent !== undefined) { - return this.parent.get(identifier) - } - } + const interpret = function (input, context) { + if (context === undefined) { + return interpret(input, new Context(library)); + } else if (input instanceof Array) { + return interpretList(input, context); + } else if (input.type === "identifier") { + return context.get(input.value); + } else if (input.type === "number" || input.type === "string") { + return input.value; } + }; - const special = { - let: function(input, context) { - const letContext = input[1].reduce(function(acc, x) { - acc.scope[x[0].value] = interpret(x[1], context) - return acc - }, new Context({}, context)) - return interpret(input[2], letContext) - }, - def: function(input, context) { - const identifier = input[1].value - const value = input[2].type === TYPES.string && input[3] ? input[3] : input[2] - context.scope[identifier] = interpret(value, context) - return value - }, - defn: function(input, context) { - const fnName = input[1].value - const fnParams = input[2].type === TYPES.string && input[3] ? input[3] : input[2] - const fnBody = input[2].type === TYPES.string && input[4] ? input[4] : input[3] - context.scope[fnName] = async function() { - const lambdaArguments = arguments - const lambdaScope = fnParams.reduce(function(acc, x, i) { - acc[x.value] = lambdaArguments[i] - return acc - }, {}) - return interpret(fnBody, new Context(lambdaScope, context)) - } - }, - lambda: function(input, context) { - return async function() { - const lambdaArguments = arguments - const lambdaScope = input[1].reduce(function(acc, x, i) { - acc[x.value] = lambdaArguments[i] - return acc - }, {}) - return interpret(input[2], new Context(lambdaScope, context)) - } - }, - if: async function(input, context) { - if (await interpret(input[1], context)) { - return interpret(input[2], context) - } - return input[3] ? interpret(input[3], context) : [] - }, - __fn: function(input, context) { - return async function() { - const lambdaArguments = arguments - const keys = [...new Set(input.slice(2).flat(100).filter(i => - i.type === TYPES.identifier && - i.value[0] === '%' - ).map(x => x.value).sort())] - const lambdaScope = keys.reduce(function(acc, x, i) { - acc[x] = lambdaArguments[i] - return acc - }, {}) - return interpret(input.slice(1), new Context(lambdaScope, context)) - } - }, - __obj: async function(input, context) { - const obj = {} - for (let i = 1; i < input.length; i += 2) { - obj[await interpret(input[i], context)] = await interpret(input[i + 1], context) - } - return obj - } + let categorize = function (input) { + if (!isNaN(parseFloat(input))) { + return { + type: "number", + value: parseFloat(input), + }; + } else if (input[0] === '"' && input.slice(-1) === '"') { + return { + type: "string", + value: input.slice(1, -1), + }; + } else { + return { + type: "identifier", + value: input, + }; } + }; - const interpretList = function(input, context) { - if (input.length > 0 && input[0].value in special) { - return special[input[0].value](input, context); - } else { - var list = input.map(function(x) { - return interpret(x, context); - }); - if (list[0] instanceof Function) { - return list[0].apply(undefined, list.slice(1)); - } else { - return list; - } + let parenthesize = function (input, list) { + if (list === undefined) { + return parenthesize(input, []); + } else { + let token = input.shift(); + if (token === undefined) { + return list.pop(); + } else if (token === "(") { + list.push(parenthesize(input, [])); + return parenthesize(input, list); + } else if (token === ")") { + return list; + } else { + return parenthesize(input, list.concat(categorize(token))); + } + } + }; + + let tokenize = function (input) { + return input.split('"') + .map(function (x, i) { + if (i % 2 === 0) { // not in string + return x.replace(/\(/g, " ( ") + .replace(/\)/g, " ) "); + } else { // in string + return x.replace(/ /g, "!whitespace!"); } - }; + }) + .join('"') + .trim() + .split(/\s+/) + .map(function (x) { + return x.replace(/!whitespace!/g, " "); + }); + }; - const interpret = function(input, context) { - if (context === undefined) { - return interpret(input, new Context(library)); - } else if (input instanceof Array) { - return interpretList(input, context); - } else if (input.type === "identifier") { - return context.get(input.value); - } else if (input.type === "number" || input.type === "string") { - return input.value; - } - }; + let parse = function (input) { + return parenthesize(tokenize(input)); + }; - let categorize = function(input) { - if (!isNaN(parseFloat(input))) { - return { - type: 'number', - value: parseFloat(input) - }; - } else if (input[0] === '"' && input.slice(-1) === '"') { - return { - type: 'string', - value: input.slice(1, -1) - }; - } else { - return { - type: 'identifier', - value: input - }; - } - }; - - let parenthesize = function(input, list) { - if (list === undefined) { - return parenthesize(input, []); - } else { - let token = input.shift(); - if (token === undefined) { - return list.pop(); - } else if (token === "(") { - list.push(parenthesize(input, [])); - return parenthesize(input, list); - } else if (token === ")") { - return list; - } else { - return parenthesize(input, list.concat(categorize(token))); - } - } - }; - - let tokenize = function(input) { - return input.split('"') - .map(function(x, i) { - if (i % 2 === 0) { // not in string - return x.replace(/\(/g, ' ( ') - .replace(/\)/g, ' ) '); - } else { // in string - return x.replace(/ /g, "!whitespace!"); - } - }) - .join('"') - .trim() - .split(/\s+/) - .map(function(x) { - return x.replace(/!whitespace!/g, " "); - }); - }; - - let parse = function(input) { - return parenthesize(tokenize(input)); - }; - - exports.lisp = { - parse: parse, - interpret: interpret - }; -})(typeof exports === 'undefined' ? this : exports); \ No newline at end of file + exports.lisp = { + parse: parse, + interpret: interpret, + }; +})(typeof exports === "undefined" ? this : exports); diff --git a/repl.js b/repl.js index d4b1154..6ea6929 100644 --- a/repl.js +++ b/repl.js @@ -4,8 +4,8 @@ var lisp = require("./lisp").lisp; repl.start({ prompt: "* ", - eval: function(cmd, context, filename, callback) { + eval: function (cmd, context, filename, callback) { var ret = lisp.interpret(lisp.parse(cmd)); callback(null, ret); - } + }, });