From 96a5cd7767e3004da042dee8588b5c167ea0127b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juhani=20Krekel=C3=A4?= Date: Sat, 26 May 2018 13:14:05 +0300 Subject: [PATCH] Add :;# --- README.md | 1 - brainfuck.md | 10 ++- gir.js | 221 ++++++++++++++++++++++++++++++++++++++++++++++++--- 3 files changed, 220 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index e737860..24bf6b8 100644 --- a/README.md +++ b/README.md @@ -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()` diff --git a/brainfuck.md b/brainfuck.md index e0fec39..ee09196 100644 --- a/brainfuck.md +++ b/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 diff --git a/gir.js b/gir.js index bff9fcf..4cdfd9c 100644 --- a/gir.js +++ b/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; }