gir/gir.js

440 lines
12 KiB
JavaScript
Raw Normal View History

2018-05-21 20:55:48 +00:00
'use strict';
2018-05-21 21:18:05 +00:00
// ------------------------------------------------------------------
// Definitions
// ------------------------------------------------------------------
2018-05-21 20:55:48 +00:00
// 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}
2018-05-22 12:08:23 +00:00
// Can have offset property
2018-05-21 20:55:48 +00:00
const add = Symbol('add');
// > → {type: moveHead, value: 1}
const moveHead = Symbol('moveHead');
// . → {type: writeByte}
2018-05-22 12:08:23 +00:00
// Can have offset property
2018-05-21 20:55:48 +00:00
const writeByte = Symbol('writeByte');
// , → {type: readByte}
2018-05-22 12:08:23 +00:00
// Can have offset property
2018-05-21 20:55:48 +00:00
const readByte = Symbol('readByte');
// [-] → {type: loop, contents: [{type: add, value: -1}]}
2018-05-22 12:08:23 +00:00
// Can have isBalanced property
2018-05-21 20:55:48 +00:00
const loop = Symbol('loop');
2018-05-22 11:26:40 +00:00
// [-] → {type: clear}
2018-05-22 12:08:23 +00:00
// Can have offset property
2018-05-22 11:26:40 +00:00
const clear = Symbol('clear');
2018-05-23 18:48:21 +00:00
// {type: jumpIfZero, target: 5}
const jumpIfZero = Symbol('jumpIfZero');
// {type: jumpIfNonZero, target: 2}
const jumpIfNonZero = Symbol('jumpIfNonZero');
2018-05-21 20:55:48 +00:00
// TODO: Add extensions from Eldis
2018-05-21 21:18:05 +00:00
// ------------------------------------------------------------------
// Parsing
// ------------------------------------------------------------------
2018-05-21 20:55:48 +00:00
class ParsingError extends Error {}
// (string) → [commandObjects]
// May throw ParsingError
2018-05-21 20:55:48 +00:00
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;
}
}
2018-05-22 11:26:40 +00:00
// Only add the command is value is not 0
if(value != 0) {
commands.push({type: add, value: value});
}
2018-05-21 20:55:48 +00:00
// 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 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;
}
}
2018-05-22 11:26:40 +00:00
// Only add the command is value is not 0
if(value != 0) {
commands.push({type: moveHead, value: value});
}
2018-05-21 20:55:48 +00:00
// 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 = [];
2018-05-23 18:48:21 +00:00
for(let i = 0; i < parsed.length; i++) {
let command = parsed[i];
let line = `${indent}${i} `;
2018-05-21 20:55:48 +00:00
if(command.type == add) {
line += `add ${command.value}`;
2018-05-22 12:08:23 +00:00
if('offset' in command) {
line += ` (${command.offset})`;
}
lines.push(line);
2018-05-21 20:55:48 +00:00
} else if(command.type == moveHead) {
line += `moveHead ${command.value}`;
lines.push(line);
2018-05-21 20:55:48 +00:00
} else if(command.type == writeByte) {
line += 'writeByte';
2018-05-22 12:08:23 +00:00
if('offset' in command) {
line += ` (${command.offset})`;
}
lines.push(line);
2018-05-21 20:55:48 +00:00
} else if(command.type == readByte) {
line += 'readByte';
2018-05-22 12:08:23 +00:00
if('offset' in command) {
line += ` (${command.offset})`;
}
lines.push(line);
2018-05-21 20:55:48 +00:00
} else if(command.type == loop) {
line += 'loop';
2018-05-22 12:08:23 +00:00
if('isBalanced' in command) {
line += ` (balanced: ${command.isBalanced})`;
}
lines.push(line);
lines = lines.concat(worker(command.contents, indent + ' '));
2018-05-22 11:26:40 +00:00
} else if(command.type == clear) {
line += 'clear';
2018-05-22 12:08:23 +00:00
if('offset' in command) {
line += ` (${command.offset})`;
}
lines.push(line);
2018-05-23 18:48:21 +00:00
} else if(command.type == jumpIfZero) {
line += `jumpIfZero ${command.target}`;
lines.push(line);
2018-05-23 18:48:21 +00:00
} else if(command.type == jumpIfNonZero) {
line += `jumpIfNonZero ${command.target}`;
lines.push(line);
2018-05-21 20:55:48 +00:00
} else {
line += `unknown ${command.type}`;
lines.push(line);
2018-05-21 20:55:48 +00:00
}
}
return lines.join('\n');
2018-05-21 20:55:48 +00:00
}
return worker(parsed);
2018-05-21 20:55:48 +00:00
}
2018-05-21 21:18:05 +00:00
// ------------------------------------------------------------------
// Optimization passes
// ------------------------------------------------------------------
2018-05-22 12:08:23 +00:00
class UnknownIRError extends Error {}
2018-05-21 21:18:05 +00:00
// ([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);
}
2018-05-22 11:26:40 +00:00
// ([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;
}
2018-05-22 12:08:23 +00:00
// ([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;
}
2018-05-22 12:10:10 +00:00
// TODO: Optimization pass to turn copy loops into copy commands
2018-05-23 18:48:21 +00:00
// ([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);
}
2018-05-22 12:08:23 +00:00
// ([commandObjects]) → [offsetCommandObjects]
2018-05-21 21:18:05 +00:00
function optimize(parsed) {
2018-05-22 12:08:23 +00:00
const optimizations = [
joinAdjacentOps,
transformClearLoops,
2018-05-23 18:48:21 +00:00
addOffsetProperties,
flattenLoops
2018-05-22 12:08:23 +00:00
]
2018-05-22 11:26:40 +00:00
return optimizations.reduce((IR, optimization) =>
optimization(IR), parsed);
2018-05-21 21:18:05 +00:00
}
2018-05-22 12:10:10 +00:00
// ------------------------------------------------------------------
// Virtual machine
// ------------------------------------------------------------------
// TODO: Implement a brainfuck VM for running the optimized programs