@webqit/subscript
Advanced tools
Comparing version 2.0.8 to 2.0.9
@@ -1,2 +0,2 @@ | ||
(()=>{"use strict";var t={829:(t,e,n)=>{n.d(e,{Z:()=>i});const i=t=>class extends t{constructor(){super(),this.attachShadow({mode:"open"})}connectedCallback(){[].concat(this.css).forEach((t=>{if(t.includes("{")&&t.includes(":")&&t.includes(";"))this.shadowRoot.appendChild(document.createElement("style")).textContent=t;else{let e=this.shadowRoot.appendChild(document.createElement("link"));e.setAttribute("rel","stylesheet"),e.setAttribute("href",t)}}))}get css(){return[]}}},790:(t,e,n)=>{function i(t){let e=t.split(/\n/g);if(e.length>1){let t=e[1].split(/[^\s]/)[0].length;if(t)return e.map(((n,i)=>{if(!i)return n;let s=n.substring(0,t);return s.trim().length?"}"===s.trim()&&i===e.length-1?"}":n:n.substring(t)})).join("\n")}return t}n.d(e,{B:()=>i})}},e={};function n(i){var s=e[i];if(void 0!==s)return s.exports;var r=e[i]={exports:{}};return t[i](r,r.exports,n),r.exports}n.d=(t,e)=>{for(var i in e)n.o(e,i)&&!n.o(t,i)&&Object.defineProperty(t,i,{enumerable:!0,get:e[i]})},n.o=(t,e)=>Object.prototype.hasOwnProperty.call(t,e),(()=>{var t=n(790),e=n(829);const i=n=>class extends((0,e.Z)(n||HTMLElement)){static get observedAttributes(){return["name","editable","placeholder"]}connectedCallback(){this._lang="javascript",this._editable=this.getAttribute("editable"),this._styledBlock=this.getAttribute("styled-block")||"pre",this._div=document.createElement("div"),this._preBlock=this._div.appendChild(document.createElement("pre")),this._codeBlock=this._preBlock.appendChild(document.createElement("code")),this._div.classList.add("line-numbers"),this._lang&&(this._preBlock.classList.add("language-"+this._lang),this._codeBlock.classList.add("language-"+this._lang)),this._contentSlot=document.createElement("slot"),this._contentSlot.setAttribute("aria-hidden","true"),this._contentSlot.setAttribute("hidden","true"),this._initialSlotEvent=!1,this._contentSlot.addEventListener("slotchange",(()=>{let e=this._contentSlot.assignedNodes().reduce(((t,e)=>t+(e.outerHTML||e.nodeValue||"")),"");this._initialSlotEvent||(e=(0,t.B)(e.trimStart()),this._initialSlotEvent=!0),this._textarea&&(this._textarea.value=e),this.source=(t=>t.replace(new RegExp("&","g"),"&").replace(new RegExp("<","g"),"<"))(e)})),"true"===this._editable&&this._addEditor(),this.shadowRoot.append(this._contentSlot,this._textarea||"",this._div),super.connectedCallback()}get source(){return this._codeBlock.textContent}set source(t){t.endsWith("\n")&&(t+=" "),this._codeBlock.innerHTML="",this._codeBlock.innerHTML=t,this._highlightCodeBlock(),this._syncScrolling()}get name(){return this._name}set name(t){return this.setAttribute("name",t)}get placeholder(){return this._placeholder}set placeholder(t){return this.setAttribute("placeholder",t)}get editable(){return this._editable}set editable(t){return this.setAttribute("editable",!0===t?"true":!1===t?"false":t)}_addEditor(){this._placeholder=this.getAttribute("placeholder"),this._name=this.getAttribute("name"),this._textarea=this._div.appendChild(document.createElement("textarea")),this._textarea.placeholder=this._placeholder||this._lang,this._textarea.spellcheck=!1,this._textarea.name=this._name||"",this._textarea.value=this._codeBlock.textContent,this._preBlock.setAttribute("aria-hidden","true"),this._scrollBlock="pre"===this.getAttribute("scroll-block")?this._preBlock:this._codeBlock,this._textarea.addEventListener("input",(t=>{this.source=t.target.value})),this._textarea.addEventListener("input",(()=>this._syncScrolling())),this._textarea.addEventListener("keydown",(t=>this._handleTabKeyEvent(t)))}_handleTabKeyEvent(t){if(!this._textarea)return;if("Tab"!==t.key)return;t.preventDefault();let e=this._textarea.value,n=this._textarea.selectionStart,i=this._textarea.selectionEnd;if(n===i){let t=e.slice(0,n),s=e.slice(i,e.length),r=i+1;this._textarea.value=t+"\t"+s,this._textarea.selectionStart=r,this._textarea.selectionEnd=r}else{let s=e.split("\n"),r=0,a=0,o=0;for(let e=0;e<s.length;e++)r+=s[e].length,n<r&&i>r-s[e].length&&(t.shiftKey?"\t"===s[e][0]&&(s[e]=s[e].slice(1),0===a&&o--,a--):(s[e]="\t"+s[e],0===a&&o++,a++));this._textarea.value=s.join("\n"),this._textarea.selectionStart=n+o,this._textarea.selectionEnd=i+a}this.source=this._textarea.value}_syncScrolling(){this._scrollBlock&&(this._scrollBlock.scrollTop=this._textarea.scrollTop,this._scrollBlock.scrollLeft=this._textarea.scrollLeft)}_highlightCodeBlock(){Prism.highlightElement(this._codeBlock)}disconnectedCallback(){Array.from(this.shadowRoot.childNodes).forEach((t=>t.remove()))}attributeChangedCallback(t,e,n){if(this.childNodes.length)switch(t){case"name":this._name=n,this._textarea.name=n;break;case"placeholder":this._placeholder=n,this._textarea.placeholder=n;break;case"editable":this._editable=n,this._textarea?this._textarea.disabled="false"===n:"true"===n&&this._addEditor(),"true"===n&&this._textarea.focus()}}get css(){return["https://unpkg.com/@webqit/subscript/src/console/assets/prism.css","https://unpkg.com/@webqit/subscript/src/console/assets/vs-code-dark.css",`\n * {\n -webkit-box-sizing: border-box;\n -moz-box-sizing: border-box;\n box-sizing: border-box;\n }\n :host {\n /* Allow other elems to be inside */\n position: relative;\n top: 0;\n left: 0;\n display: block;\n \n /* Normal inline styles */\n \n font-size: 0.8rem;\n font-family: monospace;\n line-height: 1.2rem;\n tab-size: 2;\n caret-color: darkgrey;\n white-space: pre;\n overflow: hidden;\n }\n \n textarea, ${this._styledBlock} {\n /* Both elements need the same text and space styling so they are directly on top of each other */\n margin: 0px !important;\n padding-top: var(--vertical-padding, 1.5rem) !important;\n padding-bottom: var(--vertical-padding, 1.5rem) !important;\n padding-left: var(--horizontal-padding, 1rem) !important;\n padding-right: var(--horizontal-padding, 1rem) !important;\n border: 0 !important;\n width: 100% !important;\n height: 100% !important;\n }\n ${"code"===this._styledBlock?"pre":"code"} {\n margin: 0px !important;\n border: 0px !important;\n padding: 0px !important;\n overflow: auto !important;\n width: 100% !important;\n height: 100% !important;\n }\n .line-numbers :is(textarea, pre[class*=language-]) {\n padding-left:3.8rem !important;\n }\n textarea, pre, pre * {\n /* Also add text styles to highlighing tokens */\n font-size: inherit !important;\n font-family: inherit !important;\n line-height: inherit !important;\n tab-size: inherit !important;\n }\n \n textarea, pre {\n /* In the same place */\n position: absolute;\n top: 0;\n left: 0;\n }\n textarea[disabled] {\n pointer-events: none !important;\n }\n \n /* Move the textarea in front of the result */\n \n textarea {\n z-index: 1;\n }\n pre {\n z-index: 0;\n }\n \n /* Make textarea almost completely transparent */\n \n textarea {\n color: transparent;\n background: transparent;\n caret-color: inherit!important; /* Or choose your favourite color */\n }\n \n /* Can be scrolled */\n textarea, pre {\n overflow: auto !important;\n \n white-space: inherit !important;\n word-spacing: normal !important;\n word-break: normal !important;\n word-wrap: normal !important;\n }\n \n /* No resize on textarea; stop outline */\n textarea {\n resize: none;\n outline: none !important;\n }\n .line-numbers-rows {\n border: none !important;\n color: dimgray !important;\n }\n `]}},s=t=>class extends(t||class{}){setStateCallback(t,e,n,i=100,s){this._timeouts||(this._timeouts={}),t in this._timeouts||(this._timeouts[t]=[]),n?(this._timeouts[t].length||s(),i?this._timeouts[t].unshift(setTimeout((()=>this.setState(t,e,!1)),i)):this._timeouts[t].unshift(null),this._related&&this._related.setState(t,e,!0,i)):(this._timeouts[t].shift(),this._timeouts[t].length||(s(),this._related&&this._related.setState(t,e,!1)))}};class r extends(s()){bind(t){Object.assign(this,t),this.fullPaths=[],this.$fullPaths=[],this.ownerProduction.assignee&&this.ownerProduction.assignee.refs.forEach((t=>{t.depth&&this.fullPaths.push([...this.path,...t.depth])})),this.fullPaths.length||(this.fullPaths=[this.path]),this.fullPaths.forEach(((t,e)=>{this.$fullPaths.push(t.map((t=>"memoId"in t?"[[computed]]":t.name)).join(".")),t.forEach((t=>{t.anchor.classList.add("ref-identifier"),t.anchor.classList.add(this.subscriptions?"affected":"cause");let n=t.anchor.getAttribute("title"),i="> "+this.$fullPaths[e]+(this.subscriptions?" (Creates a signal)":" (Receives a signal)");t.anchor.setAttribute("title",n?n+"\n"+i:i)})),this._on(e,"mouseenter",(()=>{this._setState(e,"path","hover",!0,0)}))._on(e,"mouseleave",(()=>{this._setState(e,"path","hover",!1)})),this.subscriptions&&this._on(e,"click",(()=>{this.ownerProduction.ownerEffect.signal(t)}))}))}_setState(t,e,n,i,s=100){this.setStateCallback(t+"|"+e,n,i,s,(()=>{i?this.fullPaths[t].forEach((t=>t.anchor.classList.add(`${e}-${n}`))):this.fullPaths[t].forEach((t=>t.anchor.classList.remove(`${e}-${n}`)))}))}setState(t,e,n,i=100){let[s,r]=t.split("|");if(void 0!==r)return this._setState(s,r,e,n,i);this.fullPaths.forEach(((s,r)=>{this._setState(r,t,e,n,i)}))}_on(t,e,n){return this.fullPaths[t].forEach((t=>t.anchor.addEventListener(e,n.bind(this)))),this}on(t,e){this.fullPaths.forEach(((n,i)=>{this._on(i,t,e)}))}}class a extends(s(HTMLElement)){bind(t){if(Object.assign(this,t),!this.graph)return;this.childEffects&&this.childEffects.forEach((t=>{t.replaceWith(...t.childNodes)})),this.childEffects=new Map,this._textNodes=this.getTextNodes();for(let t in this.graph.childEffects){let e=this.graph.childEffects[t],n=this.createChildEffect({parentEffect:this,graph:e});this.childEffects.set(e.id,n)}if(this.affecteds=new Map,this.causes=new Map,this.refAnchors)for(let t in this.refAnchors){let e=this.refAnchors[t];e.replaceWith(...e.childNodes)}this.refAnchors={},this._textNodes=this.getTextNodes();const e=t=>{for(let e in this.graph[t]){let n=this.createProduction({ownerEffect:this,...this.graph[t][e]});this[t].set(e,n)}};e("affecteds"),e("causes"),this.on("mouseenter",(()=>{this.setState("block","hover",!0,0)})).on("mouseleave",(()=>{this.setState("block","hover",!1)})),this.observe(((t,e)=>{this.setState("block","runtime-active",!0,100),e.forEach((t=>{let e=this.causes.get(t.productionId+"");e&&e.refs.get(t.id).setState("path","runtime-active",!0,100)}))}))}get program(){return this.parentEffect?this.parentEffect.program:this.runtime}signal(...t){let e=this.program.locate(this.graph.lineage);if(e)return e.signal(...t)}observe(t){return this.program.observe(this.graph.lineage,t)}createChildEffect(t){let e=document.createElement("subscript-effect");return this.insertNode(e,t.graph.loc,"effect"),e.bind(t),e}createProduction(t){let e={...t,refs:new Map};"assignee"in t&&(e.assignee=this.affecteds.get(t.assignee+""));for(let n of t.refs){let t=this.createRef({ownerProduction:e,...n});e.refs.set(n.id,t)}return e}createRef(t){let e=new r;const n=t=>{let[e,n]=t.loc,i=e+"-"+n,s=this.refAnchors[i];return s||(s=document.createElement("span"),this.insertNode(s,[e,n],"ref"),this.refAnchors[i]=s),s};return(t={...t,path:t.path.map((t=>({anchor:n(t),...t})))}).depth&&(t.depth=t.depth.map((t=>({anchor:n(t),...t})))),e.bind(t),e}insertNode(t,e,n){let[i,s]=e,r=this.graph.loc?this.graph.loc[0]:0,[a,o]=this.resolveOffset(i-r),[h,l]=this.resolveOffset(s-r,!1),c=new Range;return"effect"===n?(0===o&&"SPAN"===a.parentNode.nodeName?c.setStartBefore(a.parentNode):c.setStart(a,o),l===(h.nodeValue||"").length&&"SPAN"===h.parentNode.nodeName?c.setEndAfter(h.parentNode):c.setEnd(h,l)):(c.setStart(a,o),c.setEnd(h,l)),c.surroundContents(t),t}resolveOffset(t,e=!0){return this._textNodes.reduce((([n,i,s],r)=>{if(null===i){let a=s+r.length;if(t<=a&&!r.isBlank){let i=t-s;if(!e&&0===i)return[n.node,n.length];if(!e||i<r.length)return[r.node,i]}[n,i,s]=[r,i,a]}return[n,i,s]}),[null,null,0])}getTextNodes(t=this){let e,n={acceptNode:function(t){if("SCRIPT"!==t.parentNode.nodeName)return window.NodeFilter.FILTER_ACCEPT}},i=window.document.createTreeWalker(t||this,window.NodeFilter.SHOW_TEXT,n,!1),s=[];for(;e=i.nextNode();){let t=e.nodeValue||"";s.push({node:e,length:t.length,isBlank:0===t.trim().length})}return s}setState(t,e,n,i=100){n&&this.parentEffect&&this.parentEffect.setState(t,e,!1),this.setStateCallback(t,e,n,i,(()=>{n?this.classList.add(`${t}-${e}`):this.classList.remove(`${t}-${e}`)}))}on(t,e){return this.addEventListener(t,e.bind(this)),this}}class o extends(i(a)){bind(t,e=!0){e&&(this.innerHTML=t.originalSource),setTimeout((()=>{if(!this._codeBlock.textContent.length)return;let e=t.runtime;super.bind({runtime:e,graph:e.graph})}),0)}createRef(t){}getTextNodes(t=null){return super.getTextNodes(t||this._codeBlock)}get css(){return super.css.concat(["\n .ref-identifier.path-runtime-active {\n text-decoration: underline;\n }\n .ref-identifier:is(.path-runtime-active) {\n }\n\n .ref-identifier.cause {\n cursor: default;\n }\n\n .ref-identifier.affected {\n cursor: pointer;\n }\n\n .ref-identifier.cause:is(.path-hover, .path-runtime-active) {\n color: aqua;\n }\n .token.keyword .ref-identifier.cause:is(.path-hover, .path-runtime-active) {\n color: mediumturquoise;\n }\n \n .ref-identifier.affected:is(.path-hover, .path-runtime-active) {\n color: yellowgreen;\n text-decoration: underline;\n }\n\n .ref-identifier.cause.affected:is(.path-hover, .path-runtime-active) {\n color: lightgreen;\n }\n\n subscript-effect.block-hover,\n subscript-effect.block-runtime-active {\n outline: 1px dashed gray;\n outline-offset: 0.1rem;\n border-radius: 0.1rem;\n /*\n background-color: darkblue;\n */\n }\n subscript-effect.block-runtime-active {\n background-color: rgba(100, 100, 100, 0.35);\n }\n "])}}customElements.define("subscript-codeblock",i()),customElements.define("subscript-effect",a),customElements.define("subscript-console",o)})()})(); | ||
(()=>{"use strict";var t={829:(t,e,n)=>{n.d(e,{Z:()=>i});const i=t=>class extends t{constructor(){super(),this.attachShadow({mode:"open"})}connectedCallback(){[].concat(this.css).forEach((t=>{if(t.includes("{")&&t.includes(":")&&t.includes(";"))this.shadowRoot.appendChild(document.createElement("style")).textContent=t;else{let e=this.shadowRoot.appendChild(document.createElement("link"));e.setAttribute("rel","stylesheet"),e.setAttribute("href",t)}}))}get css(){return[]}}},790:(t,e,n)=>{function i(t){let e=t.split(/\n/g);if(e.length>1){let t=e[1].split(/[^\s]/)[0].length;if(t)return e.map(((n,i)=>{if(!i)return n;let s=n.substring(0,t);return s.trim().length?"}"===s.trim()&&i===e.length-1?"}":n:n.substring(t)})).join("\n")}return t}n.d(e,{B:()=>i})}},e={};function n(i){var s=e[i];if(void 0!==s)return s.exports;var r=e[i]={exports:{}};return t[i](r,r.exports,n),r.exports}n.d=(t,e)=>{for(var i in e)n.o(e,i)&&!n.o(t,i)&&Object.defineProperty(t,i,{enumerable:!0,get:e[i]})},n.o=(t,e)=>Object.prototype.hasOwnProperty.call(t,e),(()=>{var t=n(790),e=n(829);const i=n=>class extends((0,e.Z)(n||HTMLElement)){static get observedAttributes(){return["name","editable","placeholder"]}connectedCallback(){this._lang="javascript",this._editable=this.getAttribute("editable"),this._styledBlock=this.getAttribute("styled-block")||"pre",this._div=document.createElement("div"),this._preBlock=this._div.appendChild(document.createElement("pre")),this._codeBlock=this._preBlock.appendChild(document.createElement("code")),this._div.classList.add("line-numbers"),this._lang&&(this._preBlock.classList.add("language-"+this._lang),this._codeBlock.classList.add("language-"+this._lang)),this._contentSlot=document.createElement("slot"),this._contentSlot.setAttribute("aria-hidden","true"),this._contentSlot.setAttribute("hidden","true"),this._initialSlotEvent=!1,this._contentSlot.addEventListener("slotchange",(()=>{let e=this._contentSlot.assignedNodes().reduce(((t,e)=>t+(e.outerHTML||e.nodeValue||"")),"");this._initialSlotEvent||(e=(0,t.B)(e.trimStart()),this._initialSlotEvent=!0),this._textarea&&(this._textarea.value=e),this.source=(t=>t.replace(new RegExp("&","g"),"&").replace(new RegExp("<","g"),"<"))(e)})),"true"===this._editable&&this._addEditor(),this.shadowRoot.append(this._contentSlot,this._textarea||"",this._div),super.connectedCallback()}get source(){return this._codeBlock.textContent}set source(t){t.endsWith("\n")&&(t+=" "),this._codeBlock.innerHTML="",this._codeBlock.innerHTML=t,this._highlightCodeBlock(),this._syncScrolling()}get name(){return this._name}set name(t){return this.setAttribute("name",t)}get placeholder(){return this._placeholder}set placeholder(t){return this.setAttribute("placeholder",t)}get editable(){return this._editable}set editable(t){return this.setAttribute("editable",!0===t?"true":!1===t?"false":t)}_addEditor(){this._placeholder=this.getAttribute("placeholder"),this._name=this.getAttribute("name"),this._textarea=this._div.appendChild(document.createElement("textarea")),this._textarea.placeholder=this._placeholder||this._lang,this._textarea.spellcheck=!1,this._textarea.name=this._name||"",this._textarea.value=this._codeBlock.textContent,this._preBlock.setAttribute("aria-hidden","true"),this._scrollBlock="pre"===this.getAttribute("scroll-block")?this._preBlock:this._codeBlock,this._textarea.addEventListener("input",(t=>{this.source=t.target.value})),this._textarea.addEventListener("input",(()=>this._syncScrolling())),this._textarea.addEventListener("keydown",(t=>this._handleTabKeyEvent(t)))}_handleTabKeyEvent(t){if(!this._textarea)return;if("Tab"!==t.key)return;t.preventDefault();let e=this._textarea.value,n=this._textarea.selectionStart,i=this._textarea.selectionEnd;if(n===i){let t=e.slice(0,n),s=e.slice(i,e.length),r=i+1;this._textarea.value=t+"\t"+s,this._textarea.selectionStart=r,this._textarea.selectionEnd=r}else{let s=e.split("\n"),r=0,a=0,o=0;for(let e=0;e<s.length;e++)r+=s[e].length,n<r&&i>r-s[e].length&&(t.shiftKey?"\t"===s[e][0]&&(s[e]=s[e].slice(1),0===a&&o--,a--):(s[e]="\t"+s[e],0===a&&o++,a++));this._textarea.value=s.join("\n"),this._textarea.selectionStart=n+o,this._textarea.selectionEnd=i+a}this.source=this._textarea.value}_syncScrolling(){this._scrollBlock&&(this._scrollBlock.scrollTop=this._textarea.scrollTop,this._scrollBlock.scrollLeft=this._textarea.scrollLeft)}_highlightCodeBlock(){Prism.highlightElement(this._codeBlock)}disconnectedCallback(){Array.from(this.shadowRoot.childNodes).forEach((t=>t.remove()))}attributeChangedCallback(t,e,n){if(this.childNodes.length)switch(t){case"name":this._name=n,this._textarea.name=n;break;case"placeholder":this._placeholder=n,this._textarea.placeholder=n;break;case"editable":this._editable=n,this._textarea?this._textarea.disabled="false"===n:"true"===n&&this._addEditor(),"true"===n&&this._textarea.focus()}}get css(){return["https://unpkg.com/@webqit/subscript/src/console/assets/prism.css","https://unpkg.com/@webqit/subscript/src/console/assets/vs-code-dark.css",`\n * {\n -webkit-box-sizing: border-box;\n -moz-box-sizing: border-box;\n box-sizing: border-box;\n }\n :host {\n /* Allow other elems to be inside */\n position: relative;\n top: 0;\n left: 0;\n display: block;\n \n /* Normal inline styles */\n \n font-size: 0.8rem;\n font-family: monospace;\n line-height: 1.2rem;\n tab-size: 2;\n caret-color: darkgrey;\n white-space: pre;\n overflow: hidden;\n }\n \n textarea, ${this._styledBlock} {\n /* Both elements need the same text and space styling so they are directly on top of each other */\n margin: 0px !important;\n padding-top: var(--vertical-padding, 1.5rem) !important;\n padding-bottom: var(--vertical-padding, 1.5rem) !important;\n padding-left: var(--horizontal-padding, 1rem) !important;\n padding-right: var(--horizontal-padding, 1rem) !important;\n border: 0 !important;\n width: 100% !important;\n height: 100% !important;\n }\n ${"code"===this._styledBlock?"pre":"code"} {\n margin: 0px !important;\n border: 0px !important;\n padding: 0px !important;\n overflow: auto !important;\n width: 100% !important;\n height: 100% !important;\n }\n .line-numbers :is(textarea, pre[class*=language-]) {\n padding-left:3.8rem !important;\n }\n textarea, pre, pre * {\n /* Also add text styles to highlighing tokens */\n font-size: inherit !important;\n font-family: inherit !important;\n line-height: inherit !important;\n tab-size: inherit !important;\n }\n \n textarea, pre {\n /* In the same place */\n position: absolute;\n top: 0;\n left: 0;\n }\n textarea[disabled] {\n pointer-events: none !important;\n }\n \n /* Move the textarea in front of the result */\n \n textarea {\n z-index: 1;\n }\n pre {\n z-index: 0;\n }\n \n /* Make textarea almost completely transparent */\n \n textarea {\n color: transparent;\n background: transparent;\n caret-color: inherit!important; /* Or choose your favourite color */\n }\n \n /* Can be scrolled */\n textarea, pre {\n overflow: auto !important;\n \n white-space: inherit !important;\n word-spacing: normal !important;\n word-break: normal !important;\n word-wrap: normal !important;\n }\n \n /* No resize on textarea; stop outline */\n textarea {\n resize: none;\n outline: none !important;\n }\n .line-numbers-rows {\n border: none !important;\n color: dimgray !important;\n }\n `]}},s=t=>class extends(t||class{}){setStateCallback(t,e,n,i=100,s){this._timeouts||(this._timeouts={}),t in this._timeouts||(this._timeouts[t]=[]),n?(this._timeouts[t].length||s(),i?this._timeouts[t].unshift(setTimeout((()=>this.setState(t,e,!1)),i)):this._timeouts[t].unshift(null),this._related&&this._related.setState(t,e,!0,i)):(this._timeouts[t].shift(),this._timeouts[t].length||(s(),this._related&&this._related.setState(t,e,!1)))}};class r extends(s()){bind(t){Object.assign(this,t),this.fullPaths=[],this.$fullPaths=[],this.ownerReference.assignee&&this.ownerReference.assignee.refs.forEach((t=>{t.depth&&this.fullPaths.push([...this.path,...t.depth])})),this.fullPaths.length||(this.fullPaths=[this.path]),this.fullPaths.forEach(((t,e)=>{this.$fullPaths.push(t.map((t=>"memoId"in t?"[[computed]]":t.name)).join(".")),t.forEach((t=>{t.anchor.classList.add("ref-identifier"),t.anchor.classList.add(this.subscriptions?"effect":"signal");let n=t.anchor.getAttribute("title"),i="> "+this.$fullPaths[e]+(this.subscriptions?" (Effect Ref)":" (Signal Ref)");t.anchor.setAttribute("title",n?n+"\n"+i:i)})),this._on(e,"mouseenter",(()=>{this._setState(e,"path","hover",!0,0)}))._on(e,"mouseleave",(()=>{this._setState(e,"path","hover",!1)})),this.subscriptions&&this._on(e,"click",(()=>{this.ownerReference.ownerUnit.runThread(t)}))}))}_setState(t,e,n,i,s=100){this.setStateCallback(t+"|"+e,n,i,s,(()=>{i?this.fullPaths[t].forEach((t=>t.anchor.classList.add(`${e}-${n}`))):this.fullPaths[t].forEach((t=>t.anchor.classList.remove(`${e}-${n}`)))}))}setState(t,e,n,i=100){let[s,r]=t.split("|");if(void 0!==r)return this._setState(s,r,e,n,i);this.fullPaths.forEach(((s,r)=>{this._setState(r,t,e,n,i)}))}_on(t,e,n){return this.fullPaths[t].forEach((t=>t.anchor.addEventListener(e,n.bind(this)))),this}on(t,e){this.fullPaths.forEach(((n,i)=>{this._on(i,t,e)}))}}class a extends(s(HTMLElement)){bind(t){if(Object.assign(this,t),!this.graph)return;this.subUnits&&this.subUnits.forEach((t=>{t.replaceWith(...t.childNodes)})),this.subUnits=new Map,this._textNodes=this.getTextNodes();for(let t in this.graph.subUnits){let e=this.graph.subUnits[t],n=this.createSubUnit({ownerUnit:this,graph:e});this.subUnits.set(e.id,n)}if(this.effects=new Map,this.signals=new Map,this.refAnchors)for(let t in this.refAnchors){let e=this.refAnchors[t];e.replaceWith(...e.childNodes)}this.refAnchors={},this._textNodes=this.getTextNodes();const e=t=>{for(let e in this.graph[t]){let n=this.createReference({ownerUnit:this,...this.graph[t][e]});this[t].set(e,n)}};e("effects"),e("signals"),this.setAttribute("title",this.graph.type),this.on("mouseenter",(()=>{this.setState("block","hover",!0,0)})).on("mouseleave",(()=>{this.setState("block","hover",!1)})),this.observe(((t,e)=>{this.setState("block","runtime-active",!0,100),e.forEach((t=>{let e=this.signals.get(t.referenceId+"");e&&e.refs.get(t.id).setState("path","runtime-active",!0,100)}))}))}get program(){return this.ownerUnit?this.ownerUnit.program:this.runtime}runThread(...t){let e=this.program.locate(this.graph.lineage);if(e)return e.thread(...t)}observe(t){return this.program.observe(this.graph.lineage,t)}createSubUnit(t){let e=document.createElement("subscript-unit");return this.insertNode(e,t.graph.loc,"unit"),e.bind(t),e}createReference(t){let e={...t,refs:new Map};"assignee"in t&&(e.assignee=this.effects.get(t.assignee+""));for(let n of t.refs){let t=this.createRef({ownerReference:e,...n});e.refs.set(n.id,t)}return e}createRef(t){let e=new r;const n=t=>{let[e,n]=t.loc,i=e+"-"+n,s=this.refAnchors[i];return s||(s=document.createElement("span"),this.insertNode(s,[e,n],"ref"),this.refAnchors[i]=s),s};return(t={...t,path:t.path.map((t=>({anchor:n(t),...t})))}).depth&&(t.depth=t.depth.map((t=>({anchor:n(t),...t})))),e.bind(t),e}insertNode(t,e,n){let[i,s]=e,r=this.graph.loc?this.graph.loc[0]:0,[a,o]=this.resolveOffset(i-r),[h,l]=this.resolveOffset(s-r,!1),c=new Range;return"unit"===n?(0===o&&"SPAN"===a.parentNode.nodeName?c.setStartBefore(a.parentNode):c.setStart(a,o),l===(h.nodeValue||"").length&&"SPAN"===h.parentNode.nodeName?c.setEndAfter(h.parentNode):c.setEnd(h,l)):(c.setStart(a,o),c.setEnd(h,l)),c.surroundContents(t),t}resolveOffset(t,e=!0){return this._textNodes.reduce((([n,i,s],r)=>{if(null===i){let a=s+r.length;if(t<=a&&!r.isBlank){let i=t-s;if(!e&&0===i)return[n.node,n.length];if(!e||i<r.length)return[r.node,i]}[n,i,s]=[r,i,a]}return[n,i,s]}),[null,null,0])}getTextNodes(t=this){let e,n={acceptNode:function(t){if("SCRIPT"!==t.parentNode.nodeName)return window.NodeFilter.FILTER_ACCEPT}},i=window.document.createTreeWalker(t||this,window.NodeFilter.SHOW_TEXT,n,!1),s=[];for(;e=i.nextNode();){let t=e.nodeValue||"";s.push({node:e,length:t.length,isBlank:0===t.trim().length})}return s}setState(t,e,n,i=100){n&&this.ownerUnit&&this.ownerUnit.setState(t,e,!1),this.setStateCallback(t,e,n,i,(()=>{n?this.classList.add(`${t}-${e}`):this.classList.remove(`${t}-${e}`)}))}on(t,e){return this.addEventListener(t,e.bind(this)),this}}class o extends(i(a)){bind(t,e=!0){e&&(this.innerHTML=t.originalSource),setTimeout((()=>{if(!this._codeBlock.textContent.length)return;let e=t.runtime;super.bind({runtime:e,graph:e.graph})}),0)}createRef(t){}getTextNodes(t=null){return super.getTextNodes(t||this._codeBlock)}get css(){return super.css.concat(["\n .ref-identifier.path-runtime-active {\n text-decoration: underline;\n }\n .ref-identifier:is(.path-runtime-active) {\n }\n\n .ref-identifier.cause {\n cursor: default;\n }\n\n .ref-identifier.effect {\n cursor: pointer;\n }\n\n .ref-identifier.cause:is(.path-hover, .path-runtime-active) {\n color: aqua;\n }\n .token.keyword .ref-identifier.cause:is(.path-hover, .path-runtime-active) {\n color: mediumturquoise;\n }\n \n .ref-identifier.effect:is(.path-hover, .path-runtime-active) {\n color: yellowgreen;\n text-decoration: underline;\n }\n\n .ref-identifier.cause.effect:is(.path-hover, .path-runtime-active) {\n color: lightgreen;\n }\n\n subscript-unit.block-hover,\n subscript-unit.block-runtime-active {\n outline: 1px dashed gray;\n outline-offset: 0.1rem;\n border-radius: 0.1rem;\n /*\n background-color: darkblue;\n */\n }\n subscript-unit.block-runtime-active {\n background-color: rgba(100, 100, 100, 0.35);\n }\n "])}}customElements.define("subscript-codeblock",i()),customElements.define("subscript-unit",a),customElements.define("subscript-console",o)})()})(); | ||
//# sourceMappingURL=console-element.js.map |
@@ -11,3 +11,3 @@ { | ||
"homepage": "https://webqit.io/tooling/subscript", | ||
"version": "2.0.8", | ||
"version": "2.0.9", | ||
"license": "MIT", | ||
@@ -14,0 +14,0 @@ "repository": { |
610
README.md
@@ -9,14 +9,16 @@ # Subscript | ||
Subscript is a reactivity runtime for JavaScript. It takes any valid JavaScript code, reads its dependency graph, and gives you the mechanism to run it both in whole and in selected parts, called dependency threads. | ||
Subscript is a reactivity runtime for JavaScript. It takes any valid JavaScript code, reads its dependency graph, and offers a mechanism to run it both in whole and in *reactive* selections, called *dependency threads*. | ||
+ [What's A Dependency Thread?](#whats-a-dependency-thread) | ||
+ [What Is Subscript?](#whats-subscript) | ||
+ [What Is Subscript?](#what-is-subscript) | ||
+ [Concepts](#concepts) | ||
+ [API](#api) | ||
+ [Installation](#installation) | ||
+ [A Custom Element Example](#a-custom-element-example) | ||
+ [Motivation](#motivation) | ||
+ [Installation](#installation) | ||
+ [API](#api) | ||
+ [Issues](#issues) | ||
## What's A Dependency Thread? | ||
That's simply the dependency chain involving two or more JavaScript expressions. | ||
That's simply the dependency chain involving two or more JavaScript expressions. 👇 | ||
@@ -27,3 +29,3 @@ ```js | ||
We just expressed that `doubleCount` should be two times the value of `count`, and that `quadCount` should be two times the value of `doubleCount`. | ||
We just expressed that `doubleCount` should be two times the value of `count`, and that `quadCount` should be two times the value of `doubleCount`; each subsequent expression being a *dependent* of the previous. | ||
@@ -35,22 +37,9 @@ ```js | ||
Problem is: this mathematical relationship only holds for as long as nothing changes. Should the value of `count` change, then its dependents would be out of sync. | ||
😉 Can you spot that same dependency chain in the following hypothetical UI render function…? | ||
```js | ||
count ++; | ||
let count = 10; | ||
``` | ||
```js | ||
console.log( count, doubleCount, quadCount ); | ||
< 11, 20, 40 | ||
``` | ||
This is that reminder that expressions in JavaScript aren't automatically bound to their dependencies. And that's what we'd expect of any programming language. | ||
If we had this in real life in some sort of a UI render function… | ||
```js | ||
let count = 10, doubleCount, quadCount; | ||
``` | ||
```js | ||
let render = function() { | ||
@@ -60,7 +49,7 @@ let countElement = document.querySelector( '#count' ); | ||
doubleCount = count * 2; | ||
let doubleCount = count * 2; | ||
let doubleCountElement = document.querySelector( '#double-count' ); | ||
doubleCountElement.innerHTML = doubleCount; | ||
quadCount = doubleCount * 2; | ||
let quadCount = doubleCount * 2; | ||
let quadCountElement = document.querySelector( '#quad-count' ); | ||
@@ -71,16 +60,37 @@ quadCountElement.innerHTML = quadCount; | ||
…then, we'd have to execute `render()` in whole each time the value of `count` changes. And here comes the additional overhead of querying the DOM every time! | ||
You'll also notice one additional *dependent* at each level of the chain. That gives us the *dependency thread* for `count` as: statement `2` -> statement `3` -> statement `5` -> statement `6` -> statement `8`; excluding statements `1`, `4`, `7`. | ||
In the time it takes to take a deep breath, we could make a drop-in replacement of the render function with a hypothetical function that, in addition to being a normal function, offers us a way to run expressions in isolation. | ||
🤝 Good analysis! But what's the deal? | ||
Programs are generally expected to run **in whole**, **not in dependency threads**! It would take some magic to have the latter, but... now, that's what's for dinner with Subscript! 😁 | ||
Problem is: the mathematical relationship above only holds for as long as nothing changes. Should the value of `count` change, then its dependents are sure out of sync. | ||
```js | ||
render = new SubscriptFunction(` | ||
count ++; | ||
``` | ||
```js | ||
console.log( count, doubleCount, quadCount ); | ||
< 11, 20, 40 | ||
``` | ||
This is that reminder that expressions in JavaScript aren't automatically bound to their dependencies. (Something we'd expect of any programming language.) The `render()` function must be called again each time the value of `count` changes. | ||
An important worry is that we end up running overheads on sebsequent calls to `render()`, as those `document.querySelector()` calls traverse the DOM again, just to return the same elements as in previous runs. (In real life, there could be even more expensive operations up there.) | ||
Enter dependency threads; suddenly, we can get statements to run in isolation in response to a change! **Here comes a new way to think about reactivity and performance in JavaScript**! 👇 | ||
\> Obtain `SubscriptFunction` and use as a drop-in replacement for `Function`! 👇 | ||
```js | ||
let render = new SubscriptFunction(` | ||
let countElement = document.querySelector( '#count' ); | ||
countElement.innerHTML = count; | ||
doubleCount = count * 2; | ||
let doubleCount = count * 2; | ||
let doubleCountElement = document.querySelector( '#double-count' ); | ||
doubleCountElement.innerHTML = doubleCount; | ||
quadCount = doubleCount * 2; | ||
let quadCount = doubleCount * 2; | ||
let quadCountElement = document.querySelector( '#quad-count' ); | ||
@@ -91,4 +101,6 @@ quadCountElement.innerHTML = quadCount;` | ||
\> Run as a normal function… | ||
> More about the syntatic rhyme between `SubscriptFunction` and `Function` [ahead](#api). | ||
\> Use `render` as a normal function… | ||
```js | ||
@@ -98,3 +110,3 @@ render(); | ||
The above executes the function body in full as designed… elements are selected and assigned content. And we can see the counters in the console. | ||
*The above executes the function body in whole as we'd expect. Elements are selected and assigned content. And we can see the counters in the console.* | ||
@@ -106,10 +118,10 @@ ```js | ||
\> Run as a reactive function… | ||
\> Run `render` in dependency threads… | ||
```js | ||
count ++; | ||
render.signal( [ 'count' ] ); | ||
render.thread( [ 'count' ] ); | ||
``` | ||
This time, only statements 2, 3, 5, 6, and 8 are run - *the count dependency thread*; and the previously selected UI elements in those local variables are updated. | ||
*This time, only statements `2` -> `3` -> `5` -> `6` -> `8` are run - *the "count" dependency thread*; and the previously selected UI elements in those local variables are only now updated.* | ||
@@ -121,48 +133,6 @@ ```js | ||
Now, that's a bit of magic! But that hypothetical function is really Subscript Function! | ||
\> Use `SubscriptFunction` as a building block. | ||
But before we go into the details, there's a fever pitch that can't wait: | ||
*A Custom Element Example [ahead](#a-custom-element-example)* | ||
As trivial as our example code looks, we can see it applicable in real life places! Consider a neat reactive web component for our counter below. | ||
```js | ||
// We'll still keep count as a global variable for now | ||
let count = 10; | ||
``` | ||
```js | ||
// This custom element extends Subscript as a base class… more on this later | ||
customElements.define( 'click-counter', class extends SubscriptElement( HTMLElement ) { | ||
// This is how we designate methods as reactive methods | ||
// But this is implicit having extended SubscriptElement() | ||
static get subscriptMethods() { | ||
return [ 'render' ]; | ||
} | ||
connectedCallback() { | ||
// Full execution at connected time | ||
this.render(); | ||
// Granularly-selective execution at click time | ||
this.addEventListener( 'click', () => { | ||
count ++; | ||
this.render.signal( [ 'count' ] ); | ||
} ); | ||
} | ||
render() { | ||
let countElement = document.querySelector( '#count' ); | ||
countElement.innerHTML = count; | ||
let doubleCount = count * 2; | ||
let doubleCountElement = document.querySelector( '#double-count' ); | ||
doubleCountElement.innerHTML = doubleCount; | ||
let quadCount = doubleCount * 2; | ||
let quadCountElement = document.querySelector( '#quad-count' ); | ||
quadCountElement.innerHTML = quadCount; | ||
} | ||
} ); | ||
``` | ||
## What Is Subscript? | ||
@@ -172,14 +142,20 @@ | ||
It takes any piece of code and compiles it into an ordinary JavaScript function that can also run expressions in dependency threads! | ||
It takes any piece of code and compiles it into an ordinary JavaScript function that can also run expressions in *dependency threads*! | ||
Being function-based let's you have Subscript as a building block… to fit anywhere! | ||
Being function-based let's us have all of Subscript as a building block… to fit anywhere! | ||
## Concepts | ||
### Signals | ||
+ [Thread Events](#thread-events) | ||
+ [References And Bindings](#references-and-bindings) | ||
+ [Conditionals And Logic](#conditionals-and-logic) | ||
+ [Loops](#loops) | ||
+ [Functions](#functions) | ||
Subscript is not concerned with how changes happen or are detected on the outer scope of the function. It simply gives us a way to announce that something has changed. That announcement is called a *signal*. | ||
### Thread Events | ||
A Subscript function has a `signal()` method that lets us specify the list of outside variables or properties that have changed. | ||
Subscript is not concerned with how changes happen or are detected on the outer scope of the function. It simply gives us a way to announce that something has changed. That announcement is called a *thread event*. | ||
A Subscript function has a `thread()` method that lets us trigger a thread for the list of outside variables or properties that have changed. | ||
```js | ||
@@ -203,20 +179,20 @@ let a = 'Apple', b = 'Banana', c = { prop: 'Fruits' }; | ||
```js | ||
// Updates and signals | ||
// Updates and threads | ||
b = 'Breadfruit'; | ||
fn.signal( [ 'b' ] ); | ||
fn.thread( [ 'b' ] ); | ||
``` | ||
The array syntax allows us to send signals for property changes as paths. | ||
The array syntax allows us to represent properties as paths. | ||
```js | ||
fn.signal( [ 'c', 'prop' ] ); | ||
fn.thread( [ 'c', 'prop' ] ); | ||
``` | ||
And we can send multiple signals in one call. | ||
And we can run one thread for multiple changes. | ||
```js | ||
fn.signal( [ 'a' ], [ 'b' ] ); | ||
fn.thread( [ 'a' ], [ 'b' ] ); | ||
``` | ||
Variables declared within the function belong in their own scope and do not respond to outside signals. But when they do reference variables from the outside scope, they are included in the dependency thread of those variables. | ||
Variable declarations within the function belong in their own scope and do not respond to outside events. But when they do reference variables from the outside scope, they are included in the dependency thread of those outside variables. | ||
@@ -238,10 +214,10 @@ ```js | ||
```js | ||
// The following signals will have no effect since a and b are local variables. | ||
fn.signal( [ 'a' ], [ 'b' ] ); | ||
// The following events will have no effect since "a" and "b" are local variables. | ||
fn.thread( [ 'a' ], [ 'b' ] ); | ||
``` | ||
```js | ||
// The local variable b will be part of the dependency thread for the following signal | ||
// The local variable "b" will be part of the dependency thread of "c.prop" | ||
// (The console will therefore show the result of the last two statements in the function) | ||
fn.signal( [ 'c', 'prop' ] ); | ||
fn.thread( [ 'c', 'prop' ] ); | ||
``` | ||
@@ -253,3 +229,3 @@ | ||
Variable declarations, with `let` and `var`, and assignment expressions, are bound to any references that may be in their argument. (`const` declarations are an exception as they're always *const* in nature.) | ||
For example, variable declarations, with `let` and `var`, and assignment expressions, are bound to any references that may be in their argument. (`const` declarations are an exception as they're always *const* in nature.) | ||
@@ -260,4 +236,6 @@ ```js | ||
Above, `tense` is bound to the reference `score`. The effect of a signal from `score` is that `tense` is updated. That update, in turn, becomes a signal to any subsequent expression referencing `tense`. | ||
*Above, the assignment expression is bound to the reference `score`; and thus responds to a thread event for `score`.* | ||
The thread continues with any susequent bindings to the `tense` variable itself... | ||
```js | ||
@@ -267,34 +245,20 @@ let message = `Hi ${ candidate.firstName }, you ${ tense } this test!`; | ||
```js | ||
message += subjects.next ? ' Up next is: ' + subjects.next : ' The end!'; | ||
``` | ||
*Above, the assignment expression is bound to the references `candidate`, `candidate.firstName`, and `tense`; and thus responds to a thread event for each.* | ||
Above, `message` is bound to the references `candidate`, `candidate.firstName`, and `tense`. (Likewise, in the additional assignment for `message`, `message` is bound to the reference `subjects.next`.) The effect of a signal from any of these references is that `message` is updated. That update, in turn, becomes a signal to any subsequent expression referencing `message`. | ||
And the thread continues with any susequent bindings to the `message` variable itself... and any bindings of those bindings... | ||
And the dependency thread continues! | ||
Other types of operations like `score ++`, and `delete subjects.next` make a signal to their dependents. | ||
Array/Object expressions, as another example, are bound to any references that they may be composed of, and the expressions are reevaluated should any of those change. | ||
```js | ||
let fullName = [ candidate.firstName, candidate.lastName, ].join( ' ' ); | ||
let fullMessage = [ message, ' ', 'Thank you!' ].join( '' ); | ||
``` | ||
Above, `fullName` is updated as any of `candidate`, `candidate.firstName`, `candidate.lastName` changes. | ||
```js | ||
result = { …result, [ subjects.current ]: score }; | ||
let broadcast = { [ candidate.username ]: fullMessage }; | ||
``` | ||
Above, the `result` object gets, or updates, a property corresponding to `subjects.current`, with an associated value `score`, as any of `subjects.current` and `score` changes. | ||
References in call-time arguments are binding. | ||
```js | ||
alert( message ); // alert() runs again on receiving a change signal from message. | ||
console.log( broadcast ); | ||
``` | ||
```js | ||
let candidate = new Candidate( id ); // candidate is a new instance on receiving a change signal from id. | ||
let broadcastInstance = new BroadcastMessage( broadcast ); | ||
``` | ||
@@ -304,7 +268,7 @@ | ||
When the parameters of an *If/Else* statement, *Switch* statement, or other logical expressions contain references, the statement or logical expression is bound to those references. That lets us have reactive conditionals and logic. | ||
When the *test expression* of an "If/Else" statement, "Switch" statement, or other logical expressions contains references, the statement or logical expression is bound to those references. This lets us have *reactive conditionals and logic*. | ||
#### If/Else Statements | ||
#### "If/Else" Statements | ||
When the *test* expression of an *If* statement contains references, the *if/else* construct is bound to those references. | ||
An "If/Else" statement is bound to references in its "test" expression. | ||
@@ -319,6 +283,12 @@ ```js | ||
Above, the effect of a signal from the reference `score` is that the *test* expression (`score > 80 && passesSomeOtherTest()`) is evaluated again and the corresponding branch is executed in whole. | ||
*Above, the "If/Else" construct is bound to the references `score` and `passesSomeOtherTest` - yes, should that also change. A thread event for any of these gets the construct re-evaluated; first, the "test" expression (`score > 80 && passesSomeOtherTest()`), then, the body of the appropriate branch of the construct.* | ||
Now, adding an *else if* block would be just as adding another *if* statement in the *else* block of a parent *if* statement. | ||
Statements in the body of the "consequent" and "alternate" branches form a binding to references of their own, independent of their containing "If" construct. But they only respond to thread events for as long as the "state" of all *conditions in context* allows. | ||
*Above, the `addBadge()` expression is bound to the reference `candidate`, and joins alone in the dependency thread, independent of the "If" construct, but for as long as the condition in context (`score > 80 && passesSomeOtherTest()`) holds true.* | ||
> The "state" of all *conditions in context* are determined via *memoization*, and no re-evaluation ever takes place. | ||
An "Else/If" block is taken for just an "If" statement in the "Else" block of a parent "If" statement... | ||
```js | ||
@@ -333,12 +303,9 @@ if ( score > 80 && passesSomeOtherTest() ) { | ||
Nested *If* statements have their own *if/else* branches bound to the references in their own *test* expression. So, above, every effect of a signal from the reference `someOtherCondition` is scoped to just the nested *if* statement. | ||
...and is bound to references in its own "test" expression, independent of its parent. But it only responds to thread events for as long as the "state" of all *conditions in context* allows. | ||
Now, for as long as one side of a logical expression remains active, expressions on the other side are always unexposed to signals. Above, for as long as the parent *test* expression (`score > 80 && passesSomeOtherTest()`) holds true: | ||
+ The *If* statement nested on its inactive side remains unexposed to signals from the reference `someOtherCondition`. | ||
+ The individual expressions nested on its active side remain exposed to signals from the references they themselves might be bound to. | ||
A change of state in the parent *test* expression reverses the situation. | ||
*Above, the nested "If" statement is bound to the reference `someOtherCondition`, and joins alone in the dependency thread, independent of the parent "If" construct, but for as long as the parent condition (`score > 80 && passesSomeOtherTest()`) holds false.* | ||
#### Switch Statements | ||
#### "Switch" Statements | ||
When the *switch* expression or any of the *test* expressions of a *Switch* statement contains references, the *Switch* construct is bound to those references. | ||
A "Switch" statement is bound to references in its "test" expressions - the "switch/case" expressions. | ||
@@ -358,33 +325,49 @@ ```js | ||
Above, the effect of a signal on any of the references `score` and `maxScore` is that the *test* expressions are tested again and the body of the corresponding case is executed in whole. | ||
*Above, the "Switch" construct is bound to the references `score` and `maxScore`. A thread event for any of these gets the construct re-evaluated; first, the "switch/case" expressions (`score === 0` | `score === maxScore` | `score === null`), then, the body of the appropriate branch of the construct.* | ||
Now, for as long as the applicable case(s) of a *Switch* statement remain active, expressions in the others are always unexposed to signals. Above, for as long as the first or second case holds true, the assignment expression in the default case remains unexposed to signals from the reference `defaultRemark`. | ||
Statements in the body of the branches form a binding to references of their own, independent of the "Switch" construct. But they only respond to thread events for as long as the "state" of all *conditions in context* allows. | ||
*Above, the assignment to `candidate.remark` (in the "default" case) is bound to the reference `defaultRemark`, and joins alone in the dependency thread, independent of the "Switch" construct, but for as long as the conditions in context (`score === null`) hold true.* | ||
> The "state" of all *conditions in context* are determined via *memoization*, and no re-evaluation ever takes place. | ||
#### Logical And Ternary Operators | ||
Subscript observes the state of logical expressions (`a && b || c`), and conditional expressions with the *ternary* operator (`a ? b : c`), when running the dependency thread for a signal. | ||
Subscript observes the state of logical (`a && b || c`) and ternary (`a ? b : c`) expressions when running dependency threads. | ||
```js | ||
let a = () => 1, b = 2, c = 3, d, e; | ||
let a = () => 1; | ||
let b = 2; | ||
let c = 3; | ||
let d, e; | ||
``` | ||
A logical expression... | ||
```js | ||
d = a() ? b : c; | ||
e = a() && b || c; | ||
``` | ||
Above, because of the *truthy* nature of the condition `a()`, the logical expressions will always return the value of `b`. This logic holds true even if the value of `c` continues to change. The assignment expressions are therefore not exposed at all to any signals from `c`, and this saves the potential overhead of calling `a()` each time `c` changes. | ||
A ternary expression... | ||
Should the condition `a()` become *falsey*, as in `a = () => 0`, the scenario above changes to favour `c` over `b`. | ||
```js | ||
d = a() ? b : c; | ||
``` | ||
*Above, each of the two expressions is bound to the references `a`, `b` and `c`. A thread event for any of `a` and `b` - or `a` and `c`, as determined by the "logical state" of the expressions<sup>*</sup> - gets the expressions re-evaluated; first, the "test" expression (`a()`), then, the expression on the appropriate side of the construct.* | ||
<sup>*</sup>Since expressions in the "consequent" and "alternate" sides of a conditional or logical expression are mutually exclusive (`b` and `c` above), as determined by the "test" expression (`a()` above), only the thread events for the references in the currently active side (`b` above) are honoured by the expression. | ||
### Loops | ||
When the parameters of a loop (`for` loops, `do` and `do … while` loops) contain references, the loop is bound to those references. That lets us have reactive loops. | ||
When the parameters of a loop ("For" loops, "While" and "Do … While" loops) contain references, the loop is bound to those references. This lets us have reactive loops. | ||
#### A `for` Loop, `do` And `do … while` Loop | ||
#### A `for` Loop, `while` And `do … while` Loop | ||
When any of the three parts of a `for` loop contains references, the loop is bound to those references. | ||
A "For" loop is bound to references in its 3-part definition. | ||
```js | ||
let start = 0, items = [ 'one', 'two', 'three', 'four', 'five' ], targetItems = []; | ||
let start = 0; | ||
let items = [ 'one', 'two', 'three', 'four', 'five' ]; | ||
let targetItems = []; | ||
``` | ||
@@ -398,16 +381,22 @@ | ||
Above, the effect of a signal from `start` or `items`, or `items.length`, is that the loop runs again. | ||
*The loop above is bound to the references `start`, `items`, and `items.length`. A thread event for any of these gets the loop to run again.* | ||
```js | ||
// Say, start and items were global variables | ||
// Say, "start" were a global variable | ||
start = 2; | ||
fn.signal( [ 'start' ] ); | ||
fn.thread( [ 'start' ] ); | ||
``` | ||
```js | ||
// Say, "items" were a global variable | ||
items.unshift( 'zero' ); | ||
fn.signal( [ 'items', 'length' ] ); | ||
fn.thread( [ 'items', 'length' ] ); | ||
``` | ||
Similar to a `for` loop, when the *condition* expression of a `do` or `do … while` loop contains references, the loop is bound to those references. | ||
As with a "For" loop, a "While" and "Do ... While" loop are bound to references in their "test" expression. | ||
```js | ||
let index = 0, items = [ 'one', 'two', 'three', 'four', 'five' ], targetItems = []; | ||
let index = 0; | ||
let items = [ 'one', 'two', 'three', 'four', 'five' ]; | ||
let targetItems = []; | ||
``` | ||
@@ -422,3 +411,3 @@ | ||
Above, the effect of a signal from `items`, or `items.length`, is that the loop runs again. | ||
*The loop above is bound to the references `items` and `items.length`. A thread event for any of these gets the loop to run again.* | ||
@@ -428,3 +417,3 @@ ```js | ||
items.unshift( 'zero' ); | ||
fn.signal( [ 'items', 'length' ] ); | ||
fn.thread( [ 'items', 'length' ] ); | ||
``` | ||
@@ -434,6 +423,7 @@ | ||
When the *iteratee* of a `for … of` loop is a reference, the loop is bound to that reference. | ||
A "For ... Of" loop is bound to references in its *iteratee*. | ||
```js | ||
let entries = [ 'one', 'two', 'three', 'four', 'five' ], targetEntries = []; | ||
let entries = [ 'one', 'two', 'three', 'four', 'five' ]; | ||
let targetEntries = []; | ||
``` | ||
@@ -449,3 +439,3 @@ | ||
Above, the effect of a signal from the reference `entries` is that the loop runs again. | ||
*The loop above is bound to the reference `entries`. A thread event for `entries` gets the loop to run again.* | ||
@@ -455,3 +445,3 @@ ```js | ||
entries = [ 'six', 'seven', 'eight', 'nine', 'ten' ]; | ||
fn.signal( [ 'entries' ] ); | ||
fn.thread( [ 'entries' ] ); | ||
``` | ||
@@ -463,3 +453,3 @@ | ||
entries[ 7 ] = 'This is new eight'; | ||
fn.signal( [ 'entries', 7 ] ); | ||
fn.thread( [ 'entries', 7 ] ); | ||
``` | ||
@@ -477,6 +467,7 @@ | ||
When the *iteratee* of a `for … in` loop is a reference, the loop is bound to that reference. | ||
A "For ... In" loop is bound to references in its *iteratee*. | ||
```js | ||
let entries = { one: 'one', two: 'two', three: 'three', four: 'four', five: 'five' }, targetEntries = {}; | ||
let entries = { one: 'one', two: 'two', three: 'three', four: 'four', five: 'five' }; | ||
let targetEntries = {}; | ||
``` | ||
@@ -491,3 +482,3 @@ | ||
Above, the effect of a signal from the reference `entries` is that the loop runs again. | ||
*The loop above is bound to the reference `entries`. A thread event for `entries` gets the loop to run again.* | ||
@@ -497,3 +488,3 @@ ```js | ||
entries = { six: 'six', seven: 'seven', eight: 'eight', nine: 'nine', ten: 'ten' }; | ||
fn.signal( [ 'entries' ] ); | ||
fn.thread( [ 'entries' ] ); | ||
``` | ||
@@ -505,3 +496,3 @@ | ||
entries[ 'eight' ] = 'This is new eight'; | ||
fn.signal( [ 'entries', 'eight' ] ); | ||
fn.thread( [ 'entries', 'eight' ] ); | ||
``` | ||
@@ -519,3 +510,3 @@ | ||
Conceptually, each round of iteration in a loop is an instance that Subscript can access directly during a reactive run. A round of iteration is thus updatable in isolation in response to a directed signal. This is what happens when the *iteratee* of a `for … of` or `for … in` loop *signals* about an updated entry, as seen above. | ||
Conceptually, each round of iteration in a loop is an instance that Subscript can access directly when running a thread. A round of iteration is thus updatable in isolation, in response to a directed event. This is what happens when the *iteratee* of a "For ... Of" and "For ... In" loop has any of its properties updated, as seen above. | ||
@@ -525,3 +516,4 @@ Below is a similar case. | ||
```js | ||
let entries = { one: { name: 'one' }, two: { name: 'two' } }, targetEntries = {}; | ||
let entries = { one: { name: 'one' }, two: { name: 'two' } }; | ||
let targetEntries = {}; | ||
``` | ||
@@ -540,13 +532,13 @@ | ||
entries[ 'one' ] = { name: 'New one' }; | ||
fn.signal( [ 'entries', 'one' ] ); | ||
fn.thread( [ 'entries', 'one' ] ); | ||
``` | ||
For even more granularity, individual expressions inside a round of iteration are also responsive to signals of their own. So, if we updated just `entries.one.name`… | ||
For even more granularity, individual expressions inside a round of iteration are also responsive to thread events of their own. So, if we updated just `entries.one.name`… | ||
```js | ||
entries.one.name = 'New one'; | ||
fn.signal( [ 'entries', 'one', 'name' ] ); | ||
fn.thread( [ 'entries', 'one', 'name' ] ); | ||
``` | ||
…we would have skipped the iteration instance itself, to match just the first statement within it. | ||
…we would have skipped the iteration instance itself, to target just the first statement within it. | ||
@@ -557,3 +549,3 @@ This granular reactivity makes it often pointless to trigger a full rerun of a loop, offering multiple opportunities to deliver unmatched performance. | ||
Subscript observes `break` and `continue` statements even in a reactive run. And any of these statements may employ *labels*. | ||
Subscript observes `break` and `continue` statements even when running a thread. And any of these statements may employ *labels*. | ||
@@ -577,31 +569,161 @@ ```js | ||
[TODO] | ||
Functions are *static* definitions... | ||
## Motivation | ||
```js | ||
function sum( a, b ) { | ||
} | ||
``` | ||
[TODO] | ||
```js | ||
let sum = function( a, b ) { | ||
} | ||
``` | ||
## Installation | ||
```js | ||
let sum = ( a, b ) => { | ||
} | ||
``` | ||
\> Install via npm | ||
...and nothing about their parameters is reactive! | ||
```cmd | ||
npm i @webqit/subscript | ||
They are really only significant at *call-time*; and call-time arguments are rightly *reactive*! | ||
```js | ||
let result = sum( score, 100 ); | ||
``` | ||
*The expression above is bound to the reference `score`. A thread event for `score` gets the `sum()` function called again with its current value.* | ||
#### Side Effects | ||
When a function modifies anything outside of its scope, it is said to have *side effects*. | ||
```js | ||
import SubscriptFunction from '@webqit/subscript'; | ||
let callCount = 0; | ||
function sum( a, b ) { | ||
callCount ++; | ||
return a + b; | ||
} | ||
``` | ||
\> Include from a CDN | ||
When it does not, it is said to be a *pure function*. | ||
```html | ||
<script src="https://unpkg.com/@webqit/subscript/dist/main.js"></script> | ||
```js | ||
function sum( a, b ) { | ||
return a + b; | ||
} | ||
``` | ||
Regardless, Subscript's dependency threads are fully able to pick up changes made via a side effect. | ||
```js | ||
const SubscriptFunction = WebQit.Subscript; | ||
function sum( a, b ) { | ||
callCount ++; | ||
return a + b; | ||
} | ||
let callCount = 0; | ||
let result = sum( score, 100 ); | ||
console.log( 'Number of times we\'ve summed:', callCount ); | ||
``` | ||
*Above, each time the thread event for `score` gets the `sum()` expression to run again, `callCount` is incremented as a side effect; and the dependent `console.log()` expression joins in the thread to pick that up!* | ||
Since statements in a dependency thread are executed in normal program execution order, side effects only trigger dependent expressions that appear *after the point of call*, *not before*. | ||
```js | ||
function sum( a, b ) { | ||
callCount ++; | ||
return a + b; | ||
} | ||
let callCount = 0; | ||
console.log( 'BEFORE POINT OF CALL: Number of times we\'ve summed:', callCount ); | ||
let result = sum( score, 100 ); | ||
console.log( 'AFTER POINT OF CALL: Number of times we\'ve summed:', callCount ); | ||
``` | ||
*Above, on the thread event for `score`, the first `console.log()` expression doesn't run because at that point `sum()` hasn't been called to make the side effect!* | ||
Also, since Subscript does not change runtime expection in any way, side effects made by function calls outside of a running thread do not get to start a thread in a bid to engage its dependent expressions! | ||
```js | ||
function sum( a, b ) { | ||
callCount ++; | ||
return a + b; | ||
} | ||
let callCount = 0; | ||
document.body.addEventListener( 'click', () => { | ||
let result = sum( score, 100 ); | ||
} ); | ||
console.log( 'Number of times we\'ve summed:', callCount ); | ||
``` | ||
*This time, `sum()` is triggerred from a click event handler, not via a dependency thread, and we do not expect the `console.log()` expression to run!* | ||
#### Subscript Function Syntax (New) | ||
Subscript explores the possibility of defining functions outright as *reactive* functions using regular *Function Declaration* and *Function Expression* syntaxes! | ||
```js | ||
function** sum( a, b ) { | ||
} | ||
``` | ||
```js | ||
let sum = function**( a, b ) { | ||
} | ||
``` | ||
*Notice the double star `**` symbol above; it's just one star extra to the standard syntax for [Generator Functions](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Generator) (`function* gen() {}`) - one more thing in the same classification of a special-purpose function in JavaScript! 😎* | ||
Functions defined this way are compiled as `SubscriptFunction`, exposing a `.thread()` method for running dependency threads, and offering everything else as in when we use the `SubscriptFunction` constructor. | ||
The following syntaxes are interchangeable... | ||
```js | ||
function** sum( a, b ) { | ||
return a + b; | ||
} | ||
``` | ||
```js | ||
let sum = function**( a, b ) { | ||
return a + b; | ||
} | ||
``` | ||
```js | ||
let sum = new SubscriptFunction( `a`, `b`, `return a + b;` ); | ||
``` | ||
...but the first two (proposed syntaxes) are only currently supported within a Subscript Function itself! | ||
```js | ||
let score = 10; | ||
let program = new SubscriptFunction(` | ||
function** sum( a, b ) { | ||
callCount ++; | ||
return a + b; | ||
} | ||
let callCount = 0; | ||
// The following call results in a side effect | ||
let result = sum( score, 100 ); | ||
// and "callCount" is logged as "1" to the console | ||
console.log( 'Number of times we\'ve summed:', callCount ); | ||
// The following call runs a dependency thread that excludes the side effect | ||
// while return the sum of the previous values of "a" and "b" | ||
let result = sum.thread( [ 'a' ] ); | ||
// and "callCount" is still logged as "1", not "2", to the console | ||
console.log( 'Number of times we\'ve summed:', callCount ); | ||
`); | ||
program(); | ||
``` | ||
## API | ||
`SubscriptFunction` is a one-to-one equivalent of the [JavaScript function constructor](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/Function). You can just use them interchangeably 😎. | ||
`SubscriptFunction` is a one-to-one equivalent of the [JavaScript Function constructor](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/Function). They work interchangeably 😎. | ||
@@ -616,3 +738,3 @@ ### Syntax | ||
// With the new keyword | ||
// With the new operator | ||
let subscrFunction = new SubscriptFunction( functionBody ); | ||
@@ -686,5 +808,5 @@ let subscrFunction = new SubscriptFunction( arg1, functionBody ); | ||
### The `subscrFunction.signal()` Method | ||
### The `subscrFunction.thread()` Method | ||
The `.signal()` method is the *reactivity* API in Subscript functions that lets us send *change signals* into the *reactivity runtime*. It takes a list of the outside variables or properties that have changed; each as an array path. | ||
The `.thread()` method is the *reactivity* API in Subscript functions that lets us send *thread events* into the *reactivity runtime*. It takes a list of the outside variables or properties that have changed; each as an array path. | ||
@@ -694,3 +816,3 @@ #### Syntax | ||
```js | ||
let returnValue = subscrFunction.signal( path1, ... pathN ); | ||
let returnValue = subscrFunction.thread( path1, ... pathN ); | ||
``` | ||
@@ -702,3 +824,3 @@ | ||
An array path representing each variable, or object property, that has changed. *See [Signals](#signals) for concepts and usage.* | ||
An array path representing each variable, or object property, that has changed. *See [Thread Events](#thread-events) for concepts and usage.* | ||
@@ -728,10 +850,130 @@ #### Return Value | ||
a = 20; | ||
console.log( sum.signal( [ 'a' ] ) ); | ||
console.log( sum.thread( [ 'a' ] ) ); | ||
< Promise { 22 } | ||
``` | ||
## Documentation | ||
## Installation | ||
+ See [project homepage](https://webqit.io/tooling/subscript) for download options and full documentation. | ||
\> Install via npm | ||
```cmd | ||
npm i @webqit/subscript | ||
``` | ||
```js | ||
import SubscriptFunction from '@webqit/subscript'; | ||
``` | ||
\> Include from a CDN | ||
```html | ||
<script src="https://unpkg.com/@webqit/subscript/dist/main.js"></script> | ||
``` | ||
```js | ||
const SubscriptFunction = WebQit.Subscript; | ||
``` | ||
## A Custom Element Example | ||
As trivial as our hypothetical [`render()`](#whats-a-dependency-thread) function above is, we can see it applicable in real life places! Consider a neat reactive *Custom Element* example based on [`SubscriptElement`](https://webqit.io/tooling/oohtml/docs/spec/subscript#subscript-element-mixin) from [OOHTML](https://github.com/webqit/oohtml). | ||
```js | ||
// We'll still keep count as a global variable for now | ||
let count = 10; | ||
``` | ||
```js | ||
// This custom element extends Subscript as a base class… more on this later | ||
customElements.define( 'click-counter', class extends SubscriptElement( HTMLElement ) { | ||
// This is how we designate methods as reactive methods | ||
// But this is implicit having extended SubscriptElement() | ||
static get subscriptMethods() { | ||
return [ 'render' ]; | ||
} | ||
connectedCallback() { | ||
// Full execution at connected time | ||
this.render(); | ||
// Granularly-selective execution at click time | ||
this.addEventListener( 'click', () => { | ||
count ++; | ||
this.render.thread( [ 'count' ] ); | ||
} ); | ||
} | ||
render() { | ||
let countElement = document.querySelector( '#count' ); | ||
countElement.innerHTML = count; | ||
let doubleCount = count * 2; | ||
let doubleCountElement = document.querySelector( '#double-count' ); | ||
doubleCountElement.innerHTML = doubleCount; | ||
let quadCount = doubleCount * 2; | ||
let quadCountElement = document.querySelector( '#quad-count' ); | ||
quadCountElement.innerHTML = quadCount; | ||
} | ||
} ); | ||
``` | ||
*Continue to [SubscriptElement](https://webqit.io/tooling/oohtml/docs/spec/subscript#subscript-element-mixin) for the full story.* | ||
## Motivation | ||
### The Best Syntax Is No Syntax At All! | ||
*Frontend has a syntax problem*! Every framework has come contributing something *JavaScript-like*, *HTML-like*, or even *JavaScript/HTML-like<sup>2</sup>* to the plague! And for many of us, that bit is a non-starter! 😡 | ||
So, we're rethinking reactivity, again! This, time, to lay its very principles on nothing at all but plain JavaScript! | ||
### Performant JavaScript | ||
With an insane focus on pure JavaScript syntax, Subscript is able to keep its footprint, and your footprint, ridiculously small. This *less clutter*, is *more performance*! | ||
Subscript follows a compiler-aided approach that translates to a tiny, highly-optimized piece at runtime - no diffing; no *callback wizardry*! | ||
### Developer "Joooy" 😎 | ||
Much work goes into learning and using today's slew of reactivity primitives - those `on____` and `use____` hooks! But to explicitly construct reactive relationships is to slave over something that is *implicit* in a program's dependency graph! | ||
Subscript lets you write your code, not the hooks! Graph-based reactivity just kicks in! 🤩 | ||
Offering the full range of modern JavaScript, with zero additional clutter, none of a complex toolchain and no required build step is a new dimension to developer productivity! | ||
### Compasable Reactivity | ||
Subscript comes as *reactivity in a function* - the smallest possible unit of composition, and this is new! But that is to say: composition is king! | ||
Thinking of reactive JavaScript classes? Make one... with Subscript Function as a method! Building the next reactive system? Put Subscript Functions under the hood! | ||
### Progressive Development | ||
What's the possibility of turning reactivity *on* and *off* on an existing code base, in an afterthought? Oh that's a nobrainer with Subscript Functions! | ||
+ Using the [Function Constructor](#api) approach? Just toggle between the function type, while everything else stays intact: | ||
```js | ||
let sum = new Function( `a`, `b`, `return a + b` ); | ||
``` | ||
```js | ||
let sum = new SubscriptFunction( `a`, `b`, `return a + b` ); | ||
``` | ||
+ Using the [Function Synctax](#subscript-function-syntax-new) approach? Just toggle the *double star*, while everything else stays intact: | ||
```js | ||
function sum( a, b, ) { | ||
return a + b; | ||
} | ||
``` | ||
```js | ||
function** sum( a, b, ) { | ||
return a + b; | ||
} | ||
``` | ||
This togglability is new! | ||
## Issues | ||
@@ -738,0 +980,0 @@ |
@@ -6,5 +6,6 @@ | ||
import { generate as astringGenerate } from 'astring'; | ||
import { astNodes } from './Generators.js'; | ||
import Effect from './Effect.js'; | ||
import Scope from './Scope.js'; | ||
import EffectReference from './EffectReference.js'; | ||
import SignalReference from './SignalReference.js'; | ||
import Context from './Context.js'; | ||
import Node from './Node.js'; | ||
@@ -17,2 +18,3 @@ export default class Compiler { | ||
this._locStart = this.params.locStart || 0; | ||
this.deferredTasks = []; | ||
} | ||
@@ -31,10 +33,19 @@ | ||
serialize(ast, params = {}) { | ||
return astringGenerate( ast, { comments: true, ...params } ); | ||
} | ||
/* ------------------------------ */ | ||
/* ------------------------------ */ | ||
generate( nodes ) { | ||
let globalEffect = new Effect( null, 'Program', { type: 'Program', params: this.params, } ); | ||
globalEffect.defineSubscriptIdentifier( '$construct', [ '$x' ] ); | ||
let [ ast ] = this._transform( [ nodes ], globalEffect ); | ||
let def = { type: 'Global' }; | ||
let globalContext = new Context( null, '#', { ...def, params: this.params, } ); | ||
globalContext.defineSubscriptIdentifier( '$unit', [ '$x' ] ); | ||
let [ ast ] = globalContext.createScope( def, () => this.generateNodes( globalContext, [ nodes ] ) ); | ||
this.deferredTasks.forEach( task => task() ); | ||
return { | ||
source: this.serialize( ast ), | ||
graph: globalEffect.toJson( false ), | ||
identifier: globalEffect.getSubscriptIdentifier( '$construct' ), | ||
graph: globalContext.toJson( false ), | ||
identifier: globalContext.getSubscriptIdentifier( '$unit' ), | ||
locations: this.locations, | ||
@@ -45,230 +56,370 @@ ast, | ||
serialize(ast, params = {}) { | ||
return astringGenerate( ast, { comments: true, ...params } ); | ||
/** | ||
* ------------ | ||
* @Array of Nodes | ||
* ------------ | ||
*/ | ||
generateNodes( context, nodes ) { | ||
const total = nodes.length; | ||
if ( total > 1 ) { | ||
// Hoist FunctionDeclarations | ||
nodes = nodes.reduce( ( _nodes, node ) => { | ||
return node.type === 'FunctionDeclaration' ? [ node ].concat( _nodes ) : _nodes.concat( node ); | ||
}, [] ); | ||
} | ||
const next = index => { | ||
if ( index === total ) return []; | ||
let nextCalled = false; | ||
let _next = () => { | ||
nextCalled = true; | ||
return next( index + 1 ); | ||
}; | ||
let transformed; | ||
if ( nodes[ index ] ) { | ||
let generate = this.generateNode; | ||
if ( this[ `generate${ nodes[ index ].type }` ] ) { | ||
generate = this[ `generate${ nodes[ index ].type }` ]; | ||
} | ||
transformed = generate.call( this, context, nodes[ index ], _next ); | ||
} else { | ||
transformed = [ nodes[ index ] ]; | ||
} | ||
if ( !nextCalled ) { | ||
transformed = [].concat( transformed ).concat( _next(1) ); | ||
} | ||
return transformed; | ||
}; | ||
return next( 0 ); | ||
} | ||
_transform( nodes, effect, scope ) { | ||
/** | ||
* ------------ | ||
* @Any Node | ||
* ------------ | ||
*/ | ||
generateNode( context, node ) { | ||
return Object.keys( node ).reduce( ( _node, key ) => { | ||
let value = node[ key ]; | ||
if ( Array.isArray( value ) ) { | ||
value = this.generateNodes( context, value ); | ||
} else if ( typeof value === 'object' && value ) { | ||
[ value ] = this.generateNodes( context, [ value ] ); | ||
} | ||
return { ..._node, [ key ]: value }; | ||
}, {} ); | ||
} | ||
return nodes.reduce( ( _nodes, node ) => { | ||
/* ------------------------------ */ | ||
/* ------------------------------ */ | ||
// Helpers | ||
const _transform = ( _nodes, _effect = effect, _scope = scope ) => this._transform( _nodes, _effect, _scope ); | ||
const _returns = ( ...__nodes ) => _nodes.concat( __nodes ); | ||
const _visit = ( _node, _effect = effect ) => Object.keys( _node ).reduce( ( __node, key ) => { | ||
let value = _node[ key ]; | ||
if ( value && value.type === 'Identifier' ) { | ||
_effect.subscriptIdentifiersNoConflict( value ); | ||
} else if ( Array.isArray( value ) ) { | ||
value = value.map( v => _visit( v ) ); | ||
} else if ( typeof value === 'object' && value ) { | ||
value = _visit( value ); | ||
/** | ||
* ------------ | ||
* @Program | ||
* ------------ | ||
*/ | ||
generateProgram( context, node ) { | ||
let def = { type: node.type }; | ||
let body = context.createScope( def, () => this.generateNodes( context, node.body ) ); | ||
return { ...node, body }; | ||
} | ||
/** | ||
* ------------ | ||
* @FunctionDeclaration | ||
* @FunctionExpression | ||
* @ArrowFunctionExpression | ||
* ------------ | ||
*/ | ||
generateFunctionDeclaration( context, node, next ) { return this.generateFunction( Node.funcDeclaration, ...arguments ) } | ||
generateFunctionExpression( context, node, next ) { return this.generateFunction( Node.funcExpr, ...arguments ) } | ||
generateArrowFunctionExpression( context, node, next ) { return this.generateFunction( Node.arrowFuncExpr, ...arguments ) } | ||
generateFunction( generate, context, node, next ) { | ||
const generateId = ( id, context ) => !id ? [ id ] : context.effectReference( { type: node.type }, () => this.generateNodes( context, [ id ] ), false ); | ||
const generateFunction = ( id, params, body ) => context.defineContext( { type: node.type, isSubscriptFunction: node.isSubscriptFunction }, functionUnit => { | ||
return functionUnit.createScope( { type: node.type }, () => { | ||
// FunctionExpressions have their IDs in function scope | ||
id = generateId( id, functionUnit )[ 0 ]; | ||
// Function params go into function scope | ||
params = params.map( param => { | ||
if ( param.type === 'AssignmentPattern' ) { | ||
let right = this.generateNode( functionUnit, param.right ); | ||
let def = { type: param.left.type }; | ||
let [ left ] = functionUnit.effectReference( def, () => this.generateNodes( functionUnit, [ param.left ] ), false ); | ||
return Node.assignmentPattern( left, right ); | ||
} | ||
let def = { type: param.type }; | ||
[ param ] = functionUnit.effectReference( def, () => this.generateNodes( functionUnit, [ param ] ), false ); | ||
return param; | ||
} ); | ||
if ( node.type === 'ArrowFunctionExpression' && node.expression ) { | ||
body = this.generateNode( functionUnit, Node.blockStmt( [ Node.returnStmt( body ) ] ) ); | ||
} else { | ||
body = this.generateNode( functionUnit, body ); | ||
} | ||
return { ...__node, [ key ]: value }; | ||
}, {} ); | ||
if ( !node ) return _returns( node ); | ||
return [ functionUnit, id, params, body ]; | ||
} ); | ||
} ); | ||
/** | ||
* ------------ | ||
* #Program | ||
* ------------ | ||
*/ | ||
if ( node.type === 'Program' ) { | ||
let def = { type: 'Program' }; | ||
return effect/* global effect instance */.createScope( def, programScope => { | ||
let body = _transform( node.body, null, programScope ); | ||
return _returns( { ...node, body } ); | ||
} ); | ||
let subscript$unit = Node.identifier( context.getSubscriptIdentifier( '$unit', true ) ); | ||
let unitCreate = ( generate, functionUnit, funcName, params, body ) => functionUnit.generate( | ||
generate.call( Node, funcName, [ subscript$unit ].concat( params ), body, node.async, node.expression, node.generator ), { | ||
args: [ Node.literal( node.type ), Node.identifier( node.isSubscriptFunction ? 'true' : 'false' ) ], | ||
isFunctionUnit: true, | ||
generateForArgument: true | ||
} | ||
); | ||
/** | ||
* ------------ | ||
* #ArrowFunctionExpression | ||
* #FunctionExpression | ||
* #FunctionDeclaration | ||
* ------------ | ||
*/ | ||
if ( node.type === 'ArrowFunctionExpression' || node.type === 'FunctionExpression' || node.type === 'FunctionDeclaration' ) { | ||
let createNode = astNodes[ node.type === 'ArrowFunctionExpression' ? 'arrowFuncExpr' : ( | ||
node.type === 'FunctionExpression' ? 'funcExpr' : 'funcDeclaration' | ||
) ].bind( astNodes ); | ||
if ( node.type === 'FunctionDeclaration' ) { | ||
node = _visit( node, scope.ownerEffect ); | ||
let resultNode, functionUnit, id, params, body; | ||
if ( node.type === 'FunctionDeclaration' ) { | ||
// FunctionDeclarations have their IDs in current scope | ||
[ id ] = generateId( node.id, context ); | ||
[ functionUnit, , params, body ] = generateFunction( null, node.params, node.body ); | ||
resultNode = unitCreate( Node.funcExpr, functionUnit, null, params, body ); | ||
// We'll physically do hoisting | ||
let definitionRef = Node.memberExpr( subscript$unit, Node.identifier( 'functions' ) ); | ||
let definitionCall = ( method, ...args ) => Node.callExpr( Node.memberExpr( definitionRef, Node.identifier( method ) ), [ id, ...args ] ); | ||
// Generate now | ||
resultNode = [ | ||
Node.exprStmt( definitionCall( 'define', resultNode ) ), | ||
generate.call( Node, id, params, Node.blockStmt( [ | ||
Node.returnStmt( Node.callExpr( Node.memberExpr( definitionCall( 'get' ), Node.identifier( 'call' ) ), [ | ||
Node.thisExpr(), Node.spreadElement( Node.identifier( 'arguments' ) ) | ||
] ) ), | ||
] ) ) | ||
]; | ||
} else { | ||
// FunctionExpressions and ArrowFunctionExpressions are quite simpler | ||
[ functionUnit, id, params, body ] = generateFunction( node.id, node.params, node.body ); | ||
resultNode = unitCreate( generate, functionUnit, id, params, body ); | ||
} | ||
this.deferredTasks.unshift( () => { | ||
functionUnit.sideEffects.forEach( sideEffect => { | ||
functionUnit.ownerScope.doSideEffectUpdates( sideEffect.reference, sideEffect.remainderRefs ); | ||
} ); | ||
} ); | ||
return resultNode; | ||
} | ||
/** | ||
* ------------ | ||
* @VariableDeclaration | ||
* ------------ | ||
*/ | ||
generateVariableDeclaration( context, node ) { | ||
let def = { type: node.type, kind: node.kind }; | ||
let assignmentRefactors = []; | ||
let exec = ( unit, declarator, isForLoopInit ) => { | ||
let initReference, [ init ] = unit.signalReference( def, reference => ( initReference = reference, this.generateNodes( context, [ declarator.init ] ) ) ); | ||
let idReference, [ id ] = unit.effectReference( def, reference => ( idReference = reference, this.generateNodes( context, [ declarator.id ] ) ) ); | ||
initReference.setAssignee( idReference ); | ||
this.setLocation( unit, declarator ); | ||
if ( isForLoopInit || node.kind === 'const' || !declarator.init || !unit.references.filter( reference => reference instanceof SignalReference ).length/* note that we're not asking initReference.refs.size */ ) { | ||
// init might still have effects | ||
return Node.varDeclarator( id, init ); | ||
} | ||
// Strip out the init, and re-declare only the local identifiers | ||
// ID might be Array or Object pattern, so we're better off taking the resolved bare identifiers | ||
let isPattern = [ 'ObjectPattern', 'ArrayPattern' ].includes( declarator.id.type ); | ||
let declarations = Array.from( idReference.refs ).map( ref => Node.varDeclarator( Node.identifier( ref.path[ 0 ].name ), null ) ); | ||
// Now, covert the init part to an assignment expression | ||
let assignmentExpr = Node.assignmentExpr( id, init ); | ||
if ( isPattern ) { | ||
// The below line has been safely removed... astring automatically adds the parens | ||
//assignmentExpr = Node.parensExpr( assignmentExpr ); | ||
} | ||
if ( assignmentRefactors.length ) { | ||
assignmentRefactors.push( Node.varDeclaration( node.kind, declarations ), ...unit.generate( Node.exprStmt( assignmentExpr ) ) ); | ||
return []; | ||
} | ||
assignmentRefactors.push( ...unit.generate( Node.exprStmt( assignmentExpr ) ) ); | ||
return declarations; | ||
} | ||
let isForLoopInit = context.currentUnit && [ 'ForStatement', 'ForOfStatement', 'ForInStatement' ].includes( context.currentUnit.type ); | ||
let declarations = node.declarations.reduce( ( declarators, declarator ) => { | ||
if ( isForLoopInit ) return declarators.concat( exec( context.currentUnit, declarator, true ) ); | ||
return context.defineUnit( def, unit => declarators.concat( exec( unit, declarator ) ) ); | ||
}, [] ); | ||
if ( !declarations.length ) return assignmentRefactors; | ||
return [ Node.varDeclaration( node.kind, declarations ), ...assignmentRefactors ]; | ||
} | ||
/** | ||
* ------------ | ||
* @IfStatement | ||
* ------------ | ||
*/ | ||
generateIfStatement( context, node ) { | ||
let def = { type: node.type }; | ||
return context.defineUnit( def, unit => { | ||
let { consequent, alternate } = node; | ||
let [ test ] = unit.signalReference( def, () => this.generateNodes( context, [ node.test ] ) ), | ||
[ $test, memo ] = context.defineMemo( { expr: test } ).generate(); | ||
consequent = context.createCondition( { when: memo }, () => this.generateNodes( context, [ node.consequent ] ) ); | ||
if ( consequent[ 0 ].type !== 'BlockStatement' && consequent.length > 1 ) { | ||
consequent = Node.blockStmt( consequent ); | ||
} else { | ||
consequent = consequent[ 0 ]; | ||
} | ||
if ( node.alternate ) { | ||
alternate = context.createCondition( { whenNot: memo }, () => this.generateNodes( context, [ node.alternate ] ) ); | ||
if ( alternate[ 0 ] && alternate[ 0 ].type !== 'BlockStatement' && alternate.length > 1 ) { | ||
alternate = Node.blockStmt( alternate ); | ||
} else { | ||
node = _visit( node, effect ); | ||
alternate = alternate[ 0 ]; | ||
} | ||
return _returns( node ); | ||
} | ||
this.setLocation( unit, node ); | ||
this.setLocation( memo, node.test ); | ||
return unit.generate( Node.ifStmt( $test, consequent, alternate ) ); | ||
} ); | ||
} | ||
/** | ||
* ------------ | ||
* #VariableDeclaration | ||
* ------------ | ||
*/ | ||
if ( node.type === 'VariableDeclaration' ) { | ||
let def = { type: node.type, kind: node.kind }; | ||
let assignmentRefactors = []; | ||
let isForLoopInit = effect && [ 'ForStatement', 'ForInStatement', 'ForOfStatement' ].includes( effect.type ); | ||
let exec = ( effect, declarator ) => { | ||
let initProduction, [ init ] = effect.causesProduction( def, production => ( initProduction = production, _transform( [ declarator.init ], effect ) ) ); | ||
let idProduction, [ id ] = effect.affectedsProduction( def, production => ( idProduction = production, _transform( [ declarator.id ], effect ) ) ); | ||
initProduction.setAssignee( idProduction ); | ||
this.setLocation( effect, declarator ); | ||
if ( !isForLoopInit && node.kind !== 'const' && declarator.init && initProduction.refs.size ) { | ||
// Strip out the init, and re-declare only the local identifiers | ||
// ID might be Array or Object pattern, so we're better off taking the resolved bare identifiers | ||
let isPattern = [ 'ObjectPattern', 'ArrayPattern' ].includes( declarator.id.type ); | ||
declarator = Array.from( idProduction.refs ).map( ref => astNodes.varDeclarator( astNodes.identifier( ref.path[ 0 ].name ), null ) ); | ||
// Now, covert the init part to an assignment expression | ||
let assignmentExpr = astNodes.assignmentExpr( id, init ); | ||
if ( isPattern ) { | ||
assignmentExpr = astNodes.sequenceExpr( [ assignmentExpr ] ); | ||
} | ||
assignmentRefactors.push( ...effect.compose( astNodes.exprStmt( assignmentExpr ) ) ); | ||
} else { | ||
// init might still have effects | ||
declarator = astNodes.varDeclarator( id, init ); | ||
} | ||
return declarator; | ||
/** | ||
* ------------ | ||
* @SwitchStatement | ||
* ------------ | ||
*/ | ||
generateSwitchStatement( context, node ) { | ||
let def = { type: node.type }; | ||
return context.defineUnit( def, unit => { | ||
let [ discriminant ] = unit.signalReference( def, () => this.generateNodes( context, [ node.discriminant ] ) ); | ||
let [ $discriminant, memo ] = context.defineMemo( { expr: discriminant } ).generate(); | ||
let $cases = context.createScope( def, () => node.cases.reduce( ( casesDef, caseNode ) => { | ||
let prevCaseDef = casesDef.slice( -1 )[ 0 ]; | ||
let hasBreak = caseNode.consequent.some( stmt => stmt.type === 'BreakStatement' ); | ||
let [ test ] = unit.signalReference( { type: caseNode.type }, () => this.generateNodes( context, [ caseNode.test ] ) ); | ||
let [ $test, _memo ] = context.defineMemo( { expr: test } ).generate(); | ||
let condition = { switch: memo, cases: [ _memo ] }; | ||
if ( prevCaseDef && !prevCaseDef.hasBreak ) { | ||
condition.cases.push( ...prevCaseDef.condition.cases ); | ||
} | ||
let declarations = node.declarations.reduce( ( declarators, declarator ) => { | ||
if ( isForLoopInit ) return declarators.concat( exec( effect, declarator ) ); | ||
return declarators.concat( scope.createEffect( def, effect => exec( effect, declarator ) ) ); | ||
}, [] ); | ||
return _returns( | ||
astNodes.varDeclaration( node.kind, declarations ), ...assignmentRefactors | ||
); | ||
} | ||
this.setLocation( _memo, caseNode.test ); | ||
return casesDef.concat( { caseNode, $test, condition, hasBreak } ); | ||
}, [] ).map( ( { caseNode, $test, condition } ) => { | ||
let consequent = context.createCondition( condition, () => this.generateNodes( context, caseNode.consequent ) ); | ||
return Node.switchCase( $test, consequent ); | ||
} ) ); | ||
/** | ||
* ------------ | ||
* #IfStatement | ||
* ------------ | ||
*/ | ||
if ( node.type === 'IfStatement' ) { | ||
let def = { type: node.type }; | ||
return scope.createEffect( def, effect => { | ||
let { consequent, alternate } = node; | ||
let [ test ] = effect.causesProduction( def, () => _transform( [ node.test ], effect ) ), | ||
[ $test, memo ] = effect.createMemo( { expr: test } ).compose(); | ||
consequent = effect.createCondition( { when: memo }, () => _transform( [ node.consequent ], effect ) ); | ||
if ( consequent[ 0 ].type !== 'BlockStatement' && consequent.length > 1 ) { | ||
consequent = astNodes.blockStmt( consequent ); | ||
} else { | ||
consequent = consequent[ 0 ]; | ||
} | ||
if ( node.alternate ) { | ||
alternate = effect.createCondition( { whenNot: memo }, () => _transform( [ node.alternate ], effect ) ); | ||
if ( alternate[ 0 ] && alternate[ 0 ].type !== 'BlockStatement' && alternate.length > 1 ) { | ||
alternate = astNodes.blockStmt( alternate ); | ||
} else { | ||
alternate = alternate[ 0 ]; | ||
} | ||
} | ||
this.setLocation( effect, node ); | ||
this.setLocation( memo, node.test ); | ||
return _returns( | ||
...effect.compose( astNodes.ifStmt( $test, consequent, alternate ) ) | ||
); | ||
} ); | ||
} | ||
this.setLocation( unit, node ); | ||
this.setLocation( memo, node.discriminant ); | ||
return unit.generate( Node.switchStmt( $discriminant, $cases ) ); | ||
} ); | ||
} | ||
/** | ||
* ------------ | ||
* #SwitchStatement | ||
* ------------ | ||
*/ | ||
if ( node.type === 'SwitchStatement' ) { | ||
let def = { type: node.type }; | ||
return scope.createEffect( def, effect => { | ||
let [ discriminant ] = effect.causesProduction( def, () => _transform( [ node.discriminant ], effect ) ); | ||
let [ $discriminant, memo ] = effect.createMemo( { expr: discriminant } ).compose(); | ||
let $cases = effect.createScope( def, casesScope => node.cases.reduce( ( casesDef, caseNode ) => { | ||
let prevCaseDef = casesDef.slice( -1 )[ 0 ]; | ||
let hasBreak = caseNode.consequent.some( stmt => stmt.type === 'BreakStatement' ); | ||
let [ test ] = effect.causesProduction( { type: caseNode.type }, () => _transform( [ caseNode.test ], effect ) ); | ||
let [ $test, _memo ] = effect.createMemo( { expr: test } ).compose(); | ||
let condition = { switch: memo, cases: [ _memo ] }; | ||
if ( prevCaseDef && !prevCaseDef.hasBreak ) { | ||
condition.cases.push( ...prevCaseDef.condition.cases ); | ||
} | ||
this.setLocation( _memo, caseNode.test ); | ||
return casesDef.concat( { caseNode, $test, condition, hasBreak } ); | ||
}, [] ).map( ( { caseNode, $test, condition } ) => { | ||
let consequent = effect.createCondition( condition, () => _transform( caseNode.consequent, null, casesScope ) ); | ||
return astNodes.switchCase( $test, consequent ); | ||
} ) ); | ||
/** | ||
* ------------ | ||
* @WhileStatement | ||
* @DoWhileStatement | ||
* @ForStatement | ||
* ------------ | ||
*/ | ||
generateWhileStatement( context, node ) { return this.generateLoopStmtA( Node.whileStmt, ...arguments ); } | ||
generateDoWhileStatement( context, node ) { return this.generateLoopStmtA( Node.doWhileStmt, ...arguments ); } | ||
generateForStatement( context, node ) { return this.generateLoopStmtA( Node.forStmt, ...arguments ); } | ||
generateLoopStmtA( generate, context, node ) { | ||
let def = { type: node.type }; | ||
return context.defineUnit( { type: node.type }, iteratorUnit => { | ||
this.setLocation( iteratorUnit, node ); | ||
iteratorUnit.defineSubscriptIdentifier( '$counter', [ '$x_index' ] ); | ||
// A scope for variables declared in header | ||
return context.createScope( { type: 'Iteration' }, () => { | ||
let createNodeCallback, init, test, update; | ||
if ( node.type === 'ForStatement' ) { | ||
[ init, test, update ] = iteratorUnit.signalReference( def, () => this.generateNodes( context, [ node.init, node.test, node.update ] ) ); | ||
createNodeCallback = $body => generate.call( Node, init, test, update, $body ); | ||
} else { | ||
[ test ] = iteratorUnit.signalReference( def, () => this.generateNodes( context, [ node.test ] ) ); | ||
createNodeCallback = $body => generate.call( Node, test, $body ); | ||
} | ||
this.setLocation( effect, node ); | ||
this.setLocation( memo, node.discriminant ); | ||
return _returns( | ||
...effect.compose( astNodes.switchStmt( $discriminant, $cases ) ) | ||
); | ||
} ); | ||
} | ||
/** | ||
* ------------ | ||
* #ForStatement | ||
* #WhileStatement | ||
* #DoWhileStatement | ||
* ------------ | ||
*/ | ||
if ( node.type === 'ForStatement' || node.type === 'WhileStatement' || node.type === 'DoWhileStatement' ) { | ||
let def = { type: node.type }; | ||
let createNode = astNodes[ node.type === 'ForStatement' ? 'forStmt' : ( node.type === 'WhileStatement' ? 'whileStmt' : 'doWhileStmt' ) ].bind( astNodes ); | ||
return initializeIteration( node, scope, ( declarationScope, iteratorEffect, iterationInstanceEffect ) => { | ||
this.setLocation( iteratorEffect, node ); | ||
this.setLocation( iterationInstanceEffect, node.body ); | ||
let createNodeCallback, init, test, update, body, preIterationDeclarations; | ||
if ( node.type === 'ForStatement' ) { | ||
[ init, test, update ] = iteratorEffect.causesProduction( def, () => _transform( [ node.init, node.test, node.update ], iteratorEffect, declarationScope ) ); | ||
[ body ] = _transform( [ node.body ], iterationInstanceEffect ); | ||
createNodeCallback = $body => createNode( init, test, update, $body ); | ||
} else { | ||
[ test ] = iteratorEffect.causesProduction( def, () => _transform( [ node.test ], $effect, declarationScope ) ); | ||
[ body ] = _transform( [ node.body ], iterationInstanceEffect ); | ||
createNodeCallback = $body => createNode( test, $body ); | ||
} | ||
return context.defineContext( { type: 'Iteration', isIteration: true }, iterationContext => { | ||
this.setLocation( iterationContext, node.body ); | ||
let preIterationDeclarations; | ||
let [ body ] = this.generateNodes( iterationContext, [ node.body ] ); | ||
if ( body.body.length ) { | ||
[ preIterationDeclarations, body ] = composeIteration( iterationInstanceEffect, body ); | ||
[ preIterationDeclarations, body ] = this.composeLoopStmt( iterationContext, body ); | ||
} | ||
let statements = [].concat( preIterationDeclarations || [] ).concat( createNodeCallback( astNodes.blockStmt( body ) ) ); | ||
return _returns( | ||
...iteratorEffect.composeWith( statements, [], () => iterationInstanceEffect.dispose() ) | ||
); | ||
let statements = [].concat( preIterationDeclarations || [] ).concat( createNodeCallback( Node.blockStmt( body ) ) ); | ||
return iteratorUnit.generate( statements ); | ||
} ); | ||
} | ||
/** | ||
* -------------- | ||
* #ForInStatement | ||
* #ForOfStatement | ||
* -------------- | ||
*/ | ||
if ( node.type === 'ForInStatement' || node.type === 'ForOfStatement' ) { | ||
let def = { type: node.type }; | ||
let createNode = astNodes[ node.type === 'ForInStatement' ? 'forInStmt' : 'forOfStmt'].bind( astNodes ); | ||
return initializeIteration( node, scope, ( declarationScope, iteratorEffect, iterationInstanceEffect ) => { | ||
this.setLocation( iteratorEffect, node ); | ||
this.setLocation( iterationInstanceEffect, node.body ); | ||
let left, iterationId; | ||
if ( node.left.type === 'VariableDeclaration' ) { | ||
[ left ] = _transform( [ node.left ], iteratorEffect, declarationScope ); | ||
iterationId = left.declarations[ 0 ].id; | ||
} else { | ||
[ left ] = iteratorEffect.affectedsProduction( def, () => _transform( [ node.left ], iteratorEffect, declarationScope ) ); | ||
iterationId = left; | ||
} | ||
let [ right ] = iteratorEffect.causesProduction( def, () => _transform( [ node.right ], iteratorEffect, declarationScope ) ); | ||
let [ body ] = _transform( [ node.body ], iterationInstanceEffect ); | ||
let forStatement = createNode( left, right, body ), | ||
} ); | ||
} ); | ||
} | ||
/** | ||
* -------------- | ||
* @composeLoopStmt | ||
* -------------- | ||
*/ | ||
composeLoopStmt( iterationContext, body, params = {} ) { | ||
let disposeCallbacks = [ params.disposeCallback ], disposeCallback = () => disposeCallbacks.forEach( callback => callback && callback() ); | ||
let preIterationDeclarations = [], iterationDeclarations = []; | ||
let iterationBody = [], unitBody = body.body.slice( 0 ); | ||
// Counter? | ||
if ( !params.iterationId ) { | ||
params.iterationId = Node.identifier( iterationContext.getSubscriptIdentifier( '$counter', true ) ); | ||
let counterInit = Node.varDeclarator( Node.clone( params.iterationId ), Node.literal( -1 ) ); | ||
let counterIncr = Node.updateExpr( '++', Node.clone( params.iterationId ), false ); | ||
preIterationDeclarations.push( counterInit ); | ||
iterationBody.push( counterIncr ); | ||
// On dispose | ||
disposeCallbacks.push( () => iterationBody.pop() /* counterIncr */ ); | ||
} | ||
// The iterationDeclarations | ||
if ( iterationDeclarations.length ) { | ||
iterationBody.push( Node.varDeclaration( 'let', iterationDeclarations ) ); | ||
disposeCallbacks.push( () => iterationBody.splice( -1 ) /* iterationDeclarations */ ); | ||
} | ||
// Main | ||
iterationBody.push( ...iterationContext.generate( unitBody, { args: [ params.iterationId ], disposeCallback } ) ); | ||
// Convert to actual declaration | ||
if ( preIterationDeclarations.length ) { | ||
preIterationDeclarations = Node.varDeclaration( 'let', preIterationDeclarations ); | ||
} else { | ||
preIterationDeclarations = null; | ||
} | ||
return [ preIterationDeclarations, iterationBody ]; | ||
} | ||
/** | ||
* -------------- | ||
* @ForOfStatement | ||
* @ForInStatement | ||
* -------------- | ||
*/ | ||
generateForOfStatement( context, node ) { return this.generateLoopStmtB( Node.forOfStmt, ...arguments ); } | ||
generateForInStatement( context, node ) { return this.generateLoopStmtB( Node.forInStmt, ...arguments ); } | ||
generateLoopStmtB( generate, context, node ) { | ||
let def = { type: node.type }; | ||
return context.defineUnit( { type: node.type }, iteratorUnit => { | ||
this.setLocation( iteratorUnit, node ); | ||
iteratorUnit.defineSubscriptIdentifier( '$counter', [ node.type === 'ForInStatement' ? '$x_key' : '$x_index' ] ); | ||
// A scope for variables declared in header | ||
return context.createScope( { type: 'Iteration' }, () => { | ||
let left, iterationId; | ||
if ( node.left.type === 'VariableDeclaration' ) { | ||
[ left ] = this.generateNodes( context, [ node.left ] ); | ||
iterationId = left.declarations[ 0 ].id; | ||
} else { | ||
[ left ] = iteratorUnit.affectedsReference( def, () => this.generateNodes( context, [ node.left ] ) ); | ||
iterationId = left; | ||
} | ||
let [ right ] = iteratorUnit.signalReference( def, () => this.generateNodes( context, [ node.right ] ) ); | ||
return context.defineContext( { type: 'Iteration', isIteration: true }, iterationContext => { | ||
this.setLocation( iterationContext, node.body ); | ||
let [ body ] = this.generateNodes( iterationContext, [ node.body ] ); | ||
let forStatement = generate.call( Node, left, right, body ), | ||
preIterationDeclarations = []; | ||
if ( body.body.length ) { | ||
let composeIterationWith = params => composeIteration( iterationInstanceEffect, body, { | ||
let composeIterationWith = params => this.composeLoopStmt( iterationContext, body, { | ||
disposeCallback: () => { forStatement.left = left; }, | ||
@@ -284,440 +435,444 @@ ...params, | ||
// Its a forIn statement with a destructuring left side | ||
let iteration$counter = astNodes.identifier( declarationScope.getSubscriptIdentifier( '$counter' ) ); | ||
let iteration$counter = Node.identifier( context.getSubscriptIdentifier( '$counter' ) ); | ||
[ preIterationDeclarations, newBody ] = composeIterationWith( { iterationId: iteration$counter } ); | ||
// We'll use a plain Identifier as left | ||
newLeft = astNodes.varDeclaration( 'let', [ astNodes.varDeclarator( astNodes.clone( iteration$counter ), null ) ] ); | ||
newLeft = Node.varDeclaration( 'let', [ Node.varDeclarator( Node.clone( iteration$counter ), null ) ] ); | ||
// While we replicate the original left and send it into the start of body | ||
let leftReDeclaration; | ||
if ( node.left.type === 'VariableDeclaration' ) { | ||
leftReDeclaration = astNodes.varDeclaration( left.kind, [ astNodes.varDeclarator( iterationId, astNodes.clone( iteration$counter ) ) ] ); | ||
leftReDeclaration = Node.varDeclaration( left.kind, [ Node.varDeclarator( iterationId, Node.clone( iteration$counter ) ) ] ); | ||
} else { | ||
leftReDeclaration = astNodes.exprStmt( astNodes.sequenceExpr( [ astNodes.assignmentExpr( iterationId, astNodes.clone( iteration$counter ), '=' ) ] ) ); | ||
leftReDeclaration = Node.exprStmt( Node.sequenceExpr( [ Node.assignmentExpr( iterationId, Node.clone( iteration$counter ), '=' ) ] ) ); | ||
} | ||
newBody = [ leftReDeclaration ].concat( newBody ); | ||
} | ||
forStatement = createNode( newLeft, right, astNodes.blockStmt( newBody ) ); | ||
forStatement = generate.call( Node, newLeft, right, Node.blockStmt( newBody ) ); | ||
} | ||
return _returns( | ||
...iteratorEffect.composeWith( [].concat( preIterationDeclarations || [] ).concat( forStatement ), [], () => iterationInstanceEffect.dispose() ) | ||
); | ||
return iteratorUnit.generate( [].concat( preIterationDeclarations || [] ).concat( forStatement ) ); | ||
} ); | ||
} | ||
/** | ||
* ------------ | ||
* #LabeledStatement | ||
* ------------ | ||
*/ | ||
if ( node.type === 'LabeledStatement' ) { | ||
let def = { type: node.type, label: node.label }; | ||
return scope.createEffect( def, effect => { | ||
effect.subscriptIdentifiersNoConflict( node.label ); | ||
if ( !node.body.type.endsWith( 'Statement' ) ) { | ||
this.setLocation( effect, node.body ); | ||
let [ body ] = _transform( [ node.body ], effect ); | ||
return _returns( astNodes.labeledStmt( node.label, effect.compose( body ) ) ); | ||
} | ||
return effect.createScope( { type: node.body.type, label: node.label }, scope => { | ||
if ( node.body.type === 'BlockStatement' ) { | ||
let body = _transform( node.body.body, null, scope ); | ||
return _returns( astNodes.labeledStmt( node.label, astNodes.blockStmt( body ) ) ); | ||
} | ||
let [ body ] = _transform( [ node.body ], null, scope ); | ||
return _returns( astNodes.labeledStmt( node.label, body ) ); | ||
} ); | ||
} ); | ||
} | ||
} ); | ||
} ); | ||
} | ||
/** | ||
* ------------ | ||
* #BreakStatement | ||
* #ContinueStatement | ||
* ------------ | ||
*/ | ||
if ( node.type === 'BreakStatement' || node.type === 'ContinueStatement' ) { | ||
let createNode = astNodes[ node.type === 'BreakStatement' ? 'breakStmt' : 'continueStmt'].bind( astNodes ); | ||
let [ nearestExitTarget, tt ] = [ 'Iteration', 'SwitchStatement', 'LabeledStatement' ].reduce( ( [ prevType, prevLevel ], type ) => { | ||
let level = scope.currentEffect.inContext( type ); | ||
if ( !prevType || ( level > -1 && level < prevLevel ) ) return [ type, level ]; | ||
return [ prevType, prevLevel ]; | ||
}, [] ); | ||
if ( nearestExitTarget === 'SwitchStatement' && node.type === 'BreakStatement' && !node.label ) { | ||
return _returns( createNode( null ) ); | ||
} | ||
let subscript$construct = astNodes.identifier( scope.currentEffect.getSubscriptIdentifier( '$construct', true ) ); | ||
let keyword = astNodes.literal( node.type === 'BreakStatement' ? 'break' : 'continue' ); | ||
let label = node.label ? astNodes.literal( node.label.name ) : astNodes.identifier( 'null' ); | ||
let exitCall = astNodes.exprStmt( | ||
astNodes.callExpr( astNodes.memberExpr( subscript$construct, astNodes.identifier( 'exit' ) ), [ keyword, label ] ), | ||
); | ||
// Break / continue statement hoisting | ||
scope.currentEffect.hoistExitStatement( keyword, label ); | ||
// effect.subscriptIdentifiersNoConflict() wont be necessary | ||
// as the label definition would have had the same earlier | ||
return _returns( exitCall, astNodes.returnStmt() ); | ||
/** | ||
* ------------ | ||
* @LabeledStatement | ||
* ------------ | ||
*/ | ||
generateLabeledStatement( context, node ) { | ||
context.subscriptIdentifiersNoConflict( node.label ); | ||
let def = { type: node.type, label: node.label }; | ||
if ( !node.body.type.endsWith( 'Statement' ) ) { | ||
return context.defineUnit( def, unit => { | ||
this.setLocation( unit, node.body ); | ||
let [ body ] = this.generateNodes( context, [ node.body ] ); | ||
return Node.labeledStmt( node.label, unit.generate( body ) ); | ||
} ); | ||
} | ||
return context.createScope( { type: node.body.type, label: node.label }, scope => { | ||
if ( node.body.type === 'BlockStatement' ) { | ||
let body = this.generateNodes( context, node.body.body ); | ||
return Node.labeledStmt( node.label, Node.blockStmt( body ) ); | ||
} | ||
scope.singleStatementScope = true; | ||
let [ body ] = this.generateNodes( context, [ node.body ] ); | ||
return Node.labeledStmt( node.label, body ); | ||
} ); | ||
} | ||
/** | ||
* ------------ | ||
* #ReturnStatement | ||
* ------------ | ||
*/ | ||
if ( node.type === 'ReturnStatement' ) { | ||
if ( scope.static() && 0 ) { | ||
let [ argument ] = _transform( [ node.argument ], scope.currentEffect || scope.ownerEffect /* This is a statement that could have had its own effect */ ); | ||
return _returns( astNodes.returnStmt( argument ) ); | ||
} | ||
let def = { type: node.type }; | ||
return scope.createEffect( def, effect => { | ||
let [ argument ] = effect.causesProduction( def, () => _transform( [ node.argument ], effect ) ); | ||
let subscript$construct = astNodes.identifier( effect.getSubscriptIdentifier( '$construct', true ) ); | ||
let keyword = astNodes.literal( 'return' ); | ||
let arg = argument || astNodes.identifier( 'undefined' ); | ||
let exitCall = astNodes.exprStmt( | ||
astNodes.callExpr( astNodes.memberExpr( subscript$construct, astNodes.identifier( 'exit' ) ), [ keyword, arg ] ), | ||
); | ||
// Return statement hoisting | ||
effect.hoistExitStatement( keyword, astNodes.identifier( 'true' ) ); | ||
return _returns( ...effect.compose( [ exitCall, astNodes.returnStmt() ] ) ); | ||
} ); | ||
} | ||
/** | ||
* ------------ | ||
* @BreakStatement | ||
* @ContinueStatement | ||
* ------------ | ||
*/ | ||
generateBreakStatement( context, node ) { return this.generateExitStmt( Node.breakStmt, ...arguments ); } | ||
generateContinueStatement( context, node ) { return this.generateExitStmt( Node.continueStmt, ...arguments ); } | ||
generateExitStmt( generate, context, node ) { | ||
let nearestExitTarget = context.currentUnit.closest( [ 'Iteration', 'SwitchStatement', 'LabeledStatement' ] ); | ||
if ( nearestExitTarget && nearestExitTarget.type === 'SwitchStatement' && node.type === 'BreakStatement' && !node.label ) { | ||
return generate.call( Node, null ); | ||
} | ||
let subscript$unit = Node.identifier( context.getSubscriptIdentifier( '$unit', true ) ); | ||
let keyword = Node.literal( node.type === 'BreakStatement' ? 'break' : 'continue' ); | ||
let label = node.label ? Node.literal( node.label.name ) : Node.identifier( 'null' ); | ||
let exitCall = Node.exprStmt( | ||
Node.callExpr( Node.memberExpr( subscript$unit, Node.identifier( 'exit' ) ), [ keyword, label ] ), | ||
); | ||
// Break / continue statement hoisting | ||
context.currentUnit.hoistExitStatement( keyword, label ); | ||
// unit.subscriptIdentifiersNoConflict() wont be necessary | ||
// as the label definition would have had the same earlier | ||
return [ exitCall, Node.returnStmt() ]; | ||
} | ||
/** | ||
* ------------ | ||
* #BlockStatement | ||
* ------------ | ||
*/ | ||
if ( node.type === 'BlockStatement' ) { | ||
let __transform = scope => { | ||
let body = _transform( node.body, null, scope ); | ||
return _returns( astNodes.blockStmt( body ) ); | ||
}; | ||
if ( effect ) { | ||
// This block statement is from an effect | ||
// context, such as an IfStatement | ||
let def = { type: node.type }; | ||
return effect.createScope( def, __transform ); | ||
} | ||
// From a scope context | ||
return scope.createBlock( blockScope => __transform( blockScope ) ); | ||
} | ||
/** | ||
* ------------ | ||
* @ReturnStatement | ||
* ------------ | ||
*/ | ||
generateReturnStatement( context, node ) { | ||
let def = { type: node.type }; | ||
return context.defineUnit( def, unit => { | ||
let [ argument ] = unit.signalReference( def, () => this.generateNodes( context, [ node.argument ] ) ); | ||
let subscript$unit = Node.identifier( context.getSubscriptIdentifier( '$unit', true ) ); | ||
let keyword = Node.literal( 'return' ); | ||
let arg = argument || Node.identifier( 'undefined' ); | ||
let exitCall = Node.exprStmt( | ||
Node.callExpr( Node.memberExpr( subscript$unit, Node.identifier( 'exit' ) ), [ keyword, arg ] ), | ||
); | ||
// Return statement hoisting | ||
unit.hoistExitStatement( keyword, Node.identifier( 'true' ) ); | ||
return unit.generate( [ exitCall, Node.returnStmt() ] ); | ||
} ); | ||
} | ||
/** | ||
* ------------ | ||
* #ExpressionStatement | ||
* ------------ | ||
*/ | ||
if (node.type === 'ExpressionStatement') { | ||
let def = { type: node.type }; | ||
return scope.createEffect( def, effect => { | ||
let [ expression ] = effect.causesProduction( def, () => _transform( [ node.expression ], effect ) ); | ||
this.setLocation( effect, node.expression ); | ||
return _returns( | ||
...effect.compose( astNodes.exprStmt( expression ) ) | ||
); | ||
} ); | ||
} | ||
/** | ||
* ------------ | ||
* @BlockStatement | ||
* ------------ | ||
*/ | ||
generateBlockStatement( context, node ) { | ||
// From a scope context | ||
return context.createScope( { type: node.type }, () => { | ||
let body = this.generateNodes( context, node.body ); | ||
return Node.blockStmt( body ); | ||
} ); | ||
} | ||
/** | ||
* ------------ | ||
* #AssignmentExpression | ||
* ------------ | ||
*/ | ||
if ( node.type === 'AssignmentExpression' ) { | ||
let def = { type: node.type, kind: node.operator }; | ||
let rightProduction, [ right ] = effect.causesProduction( def, production => ( rightProduction = production, _transform( [ node.right ] ) ) ); | ||
let leftProduction, [ left ] = effect.embeddableAffectedsProduction( def, production => ( leftProduction = production, _transform( [ node.left ] ) ) ); | ||
rightProduction.setAssignee( leftProduction ); | ||
return _returns( | ||
astNodes.assignmentExpr( left, right, node.operator ) | ||
); | ||
} | ||
/** | ||
* ------------ | ||
* @ExpressionStatement | ||
* ------------ | ||
*/ | ||
generateExpressionStatement( context, node ) { | ||
if ( node.expression.type === 'SequenceExpression' ) { | ||
return Node.exprStmt( this.generateSequenceExpression( context, node.expression ) ); | ||
} | ||
let def = { type: node.type }; | ||
return context.defineUnit( def, unit => { | ||
this.setLocation( unit, node.expression ); | ||
let [ expression ] = unit.signalReference( def, () => this.generateNodes( context, [ node.expression ] ) ); | ||
return unit.generate( Node.exprStmt( expression ) ); | ||
} ); | ||
} | ||
/** | ||
* ------------ | ||
* #UpdateExpression | ||
* #UnaryExpression (delete) | ||
* ------------ | ||
*/ | ||
if ( node.type === 'UpdateExpression' || ( node.type === 'UnaryExpression' && node.operator === 'delete' ) ) { | ||
let def = { type: node.type, kind: node.operator }; | ||
let createNode = astNodes[ node.type === 'UpdateExpression' ? 'updateExpr' : 'unaryExpr'].bind( astNodes ); | ||
let [ argument ] = effect.affectedsProduction( def, () => _transform( [ node.argument ] ) ); | ||
return _returns( | ||
createNode( node.operator, argument, node.prefix ) | ||
); | ||
} | ||
/* ------------------------------ */ | ||
/* ------------------------------ */ | ||
/** | ||
* ------------ | ||
* #LogicalExpression | ||
* ------------ | ||
*/ | ||
if ( node.type === 'LogicalExpression' ) { | ||
let def = { type: node.type, kind: node.operator }; | ||
let [ left ] = effect.chainableCausesProduction( def, () => _transform( [ node.left ] ) ); | ||
let [ $left, memo ] = effect.createMemo( { expr: left } ).compose(); | ||
let conditionAdjacent = node.operator === '||' ? { whenNot: memo } : { when: memo }; | ||
let [ right ] = effect.createCondition( conditionAdjacent, () => { | ||
return effect.chainableCausesProduction( def, () => _transform( [ node.right ] ) ); | ||
} ); | ||
this.setLocation( memo, node.left ); | ||
return _returns( | ||
astNodes.logicalExpr( node.operator, $left, right ) | ||
); | ||
} | ||
/** | ||
* ------------ | ||
* @SequenceExpression | ||
* ------------ | ||
*/ | ||
generateSequenceExpression( context, node ) { | ||
let expresions = node.expressions.map( ( expr, i ) => { | ||
let def = { type: expr.type, inSequence: true }; | ||
return context.defineUnit( def, unit => { | ||
this.setLocation( unit, expr ); | ||
if ( i === node.expressions.length - 1 ) { | ||
[ expr ] = unit.chainableReference( def, () => this.generateNodes( context, [ expr ] ) ); | ||
} else { | ||
[ expr ] = unit.signalReference( def, () => this.generateNodes( context, [ expr ] ) ); | ||
} | ||
return expr.type === 'Identifier' ? expr : unit.generate( expr ); | ||
} ); | ||
} ); | ||
return Node.sequenceExpr( expresions ); | ||
} | ||
/** | ||
* ------------ | ||
* #ConditionalExpression | ||
* ------------ | ||
*/ | ||
if ( node.type === 'ConditionalExpression' ) { | ||
let def = { type: node.type }; | ||
let [ test ] = effect.causesProduction( def, () => _transform( [ node.test ] ) ), | ||
[ $test, memo ] = effect.createMemo( { expr: test } ).compose(), | ||
[ consequent ] = effect.createCondition( { when: memo }, () => effect.chainableCausesProduction( def, () => _transform( [ node.consequent ] ) ) ), | ||
[ alternate ] = effect.createCondition( { whenNot: memo }, () => effect.chainableCausesProduction( def, () => _transform( [ node.alternate ] ) ) ); | ||
this.setLocation( memo, node.test ); | ||
return _returns( | ||
astNodes.condExpr( $test, consequent, alternate ) | ||
); | ||
} | ||
/** | ||
* ------------ | ||
* @AssignmentExpression | ||
* ------------ | ||
*/ | ||
generateAssignmentExpression( context, node ) { | ||
let def = { type: node.type, kind: node.operator }; | ||
let rightReference, [ right ] = context.currentUnit.signalReference( def, reference => ( rightReference = reference, this.generateNodes( context, [ node.right ] ) ) ); | ||
let leftReference, [ left ] = context.currentUnit.embeddableEffectReference( def, reference => ( leftReference = reference, this.generateNodes( context, [ node.left ] ) ) ); | ||
rightReference.setAssignee( leftReference ); | ||
return Node.assignmentExpr( left, right, node.operator ); | ||
} | ||
/** | ||
* ------------ | ||
* #SequenceExpression | ||
* ------------ | ||
*/ | ||
if ( node.type === 'SequenceExpression' ) { | ||
let expresions = node.expressions.map( ( expr, i ) => { | ||
let def = { type: expr.type }; | ||
if ( i === node.expressions.length - 1 ) { | ||
[ expr ] = effect.chainableCausesProduction( def, () => _transform( [ expr ] ) ); | ||
} else { | ||
[ expr ] = effect.causesProduction( def, () => _transform( [ expr ] ) ); | ||
} | ||
return expr; | ||
} ); | ||
return _returns( | ||
astNodes.sequenceExpr( expresions ) | ||
); | ||
} | ||
/** | ||
* ------------ | ||
* @UpdateExpression | ||
* @UnaryExpression (delete) | ||
* ------------ | ||
*/ | ||
generateUpdateExpression( context, node ) { return this.generateMutationExpr( Node.updateExpr, ...arguments ); } | ||
generateUnaryExpression( context, node ) { | ||
if ( node.operator === 'delete' ) return this.generateMutationExpr( Node.unaryExpr, ...arguments ); | ||
let def = { type: node.type, kind: node.operator }; | ||
let [ argument ] = context.currentUnit.signalReference( def, () => this.generateNodes( context, [ node.argument ] ) ); | ||
return Node.unaryExpr( node.operator, argument, node.prefix ); | ||
} | ||
generateMutationExpr( generate, context, node ) { | ||
let def = { type: node.type, kind: node.operator }; | ||
let [ argument ] = context.currentUnit.effectReference( def, () => this.generateNodes( context, [ node.argument ] ) ); | ||
return generate.call( Node, node.operator, argument, node.prefix ); | ||
} | ||
/** | ||
* ------------ | ||
* #ArrayPattern | ||
* ------------ | ||
*/ | ||
if ( node.type === 'ArrayPattern' ) { | ||
let elements = node.elements.map( ( element, i ) => { | ||
[ element ] = effect.currentProduction.withDestructure( { name: i }, () => _transform( [ element ] ) ); | ||
return element; | ||
} ); | ||
return _returns( | ||
astNodes.arrayPattern( elements ) | ||
); | ||
} | ||
/** | ||
* ------------ | ||
* @BinaryExpression | ||
* ------------ | ||
*/ | ||
generateBinaryExpression( context, node ) { | ||
let [ left ] = context.currentUnit.signalReference( { type: node.type }, () => this.generateNodes( context, [ node.left ] ) ); | ||
let [ right ] = context.currentUnit.signalReference( { type: node.type }, () => this.generateNodes( context, [ node.right ] ) ); | ||
return Node.binaryExpr( node.operator, left, right ); | ||
} | ||
/** | ||
* ------------ | ||
* #ObjectPattern | ||
* ------------ | ||
*/ | ||
if ( node.type === 'ObjectPattern' ) { | ||
let properties = node.properties.map( property => { | ||
let { key, value } = property; | ||
if ( property.computed ) { | ||
[ key ] = effect.causesProduction( { type: key.type }, () => _transform( [ key ] ) ); | ||
} | ||
let element = { name: property.key.name }; | ||
if ( property.computed ) { | ||
if ( property.key.type === 'Literal' ) { | ||
element = { name: property.key.value }; | ||
} else { | ||
[ key, element ] = effect.createMemo( { expr: key } ).compose(); | ||
} | ||
} | ||
[ value ] = effect.currentProduction.withDestructure( element, () => _transform( [ value ] ) ); | ||
this.setLocation( element, property.key ); | ||
return astNodes.property( key, value, property.kind, property.shorthand, property.computed, property.method ); | ||
} ); | ||
return _returns( | ||
astNodes.objectPattern( properties ) | ||
); | ||
} | ||
/** | ||
* ------------ | ||
* @LogicalExpression | ||
* ------------ | ||
*/ | ||
generateLogicalExpression( context, node ) { | ||
let def = { type: node.type, kind: node.operator }; | ||
let [ left ] = context.currentUnit.chainableReference( def, () => this.generateNodes( context, [ node.left ] ) ); | ||
let [ $left, memo ] = context.defineMemo( { expr: left } ).generate(); | ||
let conditionAdjacent = node.operator === '||' ? { whenNot: memo } : { when: memo }; | ||
let [ right ] = context.createCondition( conditionAdjacent, () => { | ||
return context.currentUnit.chainableReference( def, () => this.generateNodes( context, [ node.right ] ) ) | ||
} ); | ||
this.setLocation( memo, node.left ); | ||
return Node.logicalExpr( node.operator, $left, right ); | ||
} | ||
/** | ||
* ------------ | ||
* #MemberExpression | ||
* ------------ | ||
*/ | ||
if ( node.type === 'MemberExpression' ) { | ||
let { property } = node; | ||
if ( node.computed ) { | ||
[ property ] = effect.causesProduction( { type: property.type }, () => _transform( [ property ] ) ); | ||
} | ||
let element = { name: node.property.name }; | ||
if ( node.computed ) { | ||
if ( node.property.type === 'Literal' ) { | ||
element = { name: node.property.value }; | ||
} else { | ||
[ property, element ] = effect.createMemo( { expr: property } ).compose(); | ||
} | ||
} | ||
let [ object ] = effect.currentProduction.withProperty( element, () => _transform( [ node.object ] ) ); | ||
this.setLocation( element, node.property ); | ||
return _returns( | ||
astNodes.memberExpr( object, property, node.computed, node.optional ) | ||
); | ||
} | ||
/** | ||
* ------------ | ||
* @ConditionalExpression | ||
* ------------ | ||
*/ | ||
generateConditionalExpression( context, node ) { | ||
let def = { type: node.type }; | ||
let [ test ] = context.currentUnit.signalReference( def, () => this.generateNodes( context, [ node.test ] ) ), | ||
[ $test, memo ] = context.defineMemo( { expr: test } ).generate(), | ||
[ consequent ] = context.createCondition( { when: memo }, () => context.currentUnit.chainableReference( def, () => this.generateNodes( context, [ node.consequent ] ) ) ), | ||
[ alternate ] = context.createCondition( { whenNot: memo }, () => context.currentUnit.chainableReference( def, () => this.generateNodes( context, [ node.alternate ] ) ) ); | ||
this.setLocation( memo, node.test ); | ||
return Node.condExpr( $test, consequent, alternate ); | ||
} | ||
/** | ||
* ------------ | ||
* #Identifier | ||
* #ThisExpression | ||
* ------------ | ||
*/ | ||
if ( node.type === 'Identifier' || node.type === 'ThisExpression' ) { | ||
let createNode = () => node.type === 'Identifier' ? astNodes.identifier( node.name ) : astNodes.thisExpr(); | ||
// How we'll know Identifiers within script | ||
if ( node.type === 'Identifier' ) { | ||
effect.subscriptIdentifiersNoConflict( node ); | ||
} | ||
let $identifier = { | ||
name: node.type === 'Identifier' ? node.name : 'this', | ||
}; | ||
this.setLocation( $identifier, node ); | ||
if ( effect ) { | ||
let production = effect.currentProduction; | ||
if ( production ) { | ||
do { | ||
production.addRef().unshift( $identifier ); | ||
} while( production = production.contextProduction ); | ||
} | ||
} | ||
return _returns( createNode() ); | ||
/** | ||
* ------------ | ||
* @ArrayPattern | ||
* ------------ | ||
*/ | ||
generateArrayPattern( context, node ) { | ||
let elements = node.elements.map( ( element, i ) => { | ||
[ element ] = context.currentUnit.currentReference.withDestructure( { name: i }, () => this.generateNodes( context, [ element ] ) ); | ||
return element; | ||
} ); | ||
return Node.arrayPattern( elements ); | ||
} | ||
/** | ||
* ------------ | ||
* @ObjectPattern | ||
* ------------ | ||
*/ | ||
generateObjectPattern( context, node ) { | ||
let properties = node.properties.map( property => { | ||
let { key, value } = property; | ||
if ( property.computed ) { | ||
[ key ] = context.currentUnit.signalReference( { type: key.type }, () => this.generateNodes( context, [ key ] ) ); | ||
} | ||
/** | ||
* ------------ | ||
* #Other | ||
* ------------ | ||
*/ | ||
let _node = node; | ||
if ( node.type === 'TryStatement' ) { | ||
let [ block, handler, finalizer ] = _transform( [ node.block, node.handler, node.finalizer ], scope.currentEffect /* This is a statement that could have had its own effect */ ); | ||
_node = astNodes.tryStmt( block, handler, finalizer ); | ||
} else if ( node.type === 'CatchClause' ) { | ||
let [ body ] = _transform( [ node.body ], scope.currentEffect /* This is a statement that could have had its own effect */ ); | ||
_node = astNodes.catchClause( node.param, body ); | ||
} else if ( [ 'ThrowStatement', 'AwaitExpression', 'SpreadElement', 'UnaryExpression' ].includes( node.type ) ) { | ||
let [ argument ] = effect.causesProduction( { type: node.type }, () => _transform( [ node.argument ], scope.currentEffect || scope.ownerEffect /* This is a statement that could have had its own effect */ ) ); | ||
if ( node.type === 'ThrowStatement' ) { | ||
_node = astNodes.throwStmt( argument ); | ||
} else if ( node.type === 'AwaitExpression' ) { | ||
_node = astNodes.awaitExpr( argument ); | ||
// AsyncAwait hoisting | ||
effect.hoistAwaitKeyword(); | ||
} else if ( node.type === 'SpreadElement' ) { | ||
_node = astNodes.spreadElem( argument ); | ||
let element = { name: property.key.name }; | ||
if ( property.computed ) { | ||
if ( property.key.type === 'Literal' ) { | ||
element = { name: property.key.value }; | ||
} else { | ||
_node = astNodes.unaryExpr( node.operator, argument, node.prefix ); | ||
[ key, element ] = context.defineMemo( { expr: key } ).generate(); | ||
} | ||
} else if ( node.type === 'BinaryExpression' ) { | ||
let [ left ] = effect.causesProduction( { type: node.type }, () => _transform( [ node.left ] ) ); | ||
let [ right ] = effect.causesProduction( { type: node.type }, () => _transform( [ node.right ] ) ); | ||
_node = astNodes.binaryExpr( node.operator, left, right ); | ||
} else if ( [ 'CallExpression', 'NewExpression' ].includes( node.type ) ) { | ||
// The ongoing production must be used for callee | ||
let [ callee ] = effect.currentProduction.with( { isCallee: true, callType: node.type }, () => _transform( [ node.callee ] ) ); | ||
let args = node.arguments.map( argument => effect.causesProduction( { type: argument.type }, () => _transform( [ argument ] )[ 0 ] ) ); | ||
if ( node.type === 'CallExpression' ) { | ||
_node = astNodes.callExpr( callee, args, node.optional ); | ||
} else { | ||
_node = astNodes.newExpr( callee, args ); | ||
} | ||
} else if ( [ 'ParenthesizedExpression', 'ChainExpression' ].includes( node.type ) ) { | ||
// The ongoing production must be used for these | ||
let [ expresion ] = _transform( [ node.expression ] ); | ||
if ( node.type === 'ParenthesizedExpression' ) { | ||
_node = astNodes.parensExpr( expresion ); | ||
} else { | ||
_node = astNodes.chainExpr( expresion ); | ||
} | ||
} else if ( node.type === 'ArrayExpression' ) { | ||
let elements = node.elements.map( element => effect.causesProduction( { type: element.type }, () => _transform( [ element ] )[ 0 ] ) ); | ||
_node = astNodes.arrayExpr( elements ); | ||
} else if ( node.type === 'ObjectExpression' ) { | ||
let properties = _transform( node.properties ); | ||
_node = astNodes.objectExpr( properties ); | ||
} else if ( node.type === 'Property' ) { | ||
let { key, value } = node; | ||
if ( node.computed ) { | ||
[ key ] = effect.causesProduction( { type: key.type }, () => _transform( [ key ] ) ); | ||
} | ||
[ value ] = effect.causesProduction( { type: value.type }, () => _transform( [ value ] ) ); | ||
_node = astNodes.property( key, value, node.kind, node.shorthand, node.computed, node.method ); | ||
} else if ( node.type === 'TaggedTemplateExpression' ) { | ||
let [ tag, quasi ] = effect.causesProduction( { type: node.type }, () => _transform( [ node.tag, node.quasi ] ) ); | ||
_node = astNodes.taggedTemplateExpr( tag, quasi ); | ||
} else if ( node.type === 'TemplateLiteral' ) { | ||
let expressions = node.expressions.map( expression => effect.causesProduction( { type: node.type }, () => _transform( [ expression ] )[ 0 ] ) ); | ||
_node = astNodes.templateLiteral( node.quasis, expressions ); | ||
} else if ( node.type === 'Literal' ) { | ||
_node = astNodes.clone( node ); | ||
} | ||
[ value ] = context.currentUnit.currentReference.withDestructure( element, () => this.generateNodes( context, [ value ] ) ); | ||
this.setLocation( element, property.key ); | ||
return Node.property( key, value, property.kind, property.shorthand, property.computed, property.method ); | ||
} ); | ||
return Node.objectPattern( properties ); | ||
} | ||
return _returns( _node ); | ||
/** | ||
* ------------ | ||
* @MemberExpression | ||
* ------------ | ||
*/ | ||
generateMemberExpression( context, node ) { | ||
let { property } = node; | ||
if ( node.computed ) { | ||
[ property ] = context.currentUnit.signalReference( { type: property.type }, () => this.generateNodes( context, [ property ] ) ); | ||
} | ||
let element = { name: node.property.name }; | ||
if ( node.computed ) { | ||
if ( node.property.type === 'Literal' ) { | ||
element = { name: node.property.value }; | ||
} else { | ||
[ property, element ] = context.defineMemo( { expr: property } ).generate(); | ||
} | ||
} | ||
let [ object ] = context.currentUnit.currentReference.withProperty( element, () => this.generateNodes( context, [ node.object ] ) ); | ||
this.setLocation( element, node.property ); | ||
return Node.memberExpr( object, property, node.computed, node.optional ); | ||
} | ||
}, [] ); | ||
/** | ||
* ------------ | ||
* @Identifier | ||
* @ThisExpression | ||
* ------------ | ||
*/ | ||
generateThisExpression( context, node ) { return this.generateIdentifier( ...arguments ); } | ||
generateIdentifier( context, node ) { | ||
let createNode = () => node.type === 'Identifier' ? Node.identifier( node.name ) : Node.thisExpr(); | ||
// How we'll know Identifiers within script | ||
if ( node.type === 'Identifier' ) { | ||
context.subscriptIdentifiersNoConflict( node ); | ||
} | ||
let $identifier = { | ||
name: node.type === 'Identifier' ? node.name : 'this', | ||
}; | ||
this.setLocation( $identifier, node ); | ||
const closestFunction = context.closestFunction(); | ||
let reference = ( context.currentUnit || context ).currentReference; | ||
if ( reference ) { | ||
do { | ||
if ( !closestFunction || closestFunction.isSubscriptFunction || ( reference instanceof EffectReference ) ) { | ||
reference.addRef().unshift( $identifier ); | ||
} | ||
} while( reference = reference.contextReference ); | ||
} | ||
return createNode(); | ||
} | ||
} | ||
/** | ||
* ------------ | ||
* @SpreadElement | ||
* @AwaitExpression | ||
* ------------ | ||
*/ | ||
generateSpreadElement( context, node ) { return this.generateArgumentExpr( Node.spreadElement, ...arguments ); } | ||
generateAwaitExpression( context, node ) { | ||
context.currentUnit.hoistAwaitKeyword(); | ||
return this.generateArgumentExpr( Node.awaitExpr, ...arguments ); | ||
} | ||
generateArgumentExpr( generate, context, node ) { | ||
let [ argument ] = context.currentUnit.signalReference( { type: node.type }, () => this.generateNodes( context, [ node.argument ] ) ); | ||
return generate.call( Node, argument ); | ||
} | ||
export function initializeIteration( node, scope, callback ) { | ||
return scope.createEffect( { type: node.type }, iteratorEffect => { | ||
iteratorEffect.defineSubscriptIdentifier( '$counter', [ node.type === 'ForInStatement' ? '$x_key' : '$x_index' ] ); | ||
// A scope for variables declared in header | ||
return iteratorEffect.createScope( { type: 'Iteration' }, declarationScope => { | ||
// The iteration instance closure | ||
return declarationScope.createEffect( { type: 'Iteration' }, iterationInstanceEffect => { | ||
iterationInstanceEffect.inUse( true ); | ||
return callback( declarationScope, iteratorEffect, iterationInstanceEffect ); | ||
} ); | ||
} ); | ||
} ); | ||
} | ||
/** | ||
* ------------ | ||
* @CallExpression | ||
* @NewExpression | ||
* ------------ | ||
*/ | ||
generateCallExpression( context, node ) { return this.generateCallExpr( Node.callExpr, ...arguments ); } | ||
generateNewExpression( context, node ) { return this.generateCallExpr( Node.newExpr, ...arguments ); } | ||
generateCallExpr( generate, context, node ) { | ||
// The ongoing reference must be used for callee | ||
let [ callee ] = context.currentUnit.currentReference.with( { isCallee: true, callType: node.type }, () => this.generateNodes( context, [ node.callee ] ) ); | ||
let args = node.arguments.map( argument => context.currentUnit.signalReference( { type: argument.type }, () => this.generateNodes( context, [ argument ] )[ 0 ] ) ); | ||
return generate.call( Node, callee, args, node.optional ); | ||
} | ||
/** | ||
* ------------ | ||
* @ParenthesizedExpressio | ||
* @ChainExpression | ||
* ------------ | ||
*/ | ||
generateParenthesizedExpression( context, node ) { return this.generateExprExpr( Node.parensExpr, ...arguments ); } | ||
generateChainExpression( context, node ) { return this.generateExprExpr( Node.chainExpr, ...arguments ); } | ||
generateExprExpr( generate, context, node ) { | ||
// The ongoing reference must be used for these | ||
let [ expresion ] = this.generateNodes( context, [ node.expression ] ); | ||
return generate.call( Node, expresion ); | ||
} | ||
export function composeIteration( iterationInstanceEffect, body, params = {} ) { | ||
let disposeCallbacks = [ params.disposeCallback ], disposeCallback = () => disposeCallbacks.forEach( callback => callback && callback() ); | ||
let preIterationDeclarations = [], iterationDeclarations = []; | ||
let iterationBody = [], effectBody = body.body.slice( 0 ); | ||
// Counter? | ||
if ( !params.iterationId ) { | ||
params.iterationId = astNodes.identifier( iterationInstanceEffect.getSubscriptIdentifier( '$counter', true ) ); | ||
let counterInit = astNodes.varDeclarator( astNodes.clone( params.iterationId ), astNodes.literal( -1 ) ); | ||
let counterIncr = astNodes.updateExpr( '++', astNodes.clone( params.iterationId ), false ); | ||
preIterationDeclarations.push( counterInit ); | ||
iterationBody.push( counterIncr ); | ||
// On dispose | ||
disposeCallbacks.push( () => iterationBody.pop() /* counterIncr */ ); | ||
/** | ||
* ------------ | ||
* @ArrayExpression | ||
* ------------ | ||
*/ | ||
generateArrayExpression( context, node ) { | ||
let elements = node.elements.map( element => context.currentUnit.signalReference( { type: element.type }, () => this.generateNodes( context, [ element ] )[ 0 ] ) ); | ||
return Node.arrayExpr( elements ); | ||
} | ||
// The iterationDeclarations | ||
if ( iterationDeclarations.length ) { | ||
iterationBody.push( astNodes.varDeclaration( 'let', iterationDeclarations ) ); | ||
disposeCallbacks.push( () => iterationBody.splice( -1 ) /* iterationDeclarations */ ); | ||
/** | ||
* ------------ | ||
* @ObjectExpression | ||
* ------------ | ||
*/ | ||
generateObjectExpression( context, node ) { | ||
let properties = this.generateNodes( context, node.properties ); | ||
return Node.objectExpr( properties ); | ||
} | ||
// Main | ||
iterationBody.push( ...iterationInstanceEffect.composeWith( effectBody, [ params.iterationId ], disposeCallback ) ); | ||
/** | ||
* ------------ | ||
* @Property | ||
* ------------ | ||
*/ | ||
generateProperty( context, node ) { | ||
let { key, value } = node; | ||
if ( node.computed ) { | ||
[ key ] = context.currentUnit.signalReference( { type: key.type }, () => this.generateNodes( context, [ key ] ) ); | ||
} | ||
[ value ] = context.currentUnit.signalReference( { type: value.type }, () => this.generateNodes( context, [ value ] ) ); | ||
return Node.property( key, value, node.kind, node.shorthand, node.computed, node.method ); | ||
} | ||
/** | ||
* ------------ | ||
* @TaggedTemplateExpression | ||
* ------------ | ||
*/ | ||
generateTaggedTemplateExpression( context, node ) { | ||
let [ tag, quasi ] = context.currentUnit.signalReference( { type: node.type }, () => this.generateNodes( context, [ node.tag, node.quasi ] ) ); | ||
return Node.taggedTemplateExpr( tag, quasi ); | ||
} | ||
/** | ||
* ------------ | ||
* @TemplateLiteral | ||
* ------------ | ||
*/ | ||
generateTemplateLiteral( context, node ) { | ||
let expressions = node.expressions.map( expression => context.currentUnit.signalReference( { type: node.type }, () => this.generateNodes( context, [ expression ] )[ 0 ] ) ); | ||
return Node.templateLiteral( node.quasis, expressions ); | ||
} | ||
// Convert to actual declaration | ||
if ( preIterationDeclarations.length ) { | ||
preIterationDeclarations = astNodes.varDeclaration( 'let', preIterationDeclarations ); | ||
} else { | ||
preIterationDeclarations = null; | ||
/** | ||
* ------------ | ||
* @TryStatement | ||
* ------------ | ||
*/ | ||
generateTryStatement( context, node ) { | ||
let [ block, handler, finalizer ] = this.generateNodes( context, [ node.block, node.handler, node.finalizer ] ); | ||
return Node.tryStmt( block, handler, finalizer ); | ||
} | ||
return [ preIterationDeclarations, iterationBody ]; | ||
/** | ||
* ------------ | ||
* @CatchClause | ||
* ------------ | ||
*/ | ||
generateCatchClause( context, node ) { | ||
let [ body ] = this.generateNodes( context, [ node.body ] ); | ||
return Node.catchClause( node.param, body ); | ||
} | ||
/** | ||
* ------------ | ||
* @ThrowStatement | ||
* ------------ | ||
*/ | ||
generateThrowStatement( context, node ) { return this.generateArgumentExpr( Node.throwStmt, ...arguments ); } | ||
} |
@@ -5,15 +5,15 @@ | ||
*/ | ||
import Node from './Node.js'; | ||
import Common from './Common.js'; | ||
/** | ||
* @extends Node | ||
* @extends Common | ||
* | ||
* A Condition state | ||
*/ | ||
export default class Condition extends Node { | ||
export default class Condition extends Common { | ||
constructor( ownerEffect, parent, id, def ) { | ||
constructor( ownerContext, id, def ) { | ||
super( id, def ); | ||
this.ownerEffect = ownerEffect; | ||
this.parent = parent; | ||
this.ownerContext = ownerContext; | ||
this.parent = ownerContext.currentCondition; | ||
} | ||
@@ -43,6 +43,6 @@ | ||
if ( this.parent ) { | ||
if ( this.parent.ownerEffect.id === this.ownerEffect.id ) { | ||
if ( this.parent.ownerContext.id === this.ownerContext.id ) { | ||
json.parent = this.parent.id; | ||
} else { | ||
json.parent = `${ this.parent.ownerEffect.lineage }:${ this.parent.id }`; | ||
json.parent = `${ this.parent.ownerContext.lineage }:${ this.parent.id }`; | ||
} | ||
@@ -49,0 +49,0 @@ } |
@@ -5,4 +5,4 @@ | ||
*/ | ||
import Common from './Common.js'; | ||
import Node from './Node.js'; | ||
import { astNodes } from './Generators.js'; | ||
@@ -14,32 +14,21 @@ /** | ||
*/ | ||
export default class Memo extends Node { | ||
export default class Memo extends Common { | ||
constructor( ownerEffect, id, def ) { | ||
constructor( ownerContext, id, def ) { | ||
super( id, def ); | ||
this.ownerEffect = ownerEffect; | ||
this.ownerContext = ownerContext; | ||
} | ||
compose() { | ||
generate() { | ||
if ( !this.expr ) /* such as case: null / default: */ return [ this.expr, this ]; | ||
let subscript$construct = astNodes.identifier( this.ownerEffect.getSubscriptIdentifier( '$construct', true ) ); | ||
let ref = astNodes.memberExpr( | ||
astNodes.memberExpr( subscript$construct, astNodes.identifier( 'memo' ) ), | ||
astNodes.literal( this.id ), | ||
let subscript$unit = Node.identifier( this.ownerContext.getSubscriptIdentifier( '$unit', true ) ); | ||
let ref = Node.memberExpr( | ||
Node.memberExpr( subscript$unit, Node.identifier( 'memo' ) ), | ||
Node.literal( this.id ), | ||
true | ||
); | ||
this.composed = astNodes.assignmentExpr( ref, this.expr ); | ||
this.composed = Node.assignmentExpr( ref, this.expr ); | ||
return [ this.composed, this ]; | ||
} | ||
dispose() { | ||
if ( !this.composed ) return; | ||
Object.keys( this.composed ).forEach( k => { | ||
delete this.composed[ k ]; | ||
} ); | ||
Object.keys( this.expr ).forEach( k => { | ||
this.composed[ k ] = this.expr[ k ]; | ||
} ); | ||
this.composed = null; | ||
} | ||
toJson( filter = false ) { | ||
@@ -46,0 +35,0 @@ return { |
export default class Node { | ||
export default { | ||
constructor( id, def ) { | ||
this.id = id; | ||
Object.assign( this, def ); | ||
this.dependencies = 0; | ||
} | ||
// Statements & Clauses | ||
tryStmt( block, handler, finalizer, guardedHandlers ) { return { type: 'TryStatement', block, handler, finalizer, guardedHandlers }; }, | ||
catchClause( param, body ) { return { type: 'CatchClause', param, body }; }, | ||
throwStmt( argument ) { return { type: 'ThrowStatement', argument }; }, | ||
returnStmt( argument ) { return { type: 'ReturnStatement', argument }; }, | ||
exprStmt( expression ) { return { type: 'ExpressionStatement', expression, }; }, | ||
blockStmt( body ) { return { type: 'BlockStatement', body }; }, | ||
labeledStmt( label, body ) { return { type: 'LabeledStatement', label, body }; }, | ||
withStmt( object, body ) { return { type: 'WithStatement', object, body }; }, | ||
ifStmt( test, consequent, alternate ) { return this.condExpr(test, consequent, alternate, 'IfStatement'); }, | ||
switchStmt( discriminant, cases, lexical = false ) { return { type: 'SwitchStatement', discriminant, cases, /*lexical*/ /* Failing tests and seems to be SpiderMonkey-specific*/ }; }, | ||
switchCase( test, consequent ) { return { type: 'SwitchCase', test, consequent }; }, | ||
whileStmt( test, body ) { return { type: 'WhileStatement', test, body }; }, | ||
doWhileStmt( test, body ) { return { type: 'DoWhileStatement', test, body }; }, | ||
forStmt( init, test, update, body ) { return { type: 'ForStatement', init, test, update, body }; }, | ||
forInStmt( left, right, body ) { return { type: 'ForInStatement', left, right, body }; }, | ||
forOfStmt( left, right, body ) { return { type: 'ForOfStatement', left, right, body }; }, | ||
breakStmt( label = null ) { return { type: 'BreakStatement', label }; }, | ||
continueStmt( label = null ) { return { type: 'ContinueStatement', label }; }, | ||
with( params, callback ) { | ||
let existing = {}; | ||
Object.keys( params ).forEach( key => { | ||
existing[ key ] = this[ key ]; | ||
this[ key ] = params[ key ]; | ||
} ); | ||
let result = callback(); | ||
Object.keys( params ).forEach( key => { | ||
this[ key ] = existing[ key ]; | ||
} ); | ||
return result; | ||
} | ||
// Declarations | ||
varDeclaration( kind, declarations ) { return { type: 'VariableDeclaration', kind, declarations } }, | ||
varDeclarator( id, init = null ) { return { type: 'VariableDeclarator', id, init } }, | ||
funcDeclaration( id, params, body, async = false, expression = false, generator = false ) { | ||
return this.func( 'FunctionDeclaration', ...arguments ); | ||
}, | ||
inUse( inUse ) { | ||
if ( !arguments.length ) { | ||
return this.dependencies > 0; | ||
} | ||
if ( inUse ) { | ||
this.dependencies ++; | ||
} else { | ||
this.dependencies --; | ||
} | ||
return this; | ||
} | ||
// Expressions | ||
sequenceExpr( expressions ) { return { type: 'SequenceExpression', expressions }; }, | ||
parensExpr( expression ) { return { type: 'ParenthesizedExpression', expression }; }, | ||
logicalExpr( operator, left, right ) { return { type: 'LogicalExpression', operator, left, right, }; }, | ||
binaryExpr( operator, left, right ) { return { type: 'BinaryExpression', operator, left, right, }; }, | ||
unaryExpr( operator, argument, prefix = true ) { return { type: 'UnaryExpression', operator, argument, prefix }; }, | ||
updateExpr( operator, argument, prefix = false ) { return { type: 'UpdateExpression', operator, argument, prefix }; }, | ||
assignmentExpr( left, right, operator = '=' ) { return { type: 'AssignmentExpression', operator, left, right }; }, | ||
assignmentPattern( left, right ) { return { type: 'AssignmentPattern', left, right }; }, | ||
thisExpr() { return { type: 'ThisExpression' }; }, | ||
condExpr( test, consequent, alternate, type = 'ConditionalExpression' ) { return { type, test, consequent, alternate }; }, | ||
arrayExpr( elements ) { return { type: 'ArrayExpression', elements }; }, | ||
arrayPattern( elements ) { return { type: 'ArrayPattern', elements }; }, | ||
objectExpr( properties ) { return { type: 'ObjectExpression', properties }; }, | ||
objectPattern( properties ) { return { type: 'ObjectPattern', properties }; }, | ||
chainExpr( expression ) { return { type: 'ChainExpression', expression }; }, | ||
callExpr( callee, args, optional = false ) { return { type: 'CallExpression', callee, arguments: args, optional }; }, | ||
newExpr( callee, args ) { return { type: 'NewExpression', callee, arguments: args }; }, | ||
awaitExpr( argument ) { return { type: 'AwaitExpression', argument }; }, | ||
taggedTemplateExpr( tag, quasi ) { return { type: 'TaggedTemplateExpression', tag, quasi }; }, | ||
memberExpr( object, property, computed = false, optional = false ) { | ||
return { type: 'MemberExpression', object, property, computed, optional }; | ||
}, | ||
funcExpr( id, params, body, async = false, expression = false, generator = false ) { | ||
return this.func( 'FunctionExpression', ...arguments ); | ||
}, | ||
arrowFuncExpr( id, params, body, async = false, expression = false, generator = false ) { | ||
return this.func( 'ArrowFunctionExpression', ...arguments ); | ||
}, | ||
// Other | ||
func( type, id, params, body, async = false, expression = false, generator = false ) { | ||
return { type, id, params, body, async, expression, generator, }; | ||
}, | ||
identifier( name ) { return { type: 'Identifier', name }; }, | ||
property( key, value, kind = 'init', shorthand = false, computed = false, method = false ) { return { type: 'Property', key, value, kind, shorthand, computed, method }; }, | ||
spreadElement( argument ) { return { type: 'SpreadElement', argument }; }, | ||
literal( value ) { return { type: 'Literal', value }; }, | ||
templateLiteral( quasis, expressions ) { return { type: 'TemplateLiteral', quasis, expressions }; }, | ||
// Util | ||
invert( expr ) { return this.unaryExpr( '!', expr ); }, | ||
clone( expr ) { | ||
expr = { ...expr }; | ||
delete expr.start; | ||
delete expr.end; | ||
return expr; | ||
}, | ||
} |
@@ -5,11 +5,11 @@ | ||
*/ | ||
import Node from './Node.js'; | ||
import Common from './Common.js'; | ||
import Memo from './Memo.js'; | ||
export default class Ref extends Node { | ||
export default class Ref extends Common { | ||
constructor( ownerProduction, id, def ) { | ||
constructor( ownerReference, id, def ) { | ||
super( id, def ); | ||
this.ownerProduction = ownerProduction; | ||
this.test = this.ownerEffect.currentCondition; | ||
this.ownerReference = ownerReference; | ||
this.condition = this.ownerUnit.currentCondition; | ||
this.path = []; | ||
@@ -19,4 +19,4 @@ this.isDotSafe = true; | ||
get ownerEffect() { | ||
return this.ownerProduction.ownerEffect; | ||
get ownerUnit() { | ||
return this.ownerReference.ownerUnit; | ||
} | ||
@@ -37,3 +37,3 @@ | ||
doIsDotSafe( identifiers ) { | ||
if ( identifiers.some( identifier => ( identifier instanceof Memo ) || identifier.name.includes( '.' ) ) ) { | ||
if ( identifiers.some( identifier => ( identifier instanceof Memo ) || ( identifier.name + '' ).includes( '.' ) ) ) { | ||
this.isDotSafe = false; | ||
@@ -62,4 +62,4 @@ } | ||
$path: this.isDotSafe ? this.path.map( identifier => identifier.name ).join( '.' ) : undefined, | ||
conditionId: ( this.test || {} ).id, | ||
productionId: this.ownerProduction.id, | ||
conditionId: ( this.condition || {} ).id, | ||
referenceId: this.ownerReference.id, | ||
} | ||
@@ -66,0 +66,0 @@ } |
@@ -5,4 +5,4 @@ | ||
*/ | ||
import Node from './Node.js'; | ||
import Effect from './Effect.js'; | ||
import Common from './Common.js'; | ||
import EffectReference from './EffectReference.js'; | ||
@@ -12,109 +12,85 @@ /** | ||
*/ | ||
export default class Scope extends Node { | ||
export default class Scope extends Common { | ||
constructor( ownerEffect, id, def = {} ) { | ||
constructor( ownerContext, id, def = {} ) { | ||
super( id, def ); | ||
this.ownerEffect = ownerEffect; | ||
// Effects | ||
this.effects = []; | ||
this.effectsStack = []; | ||
this.ownerContext = ownerContext; | ||
this.ownerScope = ownerContext && ( ownerContext.currentScope || ownerContext.ownerScope ); | ||
// signals | ||
this.effectReferences = []; | ||
} | ||
// ----------------- | ||
get parentScope() { | ||
return this.ownerEffect && this.ownerEffect.ownerScope; | ||
} | ||
// --------------- | ||
get $params() { | ||
return this.params || ( | ||
this.ownerEffect && this.ownerEffect.$params | ||
); | ||
pushEffectReference( effectReference ) { | ||
this.effectReferences.push( effectReference ); | ||
} | ||
get url() { | ||
let lineage = this.ownerEffect && this.ownerEffect.url; | ||
return `${ lineage ? lineage + '/' : '' }${ this.id }`; | ||
} | ||
// ----------------- | ||
createEffect( def, callback ) { | ||
let effect = new Effect( this, this.ownerEffect.nextId, def ); | ||
this.effects.unshift( effect ); | ||
// Keep in stack while callback runs | ||
this.effectsStack.unshift( effect ); | ||
let result = callback( effect ); | ||
this.effectsStack.shift(); | ||
// Return callback result | ||
return result; | ||
} | ||
get currentEffect() { | ||
return this.effectsStack[ 0 ] || ( this.parentScope || {} ).currentEffect; | ||
} | ||
createBlock( callback ) { | ||
let def = { type: 'BlockStatement' }; | ||
return this.createEffect( def, effect => effect.createScope( def, callback ) ); | ||
} | ||
// --------------- | ||
static( val ) { | ||
if ( arguments.length ) { | ||
this._static = val; | ||
return this; | ||
doSubscribe( signalReference, remainderRefs = null ) { | ||
remainderRefs = this.effectReferences.reduce( ( _remainderRefs, effectReference ) => { | ||
return effectReference.doSubscribe( signalReference, _remainderRefs ); | ||
}, remainderRefs || [ ...signalReference.refs ] ); | ||
if ( !remainderRefs.length ) return true; | ||
// Statements within functions can not subscribe to outside variables | ||
if ( !this.ownerScope || [ 'FunctionDeclaration', 'FunctionExpression', 'ArrowFunctionExpression' ].includes( this.type ) ) { | ||
remainderRefs = this.ownerContext.references.reduce( ( _remainderRefs, reference ) => { | ||
if ( !( reference instanceof EffectReference ) ) return _remainderRefs; | ||
return reference.doSubscribe( signalReference, _remainderRefs ); | ||
}, remainderRefs ); | ||
if ( !remainderRefs.length ) return true; | ||
return this.ownerContext.effectReference( {}, effectReference => { | ||
remainderRefs.forEach( signalRef => { | ||
this.canObserveGlobal( signalRef ) && effectReference.addRef().push( ...signalRef.path ); | ||
} ); | ||
signalReference.inUse( true ); | ||
return effectReference.doSubscribe( signalReference, remainderRefs ), true; | ||
}, false/* resolveInScope; a false prevents calling this.doUpdate() */ ); | ||
} | ||
return this._static || ( | ||
this.parentScope && this.parentScope.static() | ||
); | ||
if ( this.ownerScope ) { | ||
return this.ownerScope.doSubscribe( signalReference, remainderRefs ); | ||
} | ||
} | ||
// ----------------- | ||
doSubscribe( subscriber, meta = null ) { | ||
let success = this.effects.some( effect => effect.affecteds.some( affected => { | ||
return affected.doSubscribe( subscriber, meta ); | ||
} ) ); | ||
if ( this.parentScope ) { | ||
success = success || this.parentScope.doSubscribe( subscriber, meta ); | ||
} else if ( !success && this.ownerEffect ) { | ||
return this.ownerEffect.affectedsProduction( {}, production => { | ||
subscriber.refs.forEach( ref => { | ||
this.canObserveGlobal( ref ) && production.addRef().push( ...ref.path ); | ||
} ); | ||
subscriber.inUse( true ); | ||
subscriber.ownerEffect.inUse( true ); | ||
return production.doSubscribe( subscriber, meta ); | ||
}, false/* resolveInScope; a false prevents calling this.doUpdate() */ ); | ||
doUpdate( _effectReference, remainderRefs = null ) { | ||
// Not forEach()... but reduce() - only first one must be updated | ||
remainderRefs = this.effectReferences.reduce( ( _remainderRefs, effectReference ) => { | ||
return effectReference.doUpdate( _effectReference, _remainderRefs ); | ||
}, remainderRefs || [ ..._effectReference.refs ] ); | ||
if ( !this.ownerScope || [ 'FunctionDeclaration', 'FunctionExpression', 'ArrowFunctionExpression' ].includes( this.type ) ) { | ||
if ( _effectReference.type === 'VariableDeclaration' /* and ofcourse, kind: var */ ) return; | ||
_effectReference.ownerUnit.$sideEffects = true; | ||
let sideEffects = remainderRefs.length && remainderRefs || [ ..._effectReference.refs ]; | ||
return this.ownerContext.sideEffects.push( { reference: _effectReference, remainderRefs: sideEffects } ), true; | ||
} | ||
return success; | ||
if ( !remainderRefs.length ) return true; | ||
if ( this.ownerScope ) { | ||
return this.ownerScope.doUpdate( _effectReference, remainderRefs ); | ||
} | ||
} | ||
doUpdate( updater, meta = null ) { | ||
let success = this.effects.some( effect => effect.affecteds.some( affected => { | ||
let update = affected.doUpdate( updater, meta ); | ||
if ( update && affected.type === 'VariableDeclaration' && Array.isArray( meta ) ) { | ||
meta.push( 'lexical-update' ); | ||
doSideEffectUpdates( _effectReference, remainderRefs ) { | ||
// Not reduce()... but forEach() - all must be updated | ||
this.effectReferences.forEach( effectReference => { | ||
let _remainderRefs = effectReference.doUpdate( _effectReference, remainderRefs, true /*isSideEffects*/ ); | ||
if ( effectReference.type === 'VariableDeclaration' ) { | ||
// It is only here remainderRefs gets to reduce | ||
remainderRefs = _remainderRefs; | ||
} | ||
return update; | ||
} ) ); | ||
if ( this.parentScope ) { | ||
success = success || this.parentScope.doUpdate( updater, meta ); | ||
} else if ( !success && this.ownerEffect ) { | ||
this.ownerEffect.affecteds.push( updater ); | ||
if ( Array.isArray( meta ) ) { | ||
meta.push( 'globalization' ); | ||
} | ||
return true; | ||
} ); | ||
if ( !remainderRefs.length ) return true; | ||
if ( !this.ownerScope || [ 'FunctionDeclaration', 'FunctionExpression', 'ArrowFunctionExpression' ].includes( this.type ) ) { | ||
return this.ownerContext.sideEffects.push( { reference: _effectReference, remainderRefs, isSideEffects: true } ), true; | ||
} | ||
return success; | ||
if ( this.ownerScope ) { | ||
return this.ownerScope.doSideEffectUpdates( _effectReference, remainderRefs ); | ||
} | ||
} | ||
canObserveGlobal( ref ) { | ||
return ( !this.$params.globalsOnlyPaths || ref.path.length > 1 ) | ||
&& !( this.$params.globalsNoObserve || [] ).includes( ref.path[ 0 ].name ); | ||
return ( !this.ownerContext.$params.globalsOnlyPaths || ref.path.length > 1 ) | ||
&& !( this.ownerContext.$params.globalsNoObserve || [] ).includes( ref.path[ 0 ].name ); | ||
} | ||
} |
@@ -6,3 +6,3 @@ | ||
import CodeBlock from './CodeBlock.js'; | ||
import Effect from './Effect.js'; | ||
import Unit from './Unit.js'; | ||
@@ -12,3 +12,3 @@ /** | ||
*/ | ||
export default class Console extends CodeBlock( Effect ) { | ||
export default class Console extends CodeBlock( Unit ) { | ||
@@ -52,3 +52,3 @@ bind( subscriptFunction, autoRender = true ) { | ||
.ref-identifier.affected { | ||
.ref-identifier.effect { | ||
cursor: pointer; | ||
@@ -64,3 +64,3 @@ } | ||
.ref-identifier.affected:is(.path-hover, .path-runtime-active) { | ||
.ref-identifier.effect:is(.path-hover, .path-runtime-active) { | ||
color: yellowgreen; | ||
@@ -70,8 +70,8 @@ text-decoration: underline; | ||
.ref-identifier.cause.affected:is(.path-hover, .path-runtime-active) { | ||
.ref-identifier.cause.effect:is(.path-hover, .path-runtime-active) { | ||
color: lightgreen; | ||
} | ||
subscript-effect.block-hover, | ||
subscript-effect.block-runtime-active { | ||
subscript-unit.block-hover, | ||
subscript-unit.block-runtime-active { | ||
outline: 1px dashed gray; | ||
@@ -84,3 +84,3 @@ outline-offset: 0.1rem; | ||
} | ||
subscript-effect.block-runtime-active { | ||
subscript-unit.block-runtime-active { | ||
background-color: rgba(100, 100, 100, 0.35); | ||
@@ -98,3 +98,3 @@ } | ||
customElements.define( 'subscript-codeblock', CodeBlock() ); | ||
customElements.define( 'subscript-effect', Effect ); | ||
customElements.define( 'subscript-unit', Unit ); | ||
customElements.define( 'subscript-console', Console ); |
@@ -16,4 +16,4 @@ /** | ||
this.$fullPaths = []; | ||
if ( this.ownerProduction.assignee ) { | ||
this.ownerProduction.assignee.refs.forEach( ref => { | ||
if ( this.ownerReference.assignee ) { | ||
this.ownerReference.assignee.refs.forEach( ref => { | ||
if ( !ref.depth ) return; | ||
@@ -32,7 +32,7 @@ this.fullPaths.push( [ ...this.path, ...ref.depth ] ); | ||
element.anchor.classList.add( 'ref-identifier' ); | ||
element.anchor.classList.add( this.subscriptions ? 'affected' : 'cause' ); | ||
element.anchor.classList.add( this.subscriptions ? 'effect' : 'signal' ); | ||
let existingTitle = element.anchor.getAttribute( `title` ); | ||
let currentTitle = '> ' | ||
+ this.$fullPaths[ pathIndex ] | ||
+ ( this.subscriptions ? ' (Creates a signal)' : ' (Receives a signal)' ); | ||
+ ( this.subscriptions ? ' (Effect Ref)' : ' (Signal Ref)' ); | ||
element.anchor.setAttribute( `title`, existingTitle ? existingTitle + "\n" + currentTitle : currentTitle ); | ||
@@ -49,3 +49,3 @@ }); | ||
this._on( pathIndex, 'click', () => { | ||
this.ownerProduction.ownerEffect.signal( fullPath ); | ||
this.ownerReference.ownerUnit.runThread( fullPath ); | ||
} ); | ||
@@ -52,0 +52,0 @@ } |
@@ -8,2 +8,3 @@ | ||
import Subscript from './Subscript.js'; | ||
import Parser from './compiler/Parser.js'; | ||
@@ -14,2 +15,3 @@ export { | ||
Runtime, | ||
Parser, | ||
} |
@@ -5,5 +5,5 @@ | ||
*/ | ||
import Effect from './Effect.js'; | ||
import Unit from './Unit.js'; | ||
export default class Runtime extends Effect { | ||
export default class Runtime extends Unit { | ||
@@ -17,8 +17,8 @@ static create( compilation, parameters = [], params = {} ) { | ||
constructor( parentEffect, graph, callee, params = {}, exits = null ) { | ||
super( parentEffect, graph, callee, params = {}, exits ); | ||
constructor( ownerUnit, graph, callee, params = {}, exits = null ) { | ||
super( ownerUnit, graph, callee, params = {}, exits ); | ||
this.observers = []; | ||
} | ||
observe( effectUrl, callback ) { | ||
observe( unitUrl, callback ) { | ||
if ( !this.params.devMode ) { | ||
@@ -28,8 +28,8 @@ // TODO: Only allow observers in dev mode | ||
} | ||
this.observers.push( { effectUrl, callback } ); | ||
this.observers.push( { unitUrl, callback } ); | ||
} | ||
fire( effectUrl, event, refs ) { | ||
fire( unitUrl, event, refs ) { | ||
( this.observers || [] ).forEach( observer => { | ||
if ( observer.effectUrl !== effectUrl ) return; | ||
if ( observer.unitUrl !== unitUrl ) return; | ||
observer.callback( event, refs ); | ||
@@ -36,0 +36,0 @@ } ); |
/** | ||
* @imports | ||
*/ | ||
import { Compiler, Runtime } from './index.js'; | ||
import { Parser, Compiler, Runtime } from './index.js'; | ||
import { normalizeTabs } from './util.js'; | ||
import * as Acorn from 'acorn'; | ||
@@ -95,7 +94,8 @@ /** | ||
let _function = function( ...args ) { | ||
return runtime.call( this || defaultThis, ...args ); | ||
return runtime.call( this === undefined ? defaultThis : this, ...args ); | ||
}; | ||
_function.signal = runtime.signal.bind( runtime ); | ||
_function.thread = runtime.thread.bind( runtime ); | ||
_function.dispose = runtime.dispose.bind( runtime ); | ||
Object.defineProperty( _function, 'runtime', { value: runtime } ); | ||
Object.defineProperty( _function, 'sideEffects', { configurable: true, value: runtime.graph.sideEffects } ); | ||
Object.defineProperty( _function, 'subscriptSource', { configurable: true, value: compilation.source } ); | ||
@@ -117,3 +117,3 @@ Object.defineProperty( _function, 'originalSource', { configurable: true, value: originalSource } ); | ||
if ( !ast ) { | ||
ast = Acorn.parse( source, { ...Subscript.parserParams, ...params } ); | ||
ast = Parser.parse( source, { ...Subscript.parserParams, ...params } ); | ||
parseCache.set( source, ast ); | ||
@@ -120,0 +120,0 @@ } |
@@ -6,7 +6,6 @@ | ||
import { expect } from 'chai'; | ||
import { Compiler } from '../src/index.js'; | ||
import * as Acorn from 'acorn'; | ||
import { Compiler, Parser } from '../src/index.js'; | ||
const _jsonfy = ast => JSON.parse(JSON.stringify(ast)); | ||
const _parse = source => Acorn.parse(source, { | ||
export const _jsonfy = ast => JSON.parse( JSON.stringify( ast ) ); | ||
export const _parse = source => Parser.parse( source, { | ||
ecmaVersion: 'latest', | ||
@@ -17,10 +16,24 @@ allowReturnOutsideFunction: true, | ||
preserveParens: false, | ||
}); | ||
// Compiler is instantiated each time to have a clean state | ||
} ); | ||
let compiler; | ||
const _transform = ast => (compiler = new Compiler).transform(ast); | ||
const _serialize = (ast, params) => compiler.serialize(ast, params); | ||
export const _generate = ast => ( compiler = new Compiler ).generate( ast ); | ||
export const _serialize = ast => compiler.serialize( ast ); | ||
export const _noLocs = ast => { | ||
Object.keys( ast ).forEach( key => { | ||
if ( [ 'start', 'end', 'comments', 'raw' ].includes( key ) ) { | ||
delete ast[ key ]; | ||
} else if ( Array.isArray( ast[ key ] ) ) { | ||
ast[ key ].filter( node => node ).forEach( _noLocs ); | ||
} else if ( typeof ast[ key ] === 'object' && ast[ key ] ) { | ||
if ( ast[ key ].whitelist && ast[ key ].blacklist ) { | ||
ast[ key ] = ast[ key ].toString(); | ||
} else { | ||
_noLocs( ast[ key ] ); | ||
} | ||
} | ||
} ); | ||
}; | ||
const tests = []; | ||
let tests = []; | ||
export function empty() { | ||
@@ -31,38 +44,28 @@ tests.splice(0); | ||
export function run() { | ||
for (let test of tests) { | ||
it(test.desc, function() { | ||
for ( let test of tests ) { | ||
it( test.desc, function() { | ||
// Test | ||
let ast = _parse(test.source); | ||
let gen = _transform(ast); | ||
let sourceAst = _generate( _parse( test.source ) ).ast; | ||
// Expected | ||
let genFormatted, expFormatted; | ||
if (typeof test.expected === 'string') { | ||
// How many spaces make an indent? 4 | ||
let indentSpaces = ` `; | ||
// What's the indentation of expected string? | ||
let expIndentSpaceCount = test.expected.split(/[^\s]/)[0].length - (test.expected.startsWith(`\n`) ? 1 : 0); | ||
genFormatted = _serialize(gen, { | ||
indent: indentSpaces, | ||
startingIndentLevel: expIndentSpaceCount / 4 | ||
}).trim(); | ||
expFormatted = test.expected.trim(); | ||
} else { | ||
genFormatted = _jsonfy(gen); | ||
expFormatted = test.expected; | ||
let expectedAst = test.expected; | ||
if ( typeof expectedAst === 'string' ) { | ||
expectedAst = _parse( expectedAst ); | ||
} | ||
expect(genFormatted).to.eq(expFormatted); | ||
}); | ||
_noLocs( sourceAst ); | ||
_noLocs( expectedAst ); | ||
expect( sourceAst ).to.deep.eql( expectedAst ); | ||
} ); | ||
} | ||
} | ||
export function add(desc, source, expected, options = {}) { | ||
tests.push({ desc, source, expected, options }); | ||
export function add( desc, source, expected, options = {} ) { | ||
tests.push( { desc, source, expected, options } ); | ||
} | ||
export function group(desc, callback) { | ||
describe(desc, function() { | ||
export function group( desc, callback ) { | ||
describe( desc, function() { | ||
empty(); | ||
callback(); | ||
run(); | ||
}); | ||
} ); | ||
} |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Native code
Supply chain riskContains native code (e.g., compiled binaries or shared libraries). Including native code can obscure malicious behavior.
Found 7 instances in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
3197508
60
6258
946
7