Add :;#
This commit is contained in:
parent
e75a0a085d
commit
96a5cd7767
|
@ -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()`
|
||||
|
|
10
brainfuck.md
10
brainfuck.md
|
@ -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
221
gir.js
|
@ -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;
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue