diff options
Diffstat (limited to 'site')
| -rw-r--r-- | site/index.html | 8 | ||||
| -rw-r--r-- | site/inspector.mjs | 178 | ||||
| -rw-r--r-- | site/main.mjs | 172 |
3 files changed, 195 insertions, 163 deletions
diff --git a/site/index.html b/site/index.html index d62f22e..4ddb3ea 100644 --- a/site/index.html +++ b/site/index.html @@ -53,10 +53,10 @@ fps: <span id='fps'>n/a</span> </section> <!-- arena --> - <section id='robview'> - <h2>code</h2> + <x-inspector id='robview'> + <h2>inspector</h2> <details id='editor' open> - <summary>code</summary> + <summary>source code</summary> <div id='code'> <select id='src-select'> <option>rob.fs</option> @@ -91,7 +91,7 @@ <ol id='callstack'></ol> </fieldset> </details> <!-- scope --> - </section> <!-- robview --> + </x-inspector> </div> <script src='./main.mjs' type='module'></script> diff --git a/site/inspector.mjs b/site/inspector.mjs new file mode 100644 index 0000000..76e635f --- /dev/null +++ b/site/inspector.mjs @@ -0,0 +1,178 @@ +const SRC_SELECT_SELECTOR = '#src-select'; +const COMPILE_BUTTON_SELECTOR = '#compile'; +const WORDLIST_SELECTOR = '#wordlist'; +const STACK_SELECTOR = '#stack'; +const CALLSTACK_SELECTOR = '#callstack'; +const VARS_SELECTOR = '#vars'; +const SRC_SELECTOR = '#src'; +const IP_SELECTOR = '#wordlist .ip'; + +function selectorForIP(word, offset) { + return `#wordlist x-bytecode[x-index='${word}'] x-op[x-index='${offset}']`; +} + +function wordlistElts(wordlist) { + return wordlist.map((bc, i) => { + const bcElt = document.createElement('x-bytecode'); + bcElt.setAttribute('x-index', i); + bc.forEach((op, i) => { + const opElt = document.createElement('x-op'); + opElt.setAttribute('x-index', i); + opElt.textContent = op; + bcElt.appendChild(opElt); + }) + return bcElt; + }) +} + +export default class extends HTMLElement { + static name = 'x-inspector'; + static compileRequest = 'compile'; + static register() { + console.debug('registering custom element', this.name, this); + self.customElements.define(this.name, this); + } + + constructor() { + super(); + this.highRange = new Range(); + this.highlight = new Highlight(this.highRange); + CSS.highlights.set('exec', this.highlight); + console.debug('bjc class var?', this.constructor.name, this.__proto__.name, Object.getPrototypeOf(this)) + } + + connectedCallback() { + self.foo = this; + console.debug('connectedCallback()', this); + [this.srcSelect, this.compileButton, this.wordlist, this.stack, this.callstack, this.vars, this.src] = [ + SRC_SELECT_SELECTOR, + COMPILE_BUTTON_SELECTOR, + WORDLIST_SELECTOR, + STACK_SELECTOR, + CALLSTACK_SELECTOR, + VARS_SELECTOR, + SRC_SELECTOR, + ].map(this.querySelector.bind(this)); + + this.compileButton.onclick = e => { + console.debug('compile clicked', e); + + // always add a newline until i decide what to do with the parser. + const text = this.src.textContent + '\n'; + const compileEvent = new CustomEvent(this.constructor.compileRequest, { detail: { text } }); + this.dispatchEvent(compileEvent); + } + + this.srcSelect.onchange = _ => this.loadForth(this.srcSelect.value); + this.loadForth(this.srcSelect.value); + } + + attributeChangedCallback(name, old, v) { + console.debug('attributeChangedCallback', this, name, old, v); + } + + loadForth(taintedPath) { + console.debug('loadForth', this, taintedPath); + // ascii only + ‘-’, ‘_’, ‘.’, and ‘/’, but no ‘../’ + const path = + taintedPath + .replace(/[^-_A-Za-z./]/g, '') + .replace(/\.\.\//g, ''); + fetch(`./samples/${path}`) + .then(resp => { + if (!resp.ok) { + throw `http status ${resp.status}` + } + return resp.text() + }) + .then(text => { + this.src.textContent = text; + }) + .catch(e => { + console.error(`couldn't fetch ‘${path}’`, e); + }); + } + + render(robo, fresh=false) { + console.debug('render', this, robo); + if (fresh) { + this.renderWordlist(robo); + } + this.renderTextHighlight(robo); + this.renderWordlistHighlight(robo); + this.renderVars(robo); + this.renderStack(robo); + this.renderCallstack(robo); + } + + renderWordlist(vm) { + while (this.wordlist.lastChild) { + console.debug('removing child', this.wordlist.lastChild) + this.wordlist.removeChild(this.wordlist.lastChild); + } + + const wordlist = wordlistElts(vm.wordlist); + wordlist.forEach(elt => this.wordlist.appendChild(elt)); + } + + renderTextHighlight(vm) { + const { word, offset } = vm.ip; + const anno = vm.annos[word][offset]; + const src = document.querySelector(SRC_SELECTOR); + + // this assumes the text node is the first child, maybe it isn't? + this.highRange.setStart(src.childNodes[0], anno.start); + this.highRange.setEnd(src.childNodes[0], anno.end); + } + + renderWordlistHighlight(vm) { + const { word, offset } = vm.ip; + this.querySelectorAll(IP_SELECTOR).forEach(e => e.classList.remove('ip')); + const sel = selectorForIP(word, offset); + this.querySelectorAll(sel).forEach(e => { + e.classList.add('ip'); + }); + } + + renderVars(vm) { + while (this.vars.lastChild) { + this.vars.removeChild(this.vars.lastChild); + } + + ['out', 'heading', 'speed', 'doppler'].forEach(name => { + const dt = document.createElement('dt'); + dt.textContent = name; + this.vars.appendChild(dt); + + const dd = document.createElement('dd'); + dd.textContent = vm.vars[name]; + this.vars.appendChild(dd); + }); + } + + renderStack(vm) { + while (this.stack.lastChild) { + this.stack.removeChild(this.stack.lastChild); + } + vm.stack.reverse() + .forEach(datum => { + const elt = document.createElement('li'); + elt.textContent = datum; + this.stack.appendChild(elt); + return elt; + }); + } + + renderCallstack(vm) { + while (this.callstack.lastChild) { + this.callstack.removeChild(this.callstack.lastChild); + } + vm.callstack.reverse() + .forEach(datum => { + const elt = document.createElement('li'); + elt.textContent = `${datum.word}@${datum.offset}`; + this.callstack.appendChild(elt); + return elt; + }); + } +} diff --git a/site/main.mjs b/site/main.mjs index ffbda00..d99b3a3 100644 --- a/site/main.mjs +++ b/site/main.mjs @@ -1,3 +1,5 @@ +import Inspector from './inspector.mjs'; + // simulation speed let TICKS_PER_SECOND = 15; let MS_PER_TICK = 1_000 / TICKS_PER_SECOND; @@ -6,93 +8,9 @@ let MS_PER_TICK = 1_000 / TICKS_PER_SECOND; const CANVAS_SELECTOR = '#arena canvas'; const TICK_BUTTON_SELECTOR = '#tick'; const TICK_RATE_SELECTOR = '#tick-rate-select'; -const BENCH_BUTTON_SELECTOR = '#bench'; const RUN_BUTTON_SELECTOR = '#run'; const FPS_SELECTOR = '#fps'; -// one per bot -const SRC_SELECT_SELECTOR = '#src-select'; -const COMPILE_BUTTON_SELECTOR = '#compile'; -const WORDLIST_SELECTOR = '#wordlist'; -const STACK_SELECTOR = '#stack'; -const CALLSTACK_SELECTOR = '#callstack'; -const VARS_SELECTOR = '#vars'; -const SRC_SELECTOR = '#src'; -const IP_SELECTOR = '#wordlist .ip'; - -function selectorForIP(word, offset) { - return `#wordlist x-bytecode[x-index='${word}'] x-op[x-index='${offset}']`; -} - -function wordlistElts(wordlist) { - return wordlist.map((bc, i) => { - const bcElt = document.createElement('x-bytecode'); - bcElt.setAttribute('x-index', i); - bc.forEach((op, i) => { - const opElt = document.createElement('x-op'); - opElt.setAttribute('x-index', i); - opElt.textContent = op; - bcElt.appendChild(opElt); - }) - return bcElt; - }) -} - -function initWordlist() { - const sel = selectorForIP(0, 0); - document.querySelectorAll(sel).forEach(e => { - e.classList.add('ip') - }); -} - -function renderStack(vmstack) { - document.querySelectorAll(STACK_SELECTOR).forEach(e => { - while (e.lastChild) { - e.removeChild(e.lastChild); - } - vmstack.reverse() - .forEach(datum => { - const elt = document.createElement('li'); - elt.textContent = datum; - e.appendChild(elt); - return elt; - }); - }); -} - -function renderCallStack(vmcallstack) { - document.querySelectorAll(CALLSTACK_SELECTOR).forEach(e => { - while (e.lastChild) { - e.removeChild(e.lastChild); - } - vmcallstack.reverse() - .forEach(datum => { - const elt = document.createElement('li'); - elt.textContent = `${datum.word}@${datum.offset}`; - e.appendChild(elt); - return elt; - }); - }); -} - -function renderVars(vmvars) { - document.querySelectorAll(VARS_SELECTOR).forEach(e => { - while (e.lastChild) { - e.removeChild(e.lastChild); - } - - ['out', 'heading', 'speed', 'doppler'].forEach(name => { - const dt = document.createElement('dt'); - dt.textContent = name; - e.appendChild(dt); - - const dd = document.createElement('dd'); - dd.textContent = vmvars[name]; - e.appendChild(dd); - }); - }); -} - function clamp(radius, x, y, width, height) { const xx = Math.min(Math.max(radius, x), width-radius); const yy = Math.min(Math.max(radius, y), height-radius); @@ -128,41 +46,6 @@ function renderArena(robos, delta=0) { }); } -const highRange = new Range(); -const highlight = new Highlight(highRange); -CSS.highlights.set('exec', highlight); - -function renderTextHighlight(vm) { - const ip = vm.ip; - const anno = vm.annos[ip.word][ip.offset]; - const src = document.querySelector(SRC_SELECTOR); - - // this assumes the text node is the first child, maybe it isn't? - highRange.setStart(src.childNodes[0], anno.start); - highRange.setEnd(src.childNodes[0], anno.end); -} - -function loadForth(taintedPath) { - // ascii only + ‘-’, ‘_’, ‘.’, and ‘/’, but no ‘../’ - const path = - taintedPath - .replace(/[^-_A-Za-z./]/g, '') - .replace(/\.\.\//g, ''); - fetch(`./samples/${path}`) - .then(resp => { - if (!resp.ok) { - throw `http status ${resp.status}` - } - return resp.text() - }) - .then(text => { - document.querySelector(SRC_SELECTOR).textContent = text; - }) - .catch(e => { - console.error(`couldn't fetch ‘${path}’`, e); - }); -} - async function loaded() { const canvas = document.querySelector(CANVAS_SELECTOR); const roboWorker = new Worker('robo.mjs', { type: 'module' }); @@ -181,30 +64,11 @@ async function loaded() { switch (kind) { case 'compile': if (res) { - let wordlistContainer = document.querySelector(WORDLIST_SELECTOR); - const wordlist = wordlistElts(trans.wordlist); - wordlist.forEach(elt => wordlistContainer.appendChild(elt)); - initWordlist(); - renderStack(trans.stack); - renderCallStack(trans.callstack); - renderVars(trans.vars); - renderTextHighlight(trans); - renderArena([robo]); + document.querySelector('x-inspector').render(trans, true); } break; case 'tick': - const { word, offset } = trans.ip; - document.querySelectorAll(IP_SELECTOR).forEach(e => e.classList.remove('ip')); - const sel = selectorForIP(word, offset); - document.querySelectorAll(sel).forEach(e => { - e.classList.add('ip'); - }); - renderStack(trans.stack); - renderCallStack(trans.callstack); - renderVars(trans.vars); - renderTextHighlight(trans); - robo.lastTick = document.timeline.currentTime; const [x, y] = clamp(25, robo.x + robo.speedx, robo.y + robo.speedy, canvas.width, canvas.height); @@ -220,6 +84,8 @@ async function loaded() { ].map(x => robo.speed * x); robo.speedx = speedx; robo.speedy = speedy; + + document.querySelector('x-inspector').render(trans); break; default: console.error('invalid message from robo worker', e.data); @@ -229,26 +95,6 @@ async function loaded() { console.error('error in roboWorker', e); }; - document.querySelectorAll(SRC_SELECT_SELECTOR).forEach(sel => { - sel.onchange = _ => loadForth(sel.value); - loadForth(sel.value); - }); - - document.querySelector(COMPILE_BUTTON_SELECTOR).onclick = e => { - console.debug('compile clicked', e); - - let wordlistContainer = document.querySelector(WORDLIST_SELECTOR); - while (wordlistContainer.lastChild) { - console.debug('removing child', wordlistContainer.lastChild) - wordlistContainer.removeChild(wordlistContainer.lastChild); - } - - // always add a newline until i decide what to do with the parser. - const text = document.querySelector(SRC_SELECTOR).textContent + '\n'; - console.debug('compiling', text); - roboWorker.postMessage({ kind: 'compile', text }); - }; - document.querySelector(TICK_BUTTON_SELECTOR).onclick = e => { console.debug('tick clicked', e); roboWorker.postMessage({ kind: 'tick' }); @@ -307,6 +153,14 @@ async function loaded() { document.querySelector(FPS_SELECTOR).textContent = 'n/a'; } } + + Inspector.register(); + document.querySelectorAll(Inspector.name).forEach(elt => { + elt.addEventListener(Inspector.compileRequest, e => { + console.debug('compiling', e.detail.text); + roboWorker.postMessage({ kind: 'compile', text: e.detail.text }); + }); + }); } document.addEventListener('DOMContentLoaded', loaded); |
