This commit is contained in:
Juhani Krekelä 2018-05-26 13:14:05 +03:00
parent e75a0a085d
commit 96a5cd7767
3 changed files with 220 additions and 12 deletions

View File

@ -21,7 +21,6 @@ Gir supports following optimizations:
TODO
----
### gir.js
* Implement `:;#`
* Make VM and transformMultiplyLoops use a Proxied object that gives out 0
for nonexistent elements for tape and allows using [] interface
* Keep a cache of compiled programs in `run()`

View File

@ -1,6 +1,8 @@
Commands
--------
Gir brainfuck has only the base 8 commands `+-<>[].,`
Gir brainfuck has in addition to the base 8 commands `+-<>[].,` also `:;`
for printing and reading integers and `#` for triggering a breakpoint. These
can be turned off by passing a second `false` parameter to `compile()`
Tape
----
@ -10,3 +12,9 @@ cells that wrap around
IO
--
`.` and `,` operate on a utf-8 stream. `,` produces `0` on EOF
`:` produces a decimal representation of the current cell. `;` skips any
space (U+20) characters in the input stream and then reads 1 or more ASCII
digit (U+30 to U+39), clamps the number to the range [0, 255] and sets the
cell to it. If it can't read a digit and EOF has been reached it returns 0,
but if EOF hasn't been reached it raises an error flag and stops execution

221
gir.js
View File

@ -34,19 +34,29 @@ const jumpIfZero = Symbol('jumpIfZero');
// {type: jumpIfNonZero, target: 2}
const jumpIfNonZero = Symbol('jumpIfNonZero');
// TODO: Add extensions from Eldis
// : → {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');
class ParsingError extends Error {}
class UnknownIRError extends Error {}
class IntParseError extends Error {}
// ------------------------------------------------------------------
// Parsing
// ------------------------------------------------------------------
// (string) → [commandObjects]
// (string, bool) → [commandObjects]
// enableExtensions contols whether commands :;# are recognized
// May throw ParsingError
function parse(program) {
function parse(program, enableExtensions = true) {
// (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
@ -147,6 +157,19 @@ function parse(program) {
// function, we don't want to consume it a
// second time
i = lastIndex + 1;
} 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++;
} else {
// All others characters are comments,
// ignore them
@ -221,6 +244,21 @@ function prettifyIR(parsed) {
} else if(command.type == jumpIfNonZero) {
line += `jumpIfNonZero ${command.target}`;
lines.push(line);
} 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);
} else {
line += `unknown ${command.type.toString()}`;
lines.push(line);
@ -358,6 +396,15 @@ function addOffsetProperties(parsed) {
// care about its value when figuring out
// our isBalanced, which will be forced to
// false if any inner loop is not balanced
} 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});
} else {
throw new UnknownIRError(
`Unknown command ${command.type.toString()}`);
@ -530,9 +577,14 @@ function newVM(program, input) {
};
}
// (girVMState, int) → {state: girVMState, complete: bool, cycles: int}
// (girVMState, int) → {state: girVMState, complete: bool, cycles: int,
// intParseFailed: bool, breakPointReached: bool,
// lastIndex: int/null}
// complete is set to true is the program completed its execution
// cycles is the number of cycles the VM ran
// 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
// If maxCycles is null, the program runs until completion
function runVM(state, maxCycles = null) {
let program = state.program;
@ -548,7 +600,14 @@ function runVM(state, maxCycles = null) {
let input = state.input.slice();
let output = state.output.slice();
// Flags we want to return
let complete = false;
let intParseFailed = false;
let breakPointReached = false;
// Debug features
let lastIndex = null;
let cycle = 0;
for(; maxCycles === null || cycle < maxCycles; cycle++) {
// Exit the loop if we run to the end of the program
@ -569,6 +628,8 @@ function runVM(state, maxCycles = null) {
case writeByte:
case readByte:
case clear:
case writeInt:
case readInt:
// These have an offset property, add it
index += command.offset;
// Fall through
@ -579,6 +640,7 @@ function runVM(state, maxCycles = null) {
if(!memory.has(index)) {
memory.set(index, 0);
}
lastIndex = index;
}
// Run the command
@ -655,11 +717,67 @@ function runVM(state, maxCycles = null) {
}
break;
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;
default:
// Unknown command type
throw new UnknownIRError(
`Unknown command ${command.type.toString()}`);
}
// Since can't use 'break' from a switch(), do it here
if(intParseFailed || breakPointReached) {
break;
}
}
let newState = {
@ -673,7 +791,10 @@ function runVM(state, maxCycles = null) {
output
};
return {state: newState, complete: complete, cycles: cycle};
return {state: newState, complete: complete, cycles: cycle,
intParseFailed: intParseFailed,
breakPointReached: breakPointReached,
lastIndex: lastIndex};
}
// ------------------------------------------------------------------
@ -791,11 +912,12 @@ function decodeUTF8(encoded) {
// User-facing functions
// ------------------------------------------------------------------
// (string) → [flatCommandObjects]
function compile(program) {
return optimize(parse(program));
// (string, bool) → [flatCommandObjects]
function compile(program, enableExtensions = true) {
return optimize(parse(program, enableExtensions));
}
// TODO: Rename to fit purpose
// (string, string, int) → string
function run(program, input, maxCycles = null) {
// TODO; Cache programs
@ -805,10 +927,89 @@ function run(program, input, maxCycles = null) {
let result = runVM(vm, maxCycles);
let output = decodeUTF8(result.state.output);
// If didn't complete, mark it in the output
if(!result.complete) {
// 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
let min = Infinity;
let max = -Infinity;
for(let index of result.state.memory.keys()) {
if(index < min) {
min = index;
}
if(index > max) {
max = index;
}
}
// 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) {
output += '«TLE»';
}
// 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})'`);
}
return output;
}