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-25 15:24:27 +00:00
|
|
|
// [>+>++<<-] → {type: move, changes: Map { 0 → -1, 1 → 1, 2 → 2 }}
|
|
|
|
const multiply = Symbol('multiply');
|
|
|
|
|
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-26 10:14:05 +00:00
|
|
|
// : → {type: writeInt}
|
|
|
|
// Can have offset property
|
|
|
|
const writeInt = Symbol('writeInt');
|
|
|
|
// ; → {type: readInt}
|
|
|
|
// Can have offset property
|
|
|
|
const readInt = Symbol('readInt');
|
|
|
|
// # → {type: breakPoint}
|
|
|
|
const breakPoint = Symbol('breakPoint');
|
2018-05-21 20:55:48 +00:00
|
|
|
|
2018-05-23 21:11:25 +00:00
|
|
|
class ParsingError extends Error {}
|
|
|
|
|
|
|
|
class UnknownIRError extends Error {}
|
|
|
|
|
2018-05-26 10:14:05 +00:00
|
|
|
class IntParseError extends Error {}
|
|
|
|
|
2018-05-21 21:18:05 +00:00
|
|
|
// ------------------------------------------------------------------
|
|
|
|
// Parsing
|
|
|
|
// ------------------------------------------------------------------
|
|
|
|
|
2018-05-26 10:14:05 +00:00
|
|
|
// (string, bool) → [commandObjects]
|
|
|
|
// enableExtensions contols whether commands :;# are recognized
|
2018-05-23 18:57:07 +00:00
|
|
|
// May throw ParsingError
|
2018-05-26 10:14:05 +00:00
|
|
|
function parse(program, enableExtensions = true) {
|
2018-05-21 20:55:48 +00:00
|
|
|
// (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;
|
2018-05-26 10:14:05 +00:00
|
|
|
|
|
|
|
} else if(program[i] == ':' && enableExtensions) {
|
|
|
|
commands.push({type: writeInt});
|
|
|
|
i++;
|
|
|
|
|
|
|
|
} else if(program[i] == ';' && enableExtensions) {
|
|
|
|
commands.push({type: readInt});
|
|
|
|
i++;
|
|
|
|
|
|
|
|
} else if(program[i] == '#' && enableExtensions) {
|
|
|
|
commands.push({type: breakPoint});
|
|
|
|
i++;
|
|
|
|
|
2018-05-21 20:55:48 +00:00
|
|
|
} 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;
|
|
|
|
}
|
|
|
|
|
2018-05-23 18:57:07 +00:00
|
|
|
// ([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})`;
|
|
|
|
}
|
2018-05-23 18:57:07 +00:00
|
|
|
lines.push(line);
|
2018-05-21 20:55:48 +00:00
|
|
|
} else if(command.type == moveHead) {
|
|
|
|
line += `moveHead ${command.value}`;
|
2018-05-23 18:57:07 +00:00
|
|
|
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})`;
|
|
|
|
}
|
2018-05-23 18:57:07 +00:00
|
|
|
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})`;
|
|
|
|
}
|
2018-05-23 18:57:07 +00:00
|
|
|
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})`;
|
|
|
|
}
|
2018-05-23 18:57:07 +00:00
|
|
|
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})`;
|
|
|
|
}
|
2018-05-23 18:57:07 +00:00
|
|
|
lines.push(line);
|
2018-05-25 15:24:27 +00:00
|
|
|
} else if(command.type == multiply) {
|
|
|
|
let changes = [];
|
|
|
|
for(let [offset, value] of command.changes.entries()) {
|
|
|
|
changes.push(`${offset}: ${value}`);
|
|
|
|
}
|
|
|
|
line += `multiply ${changes.join(', ')}`;
|
|
|
|
lines.push(line);
|
2018-05-23 18:48:21 +00:00
|
|
|
} else if(command.type == jumpIfZero) {
|
|
|
|
line += `jumpIfZero ${command.target}`;
|
2018-05-23 18:57:07 +00:00
|
|
|
lines.push(line);
|
2018-05-23 18:48:21 +00:00
|
|
|
} else if(command.type == jumpIfNonZero) {
|
|
|
|
line += `jumpIfNonZero ${command.target}`;
|
2018-05-23 18:57:07 +00:00
|
|
|
lines.push(line);
|
2018-05-26 10:14:05 +00:00
|
|
|
} else if(command.type == writeInt) {
|
|
|
|
line += 'writeInt';
|
|
|
|
if('offset' in command) {
|
|
|
|
line += ` (${command.offset})`;
|
|
|
|
}
|
|
|
|
lines.push(line);
|
|
|
|
} else if(command.type == readInt) {
|
|
|
|
line += 'readInt';
|
|
|
|
if('offset' in command) {
|
|
|
|
line += ` (${command.offset})`;
|
|
|
|
}
|
|
|
|
lines.push(line);
|
|
|
|
} else if(command.type == breakPoint) {
|
|
|
|
line += 'breakPoint';
|
|
|
|
lines.push(line);
|
2018-05-21 20:55:48 +00:00
|
|
|
} else {
|
2018-05-25 15:24:27 +00:00
|
|
|
line += `unknown ${command.type.toString()}`;
|
2018-05-23 18:57:07 +00:00
|
|
|
lines.push(line);
|
2018-05-21 20:55:48 +00:00
|
|
|
}
|
|
|
|
}
|
2018-05-23 18:57:07 +00:00
|
|
|
|
2018-05-24 20:05:59 +00:00
|
|
|
return lines
|
2018-05-21 20:55:48 +00:00
|
|
|
}
|
2018-05-24 20:05:59 +00:00
|
|
|
return worker(parsed).join('\n');
|
2018-05-21 20:55:48 +00:00
|
|
|
}
|
2018-05-21 21:18:05 +00:00
|
|
|
|
|
|
|
// ------------------------------------------------------------------
|
|
|
|
// 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;
|
2018-05-24 19:50:31 +00:00
|
|
|
} else {
|
|
|
|
// If a run has ended, add it to optimized
|
2018-05-21 21:18:05 +00:00
|
|
|
// However, skip it if value is 0
|
2018-05-24 19:50:31 +00:00
|
|
|
if(prevOfType && value != 0) {
|
2018-05-21 21:18:05 +00:00
|
|
|
optimized.push({type: type, value: value});
|
|
|
|
}
|
2018-05-24 19:50:31 +00:00
|
|
|
|
|
|
|
// Add the command for this round
|
|
|
|
if(command.type == loop) {
|
|
|
|
// Recurse
|
|
|
|
optimized.push({type: loop,
|
|
|
|
contents: worker(
|
|
|
|
command.contents,
|
|
|
|
type)});
|
|
|
|
} else {
|
|
|
|
optimized.push(command);
|
|
|
|
}
|
2018-05-21 21:18:05 +00:00
|
|
|
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
|
2018-05-26 10:14:05 +00:00
|
|
|
} else if(command.type == writeInt) {
|
|
|
|
offsetted.push({type: writeInt,
|
|
|
|
offset: offset});
|
|
|
|
} else if(command.type == readInt) {
|
|
|
|
offsetted.push({type: readInt,
|
|
|
|
offset: offset});
|
|
|
|
} else if(command.type == breakPoint) {
|
|
|
|
// Add the breakpoint
|
|
|
|
offsetted.push({type: breakPoint});
|
2018-05-22 12:08:23 +00:00
|
|
|
} else {
|
2018-05-25 15:24:27 +00:00
|
|
|
throw new UnknownIRError(
|
|
|
|
`Unknown command ${command.type.toString()}`);
|
2018-05-22 12:08:23 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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-25 15:24:27 +00:00
|
|
|
// ([offsetCommandObjects]) → [offsetCommandObjects]
|
|
|
|
function transformMultiplyLoops(offsetted) {
|
|
|
|
let optimized = [];
|
|
|
|
|
|
|
|
for(let command of offsetted) {
|
|
|
|
let isMultiplyLoop = false;
|
|
|
|
// Not necessarily a multiply loop, since multiply loops,
|
|
|
|
// in addition to being balanced loops with only adds,
|
|
|
|
// also decrement / increment the cell under tape head
|
|
|
|
// by one. However, these are loops we can run through the
|
|
|
|
// next processing step, and we can drop unfit ones after
|
|
|
|
// that
|
|
|
|
let maybeMultiplyLoop = command.type == loop &&
|
|
|
|
command.isBalanced &&
|
|
|
|
command.contents.every(x => x.type == add);
|
|
|
|
|
|
|
|
// TODO: Change this to use a Proxy thingie
|
|
|
|
let changes = new Map();
|
|
|
|
if(maybeMultiplyLoop) {
|
|
|
|
for(let addition of command.contents) {
|
|
|
|
// We already know all of these are adds
|
|
|
|
if(!changes.has(addition.offset)) {
|
|
|
|
changes.set(addition.offset, 0);
|
|
|
|
}
|
|
|
|
|
|
|
|
let current = changes.get(addition.offset);
|
|
|
|
changes.set(addition.offset,
|
|
|
|
current + addition.value);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Did we actually have a multiply loop?
|
|
|
|
isMultiplyLoop = changes.has(0) &&
|
|
|
|
(changes.get(0) == 1 ||
|
|
|
|
changes.get(0) == -1);
|
|
|
|
}
|
|
|
|
|
|
|
|
if(isMultiplyLoop) {
|
|
|
|
// If changes[0] is 1, we are dealing with
|
|
|
|
// a loop of the type [>-<+], which is
|
|
|
|
// (except for run time) same as [>+<-].
|
|
|
|
// Transform former into latter
|
|
|
|
if(changes.get(0) == 1) {
|
|
|
|
for(let [offset, value] of changes.entries()) {
|
|
|
|
changes.set(offset, -value);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
optimized.push({type: multiply,
|
|
|
|
changes: changes});
|
|
|
|
} else if(command.type == loop) {
|
|
|
|
// Recurse
|
|
|
|
optimized.push({type: loop,
|
|
|
|
contents: transformMultiplyLoops(command.contents),
|
|
|
|
balanced: command.balanced});
|
|
|
|
} else {
|
|
|
|
// Pass through
|
|
|
|
optimized.push(command);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return optimized;
|
|
|
|
}
|
2018-05-22 12:10:10 +00:00
|
|
|
|
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-23 21:11:25 +00:00
|
|
|
// ([commandObjects]) → [flatCommandObjects]
|
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,
|
2018-05-25 15:24:27 +00:00
|
|
|
transformMultiplyLoops,
|
2018-05-23 18:48:21 +00:00
|
|
|
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
|
|
|
|
// ------------------------------------------------------------------
|
|
|
|
|
2018-05-25 11:19:09 +00:00
|
|
|
// ([flatCommandObject], [int]) → girVMState
|
2018-05-23 21:11:25 +00:00
|
|
|
function newVM(program, input) {
|
|
|
|
return {
|
|
|
|
// Initial state for the machine
|
|
|
|
program: program,
|
|
|
|
ip: 0,
|
|
|
|
|
2018-05-25 15:24:27 +00:00
|
|
|
memory: new Map(),
|
2018-05-23 21:11:25 +00:00
|
|
|
tapeHead: 0,
|
|
|
|
|
|
|
|
input: input,
|
2018-05-25 11:19:09 +00:00
|
|
|
output: []
|
2018-05-23 21:11:25 +00:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2018-05-26 10:14:05 +00:00
|
|
|
// (girVMState, int) → {state: girVMState, complete: bool, cycles: int,
|
|
|
|
// intParseFailed: bool, breakPointReached: bool,
|
|
|
|
// lastIndex: int/null}
|
2018-05-24 20:20:52 +00:00
|
|
|
// complete is set to true is the program completed its execution
|
|
|
|
// cycles is the number of cycles the VM ran
|
2018-05-26 10:14:05 +00:00
|
|
|
// intParseFailed is true is parseInt failed to read a valid number
|
|
|
|
// breakPointReached is true if a breakPoint command was executed
|
|
|
|
// lastIndex tells the last memory index accessed by the VM
|
2018-05-23 21:11:25 +00:00
|
|
|
// 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
|
2018-05-25 15:24:27 +00:00
|
|
|
let memory = new Map(state.memory.entries());
|
2018-05-23 21:11:25 +00:00
|
|
|
let tapeHead = state.tapeHead;
|
|
|
|
|
2018-05-25 11:19:09 +00:00
|
|
|
// Create copies of input and output, since we might modify them
|
|
|
|
let input = state.input.slice();
|
|
|
|
let output = state.output.slice();
|
2018-05-23 21:11:25 +00:00
|
|
|
|
2018-05-26 10:14:05 +00:00
|
|
|
// Flags we want to return
|
2018-05-24 20:20:52 +00:00
|
|
|
let complete = false;
|
2018-05-26 10:14:05 +00:00
|
|
|
let intParseFailed = false;
|
|
|
|
let breakPointReached = false;
|
|
|
|
|
|
|
|
// Debug features
|
|
|
|
let lastIndex = null;
|
|
|
|
|
2018-05-24 20:20:52 +00:00
|
|
|
let cycle = 0;
|
|
|
|
for(; maxCycles === null || cycle < maxCycles; cycle++) {
|
2018-05-23 21:11:25 +00:00
|
|
|
// Exit the loop if we run to the end of the program
|
|
|
|
if(ip >= program.length) {
|
2018-05-24 20:20:52 +00:00
|
|
|
// Program completed
|
|
|
|
complete = true;
|
2018-05-23 21:11:25 +00:00
|
|
|
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:
|
2018-05-26 10:14:05 +00:00
|
|
|
case writeInt:
|
|
|
|
case readInt:
|
2018-05-23 21:11:25 +00:00
|
|
|
// These have an offset property, add it
|
|
|
|
index += command.offset;
|
|
|
|
// Fall through
|
2018-05-25 15:24:27 +00:00
|
|
|
case multiply:
|
2018-05-23 21:11:25 +00:00
|
|
|
case jumpIfZero:
|
|
|
|
case jumpIfNonZero:
|
|
|
|
// Ensure the cell exists
|
2018-05-25 15:24:27 +00:00
|
|
|
if(!memory.has(index)) {
|
|
|
|
memory.set(index, 0);
|
2018-05-23 21:11:25 +00:00
|
|
|
}
|
2018-05-26 10:14:05 +00:00
|
|
|
lastIndex = index;
|
2018-05-23 21:11:25 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Run the command
|
|
|
|
switch(command.type) {
|
|
|
|
case add:
|
2018-05-25 15:24:27 +00:00
|
|
|
let old = memory.get(index);
|
|
|
|
memory.set(index, (command.value + old) & 0xFF);
|
|
|
|
|
2018-05-23 21:11:25 +00:00
|
|
|
ip++;
|
|
|
|
break;
|
|
|
|
|
|
|
|
case moveHead:
|
|
|
|
tapeHead += command.value;
|
2018-05-25 15:24:27 +00:00
|
|
|
|
2018-05-23 21:11:25 +00:00
|
|
|
ip++;
|
|
|
|
break;
|
|
|
|
|
|
|
|
case writeByte:
|
2018-05-25 15:24:27 +00:00
|
|
|
output.push(memory.get(index));
|
2018-05-23 21:11:25 +00:00
|
|
|
ip++;
|
|
|
|
break;
|
|
|
|
|
|
|
|
case readByte:
|
|
|
|
// Have we reached EOF?
|
|
|
|
if(input.length == 0) {
|
|
|
|
// Yes, return 0
|
2018-05-25 15:24:27 +00:00
|
|
|
memory.set(index, 0);
|
2018-05-23 21:11:25 +00:00
|
|
|
} else {
|
|
|
|
// No, return character
|
2018-05-25 15:24:27 +00:00
|
|
|
memory.set(index, input.shift());
|
2018-05-23 21:11:25 +00:00
|
|
|
}
|
|
|
|
ip++;
|
|
|
|
break;
|
|
|
|
|
|
|
|
case clear:
|
2018-05-25 15:24:27 +00:00
|
|
|
memory.set(index, 0);
|
|
|
|
ip++;
|
|
|
|
break;
|
|
|
|
|
|
|
|
case multiply:
|
|
|
|
if(command.changes.get(0) != -1) {
|
|
|
|
throw new UnknownIRError(
|
|
|
|
`multiply where change for 0 is ${command.changes.get(0)}`);
|
|
|
|
}
|
|
|
|
|
|
|
|
let multiplier = memory.get(index);
|
|
|
|
|
|
|
|
for(let [offset, change] of command.changes.entries()) {
|
|
|
|
let index = tapeHead + offset;
|
|
|
|
if(!memory.has(index)) {
|
|
|
|
memory.set(index, 0);
|
|
|
|
}
|
|
|
|
let old = memory.get(index);
|
|
|
|
memory.set(index, old +
|
|
|
|
multiplier * change);
|
|
|
|
}
|
|
|
|
|
2018-05-23 21:11:25 +00:00
|
|
|
ip++;
|
|
|
|
break;
|
|
|
|
|
|
|
|
case jumpIfZero:
|
2018-05-25 15:24:27 +00:00
|
|
|
if(memory.get(index) == 0) {
|
2018-05-23 21:11:25 +00:00
|
|
|
ip = command.target;
|
|
|
|
} else {
|
|
|
|
ip++;
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
|
|
|
|
case jumpIfNonZero:
|
2018-05-25 15:24:27 +00:00
|
|
|
if(memory.get(index) != 0) {
|
2018-05-23 21:11:25 +00:00
|
|
|
ip = command.target;
|
|
|
|
} else {
|
|
|
|
ip++;
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
|
2018-05-26 10:14:05 +00:00
|
|
|
case writeInt:
|
|
|
|
let outputStr = memory.get(index).toString();
|
|
|
|
output = output.concat(encodeUTF8(outputStr));
|
|
|
|
ip++;
|
|
|
|
break;
|
|
|
|
|
|
|
|
case readInt:
|
|
|
|
// Skip any spaces in front
|
|
|
|
while(input.length > 0 && input[0] == 0x20) {
|
|
|
|
input.shift();
|
|
|
|
}
|
|
|
|
|
|
|
|
let number = 0;
|
|
|
|
|
|
|
|
// Read digits
|
|
|
|
let consumedInput = false;
|
|
|
|
while(input.length > 0 &&
|
|
|
|
input[0] >= 0x30 &&
|
|
|
|
input[0] <= 0x39) {
|
|
|
|
let digit = input.shift() - 0x30;
|
|
|
|
number = number * 10 + digit;
|
|
|
|
consumedInput = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Did we read anything
|
|
|
|
if(consumedInput) {
|
|
|
|
// Yes, clamp value to [0, 255] and
|
|
|
|
// set cell to it
|
|
|
|
number = Math.max(
|
|
|
|
Math.min(number, 255),
|
|
|
|
0);
|
|
|
|
memory.set(index, number);
|
|
|
|
} else if(input.length == 0) {
|
|
|
|
// No, but there was an EOF, so set
|
|
|
|
// the cell to 0
|
|
|
|
memory.set(index, 0);
|
|
|
|
} else {
|
|
|
|
// No, and there wasn't an EOF, so
|
|
|
|
// signal an error
|
|
|
|
intParseFailed = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
ip++;
|
|
|
|
break;
|
|
|
|
|
|
|
|
case breakPoint:
|
|
|
|
breakPointReached = true;
|
|
|
|
|
|
|
|
ip++;
|
|
|
|
break;
|
|
|
|
|
2018-05-23 21:11:25 +00:00
|
|
|
default:
|
|
|
|
// Unknown command type
|
2018-05-25 15:24:27 +00:00
|
|
|
throw new UnknownIRError(
|
|
|
|
`Unknown command ${command.type.toString()}`);
|
2018-05-23 21:11:25 +00:00
|
|
|
}
|
2018-05-26 10:14:05 +00:00
|
|
|
|
|
|
|
// Since can't use 'break' from a switch(), do it here
|
|
|
|
if(intParseFailed || breakPointReached) {
|
|
|
|
break;
|
|
|
|
}
|
2018-05-23 21:11:25 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
let newState = {
|
|
|
|
program,
|
|
|
|
ip,
|
|
|
|
|
|
|
|
memory,
|
|
|
|
tapeHead,
|
|
|
|
|
|
|
|
input,
|
|
|
|
output
|
|
|
|
};
|
|
|
|
|
2018-05-26 10:14:05 +00:00
|
|
|
return {state: newState, complete: complete, cycles: cycle,
|
|
|
|
intParseFailed: intParseFailed,
|
|
|
|
breakPointReached: breakPointReached,
|
|
|
|
lastIndex: lastIndex};
|
2018-05-23 21:11:25 +00:00
|
|
|
}
|
|
|
|
|
2018-05-25 11:19:09 +00:00
|
|
|
// ------------------------------------------------------------------
|
|
|
|
// UTF-8
|
|
|
|
// ------------------------------------------------------------------
|
|
|
|
|
|
|
|
// string → [int]
|
|
|
|
function encodeUTF8(string) {
|
|
|
|
let encoded = [];
|
|
|
|
|
|
|
|
for(let character of string) {
|
|
|
|
let codepoint = character.codePointAt(0);
|
|
|
|
|
|
|
|
if(codepoint < 0x80) {
|
|
|
|
// 0xxxxxxx
|
|
|
|
encoded.push(codepoint);
|
|
|
|
} else if(codepoint < 0x0800) {
|
|
|
|
// 110xxxxx 10xxxxxx
|
|
|
|
let b1 = codepoint >> 6 | 0b11000000;
|
|
|
|
let b2 = codepoint & 0b00111111 | 0b10000000;
|
|
|
|
encoded.push(b1);
|
|
|
|
encoded.push(b2);
|
|
|
|
} else if(codepoint < 0x10000) {
|
|
|
|
// 1110xxxx 10xxxxxx 10xxxxxx
|
|
|
|
let b1 = codepoint >> 12 | 0b11100000;
|
|
|
|
let b2 = codepoint >> 6 & 0b00111111 | 0b10000000;
|
|
|
|
let b3 = codepoint & 0b00111111 | 0b10000000;
|
|
|
|
encoded.push(b1);
|
|
|
|
encoded.push(b2);
|
|
|
|
encoded.push(b3);
|
|
|
|
} else {
|
|
|
|
// 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
|
|
|
|
let b1 = codepoint >> 18 | 0b11110000;
|
|
|
|
let b2 = codepoint >> 12 & 0b00111111 | 0b10000000;
|
|
|
|
let b3 = codepoint >> 6 & 0b00111111 | 0b10000000;
|
|
|
|
let b4 = codepoint & 0b00111111 | 0b10000000;
|
|
|
|
encoded.push(b1);
|
|
|
|
encoded.push(b2);
|
|
|
|
encoded.push(b3);
|
|
|
|
encoded.push(b4);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return encoded;
|
|
|
|
}
|
|
|
|
|
|
|
|
// [int] → string
|
|
|
|
function decodeUTF8(encoded) {
|
|
|
|
let codePoints = [];
|
|
|
|
|
|
|
|
for(let i = 0; i < encoded.length;) {
|
|
|
|
let codePoint = 0;
|
|
|
|
|
|
|
|
let firstByte = encoded[i];
|
|
|
|
i++;
|
|
|
|
|
|
|
|
let toRead = null;
|
|
|
|
// Determine number of continuation bytes to read and
|
|
|
|
// decode the first byte into codePoint
|
|
|
|
// Since we'll do the shifts later, we just mask here
|
|
|
|
if(firstByte >> 7 == 0) {
|
|
|
|
// 0xxxxxxx
|
|
|
|
toRead = 0;
|
|
|
|
codePoint = firstByte;
|
|
|
|
} else if(firstByte >> 5 == 0b110) {
|
|
|
|
// 110xxxxx 10xxxxxx
|
|
|
|
toRead = 1;
|
|
|
|
codePoint = firstByte & 0b00011111;
|
|
|
|
} else if(firstByte >> 4 == 0b1110) {
|
|
|
|
// 1110xxxx 10xxxxxx 10xxxxxx
|
|
|
|
toRead = 2;
|
|
|
|
codePoint = firstByte & 0b00001111;
|
|
|
|
} else if(firstByte >> 3 == 0b11110) {
|
|
|
|
// 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
|
|
|
|
toRead = 3;
|
|
|
|
codePoint = firstByte & 0b00000111;
|
|
|
|
} else {
|
|
|
|
// Illegal sequence, push replacement char
|
|
|
|
codePoints.push(0xFFFD);
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
for(; toRead > 0 && i < encoded.length; toRead--) {
|
|
|
|
let continuationByte = encoded[i];
|
|
|
|
i++;
|
|
|
|
|
|
|
|
// Check that we have a valid continuation byte
|
|
|
|
if(continuationByte >> 6 == 0b10) {
|
|
|
|
// We do, add its contents to codePoint
|
|
|
|
codePoint = codePoint << 6 |
|
|
|
|
continuationByte & 0b00111111;
|
|
|
|
} else {
|
|
|
|
// We don't, break out of the loop
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Did we read all required continuation bytes?
|
|
|
|
if(toRead == 0) {
|
|
|
|
// We did, add the codepoint to the array
|
|
|
|
codePoints.push(codePoint);
|
|
|
|
} else {
|
|
|
|
// We didn't, push replacement char
|
|
|
|
codePoints.push(0xFFFD);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Convert to a string
|
|
|
|
let decoded = codePoints.map(x => String.fromCodePoint(x)).join('');
|
|
|
|
|
|
|
|
return decoded;
|
|
|
|
}
|
|
|
|
|
2018-05-23 21:11:25 +00:00
|
|
|
// ------------------------------------------------------------------
|
|
|
|
// User-facing functions
|
|
|
|
// ------------------------------------------------------------------
|
|
|
|
|
2018-05-26 10:14:05 +00:00
|
|
|
// (string, bool) → [flatCommandObjects]
|
|
|
|
function compile(program, enableExtensions = true) {
|
|
|
|
return optimize(parse(program, enableExtensions));
|
2018-05-23 21:11:25 +00:00
|
|
|
}
|
|
|
|
|
2018-05-25 11:19:09 +00:00
|
|
|
// (string, string, int) → string
|
2018-05-26 10:19:16 +00:00
|
|
|
function ircbotRun(program, input, maxCycles = 400000) {
|
2018-05-23 21:11:25 +00:00
|
|
|
// TODO; Cache programs
|
|
|
|
let compiled = compile(program);
|
2018-05-25 11:19:09 +00:00
|
|
|
let vm = newVM(compiled, encodeUTF8(input));
|
2018-05-24 20:36:24 +00:00
|
|
|
|
|
|
|
let result = runVM(vm, maxCycles);
|
2018-05-25 11:19:09 +00:00
|
|
|
let output = decodeUTF8(result.state.output);
|
2018-05-24 20:36:24 +00:00
|
|
|
|
2018-05-26 10:14:05 +00:00
|
|
|
// 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
|
|
|
|
// Since commands in the virtual machine can act at offsets,
|
|
|
|
// what we want is the last offset accessed, unless that is
|
|
|
|
// null
|
|
|
|
let tapeHead = 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
|
2018-05-26 18:58:18 +00:00
|
|
|
let min = Array.from(result.state.memory.keys()).reduce(
|
|
|
|
(x, y) => Math.min(x, y));
|
|
|
|
let max = Array.from(result.state.memory.keys()).reduce(
|
|
|
|
(x, y) => Math.max(x, y));
|
2018-05-26 10:14:05 +00:00
|
|
|
|
|
|
|
// 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 ? ' …' : ''})`
|
|
|
|
}
|
|
|
|
|
|
|
|
// Did we run into maxCycles?
|
|
|
|
if(maxCycles != null && result.cycles >= maxCycles) {
|
2018-05-24 20:36:24 +00:00
|
|
|
output += '«TLE»';
|
|
|
|
}
|
|
|
|
|
2018-05-26 10:14:05 +00:00
|
|
|
// If there was a problem with parsing an int, throw an Error
|
|
|
|
if(result.intParseFailed) {
|
|
|
|
let context = decodeUTF8(result.state.input).slice(0, 3);
|
|
|
|
throw new IntParseError(`';': couldn't read number (near '${context})'`);
|
|
|
|
}
|
|
|
|
|
2018-05-24 20:36:24 +00:00
|
|
|
return output;
|
2018-05-23 21:11:25 +00:00
|
|
|
}
|