From bd9220326bb7882e73b54f81b8edd615f542e9c5 Mon Sep 17 00:00:00 2001 From: Bernard Teo Date: Fri, 7 Apr 2017 16:39:02 +0800 Subject: [PATCH] added interactive mode Interactive mode utilizes SharedArrayBuffers for I/O passing between main thread and worker thread. Only works in Firefox right now. Chrome's implementation of Atomic wait/wake seems to be bugged, --- bf.css | 204 +++++++++++++++++++++++++++++++++++-- bf.html | 53 +++++++++- bf.js | 98 +++++++++++++++--- interactive-console.js | 102 +++++++++++++++++++ jelly-bf-processhandler.js | 118 +++++++++++++++++++++ jelly-bf-sync.js | 8 +- jelly-bf-worker.js | 6 +- jelly-bf-worker.max.js | 18 +++- 8 files changed, 581 insertions(+), 26 deletions(-) create mode 100644 interactive-console.js diff --git a/bf.css b/bf.css index 031d222..59ea1de 100644 --- a/bf.css +++ b/bf.css @@ -3,6 +3,10 @@ blue (50%): rgb(0,102,255) blue (75%): rgb(128,179,255) blue (90%): rgb(204,224,255) blue (95%): rgb(230,240,255) + +red (30%): rgb(153,0,0) + +grey (25%): rgb(64,64,64) */ html, body{ @@ -75,7 +79,20 @@ body>.wrapper>*{ font-family:"Chewy","Signika",sans-serif; font-size:32px; color:rgb(153,0,0); - text-shadow:0 0 10px white; + position:relative; +} + +#header .title::before{ + position:absolute; + display:block; + content:"Jelly"; + padding:0px 20px; + left:0; + top:0; + font-family:"Chewy","Signika",sans-serif; + font-size:32px; + color:white; + filter:blur(4px); } #header .headerbutton{ @@ -166,9 +183,10 @@ body>.wrapper>*{ } #compilationinfo{ - flex:0 0 26px; - background-color:rgb(204,224,255); - line-height:26px; + flex:0 0 20px; + background-color:rgb(230,240,255); + line-height:20px; + font-size:14px; } #compilationinfo .compilationinfospan{ @@ -183,9 +201,135 @@ body>.wrapper>*{ color:green; } -#iooptions{ - flex:0 0 0px; +#options{ + flex:0 0 48px; background-color:rgb(204,224,255); + font-size:14px; +} + +#options>.wrapper{ + height:100%; + width:100%; + display:flex; + flex-direction:row; + flex-wrap:wrap; + justify-content:center; + align-items:stretch; +} + +#options>.wrapper>*{ + display:block; + overflow:hidden; + height:48px; + box-sizing:border-box; + flex:0 0 auto; +} + +#options>.wrapper>.executionoptions, #options>.wrapper>.compilationoptions{ + display:flex; + flex-direction:row; + flex-wrap:wrap; + justify-content:center; + align-items:stretch; +} + + + +#options>.wrapper>.vertical-bar{ + flex:0 0 18px; + width:18px; + overflow:visible; +} + +#options>.wrapper>.vertical-bar::before{ + display:block; + content:""; + margin:0 auto; + width:2px; + height:100%; + background-color:white; + filter:blur(2px); +} + +#options .optiongroup{ + flex:0 0 auto; + padding:4px 12px; + border:none; + outline:none; + margin:0; +} + +#options .optiongroup .radiooption{ + height:20px; +} + +#options .optiongroup .radiooption input[type="radio"]{ + position:absolute; + visibility:hidden; +} + +#options .optiongroup .radiooption label{ + line-height:20px; + width:100%; + display:flex; + flex-direction:row; + flex-wrap:nowrap; + justify-content:flex-start; + align-items:center; + cursor:pointer; +} + +#options .optiongroup .radiooption label>span{ + display:block; + flex:0 0 auto; +} + +#options .optiongroup .radiooption label>span.fakeradiobutton{ + width:16px; + height:16px; + box-sizing:border-box; + border:2px solid rgb(64,64,64); + border-radius:100%; + transition:border-color 0.5s; + display:flex; + flex-direction:row; + flex-wrap:nowrap; + justify-content:center; + align-items:center; +} + +#options .optiongroup .radiooption label>span.fakeradiobutton::before{ + content:""; + display:block; + background-color:blue; + border-radius:100%; + flex:0 0 auto; + width:0; + height:0; + transition:width 0.5s,height 0.5s; +} + +#options .optiongroup .radiooption label>span.radiotext{ + padding-left:4px; + color:rgb(64,64,64); + transition:color 0.5s; +} + +#options .optiongroup .radiooption input[type="radio"]:checked ~ label>span.radiotext{ + color:blue; +} + +#options .optiongroup .radiooption input[type="radio"]:checked ~ label>span.fakeradiobutton{ + border-color:blue; +} + +#options .optiongroup .radiooption input[type="radio"]:checked ~ label>span.fakeradiobutton::before{ + width:8px; + height:8px; +} + +#options>.wrapper>.extraspace{ + flex:1 1 auto; } #ioblock{ @@ -203,6 +347,10 @@ body>.wrapper>*{ position:relative; } +#ioblock .separate:not(.selected){ + display:none; +} + #ioblock .separate>*{ display:block; overflow:hidden; @@ -231,6 +379,7 @@ body>.wrapper>*{ padding:0 6px; background-color:rgb(230,240,255); font-size:14px; + color:rgb(64,64,64); } #ioblock .separate .ioseparateindividualblock>.iocontent{ @@ -260,6 +409,49 @@ body>.wrapper>*{ width:100%; } +#ioblock .combined{ + height:100%; + width:100%; + display:flex; + flex-direction:column; + flex-wrap:nowrap; + justify-content:center; + align-items:stretch; +} + +#ioblock .combined:not(.selected){ + display:none; +} + +#ioblock .combined>.iotitle{ + flex:0 0 20px; + height:20px; + line-height:20px; + padding:0 6px; + background-color:rgb(230,240,255); + font-size:14px; + color:rgb(64,64,64); +} + +#ioblock .combined>.iocontent{ + flex:1 1 0px; + height:100%; + width:100%; +} + +#ioblock .combined>.iocontent .terminal{ + height:100%; + width:100%; + -webkit-touch-callout: initial; + -webkit-user-select: initial; + -khtml-user-select: initial; + -moz-user-select: initial; + -ms-user-select: initial; + user-select: initial; + font:16px/normal 'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', 'source-code-pro', monospace; + padding:0 4px; +} + body>.wrapper>.horizontal-spacer{ flex:0 0 0px; height:0px; diff --git a/bf.html b/bf.html index 725a3bf..2407a3e 100644 --- a/bf.html +++ b/bf.html @@ -8,6 +8,7 @@ + @@ -33,12 +34,54 @@
-
+
+
+
+
+
+
+
+
+ + +
+
+ + +
+
+
+
+
+
+
+ + +
+
+ + +
+
+ +
+
+
+
-
+
Input
@@ -55,6 +98,12 @@
+
+
Console
+
+
+
+
diff --git a/bf.js b/bf.js index 689831a..854e2cb 100644 --- a/bf.js +++ b/bf.js @@ -26,6 +26,13 @@ window.addEventListener("load",function(){ outputEditor.setReadOnly(true); outputEditor.getSession().setUseWorker(false); + var interactiveConsole=new InteractiveConsole(document.getElementById("ioblock").getElementsByClassName("combined")[0].getElementsByClassName("terminal")[0]); + /*interactiveConsole.write("Test 1"); + interactiveConsole.write("Test 2\n"); + interactiveConsole.read(function(e){ + alert(e); + });*/ + // buttons var openbutton=document.getElementById("openbutton"); var downloadbutton=document.getElementById("downloadbutton"); @@ -148,6 +155,12 @@ window.addEventListener("load",function(){ compilebutton.click(); } } + else if(runTerminator){ // this is here because i don't know any way to kill execution without terminating the worker + runTerminator(); + runTerminator=undefined; + toRunAfterCompiling=true; + compilebutton.click(); + } else{ if(runTerminator){ runTerminator(); @@ -158,23 +171,82 @@ window.addEventListener("load",function(){ }; executionSpan.firstChild.nodeValue="Executing…"; var start_time=Date.now(); - processHandler.execute(inputEditor.getValue(),{},function(message){ - if(!to_terminate){ - runTerminator=undefined; - if(message.success){ - var end_time=Date.now(); - outputEditor.setValue(message.output,1); - console.log("Executed in "+Math.round(end_time-start_time)+" ms."); - executionSpan.firstChild.nodeValue="Executed in "+Math.round(end_time-start_time)+" ms."; + if(radio_interactive_no.checked){ + processHandler.execute(inputEditor.getValue(),{},function(message){ + if(!to_terminate){ + runTerminator=undefined; + if(message.success){ + var end_time=Date.now(); + outputEditor.setValue(message.output,1); + console.log("Executed in "+Math.round(end_time-start_time)+" ms."); + executionSpan.firstChild.nodeValue="Executed in "+Math.round(end_time-start_time)+" ms."; + } + else{ + executionSpan.firstChild.nodeValue="Execution failed."; + } } - else{ - executionSpan.firstChild.nodeValue="Execution failed."; + }); + } + else{ + interactiveConsole.clear(); + var interactiveObj=processHandler.executeInteractive({},function(){ + if(!to_terminate){ + interactiveConsole.read(function(text){ + interactiveObj.inputAddedCallback(text); + }); } - } - }); + },function(outputText){ + if(!to_terminate){ + interactiveConsole.write(outputText); + } + },function(message){ + if(!to_terminate){ + runTerminator=undefined; + if(message.success){ + var end_time=Date.now(); + outputEditor.setValue(message.output,1); + console.log("Executed in "+Math.round(end_time-start_time)+" ms."); + executionSpan.firstChild.nodeValue="Executed in "+Math.round(end_time-start_time)+" ms."; + } + else{ + executionSpan.firstChild.nodeValue="Execution failed."; + } + } + }); + } } }); + // options + var radio_interactive_yes=document.getElementById("radio-interactive-yes"); + var radio_interactive_no=document.getElementById("radio-interactive-no"); + + var ioblock=document.getElementById("ioblock"); + var separate_ioblock=ioblock.getElementsByClassName("separate")[0]; + var combined_ioblock=ioblock.getElementsByClassName("combined")[0]; + + radio_interactive_yes.addEventListener("change",function(){ + separate_ioblock.classList.remove("selected"); + combined_ioblock.classList.add("selected"); + localStorage.setItem("option-interactive","yes"); + }); + radio_interactive_no.addEventListener("change",function(){ + combined_ioblock.classList.remove("selected"); + separate_ioblock.classList.add("selected"); + localStorage.setItem("option-interactive","no"); + }); + + var interactive=localStorage.getItem("option-interactive"); + if(interactive==="no"||!window.SharedArrayBuffer){ + radio_interactive_no.checked=true; + radio_interactive_no.dispatchEvent(new Event("change")); + } + else{ + radio_interactive_yes.checked=true; + radio_interactive_yes.dispatchEvent(new Event("change")); + } + + // splitters Array.prototype.forEach.call(document.getElementById("ioblock").getElementsByClassName("vertical-spacer"),function(el){ var splitter=new FlexSplitter(el,el.getElementsByClassName("actual-spacer")[0],0.1,0.1); @@ -191,4 +263,6 @@ window.addEventListener("load",function(){ outputEditor.resize(); }; }); + + }); \ No newline at end of file diff --git a/interactive-console.js b/interactive-console.js new file mode 100644 index 0000000..2f17c99 --- /dev/null +++ b/interactive-console.js @@ -0,0 +1,102 @@ +var InteractiveConsole=function(el){ + this.wrappingElement=el; + this.flushEveryChar=false; + this.newLineChar='\n'; + this.wrappingElement.setAttribute("tabindex","0"); + this.lineDivs=[]; + this.inputBuffer=""; + var that=this; + this.wrappingElement.addEventListener("keydown",function(e){ + if(e.key.length===1){ + e.preventDefault(); + that.inputBuffer+=e.key; + that.notifyReader(); + } + else if(e.key==="Enter"&&that.newLineChar==='\n'){ + e.preventDefault(); + that.inputBuffer+='\n'; + that.notifyReader(); + } + else if(e.key==="Backspace"){ + e.preventDefault(); + if(that.inputBuffer.length>0){ + that.inputBuffer=that.inputBuffer.slice(0,-1); + } + else{ + that.attemptBackspace(); + } + } + }); + + this.clear(); + +}; + +InteractiveConsole.prototype.makeNewLine=function(){ + var lineDiv=document.createElement("div"); + lineDiv.classList.add("interactive-console-line"); + lineDiv.appendChild(document.createTextNode("")); + return lineDiv; +} + +InteractiveConsole.prototype.makeInputSpan=function(){ + var lineDiv=document.createElement("span"); + lineDiv.classList.add("interactive-console-input"); + lineDiv.appendChild(document.createTextNode("")); + return lineDiv; +} + +InteractiveConsole.prototype.read=function(callback){ + var lastLineDiv=this.lineDivs[this.lineDivs.length-1]; + this.inputSpan=this.makeInputSpan(); + this.inputCallback=callback; + lastLineDiv.appendChild(this.inputSpan); + this.notifyReader(); +}; + +InteractiveConsole.prototype.notifyReader=function(){ + while(this.inputSpan&&this.inputBuffer.length>0){ + var newChar=this.inputBuffer[0]; + this.inputBuffer=this.inputBuffer.substr(1); + if(this.flushEveryChar||newChar===this.newLineChar){ + var callbackText=this.inputSpan.firstChild.nodeValue+newChar; + this.inputSpan.parentNode.removeChild(this.inputSpan); + this.inputSpan===undefined; + this.write(callbackText); + this.inputCallback(callbackText); + } + else{ + this.inputSpan.firstChild.nodeValue+=newChar; + } + } +}; + +InteractiveConsole.prototype.attemptBackspace=function(){ + if(this.inputSpan&&this.inputSpan.firstChild.nodeValue.length>0){ + this.inputSpan.firstChild.nodeValue=this.inputSpan.firstChild.nodeValue.slice(0,-1); + } +} + +InteractiveConsole.prototype.write=function(text){ + var lines=text.split(this.newLineChar); + for(var i=0;i0){ + var lineDiv=this.makeNewLine(); + this.lineDivs.push(lineDiv); + this.wrappingElement.appendChild(lineDiv); + } + var lastLineDiv=this.lineDivs[this.lineDivs.length-1]; + lastLineDiv.firstChild.nodeValue+=lines[i]; + } +}; + +InteractiveConsole.prototype.clear=function(){ + this.lineDivs=[]; + this.inputBuffer=""; + this.inputSpan=undefined; + this.inputCallback=undefined; + while(this.wrappingElement.firstChild)this.wrappingElement.removeChild(this.wrappingElement.firstChild); + var lineDiv=this.makeNewLine(); + this.lineDivs.push(lineDiv); + this.wrappingElement.appendChild(lineDiv); +} \ No newline at end of file diff --git a/jelly-bf-processhandler.js b/jelly-bf-processhandler.js index 9371312..dd28bbb 100644 --- a/jelly-bf-processhandler.js +++ b/jelly-bf-processhandler.js @@ -42,6 +42,124 @@ JellyBFProcessHandler.prototype.execute=function(inputstr,options,callback){ }); }; +JellyBFProcessHandler.prototype.executeInteractive=function(options,inputRequestCallback,outputCallback,doneCallback){ + var WaitArrayId={ + READ_HEAD:0, + WRITE_HEAD:1, + TERMINATED_FLAG:2 + }; + + options.bufferlength=options.bufferlength||1024; + + + var inputBuffer=new SharedArrayBuffer(1024); + var outputBuffer=new SharedArrayBuffer(1024); + var inputWaitBuffer=new SharedArrayBuffer(3*4); + var outputWaitBuffer=new SharedArrayBuffer(3*4); + + var pendingInputData=[];//{data:typedarray,ptrdone:integer} + var inputTimeout=undefined; + + var inputuint8array=new Uint8Array(inputBuffer); + var outputuint8array=new Uint8Array(outputBuffer); + var inputwaitint32array=new Int32Array(inputWaitBuffer); + var outputwaitint32array=new Int32Array(outputWaitBuffer); + + + var output_read_head=0,output_write_head=0,output_terminated=false; // cache values + + var outputUpdated=function(){ + output_write_head=Atomics.load(outputwaitint32array,WaitArrayId.WRITE_HEAD); + output_terminated=(Atomics.load(outputwaitint32array,WaitArrayId.TERMINATED_FLAG)!==0); + if(output_terminated){ + output_write_head=Atomics.load(outputwaitint32array,WaitArrayId.WRITE_HEAD); + } + var newData; + if(output_terminated){ + newData=new Uint8Array(output_write_head-1-output_read_head); + for(var i=output_read_head;iinput_write_head){ + Atomics.store(inputuint8array,(input_write_head++)%options.bufferlength,pendingInputData[0].data[pendingInputData[0].ptrdone++]); + } + if(pendingInputData[0].ptrdone===pendingInputData[0].data.length){ + pendingInputData.shift(); + } + Atomics.store(inputwaitint32array,WaitArrayId.WRITE_HEAD,input_write_head); + console.log(Atomics.wake(inputwaitint32array,WaitArrayId.WRITE_HEAD)); + + if(pendingInputData.length>0){ + inputTimeout=setTimeout(do_input,40); + } + }; + + var inputRequested=function(read_head){ + do_input(); + if(input_write_head===read_head){ + inputRequestCallback(); + } + }; + + + var outputUpdatedHandler=function(e){ + if(e.data.type==="output-updated"){ + outputUpdated(); + } + else if(e.data.type==="input-requested"){ + inputRequested(e.data.readhead); + } + }; + + this.worker.addEventListener("message",outputUpdatedHandler); + + var that=this; + wait_for_message(this.worker,"executed",function(message){ + that.worker.removeEventListener("message",outputUpdatedHandler); + doneCallback({success:true}); + }); + wait_for_message(this.worker,"executeerror",function(message){ + that.worker.removeEventListener("message",outputUpdatedHandler); + doneCallback({success:false}); + }); + + this.worker.postMessage({type:"execute-interactive",inputbuffer:inputBuffer,outputbuffer:outputBuffer,inputwaitbuffer:inputWaitBuffer,outputwaitbuffer:outputWaitBuffer,options:options}); + + return {inputAddedCallback:inputAdded}; +}; + JellyBFProcessHandler.prototype.terminate=function(){ this.worker.terminate(); }; diff --git a/jelly-bf-sync.js b/jelly-bf-sync.js index 4c5ce2a..a14f62b 100644 --- a/jelly-bf-sync.js +++ b/jelly-bf-sync.js @@ -26,7 +26,7 @@ var JellyBFSync={ instance.exports.main(); return outputdata.toUint8Array(); }, - executeInteractive:function(module,inputuint8array,outputuint8array,inputwaitint32array,outputwaitint32array,options){ + executeInteractive:function(module,inputuint8array,outputuint8array,inputwaitint32array,outputwaitint32array,options,updatedOutputCallback,requestInputCallback){ var WaitArrayId={ READ_HEAD:0, WRITE_HEAD:1, @@ -35,10 +35,12 @@ var JellyBFSync={ options.bufferlength=options.bufferlength||1024; // 1024 element buffer by default options.eof_value=options.eof_value||0; // two elements - next read index, next write index + // TODO: loading & storing from the data arrays may not need to be done atomically, due to the barriers issued by the wait array. var input_read_head=0,input_write_head=0,input_terminated=false; // cache values var get_input=function(){ if(input_read_head===input_write_head){ - Atomics.wait(inputwaitint32array,WaitArrayId.WRITE_HEAD,input_write_head); + requestInputCallback(input_read_head); + console.log(Atomics.wait(inputwaitint32array,WaitArrayId.WRITE_HEAD,input_write_head)); input_write_head=Atomics.load(inputwaitint32array,WaitArrayId.WRITE_HEAD); if(!input_terminated){ input_terminated=(Atomics.load(inputwaitint32array,WaitArrayId.TERMINATED_FLAG)!==0); @@ -61,6 +63,7 @@ var JellyBFSync={ } Atomics.store(outputuint8array,(output_write_head++)%options.bufferlength,byte); Atomics.store(outputwaitint32array,WaitArrayId.WRITE_HEAD,output_write_head); + updatedOutputCallback(); }; var terminate_output=function(){ if(output_read_head+options.bufferlength===output_write_head){ @@ -69,6 +72,7 @@ var JellyBFSync={ } Atomics.store(outputwaitint32array,WaitArrayId.TERMINATED_FLAG,1); Atomics.store(outputwaitint32array,WaitArrayId.WRITE_HEAD,output_write_head+1); + updatedOutputCallback(); }; var instance=new WebAssembly.Instance(module,{ interaction:{ diff --git a/jelly-bf-worker.js b/jelly-bf-worker.js index 72dda8c..5b33c9c 100644 --- a/jelly-bf-worker.js +++ b/jelly-bf-worker.js @@ -24,7 +24,11 @@ // all wait buffers expected to be zeroed var options=message.options; try{ - JellyBFSync.executeInteractive(module,UInt8Array(inputbuffer),UInt8Array(outputbuffer),Int32Array(inputwaitbuffer),Int32Array(outputwaitbuffer),options); + JellyBFSync.executeInteractive(module, new Uint8Array(inputbuffer), new Uint8Array(outputbuffer), new Int32Array(inputwaitbuffer), new Int32Array(outputwaitbuffer),options,function(){ + self.postMessage({type:"output-updated"}); + },function(readhead){ + self.postMessage({type:"input-requested",readhead:readhead}); + }); self.postMessage({type:"executed"}); } catch(e){ diff --git a/jelly-bf-worker.max.js b/jelly-bf-worker.max.js index d0017ec..5cd7201 100644 --- a/jelly-bf-worker.max.js +++ b/jelly-bf-worker.max.js @@ -1567,7 +1567,7 @@ var JellyBFSync = { instance.exports.main(); return outputdata.toUint8Array(); }, - executeInteractive: function(module, inputuint8array, outputuint8array, inputwaitint32array, outputwaitint32array, options) { + executeInteractive: function(module, inputuint8array, outputuint8array, inputwaitint32array, outputwaitint32array, options, updatedOutputCallback, requestInputCallback) { var WaitArrayId = { READ_HEAD: 0, WRITE_HEAD: 1, @@ -1578,7 +1578,8 @@ var JellyBFSync = { var input_read_head = 0, input_write_head = 0, input_terminated = false; var get_input = function() { if (input_read_head === input_write_head) { - Atomics.wait(inputwaitint32array, WaitArrayId.WRITE_HEAD, input_write_head); + requestInputCallback(input_read_head); + console.log(Atomics.wait(inputwaitint32array, WaitArrayId.WRITE_HEAD, input_write_head)); input_write_head = Atomics.load(inputwaitint32array, WaitArrayId.WRITE_HEAD); if (!input_terminated) { input_terminated = Atomics.load(inputwaitint32array, WaitArrayId.TERMINATED_FLAG) !== 0; @@ -1600,6 +1601,7 @@ var JellyBFSync = { } Atomics.store(outputuint8array, output_write_head++ % options.bufferlength, byte); Atomics.store(outputwaitint32array, WaitArrayId.WRITE_HEAD, output_write_head); + updatedOutputCallback(); }; var terminate_output = function() { if (output_read_head + options.bufferlength === output_write_head) { @@ -1608,6 +1610,7 @@ var JellyBFSync = { } Atomics.store(outputwaitint32array, WaitArrayId.TERMINATED_FLAG, 1); Atomics.store(outputwaitint32array, WaitArrayId.WRITE_HEAD, output_write_head + 1); + updatedOutputCallback(); }; var instance = new WebAssembly.Instance(module, { interaction: { @@ -1649,7 +1652,16 @@ var JellyBFSync = { var outputwaitbuffer = message.outputwaitbuffer; var options = message.options; try { - JellyBFSync.executeInteractive(module, UInt8Array(inputbuffer), UInt8Array(outputbuffer), Int32Array(inputwaitbuffer), Int32Array(outputwaitbuffer), options); + JellyBFSync.executeInteractive(module, new Uint8Array(inputbuffer), new Uint8Array(outputbuffer), new Int32Array(inputwaitbuffer), new Int32Array(outputwaitbuffer), options, function() { + self.postMessage({ + type: "output-updated" + }); + }, function(readhead) { + self.postMessage({ + type: "input-requested", + readhead: readhead + }); + }); self.postMessage({ type: "executed" });