Never restarting is the thesis; say where the holes in it are
The manual described the signature-change refusal as though it were the design. It is not, and session.ml already said so at the refusal: a signature change should make a new version, leave old callers on the old one, and warn at the stale sites. plan.org calls it signature generations and stale-caller warnings, and it is milestone 7's unfinished half. The struct-layout rejection is the decided one and stays. Conflating them made a placeholder look like a rule.
This commit is contained in:
parent
50ed2cbef0
commit
3afce2aeac
17
NEXT.md
17
NEXT.md
@ -210,6 +210,23 @@ vague intention — if it is listed, someone has already established it is real.
|
||||
|
||||
### Unblocked now, and ranked
|
||||
|
||||
**0. Signature generations and stale-caller warnings — milestone 7's unfinished half.**
|
||||
Promoted here on the author's correction, and `session.ml:146` already says the same thing at the refusal itself. A
|
||||
changed signature is refused today and **that is a placeholder, not the design**. plan.org's open decision #6 says what
|
||||
should happen: a signature change makes a new version of the function, new callers resolve it, existing callers and any
|
||||
stored `Fn` value stay safely on the old one, and the session *warns* at each tracked stale caller site. Milestone 7
|
||||
names it outright — "signature generations and stale-caller warnings".
|
||||
|
||||
**The thesis of this project is that you never restart the program.** Every refusal that ends in "restart to change it"
|
||||
is a hole in that, and this is the biggest one. It needs three things that do not exist: function versions, a
|
||||
trampoline per version, and caller tracking good enough to name the sites. The cell already gives the indirection; what
|
||||
is missing is that a cell holds one bare pointer with no signature, so there is nowhere to put a second version.
|
||||
|
||||
A changed **struct layout** is the genuinely hard case and plan.org still specifies it as a rejection — storage already
|
||||
allocated has the old shape and a new body reads its fields at the wrong offsets. Managed classes are the planned way
|
||||
through, with an explicit migration at a frame boundary. Do not conflate the two: one is unbuilt, the other is decided.
|
||||
|
||||
|
||||
**From porting ten raylib examples** — the first code the language was pushed by that it was not designed around.
|
||||
Ranked by how often they were hit, top two first because they are walls rather than conveniences:
|
||||
|
||||
|
||||
@ -194,19 +194,39 @@ press rather than a timer.
|
||||
|
||||
## When a change is refused
|
||||
|
||||
Some changes cannot be made to a running program. Change a struct's layout, or a
|
||||
function's signature, and the daemon refuses and tells you to restart. It is
|
||||
being honest: the session's idea of your types has to describe the memory of the
|
||||
process it is talking to, and after a layout change it no longer does.
|
||||
Two different things wear the same refusal today, and only one of them is the
|
||||
design.
|
||||
|
||||
**`C-c C-x`** (`flan-dev-restart-program`) is the way out. It stops the program,
|
||||
rebuilds from source, starts it again, and reconnects.
|
||||
### A changed signature — a placeholder, not a rule
|
||||
|
||||
The program's state goes with it. That is the whole cost, and it is why this is a
|
||||
separate key rather than something `C-c C-c` quietly falls back to — losing your
|
||||
game's state should be something you asked for.
|
||||
The intended behaviour, and what plan.org specifies, is that changing a
|
||||
function's signature makes a **new version** of it: new callers resolve the new
|
||||
one, existing callers and any stored `Fn` value stay safely on the old one, and
|
||||
the session **warns** at each tracked stale caller site so you know what to
|
||||
re-evaluate. Nothing should have to restart.
|
||||
|
||||
---
|
||||
That needs function versions, trampolines and caller tracking, none of which are
|
||||
built yet. Until they are, the session refuses rather than letting an
|
||||
indirection cell hand old arguments to a new body — a wrong answer would be
|
||||
worse than a refusal. `lib/session.ml` says so at the refusal itself, and
|
||||
plan.org tracks it as open decision #6.
|
||||
|
||||
So if you hit this: it is a limitation with a date on it, not how the language
|
||||
is meant to work.
|
||||
|
||||
### A changed struct layout — the genuinely hard one
|
||||
|
||||
Rejected while live values of that struct exist, and this one plan.org does
|
||||
still specify as a rejection. Storage already allocated has the old shape; a new
|
||||
body would read its fields at the wrong offsets and nothing at run time would
|
||||
say so. Managed classes are the planned way through — an explicit migration at a
|
||||
frame boundary — and they are not built either.
|
||||
|
||||
### The way out, for now
|
||||
|
||||
`C-c C-x` stops the program, rebuilds from source, starts it again and
|
||||
reconnects. It costs the program's state, which is why it is a key you press
|
||||
rather than something `C-c C-c` quietly falls back to.
|
||||
|
||||
## Under the debugger
|
||||
|
||||
|
||||
249
web/index.html
249
web/index.html
@ -116,6 +116,34 @@ th, td { text-align: left; padding: .38rem .6rem .38rem 0; border-bottom: 1px so
|
||||
th { font-weight: 600; color: var(--muted); }
|
||||
.scroll { overflow-x: auto; margin: 0 0 1.15rem; }
|
||||
|
||||
/* Tabs. The panels are visible by default and only become switchable once
|
||||
the script adds .tabbed to <main> — with no script the page is the one long
|
||||
document it has always been, which is also what a printer and a text browser
|
||||
get. */
|
||||
.tabs {
|
||||
display: flex; flex-wrap: wrap; gap: .4rem;
|
||||
margin: 0 0 2rem; padding: 0 0 .6rem;
|
||||
border-bottom: 1px solid var(--rule);
|
||||
}
|
||||
.tabs button {
|
||||
font: inherit; font-size: .93rem; cursor: pointer;
|
||||
background: none; border: 1px solid transparent; border-radius: 4px;
|
||||
padding: .3rem .7rem; color: var(--muted);
|
||||
}
|
||||
.tabs button:hover { color: var(--link); }
|
||||
.tabs button[aria-selected="true"] {
|
||||
color: var(--text); background: var(--panel); border-color: var(--rule);
|
||||
}
|
||||
.tabs button:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
|
||||
main:not(.tabbed) .tabs { display: none; }
|
||||
main.tabbed .panel[hidden] { display: none; }
|
||||
|
||||
.toc-group {
|
||||
margin: 1.1rem 0 .3rem; font-size: .72rem; letter-spacing: .09em;
|
||||
text-transform: uppercase; color: var(--muted); opacity: .75;
|
||||
}
|
||||
.toc-group:first-child { margin-top: 0; }
|
||||
|
||||
.lede { font-size: 1.1rem; }
|
||||
.note { color: var(--muted); font-size: .94rem; }
|
||||
|
||||
@ -190,6 +218,7 @@ footer { margin-top: 3.5rem; padding-top: 1.5rem; border-top: 1px solid var(--ru
|
||||
|
||||
<nav class="toc" aria-label="Contents">
|
||||
<p class="toc-title">Contents</p>
|
||||
<p class="toc-group">Language</p>
|
||||
<ul>
|
||||
<li><a href="#what">What Flan is</a></li>
|
||||
<li><a href="#start">Getting started</a></li>
|
||||
@ -203,11 +232,23 @@ footer { margin-top: 3.5rem; padding-top: 1.5rem; border-top: 1px solid var(--ru
|
||||
<li><a href="#printing">Printing</a></li>
|
||||
<li><a href="#prelude">The prelude</a></li>
|
||||
<li><a href="#packages">Packages</a></li>
|
||||
</ul>
|
||||
<p class="toc-group">Conditions</p>
|
||||
<ul>
|
||||
<li><a href="#conditions">Conditions and restarts</a></li>
|
||||
<li><a href="#break">The break loop</a></li>
|
||||
</ul>
|
||||
<p class="toc-group">The FFI</p>
|
||||
<ul>
|
||||
<li><a href="#ffi">The FFI</a></li>
|
||||
</ul>
|
||||
<p class="toc-group">The dev loop</p>
|
||||
<ul>
|
||||
<li><a href="#devloop">The dev loop</a></li>
|
||||
<li><a href="#emacs">Emacs</a></li>
|
||||
</ul>
|
||||
<p class="toc-group">Builds</p>
|
||||
<ul>
|
||||
<li><a href="#targets">Targets and builds</a></li>
|
||||
<li><a href="#notyet">Not implemented yet</a></li>
|
||||
<li><a href="#reading">Further reading</a></li>
|
||||
@ -217,6 +258,14 @@ footer { margin-top: 3.5rem; padding-top: 1.5rem; border-top: 1px solid var(--ru
|
||||
<main>
|
||||
<div class="wrap">
|
||||
|
||||
<div class="tabs" role="tablist" aria-label="Sections">
|
||||
<button type="button" role="tab" data-panel="panel-language" aria-selected="true">Language</button>
|
||||
<button type="button" role="tab" data-panel="panel-conditions" aria-selected="false">Conditions</button>
|
||||
<button type="button" role="tab" data-panel="panel-ffi" aria-selected="false">The FFI</button>
|
||||
<button type="button" role="tab" data-panel="panel-devloop" aria-selected="false">The dev loop</button>
|
||||
<button type="button" role="tab" data-panel="panel-builds" aria-selected="false">Builds</button>
|
||||
</div>
|
||||
|
||||
<header>
|
||||
<svg class="mark" viewBox="0 0 118 44" role="img" aria-label="Flan">
|
||||
<!-- The dot is a full stop in the same run, so it stays beside the n
|
||||
@ -242,6 +291,8 @@ with its output recorded beside it; <code>sh web/examples/check.sh</code> runs t
|
||||
and compares, and <code>quotes.sh</code> re-derives the blocks that are transcripts
|
||||
rather than programs.</p>
|
||||
|
||||
<div class="panel" id="panel-language">
|
||||
|
||||
<h2 id="what">What Flan is</h2>
|
||||
|
||||
<p>A minimal Lisp for games. In one line: Odin with a Lisp frontend and a live REPL.
|
||||
@ -854,6 +905,10 @@ rather than from the import list: the compiler starts at <code>main</code>, foll
|
||||
call, and a package none of whose externs survive contributes no C and no linker
|
||||
argument, so a file that imports raylib still builds for wasm32.</p>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="panel" id="panel-conditions">
|
||||
|
||||
<h2 id="conditions">Conditions and restarts</h2>
|
||||
|
||||
<p>A condition is a struct. There is no class hierarchy; matching is by type. The
|
||||
@ -1014,6 +1069,10 @@ unhandled Missing
|
||||
$ echo $?
|
||||
134</code></pre>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="panel" id="panel-ffi">
|
||||
|
||||
<h2 id="ffi">The FFI</h2>
|
||||
|
||||
<p>There are two declaration forms, and they are two forms rather than one because no
|
||||
@ -1094,6 +1153,10 @@ refused too, so the narrow boundary cannot quietly acquire one.</p>
|
||||
would not exist in the running process. Editing the body of a function that calls an
|
||||
existing binding is unaffected.</p>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="panel" id="panel-devloop">
|
||||
|
||||
<h2 id="devloop">The dev loop</h2>
|
||||
|
||||
<p>Edit the code, keep the sand.</p>
|
||||
@ -1254,11 +1317,34 @@ retains three, permanently and correctly.</p>
|
||||
|
||||
<p><code>emacs/flan-mode.el</code> derives from <code>prog-mode</code> with
|
||||
<code>lisp-mode</code>'s syntax table, so sexp motion, paren matching and indentation are
|
||||
already right. It adds Flan's brackets — <code>[</code> and <code>{</code> are brackets,
|
||||
not symbol characters, since every binding list and every type is written with them — and
|
||||
already right. It adds Flan's brackets — <code>[</code> and <code>{</code> are brackets,
|
||||
not symbol characters, since every binding list and every type is written with them — and
|
||||
the characters a Flan name may contain. <code>emacs/flan-dev.el</code> is the client;
|
||||
there is no parser in it.</p>
|
||||
|
||||
<h3>Setting up</h3>
|
||||
|
||||
<p>Put <code>emacs/</code> on your load path and require the mode. Nothing else is needed:
|
||||
the client, the REPL, the inspector and the conditions buffer all load on first use.</p>
|
||||
|
||||
<pre><code class="sh">(add-to-list 'load-path "~/Development/flan/emacs")
|
||||
(require 'flan-mode)
|
||||
(require 'flan-dape) ; optional — lldb, and the only thing that binds C-c C-g</code></pre>
|
||||
|
||||
<p><code>flan-dape.el</code> is separate on purpose, so <code>flan-mode</code> works
|
||||
without dape installed. You also need <code>flan</code> on your <code>PATH</code>, or
|
||||
<code>flan-dev-command</code> pointed at it.</p>
|
||||
|
||||
<h3>Starting a program</h3>
|
||||
|
||||
<p><code>M-x flan-dev</code> runs <code>flan dev</code> on a file, waits for it to come up
|
||||
and connects. <kbd>C-c C-z</kbd> attaches to one that is already running, looking for
|
||||
<code>.flan-dev.sock</code> upward from the buffer, so from anywhere in the project it
|
||||
finds the one program you have going. <kbd>C-c C-q</kbd> disconnects and leaves it
|
||||
running; <code>M-x flan-dev-quit</code> stops it, but only one this Emacs started — a
|
||||
daemon you launched in a terminal is not Emacs' to kill, and it says so rather than doing
|
||||
something surprising.</p>
|
||||
|
||||
<div class="scroll">
|
||||
<table>
|
||||
<tr><th>Key</th><th>Does</th></tr>
|
||||
@ -1275,7 +1361,7 @@ there is no parser in it.</p>
|
||||
<tr><td><kbd>C-c C-g</kbd></td><td>debug under lldb, through dape — bound only once <code>flan-dape.el</code> is loaded, so <code>flan-mode</code> works without dape installed</td></tr>
|
||||
<tr><td><kbd>C-c C-d</kbd></td><td>what the running program currently defines</td></tr>
|
||||
<tr><td><kbd>C-c C-v</kbd></td><td>help on the name at point</td></tr>
|
||||
<tr><td><kbd>C-c C-x</kbd></td><td>rebuild, relaunch and reconnect — the way out when a reload is refused</td></tr>
|
||||
<tr><td><kbd>C-c C-x</kbd></td><td>rebuild, relaunch and reconnect</td></tr>
|
||||
<tr><td><kbd>M-.</kbd> / <kbd>M-,</kbd></td><td>where a name is written, through an <code>xref</code> backend</td></tr>
|
||||
</table>
|
||||
</div>
|
||||
@ -1284,17 +1370,103 @@ there is no parser in it.</p>
|
||||
<code>defvar</code> and the function that uses it have to arrive in the same load, or the
|
||||
first refers to storage that does not exist yet.</p>
|
||||
|
||||
<p><kbd>C-x C-e</kbd> compiles the expression before point and runs it <em>inside</em> the
|
||||
running program — the actual process, with its actual state, not a copy. Both it and
|
||||
<kbd>C-c C-c</kbd> work on buffer text rather than the saved file, and a change installs at
|
||||
the next frame boundary.</p>
|
||||
|
||||
<h3>The buffers, and their own keys</h3>
|
||||
|
||||
<p>Three buffers have keymaps of their own. The conditions buffer
|
||||
(<kbd>C-c C-b</kbd>) shows the condition, then the restarts, then the stack — in that
|
||||
order, because the decision in front of you is which restart to take and the stack is only
|
||||
the explanation for it.</p>
|
||||
|
||||
<div class="scroll">
|
||||
<table>
|
||||
<tr><th>Buffer</th><th>Keys</th></tr>
|
||||
<tr><td>conditions</td>
|
||||
<td><kbd>RET</kbd> take the restart at point · <kbd>0</kbd>–<kbd>9</kbd> take one by number ·
|
||||
<kbd>TAB</kbd>/<kbd>n</kbd>, <kbd>S-TAB</kbd>/<kbd>p</kbd> move ·
|
||||
<kbd>f</kbd> fold a frame · <kbd>i</kbd> inspect a local ·
|
||||
<kbd>a</kbd> abort · <kbd>g</kbd> re-read · <kbd>q</kbd> close</td></tr>
|
||||
<tr><td>inspector</td>
|
||||
<td><kbd>RET</kbd> into the field at point · <kbd>l</kbd> back out ·
|
||||
<kbd>g</kbd> re-read · <kbd>TAB</kbd>/<kbd>n</kbd>, <kbd>S-TAB</kbd>/<kbd>p</kbd> move ·
|
||||
<kbd>q</kbd> close</td></tr>
|
||||
<tr><td>repl</td>
|
||||
<td><code>comint</code>, plus <kbd>C-c C-o</kbd>, <kbd>C-c C-d</kbd> and <kbd>C-c C-q</kbd></td></tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<p>A restart is taken by <em>position</em>, which is why the list is numbered: a name
|
||||
resolves to the innermost frame offering it, so an outer <code>retry</code> shadowed by an
|
||||
inner one is real, is on the list, and cannot be reached by name. One that genuinely cannot
|
||||
be taken is drawn and refused with the reason rather than quietly omitted.</p>
|
||||
|
||||
<p>The inspector is unlike most: the view is never stale, because every step re-reads the
|
||||
program as it is now. The cost is that <strong>the root expression runs again on every
|
||||
step</strong> — going into a field sends <code>(.pos b)</code> where the last one sent
|
||||
<code>b</code>, which is harmless, but inspecting <code>(spawn-enemy)</code> spawns one per
|
||||
keystroke. That is why there is no auto-refresh and why <kbd>g</kbd> is a key you press.</p>
|
||||
|
||||
<p>The repl is program-scoped rather than buffer-scoped, so in sand you write
|
||||
<code>sim/settle</code> and not <code>settle</code>. <code>*flan-output*</code> is separate
|
||||
from it, because the program's stdout belongs to the program.</p>
|
||||
|
||||
<h3>When a change is refused</h3>
|
||||
|
||||
<p>Two different things wear the same refusal today, and only one of them is the design.</p>
|
||||
|
||||
<p><strong>A changed signature is a placeholder refusal.</strong> The intended behaviour,
|
||||
and what <a href="#reading">plan.org</a> specifies, is that a signature change makes a new
|
||||
version of the function: new callers resolve it, existing callers and any stored
|
||||
<code>Fn</code> value stay safely on the old one, and the session <em>warns</em> at each
|
||||
tracked stale caller site so you know what to re-evaluate. Nothing should have to restart.
|
||||
That needs function versions, trampolines and caller tracking, none of which are built yet
|
||||
— so until they are, the session refuses rather than letting a cell hand old arguments
|
||||
to a new body. The refusal is a limitation with a date on it, not a rule.</p>
|
||||
|
||||
<p><strong>A changed struct layout is the genuinely hard case</strong>, and is rejected
|
||||
while live values of that struct exist: storage already allocated has the old shape, and a
|
||||
new body would read its fields at the wrong offsets with nothing to say so. plan.org keeps
|
||||
this one as a rejection, and gives managed classes an explicit migration at a frame
|
||||
boundary as the eventual way through.</p>
|
||||
|
||||
<p><kbd>C-c C-x</kbd> rebuilds, relaunches and reconnects, and is the way out while the
|
||||
above is true. It costs the program's state, which is why it is a key you press rather than
|
||||
something <kbd>C-c C-c</kbd> falls back to.</p>
|
||||
|
||||
<h3>Around the edges</h3>
|
||||
|
||||
<p>eldoc, completion and <kbd>M-.</kbd> all read one cached reply rather than asking per
|
||||
keystroke, refreshed at the two moments the answer can have changed: on connect, and after
|
||||
an evaluation the daemon accepted. The modeline says whether there is a program on the
|
||||
other end, and says <code>stopped</code> when there is one sitting in the break loop — a
|
||||
other end, and says <code>stopped</code> when there is one sitting in the break loop — a
|
||||
stopped program looks exactly like a running one from anywhere else in Emacs.</p>
|
||||
|
||||
<p>An error comes back with a location and the client draws an overlay there, cleared the
|
||||
next time that buffer's evaluation is accepted. The <code>repl</code> buffer is
|
||||
<code>comint</code>-derived and every line goes through the same request <kbd>C-x C-e</kbd>
|
||||
uses; it is program-scoped, so in sand you write <code>sim/settle</code> and not
|
||||
<code>settle</code>.</p>
|
||||
next time that buffer's evaluation is accepted. Under <kbd>C-c C-g</kbd> lldb needs no
|
||||
plugin to read a Flan value, since a struct is its C struct; locals show under their real
|
||||
names, and a shadowed one appears as <code>v~2</code> while plain <code>v</code> still
|
||||
answers with the outer binding.</p>
|
||||
|
||||
<div class="scroll">
|
||||
<table>
|
||||
<tr><th>Setting</th><th>Default</th><th>What it is</th></tr>
|
||||
<tr><td><code>flan-dev-command</code></td><td><code>"flan"</code></td><td>the compiler binary</td></tr>
|
||||
<tr><td><code>flan-dev-socket-name</code></td><td><code>".flan-dev.sock"</code></td><td>what <kbd>C-c C-z</kbd> searches for</td></tr>
|
||||
<tr><td><code>flan-dev-echo-result</code></td><td><code>t</code></td><td>print <kbd>C-x C-e</kbd>'s value in the echo area</td></tr>
|
||||
<tr><td><code>flan-dev-names-shown</code></td><td><code>4</code></td><td>how many names to list before counting them</td></tr>
|
||||
<tr><td><code>flan-dev-output-buffer</code></td><td><code>"*flan-output*"</code></td><td>where the program's output goes</td></tr>
|
||||
<tr><td><code>flan-dev-poll-interval</code></td><td><code>1.0</code></td><td>seconds between checks for whether it stopped</td></tr>
|
||||
<tr><td><code>flan-dev-start-timeout</code></td><td><code>60</code></td><td>seconds to wait for a program to come up</td></tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="panel" id="panel-builds">
|
||||
|
||||
<h2 id="targets">Targets and builds</h2>
|
||||
|
||||
@ -1404,6 +1576,8 @@ macro is a function from <code>Form</code> to <code>Form</code>, which needs
|
||||
real programs that run.</li>
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
|
||||
<footer>
|
||||
<p>Flan is a custard. This page describes the compiler on branch
|
||||
<code>dev-loop</code>; where a document and the compiler disagree, the compiler is what
|
||||
@ -1413,6 +1587,65 @@ macro is a function from <code>Form</code> to <code>Form</code>, which needs
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<script>
|
||||
// Tabs. The panels are plain markup and stay visible until this runs, so the
|
||||
// page without a script is the one long document it was before. A link into a
|
||||
// panel that is not showing switches to it first -- every #anchor on the page,
|
||||
// and every link from outside, has to keep working.
|
||||
(function () {
|
||||
var main = document.querySelector("main");
|
||||
var bar = document.querySelector(".tabs");
|
||||
if (!main || !bar) return;
|
||||
var tabs = Array.prototype.slice.call(bar.querySelectorAll("button"));
|
||||
var panels = tabs.map(function (t) { return document.getElementById(t.dataset.panel); });
|
||||
if (panels.some(function (p) { return !p; })) return; // markup moved; leave it long
|
||||
main.classList.add("tabbed");
|
||||
|
||||
function show(i, focus) {
|
||||
tabs.forEach(function (t, j) {
|
||||
t.setAttribute("aria-selected", j === i ? "true" : "false");
|
||||
panels[j].hidden = j !== i;
|
||||
});
|
||||
if (focus) tabs[i].focus();
|
||||
}
|
||||
|
||||
function panelOf(id) {
|
||||
var el = id && document.getElementById(id);
|
||||
while (el && el !== document.body) {
|
||||
if (el.classList && el.classList.contains("panel")) return el.id;
|
||||
el = el.parentElement;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function toHash() {
|
||||
var want = panelOf(location.hash.slice(1));
|
||||
if (!want) return false;
|
||||
var i = panels.findIndex(function (p) { return p.id === want; });
|
||||
if (i < 0) return false;
|
||||
show(i);
|
||||
var t = document.getElementById(location.hash.slice(1));
|
||||
if (t) t.scrollIntoView();
|
||||
return true;
|
||||
}
|
||||
|
||||
tabs.forEach(function (t, i) {
|
||||
t.addEventListener("click", function () { show(i); });
|
||||
// Left and right move between tabs, which is what a tablist is expected
|
||||
// to do once it says it is one.
|
||||
t.addEventListener("keydown", function (e) {
|
||||
var d = e.key === "ArrowRight" ? 1 : e.key === "ArrowLeft" ? -1 : 0;
|
||||
if (!d) return;
|
||||
e.preventDefault();
|
||||
show((i + d + tabs.length) % tabs.length, true);
|
||||
});
|
||||
});
|
||||
|
||||
window.addEventListener("hashchange", toHash);
|
||||
if (!toHash()) show(0);
|
||||
})();
|
||||
</script>
|
||||
|
||||
<script>
|
||||
// A small hand-written highlighter for the Flan blocks. One pass, no library.
|
||||
(function () {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user