'use strict'; // ------------------------------------------------------------------ // Definitions // ------------------------------------------------------------------ // Use symbols for the names of the instructions // Unsure if this helps in any way over strings, but I feel it neater // +++++ → {type: add, value: 5} // Can have offset property const add = Symbol('add'); // > → {type: moveHead, value: 1} const moveHead = Symbol('moveHead'); // . → {type: writeByte} // Can have offset property const writeByte = Symbol('writeByte'); // , → {type: readByte} // Can have offset property const readByte = Symbol('readByte'); // [-] → {type: loop, contents: [{type: add, value: -1}]} // Can have isBalanced property const loop = Symbol('loop'); // [-] → {type: clear} // Can have offset property const clear = Symbol('clear'); // TODO: Add extensions from Eldis // ------------------------------------------------------------------ // Parsing // ------------------------------------------------------------------ class ParsingError extends Error {} // (string) → [commandObjects] // May throw function parse(program) { // (string, int, bool) → {parsed: [commandObjects], lastIndex: int} // index is the index of the next character to consume // inLoop tells whether we're parsing a loop or the top level // lastIndex is the last index the function consumed function constructTree(program, index = 0, inLoop = false) { let commands = []; // Move this out of the loop body since we need to return // the index of the last character we parsed let i = index; for(;;) { if(i >= program.length) { // If we're parsing a loop, we have a // missing ] // If we're parsing the top level, this is // where we should exit if(inLoop) { throw new ParsingError('Missing ]'); } else { break; } i++; } else if(program[i] == ']') { // If we're parsing a loop, this is where we // should exit // If we're parsing the top level, we have a // missing [ if(inLoop) { break; } else { throw new ParsingError('Missing ['); } i++; } else if(program[i] == '+' || program[i] == '-') { // Fold a run of +s and -s into one node let value = 0; for(; i < program.length; i++) { if(program[i] == '+') { value++; } else if(program[i] == '-') { value--; } else { // Reached end of the run break; } } // Only add the command is value is not 0 if(value != 0) { commands.push({type: add, value: value}); } // i is not incremented, since it already // points to a location containig a char we // have not yet handled } else if(program[i] == '<' || program[i] == '>') { // Fold a run of s into one node let value = 0; for(; i < program.length; i++) { if(program[i] == '>') { value++; } else if(program[i] == '<') { value--; } else { // Reached end of the run break; } } // Only add the command is value is not 0 if(value != 0) { commands.push({type: moveHead, value: value}); } // see +/- for why we don't increment i } else if(program[i] == '.') { commands.push({type: writeByte}); i++; } else if(program[i] == ',') { commands.push({type: readByte}); i++; } else if(program[i] == '[') { // Parse a loop. This is done by calling the // same parser function recursively // Due to this the loop appears as one node // in the parsed result let {parsed, lastIndex} = constructTree( program, // Same program data i + 1, // Start at the next char true // We're parsing a loop ); commands.push({type: loop, contents: parsed}); // Since lastIndex was consumed by the inner // function, we don't want to consume it a // second time i = lastIndex + 1; } else { // All others characters are comments, // ignore them i++; } } return {parsed: commands, lastIndex: i}; } // We only care about the parsed contents, since under normal // operarion we only get out of the loop if we've reached the end // of the program let {parsed} = constructTree(program); return parsed; } // ([commandObjects/offsetCommandObjects]) function prettyPrint(parsed) { // ([commandObjects/offsetCommandObjects], string) function printIndented(parsed, indent = '') { for(let command of parsed) { let line = indent; if(command.type == add) { line += `add ${command.value}`; if('offset' in command) { line += ` (${command.offset})`; } console.log(line); } else if(command.type == moveHead) { line += `moveHead ${command.value}`; console.log(line); } else if(command.type == writeByte) { line += 'writeByte'; if('offset' in command) { line += ` (${command.offset})`; } console.log(line); } else if(command.type == readByte) { line += 'readByte'; if('offset' in command) { line += ` (${command.offset})`; } console.log(line); } else if(command.type == loop) { line += 'loop'; if('isBalanced' in command) { line += ` (balanced: ${command.isBalanced})`; } console.log(line); printIndented(command.contents, indent + ' '); } else if(command.type == clear) { line += 'clear'; if('offset' in command) { line += ` (${command.offset})`; } console.log(line); } else { line += `unknown ${command.type}`; console.log(line); } } } printIndented(parsed); } // ------------------------------------------------------------------ // Optimization passes // ------------------------------------------------------------------ class UnknownIRError extends Error {} // ([commandObjects]) → [commandObjects] function joinAdjacentOps(parsed) { // ([commandObjects], commandType) → [commandObjects] function worker(parsed, type) { let optimized = []; let prevOfType = false, value = 0; for(let command of parsed) { if(prevOfType && command.type == type) { // Update value, don't add to optimized yet value += command.value; } else if(!prevOfType && command.type == type) { // Start of a possible run of commands prevOfType = true; value = command.value; } else if(prevOfType && command.type != type) { // A run has ended, add it to optimized // However, skip it if value is 0 if(value != 0) { optimized.push({type: type, value: value}); } // Also add the command for this round optimized.push(command); prevOfType = false; } else { optimized.push(command); prevOfType = false; } } // Did we end with a command of given type if(prevOfType) { // Yes, add it to optimized (unless value is 0) if(value != 0) { optimized.push({type: type, value: value}); } } return optimized; } return worker(worker(parsed, moveHead), add); } // ([commandObjects]) → [commandObjects] function transformClearLoops(parsed) { let optimized = []; for(let command of parsed) { // Only match loops like [-] or [+] let isClearLoop = command.type == loop && command.contents.length == 1 && command.contents[0].type == add && (command.contents[0].value == 1 || command.contents[0].value == -1); if(isClearLoop) { optimized.push({type: clear}); } else if(command.type == loop) { // Run for inner loops optimized.push({type: loop, contents: transformClearLoops(command.contents)}); } else { optimized.push(command); } } return optimized; } // ([commandObjects]) → [offsetCommandObjects] function addOffsetProperties(parsed) { // ([commandObjects]) → {offsetted: [offsetCommandObjects], isBalanced: bool} function worker(parsed) { let offsetted = []; let isBalanced = true; let headChange = 0; let offset = 0; for(let command of parsed) { if(command.type == add) { offsetted.push({type: add, value: command.value, offset: offset}); } else if(command.type == moveHead) { offset += command.value; } else if(command.type == writeByte) { offsetted.push({type: writeByte, offset: offset}); } else if(command.type == readByte) { offsetted.push({type: readByte, offset: offset}); } else if(command.type == clear) { offsetted.push({type: clear, offset: offset}); } else if(command.type == loop) { // A loop should be self-contained // If offset is not 0, add a moveHead if(offset != 0) { offsetted.push({type: moveHead, value: offset}); // Mark we've moved the head headChange += offset; } offset = 0; // Run optimization on the loop let result = worker(command.contents); // We're only balanced if our loops are isBalanced = isBalanced && result.isBalanced; offsetted.push({type: loop, contents: result.offsetted, isBalanced: result.isBalanced}); // headChange's value becomes invalid if the // loop is not balanced. However, we only // care about its value when figuring out // our isBalanced, which will be forced to // false if any inner loop is not balanced } else { throw new UnknownIRError(); } } // We need to move the tape head in the end anyways, so // generate moveHead is offseet is not 0 if(offset != 0) { offsetted.push({type: moveHead, value: offset}); } // We're only balanced if relative to start of the loop ends // up as 0 isBalanced = isBalanced && offset + headChange == 0; return {offsetted, isBalanced}; } return worker(parsed).offsetted; } // TODO: Optimization pass to turn copy loops into copy commands // ([commandObjects]) → [offsetCommandObjects] function optimize(parsed) { const optimizations = [ joinAdjacentOps, transformClearLoops, addOffsetProperties ] return optimizations.reduce((IR, optimization) => optimization(IR), parsed); } // ------------------------------------------------------------------ // Virtual machine // ------------------------------------------------------------------ // TODO: Implement a brainfuck VM for running the optimized programs