gir/ircbot.js

193 lines
5.5 KiB
JavaScript

'use strict';
const gir = require('./gir.js');
class IntParseError extends Error {}
const programCacheSize = 16;
// string → {compiled: [flatCommandObjects], extensions: bool}
let programCache = new Map();
// (string, bool) → [flatCommandObjects]
function cachedCompile(program, enableExtensions = true) {
function compileToCache() {
// Are we already in the cache? If yes, drop the old one,
// since something must have been changed if we're called
if(programCache.has(program)) {
programCache.delete(program);
}
let compiled = gir.compile(program, enableExtensions);
programCache.set(program, {compiled: compiled,
extensions: enableExtensions});
// Are we over the cache size? If yes, drop the least
// recently added
if(programCache.size > programCacheSize) {
console.log('delete');
// .keys() gives them in insertion order. Since
// there isn't a nice way to extract a thing out
// of an iterator, use a for loop and break out
// after first round
for(let leastRecentlyUsed of programCache.keys()) {
programCache.delete(leastRecentlyUsed);
break;
}
}
return compiled;
}
if(programCache.has(program)) {
// There is a compiled version of this program
let {compiled, extensions} = programCache.get(program);
// If extensions enabled state is the same as ours, we can
// just return this
// If not, we need to compile a new program
if(extensions == enableExtensions) {
return compiled;
} else {
return compileToCache()
}
} else {
return compileToCache()
}
}
// (string, string, int) → string
function ircbotRun(program, input, maxCycles = 400000) {
let compiled = cachedCompile(program);
let vm = gir.newVM(compiled, gir.encodeUTF8(input));
let result = gir.runVM(vm, maxCycles);
let output = gir.decodeUTF8(result.state.output);
// Replace all characters < 0x20 except for IRC formatting codes
// with their graphical representations at U+24xx
// Also replace DEL with U+2421
let formattingChars = [0x02, 0x03, 0x0f, 0x12, 0x15];
output = Array.from(output).map(c => {
let codePoint = c.codePointAt(0);
if(codePoint < 0x20 && !formattingChars.includes(codePoint)) {
return String.fromCodePoint(0x2400 + codePoint);
} else if(codePoint == 0x7f) {
return String.fromCodePoint(0x2421);
} else {
return c;
}
}).join('');
// Did we run into maxCycles?
let executedTooLong = maxCycles != null &&
result.cycles >= maxCycles;
// If there was either no output or a breakpoint triggered, dump
// tape to output instead
if(output.length == 0 || result.breakPointReached) {
// If it was a breakpoint, mark it with [BP]
if(result.breakPointReached) {
output += '[BP]';
}
// Get the tape head we should have here
// Both the program completing succesfully and a breakpoint
// will leave the tape head "where it should be". The cycle
// limit can however stop a program at any point. Therefore
// in such cases more useful is the last index that was
// accessed
let tapeHead = executedTooLong ? result.lastIndex : result.state.tapeHead;
// Find min and max of the existant array indinces, since
// there is no good way to easily get them and we need them
// for the output
// Default to both being set to tapeHead, because that way
// even if there are no indices we can get values that
// the rest of the code can work with
let memoryIndices = Array.from(result.state.memory.keys());
let min = memoryIndices.reduce((x,y) => Math.min(x, y), tapeHead);
let max = memoryIndices.reduce((x,y) => Math.max(x, y), tapeHead);
// Get 15 cells of context on each side of tape head
// Exception is if max or min comes up before that, in which
// Case move the extra to the other
let start = tapeHead - 15;
let end = tapeHead + 15;
if(start < min && end > max) {
// Both ends fall out of bounds
start = min;
end = max;
} else if(start < min && end <= max) {
// Only start falls out of bounds
// Add the number of cells to the part after the
// head, but clamp that to at maximum max
end = Math.min(end + (min - start), max);
start = min;
} else if(start >= min && end > max) {
// Only end falls out of bounds
// Do reverse of previous
start = Math.max(start - (end - max), min);
end = max;
}
let cells = [];
for(let i = start; i <= end; i++) {
let cell = ''
// 0 if cell doesn't exist
if(result.state.memory.has(i)) {
cell = result.state.memory.get(i).toString();
} else {
cell = '0';
}
// Add [] around the cell if tape head is there
if(i == tapeHead) {
cell = `[${cell}]`;
}
cells.push(cell);
}
// If we don't display the start/end of the tape, add …
output += `{${min}${max}}(${start > min ? '… ' : ''}${cells.join(' ')}${end < max ? ' …' : ''})`
}
// Add «TLE» to signify execution taking too long
if(executedTooLong) {
output += '«TLE»';
}
// If there was a problem with parsing an int, throw an Error
if(result.intParseFailed) {
let context = gir.decodeUTF8(result.state.input).slice(0, 3);
throw new IntParseError(`';': couldn't read number (near '${context})'`);
}
return output;
}
exports.ircbotRun = ircbotRun;
function main() {
const readline = require('readline');
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
prompt: 'program!input> '
});
rl.prompt();
rl.on('line', line => {
let [program, ...input] = line.split('!');
input = input.join('!');
console.log(ircbotRun(program, input));
rl.prompt();
});
}
if(require.main === module) {
main();
}