'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(); }