website

Personal website
git clone https://github.com/michaelneuper/website.git
README | Files | Log | Refs

website/src/scripts/demos/bytecode.js (8207B)


// Integer expressions compiled to JVM bytecode, then run on an operand stack
// with Java's int semantics (wrap-around, truncating division, / by zero).

import { reducedMotion } from "../util.js";
import "../../styles/demos/bytecode.css";

const LOCALS = { x: 6, y: 7, z: -2 }; // slots 0, 1, 2 of a static method
const SLOT = { x: 0, y: 1, z: 2 };
const OPS = { "+": "iadd", "-": "isub", "*": "imul", "/": "idiv", "%": "irem" };

function tokenize(src) {
  const out = [];
  const re = /\s*(?:(\d+)|([A-Za-z_]\w*)|(.))/gy;
  let m;
  while (re.lastIndex < src.length && (m = re.exec(src))) {
    const pos = m.index + m[0].length - (m[1] || m[2] || m[3] || "").length;
    if (m[1]) out.push({ t: "num", v: m[1], pos });
    else if (m[2]) out.push({ t: "id", v: m[2], pos });
    else if (m[3] && m[3].trim()) out.push({ t: "op", v: m[3], pos });
  }
  out.push({ t: "end", pos: src.length });
  return out;
}

class CompileError extends Error {
  constructor(msg, pos) { super(msg); this.pos = pos; }
}

function parse(src) {
  const toks = tokenize(src);
  let i = 0;
  const peek = () => toks[i];
  const next = () => toks[i++];
  const expect = (v) => {
    const t = next();
    if (t.v !== v) throw new CompileError(`'${v}' expected`, t.pos);
  };
  function expr() {
    let l = term();
    while (peek().t === "op" && "+-".includes(peek().v)) l = { t: "bin", op: next().v, l, r: term() };
    return l;
  }
  function term() {
    let l = unary();
    while (peek().t === "op" && "*/%".includes(peek().v)) l = { t: "bin", op: next().v, l, r: unary() };
    return l;
  }
  function unary() {
    if (peek().t === "op" && peek().v === "-") { next(); return { t: "neg", e: unary() }; }
    if (peek().t === "op" && peek().v === "+") { next(); return unary(); }
    return primary();
  }
  function primary() {
    const t = next();
    if (t.t === "num") {
      const v = Number(t.v);
      if (v > 2147483647) throw new CompileError("integer number too large", t.pos);
      return { t: "num", v };
    }
    if (t.t === "id") {
      if (!(t.v in SLOT)) throw new CompileError(`cannot find symbol: variable ${t.v}`, t.pos);
      return { t: "var", name: t.v };
    }
    if (t.v === "(") { const e = expr(); expect(")"); return e; }
    throw new CompileError(t.t === "end" ? "illegal start of expression" : `unexpected '${t.v}'`, t.pos);
  }
  const tree = expr();
  if (peek().t !== "end") throw new CompileError(`unexpected '${peek().v}'`, peek().pos);
  return tree;
}

function codegen(tree) {
  const code = [];
  const pool = [];
  const emit = (op, arg, size = 1) => code.push({ op, arg, size });
  (function gen(n) {
    if (n.t === "num") {
      const v = n.v;
      if (v >= -1 && v <= 5) emit(v === -1 ? "iconst_m1" : `iconst_${v}`);
      else if (v >= -128 && v <= 127) emit("bipush", v, 2);
      else if (v >= -32768 && v <= 32767) emit("sipush", v, 3);
      else {
        let k = pool.indexOf(v);
        if (k < 0) k = pool.push(v) - 1;
        emit("ldc", v, 2);
        code[code.length - 1].text = `ldc #${k + 2}`;
      }
    } else if (n.t === "var") emit(`iload_${SLOT[n.name]}`, n.name);
    else if (n.t === "neg") { gen(n.e); emit("ineg"); }
    else { gen(n.l); gen(n.r); emit(OPS[n.op]); }
  })(tree);
  emit("ireturn");
  let off = 0;
  for (const c of code) { c.off = off; off += c.size; }
  return code;
}

