UNPKG

unfuck

Version:

A brainfuck to Javascript compiler with optimizations.

458 lines (398 loc) 20.1 kB
let prepare = require('../src/steps/prepare'); let analyse = require('../src/steps/analyse'); let optimise = require('../src/steps/optimise'); let convert = require('../src/steps/convert'); let wrap = require('../src/steps/wrap'); let uf = require('../dist/unfuck.js'); let assert = require('assert'); describe('Source Prepping', function() { describe('Character Stripping', function() { it('should return a string containing only valid brainfuck instructions', function() { assert.equal(prepare({}, '+H+e-l-l,o,w.o.r[ld]!'), '++--,,..[]'); }); }); describe('Skipped-Loop Comment Pattern', function() { it('should return a string without an inital loop', function() { assert.equal(prepare({}, '[test.example]++--'), '++--'); }); }); describe('Output Source Wrapping', function() { describe('Simple ES6', function() { describe('Number -> Number', function() { it('should return a JS header & footer that take a Number and return a Number, with CONTENT HERE as the main function body', function() { var settings = {language: "standard",target: "simple-es6",type: Uint8Array,width: 10240,in: Number,out: Number}; assert.equal(wrap(settings, ['CONTENT', 'HERE']), '(function(i){var i=i||[];var o=[];var t=new Uint8Array(10240);var p=0;CONTENT;HERE;return o;})'); }); }); describe('String -> Number', function() { it('should return a JS header & footer that take a Number and return a String, with CONTENT HERE as the main function body', function() { var settings = {language: "standard",target: "simple-es6",type: Uint8Array,width: 10240,in: String,out: Number}; assert.equal(wrap(settings, ['CONTENT', 'HERE']), '(function(i){var i=i.split(\'\').map(x=>x.charCodeAt())||[];var o=[];var t=new Uint8Array(10240);var p=0;CONTENT;HERE;return o;})'); }); }); describe('Number -> String', function() { it('should return a JS header & footer that take a String and return a Number, with CONTENT HERE as the main function body', function() { var settings = {language: "standard",target: "simple-es6",type: Uint8Array,width: 10240,in: Number,out: String}; assert.equal(wrap(settings, ['CONTENT', 'HERE']), '(function(i){var i=i||[];var o=[];var t=new Uint8Array(10240);var p=0;CONTENT;HERE;return o.map(x=>String.fromCharCode(x)).join(\'\');})'); }); }); describe('String -> String', function() { it('should return a JS header & footer that take a String and return a String, with CONTENT HERE as the main function body', function() { var settings = {language: "standard",target: "simple-es6",type: Uint8Array,width: 10240,in: String,out: String}; assert.equal(wrap(settings, ['CONTENT', 'HERE']), '(function(i){var i=i.split(\'\').map(x=>x.charCodeAt())||[];var o=[];var t=new Uint8Array(10240);var p=0;CONTENT;HERE;return o.map(x=>String.fromCharCode(x)).join(\'\');})'); }); }); }); describe('Interactive ES6', function() { describe('Number -> Number', function() { it('should return a JS header & footer that take a Number and return a Number, with CONTENT HERE as the main function body', function() { var settings = {language: "standard",target: "interactive-es6",type: Uint8Array,width: 10240,in: Number,out: Number}; assert.equal(wrap(settings, ['CONTENT', 'HERE']), '(function(iFn,oFn){var i=iFn;var o=oFn;var t=new Uint8Array(10240);var p=0;CONTENT;HERE;})'); }); }); describe('String -> Number', function() { it('should return a JS header & footer that take a Number and return a String, with CONTENT HERE as the main function body', function() { var settings = {language: "standard",target: "interactive-es6",type: Uint8Array,width: 10240,in: String,out: Number}; assert.equal(wrap(settings, ['CONTENT', 'HERE']), '(function(iFn,oFn){var i=(c)=>{let x=iFn(c); return (x ? x.charCodeAt(0) : 0)};var o=oFn;var t=new Uint8Array(10240);var p=0;CONTENT;HERE;})'); }); }); describe('Number -> String', function() { it('should return a JS header & footer that take a String and return a Number, with CONTENT HERE as the main function body', function() { var settings = {language: "standard",target: "interactive-es6",type: Uint8Array,width: 10240,in: Number,out: String}; assert.equal(wrap(settings, ['CONTENT', 'HERE']), '(function(iFn,oFn){var i=iFn;var o=(c)=>oFn(String.fromCharCode(c));var t=new Uint8Array(10240);var p=0;CONTENT;HERE;})'); }); }); describe('String -> String', function() { it('should return a JS header & footer that take a String and return a String, with CONTENT HERE as the main function body', function() { var settings = {language: "standard",target: "interactive-es6",type: Uint8Array,width: 10240,in: String,out: String}; assert.equal(wrap(settings, ['CONTENT', 'HERE']), '(function(iFn,oFn){var i=(c)=>{let x=iFn(c); return (x ? x.charCodeAt(0) : 0)};var o=(c)=>oFn(String.fromCharCode(c));var t=new Uint8Array(10240);var p=0;CONTENT;HERE;})'); }); }); }); }); }); describe('Basic Compilation', function() { describe('"+"', function() { it('should return a AST with one positive SFT action', function() { assert.deepEqual(analyse({}, '+'), [ { is: 'SFT', body: 1 } ]); }); }); describe('"-"', function() { it('should return a AST with one negative SFT action', function() { assert.deepEqual(analyse({}, '-'), [ { is: 'SFT', body: -1 } ]); }); }); describe('">"', function() { it('should return a AST with one positive MOV action', function() { assert.deepEqual(analyse({}, '>'), [ { is: 'MOV', body: 1 } ]); }); }); describe('"<"', function() { it('should return a AST with one negative MOV action', function() { assert.deepEqual(analyse({}, '<'), [ { is: 'MOV', body: -1 } ]); }); }); describe('","', function() { it('should return a AST with one INP action', function() { assert.deepEqual(analyse({}, ','), [ { is: 'INP' } ]); }); }); describe('"."', function() { it('should return a AST with one OUT action', function() { assert.deepEqual(analyse({}, '.'), [ { is: 'OUT' } ]); }); }); describe('"[ ... ]"', function() { it('should return a AST with one simple IF block containing a OUT action both inside and outside of the IF block, with an INP at the end', function() { assert.deepEqual(analyse({}, '.[.],'), [ { is: 'OUT' }, { is: 'IF', body: [ { is: 'OUT' } ] }, { is: 'INP' } ]); }); }); }); describe('Optimisation', function() { describe('Collapsing', function() { it('should return a AST with one positive SFT action, adding 2 to the current cell', function() { var ast = [{"is":"SFT","body":1},{"is":"SFT","body":1}]; assert.deepEqual(optimise({}, ast), [ { is: 'SFT', body: 2 } ]); }); }); describe('Nullifying', function() { it('should return a AST with no content', function() { var ast = [{"is":"SFT","body":1},{"is":"SFT","body":1},{"is":"SFT","body":-1},{"is":"SFT","body":1},{"is":"SFT","body":1},{"is":"SFT","body":-1},{"is":"SFT","body":-1},{"is":"SFT","body":-1},{"is":"SFT","body":1},{"is":"SFT","body":-1},{"is":"SFT","body":1},{"is":"SFT","body":1},{"is":"SFT","body":1},{"is":"SFT","body":-1},{"is":"SFT","body":1},{"is":"SFT","body":-1},{"is":"SFT","body":-1},{"is":"SFT","body":-1}]; assert.deepEqual(optimise({}, ast), [ ]); }); }); describe('Cell Clearing', function() { it('should return a AST with SFT+10 and SET=0', function() { var ast = [{"is":"SFT","body":1},{"is":"SFT","body":1},{"is":"SFT","body":1},{"is":"SFT","body":1},{"is":"SFT","body":1},{"is":"SFT","body":1},{"is":"SFT","body":1},{"is":"SFT","body":1},{"is":"SFT","body":1},{"is":"SFT","body":1},{"is":"IF","body":[{"is":"SFT","body":-1}]}]; assert.deepEqual(optimise({}, ast), [ { is: 'SFT', body: 10 }, { is: 'SET', body: 0 } ]); }); }); describe('Fusing', function() { it('should return a AST with FOUR REL* instructions, and ends with a MOV of the sum of steps taken', function() { var ast = [{"is":"SFT","body":1},{"is":"SFT","body":1},{"is":"MOV","body":1},{"is":"MOV","body":1},{"is":"SET","body":0},{"is":"SFT","body":-1},{"is":"MOV","body":-1},{"is":"SFT","body":1},{"is":"OUT"}]; assert.deepEqual(optimise({}, ast), [{"is":"RELSFT","body":{"value":2,"move":0}},{"is":"RELSET","body":{"value":0,"move":2}},{"is":"RELSFT","body":{"value":-1,"move":2}},{"is":"RELSFT","body":{"value":1,"move":1}},{"is":"MOV","body":1},{"is":"OUT"}]); }); }); describe('Multiplication', function() { it('should return a AST with ONE MUL instruction, with two factors (x3 and x4)', function() { var ast = [{"is":"SFT","body":1},{"is":"SFT","body":1},{"is":"IF","body":[{"is":"SFT","body":-1},{"is":"MOV","body":1},{"is":"SFT","body":1},{"is":"SFT","body":1},{"is":"SFT","body":1},{"is":"MOV","body":1},{"is":"SFT","body":1},{"is":"SFT","body":1},{"is":"SFT","body":1},{"is":"SFT","body":1},{"is":"MOV","body":-1},{"is":"MOV","body":-1}]}]; assert.deepEqual(optimise({}, ast), [{"is":"SFT","body":2},{"is":"MUL","body":{"factors":[{"move":1,"factor":3},{"move":2,"factor":4}]}}]); }); }); describe('Alternate Multiplication', function() { it('should return a AST with ONE MUL instruction, with two factors (x3 and x4)', function() { var ast = [{"is":"SFT","body":1},{"is":"SFT","body":1},{"is":"IF","body":[{"is":"MOV","body":1},{"is":"SFT","body":1},{"is":"SFT","body":1},{"is":"SFT","body":1},{"is":"MOV","body":1},{"is":"SFT","body":1},{"is":"SFT","body":1},{"is":"SFT","body":1},{"is":"SFT","body":1},{"is":"MOV","body":-1},{"is":"MOV","body":-1},{"is":"SFT","body":-1}]}]; assert.deepEqual(optimise({}, ast), [{"is":"SFT","body":2},{"is":"MUL","body":{"factors":[{"move":1,"factor":3},{"move":2,"factor":4}]}}]); }); }); }); describe('Settings', function() { describe('Cell Bit Depth', function() { it('should return an array containing 65535, the highest possible unsigned 16bit integer', function() { let bf = '-.'; let c = uf.compiler({ type: Uint16Array, out: Number }); assert.deepEqual(c.run(bf, ''), [ 65535 ]); }); }); describe('Tape Width', function() { it('should return undefined', function() { let bf = '>>+.'; let c = uf.compiler({ width: 2, out: Number }); assert.deepEqual(c.run(bf, ''), [ undefined ]); }); }); describe('Interactive Target with optimization', function() { it('should return 0 and NOT 10', function( done ) { let bf = ',++--[-].'; let c = uf.compiler({ target: 'interactive-es6', in: Number, out: Number }); let inp = () => 10; let out = (cell) => { done(assert.equal(cell, 0)); } c.run(bf, inp, out); }); }); describe('Array tape type', function() { it('should return [256,0], NOT [0,0]', function() { let bf = '+++++++++++++++[>+++++++++++++++++<-]>+.[-].'; let c = uf.compiler({ type: Array, out: Number }); assert.deepEqual(c.run(bf, ''), [256, 0]); }); }); }); describe('AST Conversion', function() { describe('SFT', function() { describe('Positive', function() { it('should return an array containing a JS program that adds 2 to the current cell', function() { var ast = [{"is":"SFT","body":2}]; assert.deepEqual(convert({ target: 'simple-es6' }, ast), ["t[p]+=2"]); }); }); describe('Negative', function() { it('should return an array containing a JS program that subtracts 2 from the current cell', function() { var ast = [{"is":"SFT","body":-2}]; assert.deepEqual(convert({ target: 'simple-es6' }, ast), ["t[p]-=2"]); }); }); }); describe('RELSFT', function() { it('should return an array containing a JS program that adds 1 to the current cell and the next cell', function() { var ast = [{"is":"RELSFT","body":{"value":1,"move":0}},{"is":"RELSFT","body":{"value":1,"move":1}}]; assert.deepEqual(convert({ target: 'simple-es6' }, ast), ["t[p]+=1","t[p+1]+=1"]); }); }); describe('MOV', function() { it('should return an array containing a JS program that moves the current cell pointer 2 cells to the right', function() { var ast = [{"is":"MOV","body":2}]; assert.deepEqual(convert({ target: 'simple-es6' }, ast), ["p+=2"]); }); }); describe('SET', function() { it('should return an array containing a JS program that sets the current cell to 5', function() { var ast = [{"is":"SET","body":5}]; assert.deepEqual(convert({ target: 'simple-es6' }, ast), ["t[p]=5"]); }); }); describe('RELSET', function() { it('should return an array containing a JS program that sets the next cell to 5', function() { var ast = [{"is":"RELSET","body":{"value":5,"move":1}}]; assert.deepEqual(convert({ target: 'simple-es6' }, ast), ["t[p+1]=5"]); }); }); describe('MUL', function() { describe('Copying', function() { it('should return an array containing a JS program that sets the next cell to the same value as the current cell', function() { var ast = [{"is":"SFT","body":1},{"is":"MUL","body":{"factors":[{"move":1,"factor":1}]}}]; assert.deepEqual(convert({ target: 'simple-es6' }, ast), ["t[p]+=1","t[p+1]+=t[p]","t[p]=0"]); }); }); describe('Multiplicative Factor', function() { it('should return an array containing a JS program that sets the next cell to the result of 2*3', function() { var ast = [{"is":"SFT","body":3},{"is":"MUL","body":{"factors":[{"move":1,"factor":2}]}}]; assert.deepEqual(convert({ target: 'simple-es6' }, ast), ["t[p]+=3","t[p+1]+=t[p]*2","t[p]=0"]); }); }); }); describe('INP', function() { describe('Simple ES6', function() { it('should return an array containing a JS program that pushes the next item in the input stack onto the current cell', function() { var ast = [{"is":"INP"}]; assert.deepEqual(convert({ target: 'simple-es6' }, ast), ["t[p]=(i.length<1?0:i.shift())"]); }); }); describe('Interactive ES6', function() { it('should return an array containing a JS program that pushes the result of the input function onto the current cell', function() { var ast = [{"is":"INP"}]; assert.deepEqual(convert({ target: 'interactive-es6' }, ast), ["t[p]=i(t[p])"]); }); }); }); describe('OUT', function() { describe('Simple ES6', function() { it('should return an array containing a JS program that pushes the current cell onto the output stack', function() { var ast = [{"is":"OUT"}]; assert.deepEqual(convert({ target: 'simple-es6' }, ast), ["o.push(t[p])"]); }); }); describe('Interactive ES6', function() { it('should return an array containing a JS program that runs the output function with the current cell\'s data', function() { var ast = [{"is":"OUT"}]; assert.deepEqual(convert({ target: 'interactive-es6' }, ast), ["o(t[p])"]); }); }); }); describe('IF', function() { it('should return an array containing a JS program that begins a while loop that will run once and shift the cell pointer one cell to the right', function() { var ast = [{"is":"SFT","body":1},{"is":"IF","body":[{"is":"MOV","body":1}]}]; assert.deepEqual(convert({ target: 'simple-es6' }, ast), ["t[p]+=1","while(t[p]!=0){p+=1}"]); }); }); }); describe('Simple Programs', function() { describe('Addition', function() { it('should return an array containing the number 15 (the result of 5+10)', function() { let bf = '+++++>++++++++++<[->+<]>.'; let c = uf.compiler({ out: Number }); assert.deepEqual(c.run(bf, ''), [ 15 ]); }); }); describe('Multiplication', function() { it('should return an array containing the number 15 (the result of 3*5)', function() { let bf = '+++[>+++++<-]>.'; let c = uf.compiler({ out: Number }); assert.deepEqual(c.run(bf, ''), [ 15 ]); }); }); describe('Nested IF blocks', function() { it('should return an that says "Hello, World!"', function() { let bf = '>>>>--<-<<+[+[<+>--->->->-<<<]>]<<--.<++++++.<<-..<<.<+.>>.>>.<<<.+++.>>.>>-.<<<+.'; let c = uf.compiler(); assert.equal(c.run(bf, ''), 'Hello, World!'); }); }); describe('I/O', function() { it('should return an array containing the character "a"', function() { let bf = ',.'; let c = uf.compiler(); assert.deepEqual(c.run(bf, 'a'), 'a'); }); }); describe('I/O + Addition', function() { it('should return an array containing the number 15 (from the addition of the input values 5 and 10)', function() { let bf = ',>,<[>+<-]>.'; let c = uf.compiler({ in: Number, out: Number }); assert.deepEqual(c.run(bf, [5, 10]), [ 15 ]); }); }); describe('Async I/O', function() { it('should return 5, the number inputted by the input lambda', function( done ) { let bf = ',.'; let c = uf.compiler({ target: 'interactive-es6', in: Number, out: Number }); let inp = () => { return 5 } let out = (cell) => { done( assert.equal(cell, 5) ) } c.run(bf, inp, out); }); }); }); describe('Complex Programs', function() { describe('Bubble Sort', function() { it('should return the sorted string of "cba" ("abc")', function() { let bf = ` [bsort.b -- bubble sort (c) 2016 Daniel B. Cristofani http://brainfuck.org/] >>,[>>,]<<[ [<<]>>>>[ <<[>+<<+>-] >>[>+<<<<[->]>[<]>>-] <<<[[-]>>[>+<-]>>[<<<+>>>-]] >>[[<+>-]>>]< ]<<[>>+<<-]<< ]>>>>[.>>] `; let c = uf.compiler(); assert.deepEqual(c.run(bf, 'cba'), 'abc'); }); }); describe('Square Numbers', function() { it('should return the character 3 on the 245th output call', function( done ) { let bf = ` ++++[>+++++<-]>[<+++++>-]+<+[ >[>+>+<<-]++>>[<<+>>-]>>>[-]++>[-]+ >>>+[[-]++++++>>>]<<<[[<++++++++<++>>-]+<.<[>----<-]<] <<[>>>>>[>>>[-]+++++++++<[>-<-]+++++++++>[-[<->-]+[<<<]]<[>+<-]>]<<-]<<- ] [Outputs square numbers from 0 to 10000. Daniel B Cristofani (cristofdathevanetdotcom) http://www.hevanet.com/cristofd/brainfuck/] `; let c = uf.compiler({ target: 'interactive-es6' }); let i = 0; let inp = (cell) => { return cell }; let out = (cell) => { i++; if (i === 245) { done(assert.equal(cell, '3')); } }; c.run(bf, inp, out); }); }); describe('ASCII Tree', function() { it('should return an ASCII evergreen tree', function() { let bf = ` [xmastree.b -- print Christmas tree (c) 2016 Daniel B. Cristofani http://brainfuck.org/] >>>--------<,[<[>++++++++++<-]>>[<------>>-<+],]++>>++<--[<++[+>]>+<<+++<]< <[>>+[[>>+<<-]<<]>>>>[[<<+>.>-]>>]<.<<<+<<-]>>[<.>--]>.>>. `; let c = uf.compiler(); let tree = ` * *** ***** ******* ********* * ` assert.deepEqual(c.run(bf, '5'), tree); }); }); describe('Mandlebrot Set', function() { this.timeout(8000); it('should return an ASCII Mandlebrot set', function( done ) { let bf = require('./mandlebrot.b.js').bf; let c = uf.compiler({target: 'interactive-es6', in: Number, width: 30000}); let goal = require('./mandlebrot.out.js').out; let out = ''; let i = 0; let inpFn = (x) => 0; let outFn = (x) => { i++; if (i < 6239) out += x; else done(assert.equal(out + x, goal)) }; c.run(bf, inpFn, outFn) }); }); });