import init, { make_vm } from './wasm/automathon.js'; function wordlistElts(wordlist) { return wordlist.map((bc, i) => { const bcElt = document.createElement('x-bytecode'); bcElt.setAttribute('x-index', i); for (let i = 0; i < bc.len(); i++) { const opElt = document.createElement('x-op'); opElt.setAttribute('x-index', i); opElt.textContent = bc.at(i); bcElt.appendChild(opElt); } return bcElt; }) } function selectorForIP(word, offset) { return `#wordlist x-bytecode[x-index='${word}'] x-op[x-index='${offset}']`; } function initWordlist() { const sel = selectorForIP(0, 0); document.querySelectorAll(sel).forEach(e => { e.classList.add('ip') }); } function renderStack(vm) { document.querySelectorAll('#stack').forEach(e => { while (e.lastChild) { e.removeChild(e.lastChild); } vm.stack().reverse() .forEach(datum => { const elt = document.createElement('li'); elt.textContent = datum; e.appendChild(elt); return elt; }); }); } function renderCallStack(vm) { document.querySelectorAll('#callstack').forEach(e => { while (e.lastChild) { e.removeChild(e.lastChild); } vm.callstack().reverse() .forEach(datum => { const elt = document.createElement('li'); elt.textContent = `${datum.word}@${datum.offset}`; e.appendChild(elt); return elt; }); }); } function renderVars(vm) { document.querySelectorAll('#vars').forEach(e => { while (e.lastChild) { e.removeChild(e.lastChild); } ['out', 'heading', 'velocity', 'doppler'].forEach(name => { const dt = document.createElement('dt'); dt.textContent = name; e.appendChild(dt); const dd = document.createElement('dd'); dd.textContent = vm[name](); e.appendChild(dd); }); }); } function renderRobo(ctx, x, y) { ctx.fillStyle = 'rgb(200 0 0)'; ctx.fillRect(x, y, 50, 50); } let [offx, offy] = [125, 25]; function renderArena(vm) { const canvas = document.querySelector('#arena'); const ctx = canvas.getContext('2d'); ctx.clearRect(0, 0, canvas.width, canvas.height); let heading = vm.heading(); let velocity = vm.velocity(); let [velx, vely] = [ Math.cos(2 * Math.PI * heading / 360), Math.sin(2 * Math.PI * heading / 360) ].map(x => velocity * x); console.debug('velocity', heading, velx, vely) renderRobo(ctx, offx, offy); offx += velx; offy += vely; } const highRange = new Range(); const highlight = new Highlight(highRange); CSS.highlights.set('exec', highlight); function renderTextHighlight(vm) { const ip = vm.ip(); const anno = vm.annotation_at(ip) const src = document.querySelector('#src'); // 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 tick(vm) { if (!vm.tick()) { vm.reset_ip(); } const { word, offset } = vm.ip(); document.querySelectorAll('#wordlist .ip').forEach(e => e.classList.remove('ip')); const sel = selectorForIP(word, offset); document.querySelectorAll(sel).forEach(e => { e.classList.add('ip'); }); renderStack(vm); renderCallStack(vm); renderVars(vm); renderTextHighlight(vm); renderArena(vm); } 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').textContent = text; }) .catch(e => { console.error(`couldn't fetch ‘${path}’`, e); }); } async function loaded() { console.debug('running init'); const mod = await init(); console.debug('init done', mod); const vm = make_vm(); document.querySelectorAll('#src-select').forEach(async sel => { sel.onchange = _ => loadForth(sel.value); loadForth(sel.value); }); document.querySelector('#compile').onclick = e => { console.debug('compile clicked', e); let wordlistContainer = document.querySelector('#wordlist'); 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').textContent + '\n'; console.debug('compiling', text); const start = performance.now(); const res = vm.compile(text); const end = performance.now(); console.info(`compile took ${end-start} ms`); if (res) { const wordlist = wordlistElts(vm.wordlist()); wordlist.forEach(elt => wordlistContainer.appendChild(elt)); initWordlist(); renderStack(vm); renderCallStack(vm); renderVars(vm); renderTextHighlight(vm); renderArena(vm); } }; document.querySelector('#tick').onclick = e => { console.debug('tick clicked', e); tick(vm); }; document.querySelector('#bench').onclick = e => { console.debug('bench clicked', e); const start = performance.now(); let tickCount = 0; for (let i = 0; i < 1_000_000; i++) { tickCount += vm.run(); } const end = performance.now(); console.info(`run took ${end-start} ms for ${tickCount} ticks (${(end-start)/tickCount * 1_000_000} ns/tick).`); console.info('result', vm.stack()); }; let blinkenRun = false; document.querySelector('#blinken').onclick = e => { console.debug('blinken clicked', e); blinkenRun = !blinkenRun; if (blinkenRun) { e.target.textContent = 'haltenblinken'; } else { e.target.textContent = 'blinken'; } const frameRate = 6; const onTimeout = _ => { if (blinkenRun) { tick(vm); setTimeout(onTimeout, 1_000 / frameRate); } } setTimeout(onTimeout); } window.vm = vm; } document.addEventListener('DOMContentLoaded', loaded);