function exec(ins, stack) {
  const pop = () => stack.pop();
  if (ins.op.startsWith("iconst_")) stack.push(ins.op === "iconst_m1" ? -1 : +ins.op.slice(7));
  else if (ins.op === "bipush" || ins.op === "sipush" || ins.op === "ldc") stack.push(ins.arg);
  else if (ins.op.startsWith("iload_")) stack.push(LOCALS[ins.arg]);
  else if (ins.op === "ineg") stack.push(-pop() | 0);
  else if (ins.op === "ireturn") return { done: true, value: stack[stack.length - 1] };
  else {
    const b = pop();
    const a = pop();
    if ((ins.op === "idiv" || ins.op === "irem") && b === 0) throw new Error("java.lang.ArithmeticException: / by zero");
    stack.push(
      ins.op === "iadd" ? (a + b) | 0
        : ins.op === "isub" ? (a - b) | 0
          : ins.op === "imul" ? Math.imul(a, b)
            : ins.op === "idiv" ? Math.trunc(a / b) | 0
              : (a % b) | 0,
    );
  }
  return { done: false };
}

export function mount(host) {
  host.innerHTML = `
    <div class="bc-input">
      <input type="text" name="expression" value="(x + 3) * y - 10 / z" autocomplete="off" spellcheck="false" autocapitalize="off" aria-label="Expression to compile">
      <button type="button" class="btn" data-k="step">step</button>
      <button type="button" class="btn" data-k="run">run</button>
    </div>
    <p class="dim" style="margin:0 0 12px">int x = 6, y = 7, z = -2 (local variable slots 0 to 2)</p>
    <div class="bc-cols">
      <div><div class="dim">javap -c</div><ol class="bc-code" translate="no"></ol></div>
      <div><div class="dim">operand stack</div><div class="bc-stack"></div><p class="dim" data-k="result" aria-live="polite" style="margin:8px 0 0"></p></div>
    </div>`;
  const input = host.querySelector("input");
  const list = host.querySelector(".bc-code");
  const stackEl = host.querySelector(".bc-stack");
  // Only the outcome is announced: what it returns, the exception, or why it
  // won't compile when you try to step or run it (not on every keystroke).
  const result = host.querySelector('[data-k="result"]');
  let code = [];
  let error = "";
  let pc = 0;
  let stack = [];
  let timer = 0;
  let finished = false;

  function compile() {
    clearInterval(timer);
    pc = 0;
    stack = [];
    finished = false;
    error = "";
    try {
      code = codegen(parse(input.value));
      list.replaceChildren(...code.map((c) => {
        const li = document.createElement("li");
        li.innerHTML = `<span class="off">${c.off}:</span><span><span class="op">${c.text ? "ldc" : c.op}</span>${c.text ? ` ${c.text.slice(4)}<span class="dim">  // int ${c.arg}</span>` : c.arg !== undefined && !c.op.startsWith("iload") ? ` ${c.arg}` : ""}</span>`;
        return li;
      }));
      result.textContent = "";
    } catch (e) {
      code = [];
      error = `error: ${e.message}`;
      const pre = document.createElement("li");
      pre.className = "bc-err";
      pre.textContent = `${error}\n  ${input.value}\n  ${" ".repeat(e.pos ?? 0)}^`;
      list.replaceChildren(pre);
      result.textContent = "";
    }
    paint();
  }

  function paint() {
    [...list.children].forEach((li, i) => {
      if (i === pc && !finished && code.length > 0) li.setAttribute("aria-current", "step");
      else li.removeAttribute("aria-current");
    });
    stackEl.replaceChildren(...stack.map((v) => {
      const s = document.createElement("span");
      s.textContent = v;
      return s;
    }));
  }

  function step() {
    if (!code.length) {
      result.textContent = error;
      return false;
    }
    if (finished) { pc = 0; stack = []; finished = false; result.textContent = ""; }
    try {
      const r = exec(code[pc], stack);
      if (r.done) {
        finished = true;
        result.textContent = `returns ${r.value}`;
      } else pc++;
    } catch (e) {
      finished = true;
      result.textContent = e.message;
    }
    paint();
    return !finished;
  }

  function run() {
    clearInterval(timer);
    // nothing compiled: step says why
    if (!code.length) { step(); return; }
    if (finished) step();
    if (reducedMotion.matches) {
      while (step()) { /* run to the end */ }
      return;
    }
    timer = setInterval(() => { if (!step()) clearInterval(timer); }, 320);
  }

  let debounce = 0;
  input.addEventListener("input", () => { clearTimeout(debounce); debounce = setTimeout(compile, 180); });
  // Enter compiles now, so a compile still pending from typing mustn't stop the run
  input.addEventListener("keydown", (e) => { if (e.key === "Enter") { e.preventDefault(); clearTimeout(debounce); compile(); run(); } });
  host.querySelector('[data-k="step"]').addEventListener("click", () => { clearInterval(timer); step(); });
  host.querySelector('[data-k="run"]').addEventListener("click", run);
  compile();
  return () => { clearInterval(timer); clearTimeout(debounce); };
}