'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'); // {type: jumpIfZero, target: 5} const jumpIfZero = Symbol('jumpIfZero'); // {type: jumpIfNonZero, target: 2} const jumpIfNonZero = Symbol('jumpIfNonZero'); // TODO: Add extensions from Eldis class ParsingError extends Error {} class UnknownIRError extends Error {} // ------------------------------------------------------------------ // Parsing // ------------------------------------------------------------------ // (string) → [commandObjects] // May throw ParsingError 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]) → str function prettifyIR(parsed) { // ([commandObjects/offsetCommandObjects], string) → str function worker(parsed, indent = '') { let lines = []; for(let i = 0; i < parsed.length; i++) { let command = parsed[i]; let line = `${indent}${i} `; if(command.type == add) { line += `add ${command.value}`; if('offset' in command) { line += ` (${command.offset})`; } lines.push(line); } else if(command.type == moveHead) { line += `moveHead ${command.value}`; lines.push(line); } else if(command.type == writeByte) { line += 'writeByte'; if('offset' in command) { line += ` (${command.offset})`; } lines.push(line); } else if(command.type == readByte) { line += 'readByte'; if('offset' in command) { line += ` (${command.offset})`; } lines.push(line); } else if(command.type == loop) { line += 'loop'; if('isBalanced' in command) { line += ` (balanced: ${command.isBalanced})`; } lines.push(line); lines = lines.concat(worker(command.contents, indent + ' ')); } else if(command.type == clear) { line += 'clear'; if('offset' in command) { line += ` (${command.offset})`; } lines.push(line); } else if(command.type == jumpIfZero) { line += `jumpIfZero ${command.target}`; lines.push(line); } else if(command.type == jumpIfNonZero) { line += `jumpIfNonZero ${command.target}`; lines.push(line); } else { line += `unknown ${command.type}`; lines.push(line); } } return lines } return worker(parsed).join('\n'); } // ------------------------------------------------------------------ // Optimization passes // ------------------------------------------------------------------ // ([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 a run has ended, add it to optimized // However, skip it if value is 0 if(prevOfType && value != 0) { optimized.push({type: type, value: value}); } // Add the command for this round if(command.type == loop) { // Recurse optimized.push({type: loop, contents: worker( command.contents, type)}); } 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 // ([offsetCommandObjects]) → [flatCommandObjects] function flattenLoops(offsetted) { // ([offsetCommandObjects], int) → [flatCommandObjects] // prevLength tells length of the flattened program up until now function worker(offsetted, prevLength = 0) { let flattened = []; for(let command of offsetted) { if(command.type == loop) { // flattened.length is the index of the next // command in out flattened // flattened.length + prevLength is the // index of it in the resulting combined // flattened // Since this should be the index of the // start of the loop body we want to point // after the next command, which is going // to be the jump let startIndex = flattened.length + prevLength + 1; let loopBody = worker(command.contents, startIndex // length = index of next ); // startIndex + loopBody.length is the index // of the next command after the loop body // The command after it is going to be the // jump back to the start of the body, so we // want to point to the command after it let endIndex = startIndex + loopBody.length + 1; // Add the first jump flattened.push({type: jumpIfZero, target: endIndex}); // Add the loop body flattened = flattened.concat(loopBody); // Add the second loop flattened.push({type: jumpIfNonZero, target: startIndex}); } else { flattened.push(command); } } return flattened; } return worker(offsetted); } // ([commandObjects]) → [flatCommandObjects] function optimize(parsed) { const optimizations = [ joinAdjacentOps, transformClearLoops, addOffsetProperties, flattenLoops ] return optimizations.reduce((IR, optimization) => optimization(IR), parsed); } // ------------------------------------------------------------------ // Virtual machine // ------------------------------------------------------------------ // ([flatCommandObject]) → girVMState function newVM(program, input) { return { // Initial state for the machine program: program, ip: 0, memory: Object.create(null), tapeHead: 0, input: input, output: '' }; } // (girVMState, int) → {state: girVMState, reachedLimit: bool} // reachedLimit is set to true if the program ran for maxCycles and did not // terminate // If maxCycles is null, the program runs until completion function runVM(state, maxCycles = null) { let program = state.program; let ip = state.ip; // Create a copy of the memory, since we're going to modify it // TODO: Make memory into a Proxied thing that returns 0 if it // doesn't have the requested cell let memory = Object.create(null); for(let key in state.memory) { memory[key] = state.memory[key]; } let tapeHead = state.tapeHead; let input = state.input; let output = state.output; let reachedLimit = true; for(let cycle = 0; maxCycles === null || cycle < maxCycles; cycle++) { // Exit the loop if we run to the end of the program if(ip >= program.length) { // Did not reach limit, program finished reachedLimit = false; break; } let command = program[ip]; // See if we need to make sure the cell we're on exists and // calculate the index into the array of the cell we're // accessing let index = tapeHead; switch(command.type) { case add: case writeByte: case readByte: case clear: // These have an offset property, add it index += command.offset; // Fall through case jumpIfZero: case jumpIfNonZero: // Ensure the cell exists if(!(index in memory)) { memory[index] = 0; } } // Run the command switch(command.type) { case add: if(!(index in memory)) memory[index] = 0; memory[index] += command.value; // Implement wraparound memory[index] = memory[index] & 0xFF; ip++; break; case moveHead: tapeHead += command.value; ip++; break; case writeByte: if(!(index in memory)) memory[index] = 0; // TODO: utf-8 output += String.fromCodePoint(memory[index]); ip++; break; case readByte: // TODO: utf-8 // Have we reached EOF? if(input.length == 0) { // Yes, return 0 memory[index] = 0; } else { // No, return character memory[index] = input.codePointAt(0); // FIXME: This only works for BMP input = input.slice(1); } ip++; break; case clear: memory[index] = 0; ip++; break; case jumpIfZero: if(!(index in memory)) memory[index] = 0; if(memory[index] == 0) { ip = command.target; } else { ip++; } break; case jumpIfNonZero: if(!(index in memory)) memory[index] = 0; if(memory[index] != 0) { ip = command.target; } else { ip++; } break; default: // Unknown command type throw new UnknownIRError(); } } let newState = { program, ip, memory, tapeHead, input, output }; return {state: newState, reachedLimit: reachedLimit}; } // ------------------------------------------------------------------ // User-facing functions // ------------------------------------------------------------------ // (string) → [flatCommandObjects] function compile(program) { return optimize(parse(program)); } // (string, string) → string function run(program, input) { // TODO: Allow setting cycle maximum // TODO; Cache programs let compiled = compile(program); let vm = newVM(compiled, input); return runVM(vm).state.output; }