This commit is contained in:
eli_oat 2023-06-19 11:05:32 -04:00
parent 6d4ee6470c
commit 5a8ea935cc
4 changed files with 411 additions and 401 deletions

View File

@ -1,3 +1,3 @@
# sans-bot
small and nearly silent IRC bot
small and nearly silent IRC bot

39
bot.js
View File

@ -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
// run w/
// $ node bot.js

767
lisp.js
View File

@ -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);
exports.lisp = {
parse: parse,
interpret: interpret,
};
})(typeof exports === "undefined" ? this : exports);

View File

@ -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);
}
},
});