summaryrefslogtreecommitdiffstats
path: root/site
diff options
context:
space:
mode:
authorBrian Cully <bjc@spork.org>2025-08-24 15:46:22 -0400
committerBrian Cully <bjc@spork.org>2025-08-24 15:46:22 -0400
commit411f2735bffc42d11a3b1b3447bbb1603e48a3eb (patch)
tree9444596251baf8625522190a721d422adb69ce2e /site
parent0efb15a9eb706896cdabb9ca5d2b0c295c2dffcf (diff)
downloadautomathon-411f2735bffc42d11a3b1b3447bbb1603e48a3eb.tar.gz
automathon-411f2735bffc42d11a3b1b3447bbb1603e48a3eb.zip
move html stuff into ‘site’
Diffstat (limited to 'site')
-rw-r--r--site/index.html50
-rw-r--r--site/main.css80
-rw-r--r--site/main.mjs156
3 files changed, 286 insertions, 0 deletions
diff --git a/site/index.html b/site/index.html
new file mode 100644
index 0000000..77c2bdc
--- /dev/null
+++ b/site/index.html
@@ -0,0 +1,50 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>automathon</title>
+ <link rel='stylesheet' href='main.css'>
+ </head>
+
+ <body>
+ <h1>automathon</h1>
+
+ <div id='editor'>
+ <div id='code'>
+ <button id='compile'>compile</button>
+ <br>
+ <div id='src' contenteditable='plaintext-only'>: fac
+ dup
+ 1 > if
+ dup 1 - fac *
+ then
+ ;
+5 fac
+drop</div>
+ </div>
+
+ <div id='state-container'>
+ <div id='state'>
+ <div class='controls'>
+ <button id='tick'>tick</button>
+ <button id='bench'>bench</button>
+ <button id='blinken'>blinken</button>
+ </div>
+ <fieldset class='wordlist'>
+ <legend>word list</legend>
+ <div id='wordlist'></div>
+ </fieldset>
+ <fieldset class='stack'>
+ <legend>stack</legend>
+ <ol id='stack'></ol>
+ </fieldset>
+ <fieldset class='callstack'>
+ <legend>call stack</legend>
+ <ol id='callstack'></ol>
+ </fieldset>
+ </div>
+ </div>
+ </div>
+ <br>
+ <script src='./main.mjs' type='module'></script>
+ </body>
+</html>
diff --git a/site/main.css b/site/main.css
new file mode 100644
index 0000000..9f7381d
--- /dev/null
+++ b/site/main.css
@@ -0,0 +1,80 @@
+html {
+ box-sizing: border-box;
+}
+*, *::before, *::after {
+ box-sizing: inherit;
+}
+
+#editor {
+ display: grid;
+ grid-template-columns: repeat(2, 1fr);
+ grid-gap: 1em;
+}
+
+#code {
+ position: relative;
+ grid-row: 1;
+}
+
+#code #src {
+ width: 100%;
+ height: 25ex;
+ margin-top: 1ex;
+ padding: 1em;
+ border: 2px inset lightgray;
+ white-space: pre-wrap;
+}
+
+::highlight(exec) {
+ background-color: yellowgreen;
+}
+
+#compile {
+ position: absolute;
+ right: 0;
+}
+
+#state-container {
+ grid-row: 1;
+}
+
+#state {
+ display: grid;
+ grid-template-columns: repeat(2, 1fr);
+}
+
+#state .controls {
+ grid-row: 1;
+ grid-column: 1 / span 2;
+}
+
+#state .wordlist {
+ grid-row: 2;
+ grid-column: 1 / span 2;
+}
+
+#wordlist .ip {
+ background-color: yellow;
+}
+
+x-bytecode {
+ display: block;
+}
+
+x-bytecode::before {
+ display: inline-block;
+ width: 1em;
+ content: attr(x-index);
+ text-align: right;
+ padding-right: 0.4em;
+}
+
+x-op {
+ display: inline-block;
+ padding: 5px;
+ border: 1px solid grey;
+}
+
+canvas {
+ border: 1px solid greenyellow;
+}
diff --git a/site/main.mjs b/site/main.mjs
new file mode 100644
index 0000000..3add1fe
--- /dev/null
+++ b/site/main.mjs
@@ -0,0 +1,156 @@
+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;
+ });
+ });
+}
+
+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);
+ renderTextHighlight(vm);
+}
+
+async function loaded() {
+ console.debug('running init');
+ const mod = await init();
+ console.debug('init done', mod);
+ const vm = make_vm();
+
+ 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);
+ renderTextHighlight(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);