tla-checker 0.6.0

A TLA+ model checker written in Rust
Documentation
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>TLA+ demo walkthrough</title>
<style>
  :root {
    --bg: #0f1115; --panel: #171a21; --line: #2a2f3a; --fg: #e6e9ef;
    --dim: #8b93a3; --accent: #6ea8fe; --ok: #4ec98a; --bad: #ff6b6b;
    --chg: #3a2f12; --chgline: #d8a534;
  }
  * { box-sizing: border-box; }
  body {
    margin: 0; background: var(--bg); color: var(--fg);
    font: 15px/1.5 -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
  }
  header { padding: 16px 22px; border-bottom: 1px solid var(--line); }
  header h1 { margin: 0; font-size: 19px; font-weight: 650; }
  nav#beat-list { display: flex; flex-wrap: wrap; gap: 8px; padding: 14px 22px; border-bottom: 1px solid var(--line); }
  .beat-tab {
    background: var(--panel); color: var(--fg); border: 1px solid var(--line);
    border-radius: 7px; padding: 7px 12px; cursor: pointer; font-size: 13px;
  }
  .beat-tab:hover { border-color: var(--accent); }
  .beat-tab.active { border-color: var(--accent); background: #1d2330; }
  .beat-tab.fail { border-left: 3px solid var(--bad); }
  .beat-tab.pass { border-left: 3px solid var(--ok); }
  main { padding: 20px 22px 60px; max-width: 1100px; }
  main h2 { margin: 4px 0 6px; font-size: 18px; }
  .note { color: var(--dim); margin: 0 0 16px; max-width: 760px; }
  .scenario { background: var(--panel); border: 1px solid var(--line); border-radius: 8px; padding: 8px 10px; margin-bottom: 14px; }
  .sline { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 13px; color: var(--dim); padding: 2px 6px; border-radius: 4px; }
  .sline.cur { color: var(--fg); background: #1d2330; }
  .steps { display: flex; align-items: center; gap: 12px; margin-bottom: 14px; }
  .steps button { background: var(--panel); color: var(--fg); border: 1px solid var(--line); border-radius: 6px; padding: 6px 12px; cursor: pointer; }
  .steps button:hover { border-color: var(--accent); }
  .steps .pos { color: var(--dim); font-variant-numeric: tabular-nums; }
  .cols { display: flex; gap: 16px; flex-wrap: wrap; }
  .col { flex: 1 1 280px; background: var(--panel); border: 1px solid var(--line); border-radius: 8px; padding: 12px 14px; min-width: 260px; }
  .vhead { font-weight: 650; margin-bottom: 4px; }
  .label { color: var(--accent); font-family: ui-monospace, monospace; font-size: 13px; margin-bottom: 8px; }
  table.state { width: 100%; border-collapse: collapse; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 13px; }
  table.state td { padding: 3px 6px; border-bottom: 1px solid var(--line); vertical-align: top; }
  table.state .vn { color: var(--dim); white-space: nowrap; }
  table.state .vv { text-align: right; word-break: break-all; }
  table.state tr.chg { background: var(--chg); }
  table.state tr.chg .vv { color: var(--chgline); font-weight: 650; }
  ul.asserts { list-style: none; margin: 12px 0 0; padding: 0; font-size: 13px; }
  ul.asserts li { padding: 2px 0; }
  code { font-family: ui-monospace, monospace; background: #1d2330; padding: 1px 5px; border-radius: 4px; }
  .ok { color: var(--ok); }
  .bad { color: var(--bad); }
  footer { position: fixed; bottom: 0; left: 0; right: 0; padding: 8px 22px; background: var(--panel); border-top: 1px solid var(--line); color: var(--dim); font-size: 12px; }
</style>
</head>
<body>
<header><h1 id="demo-title"></h1></header>
<nav id="beat-list"></nav>
<main id="beat-view"></main>
<footer>← / → step &nbsp;·&nbsp; [ / ] beat &nbsp;·&nbsp; click a beat above</footer>
<script>
const DATA = /*DEMO_DATA*/;
let beatIdx = 0, step = 0;

function esc(s) {
  return String(s).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
}
function maxStep(beat) {
  return Math.max(0, ...beat.runs.map(r => r.trace.length - 1));
}
function badge(passed) {
  return passed ? '<span class="ok">✓</span>' : '<span class="bad">✗</span>';
}

function renderBeatList() {
  const nav = document.getElementById("beat-list");
  nav.innerHTML = "";
  DATA.beats.forEach((b, i) => {
    const el = document.createElement("button");
    el.className = "beat-tab " + (b.passed ? "pass" : "fail") + (i === beatIdx ? " active" : "");
    el.textContent = (i + 1) + ". " + b.title + (b.passed ? "" : "");
    el.onclick = () => { beatIdx = i; step = 0; render(); };
    nav.appendChild(el);
  });
}

function renderBeat() {
  const beat = DATA.beats[beatIdx];
  const top = maxStep(beat);
  if (step > top) step = top;

  let h = "";
  h += "<h2>" + esc(beat.title) + " " + badge(beat.passed) + "</h2>";
  if (beat.note) h += '<p class="note">' + esc(beat.note) + "</p>";

  h += '<div class="scenario">';
  beat.scenario.forEach((line, k) => {
    h += '<div class="sline' + (k + 1 === step ? " cur" : "") + '">' + esc(line) + "</div>";
  });
  h += "</div>";

  h += '<div class="steps">';
  h += '<button id="prev">‹ prev</button>';
  h += '<span class="pos">step ' + step + " / " + top + "</span>";
  h += '<button id="next">next ›</button>';
  h += "</div>";

  h += '<div class="cols">';
  beat.runs.forEach(run => {
    h += '<div class="col">';
    h += '<div class="vhead">' + esc(run.variant) + " " + badge(run.passed) + "</div>";
    if (run.failure) h += '<div class="bad">error: ' + esc(run.failure) + "</div>";
    const cur = run.trace[Math.min(step, run.trace.length - 1)];
    const prev = step > 0 ? run.trace[Math.min(step - 1, run.trace.length - 1)] : null;
    h += '<div class="label">' + esc(cur.label) + "</div>";
    h += '<table class="state">';
    run.vars.forEach(v => {
      const val = cur.vars[v] !== undefined ? cur.vars[v] : "";
      const changed = prev && prev.vars[v] !== val;
      h += "<tr class=\"" + (changed ? "chg" : "") + "\">";
      h += '<td class="vn">' + esc(v) + "</td>";
      h += '<td class="vv">' + esc(val) + "</td></tr>";
    });
    h += "</table>";
    if (run.assertions.length) {
      h += '<ul class="asserts">';
      run.assertions.forEach(a => {
        let line = (a.passed ? "" : "") + " <code>" + esc(a.expectation) + "</code>";
        if (!a.passed && a.detail) line += "" + esc(a.detail);
        h += '<li class="' + (a.passed ? "ok" : "bad") + '">' + line + "</li>";
      });
      h += "</ul>";
    }
    h += "</div>";
  });
  h += "</div>";

  const view = document.getElementById("beat-view");
  view.innerHTML = h;
  document.getElementById("prev").onclick = () => { if (step > 0) { step--; render(); } };
  document.getElementById("next").onclick = () => { if (step < maxStep(beat)) { step++; render(); } };
}

function render() {
  document.getElementById("demo-title").textContent = DATA.title;
  document.title = DATA.title;
  renderBeatList();
  renderBeat();
}

document.addEventListener("keydown", e => {
  const beat = DATA.beats[beatIdx];
  if (e.key === "ArrowRight") { if (step < maxStep(beat)) { step++; render(); } }
  else if (e.key === "ArrowLeft") { if (step > 0) { step--; render(); } }
  else if (e.key === "]") { beatIdx = (beatIdx + 1) % DATA.beats.length; step = 0; render(); }
  else if (e.key === "[") { beatIdx = (beatIdx - 1 + DATA.beats.length) % DATA.beats.length; step = 0; render(); }
});

render();
</script>
</body>
</html>