Add :;#
This commit is contained in:
parent
e75a0a085d
commit
96a5cd7767
|
@ -21,7 +21,6 @@ Gir supports following optimizations:
|
||||||
TODO
|
TODO
|
||||||
----
|
----
|
||||||
### gir.js
|
### gir.js
|
||||||
* Implement `:;#`
|
|
||||||
* Make VM and transformMultiplyLoops use a Proxied object that gives out 0
|
* Make VM and transformMultiplyLoops use a Proxied object that gives out 0
|
||||||
for nonexistent elements for tape and allows using [] interface
|
for nonexistent elements for tape and allows using [] interface
|
||||||
* Keep a cache of compiled programs in `run()`
|
* Keep a cache of compiled programs in `run()`
|
||||||
|
|
10
brainfuck.md
10
brainfuck.md
|
@ -1,6 +1,8 @@
|
||||||
Commands
|
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
|
Tape
|
||||||
----
|
----
|
||||||
|
@ -10,3 +12,9 @@ cells that wrap around
|
||||||
IO
|
IO
|
||||||
--
|
--
|
||||||
`.` and `,` operate on a utf-8 stream. `,` produces `0` on EOF
|
`.` 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
221
gir.js
|
@ -34,19 +34,29 @@ const jumpIfZero = Symbol('jumpIfZero');
|
||||||
// {type: jumpIfNonZero, target: 2}
|
// {type: jumpIfNonZero, target: 2}
|
||||||
const jumpIfNonZero = Symbol('jumpIfNonZero');
|
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 ParsingError extends Error {}
|
||||||
|
|
||||||
class UnknownIRError extends Error {}
|
class UnknownIRError extends Error {}
|
||||||
|
|
||||||
|
class IntParseError extends Error {}
|
||||||
|
|
||||||
// ------------------------------------------------------------------
|
// ------------------------------------------------------------------
|
||||||
// Parsing
|
// Parsing
|
||||||
// ------------------------------------------------------------------
|
// ------------------------------------------------------------------
|
||||||
|
|
||||||
// (string) → [commandObjects]
|
// (string, bool) → [commandObjects]
|
||||||
|
// enableExtensions contols whether commands :;# are recognized
|
||||||
// May throw ParsingError
|
// May throw ParsingError
|
||||||
function parse(program) {
|
function parse(program, enableExtensions = true) {
|
||||||
// (string, int, bool) → {parsed: [commandObjects], lastIndex: int}
|
// (string, int, bool) → {parsed: [commandObjects], lastIndex: int}
|
||||||
// index is the index of the next character to consume
|
// index is the index of the next character to consume
|
||||||
// inLoop tells whether we're parsing a loop or the top level
|
// 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
|
// function, we don't want to consume it a
|
||||||
// second time
|
// second time
|
||||||
i = lastIndex + 1;
|
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 {
|
} else {
|
||||||
// All others characters are comments,
|
// All others characters are comments,
|
||||||
// ignore them
|
// ignore them
|
||||||
|
@ -221,6 +244,21 @@ function prettifyIR(parsed) {
|
||||||
} else if(command.type == jumpIfNonZero) {
|
} else if(command.type == jumpIfNonZero) {
|
||||||
line += `jumpIfNonZero ${command.target}`;
|
line += `jumpIfNonZero ${command.target}`;
|
||||||
lines.push(line);
|
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 {
|
} else {
|
||||||
line += `unknown ${command.type.toString()}`;
|
line += `unknown ${command.type.toString()}`;
|
||||||
lines.push(line);
|
lines.push(line);
|
||||||
|
@ -358,6 +396,15 @@ function addOffsetProperties(parsed) {
|
||||||
// care about its value when figuring out
|
// care about its value when figuring out
|
||||||
// our isBalanced, which will be forced to
|
// our isBalanced, which will be forced to
|
||||||
// false if any inner loop is not balanced
|
// 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 {
|
} else {
|
||||||
throw new UnknownIRError(
|
throw new UnknownIRError(
|
||||||
`Unknown command ${command.type.toString()}`);
|
`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
|
// complete is set to true is the program completed its execution
|
||||||
// cycles is the number of cycles the VM ran
|
// 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
|
// If maxCycles is null, the program runs until completion
|
||||||
function runVM(state, maxCycles = null) {
|
function runVM(state, maxCycles = null) {
|
||||||
let program = state.program;
|
let program = state.program;
|
||||||
|
@ -548,7 +600,14 @@ function runVM(state, maxCycles = null) {
|
||||||
let input = state.input.slice();
|
let input = state.input.slice();
|
||||||
let output = state.output.slice();
|
let output = state.output.slice();
|
||||||
|
|
||||||
|
// Flags we want to return
|
||||||
let complete = false;
|
let complete = false;
|
||||||
|
let intParseFailed = false;
|
||||||
|
let breakPointReached = false;
|
||||||
|
|
||||||
|
// Debug features
|
||||||
|
let lastIndex = null;
|
||||||
|
|
||||||
let cycle = 0;
|
let cycle = 0;
|
||||||
for(; maxCycles === null || cycle < maxCycles; cycle++) {
|
for(; maxCycles === null || cycle < maxCycles; cycle++) {
|
||||||
// Exit the loop if we run to the end of the program
|
// Exit the loop if we run to the end of the program
|
||||||
|
@ -569,6 +628,8 @@ function runVM(state, maxCycles = null) {
|
||||||
case writeByte:
|
case writeByte:
|
||||||
case readByte:
|
case readByte:
|
||||||
case clear:
|
case clear:
|
||||||
|
case writeInt:
|
||||||
|
case readInt:
|
||||||
// These have an offset property, add it
|
// These have an offset property, add it
|
||||||
index += command.offset;
|
index += command.offset;
|
||||||
// Fall through
|
// Fall through
|
||||||
|
@ -579,6 +640,7 @@ function runVM(state, maxCycles = null) {
|
||||||
if(!memory.has(index)) {
|
if(!memory.has(index)) {
|
||||||
memory.set(index, 0);
|
memory.set(index, 0);
|
||||||
}
|
}
|
||||||
|
lastIndex = index;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Run the command
|
// Run the command
|
||||||
|
@ -655,11 +717,67 @@ function runVM(state, maxCycles = null) {
|
||||||
}
|
}
|
||||||
break;
|
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:
|
default:
|
||||||
// Unknown command type
|
// Unknown command type
|
||||||
throw new UnknownIRError(
|
throw new UnknownIRError(
|
||||||
`Unknown command ${command.type.toString()}`);
|
`Unknown command ${command.type.toString()}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Since can't use 'break' from a switch(), do it here
|
||||||
|
if(intParseFailed || breakPointReached) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let newState = {
|
let newState = {
|
||||||
|
@ -673,7 +791,10 @@ function runVM(state, maxCycles = null) {
|
||||||
output
|
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
|
// User-facing functions
|
||||||
// ------------------------------------------------------------------
|
// ------------------------------------------------------------------
|
||||||
|
|
||||||
// (string) → [flatCommandObjects]
|
// (string, bool) → [flatCommandObjects]
|
||||||
function compile(program) {
|
function compile(program, enableExtensions = true) {
|
||||||
return optimize(parse(program));
|
return optimize(parse(program, enableExtensions));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Rename to fit purpose
|
||||||
// (string, string, int) → string
|
// (string, string, int) → string
|
||||||
function run(program, input, maxCycles = null) {
|
function run(program, input, maxCycles = null) {
|
||||||
// TODO; Cache programs
|
// TODO; Cache programs
|
||||||
|
@ -805,10 +927,89 @@ function run(program, input, maxCycles = null) {
|
||||||
let result = runVM(vm, maxCycles);
|
let result = runVM(vm, maxCycles);
|
||||||
let output = decodeUTF8(result.state.output);
|
let output = decodeUTF8(result.state.output);
|
||||||
|
|
||||||
// If didn't complete, mark it in the output
|
// If there was either no output or a breakpoint triggered, dump
|
||||||
if(!result.complete) {
|
// 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»';
|
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;
|
return output;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue