Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,12 @@ tag, never to make the page answer on the compiler's behalf.
Concretely, that rules out a few things that would otherwise be tempting:

- No second formatter. `fmt` is one of the exports.
- No syntax highlighting that knows more than the grammar the compiler ships.
- No syntax highlighting written here. The colouring in the playground is the
compiler's own lexer, asked on every keystroke, so there is no second
grammar to keep in step.
- No error messages written here. Diagnostics are rendered from what the
compiler returns, carets and all.
compiler returns, carets and all, and the index at `errors/` is the set of
pages the compiler generates rather than a list maintained here.
- No examples written here. The playground's are the compiler's corpus at the
pinned tag, and the summary under each one is the comment at the top of the
file. The landing page's program is the exception, and it is short and its
Expand Down Expand Up @@ -88,7 +91,9 @@ checked through the pinned artifact and every one of them is clean.
```
index.html what the language is
play/ the playground
errors/ every diagnostic code, read out of the compiler
install/ how to get a binary running
one-clause/ what a signature turns into
examples/ the compiler's corpus at the pinned tag
assets/ the stylesheet, the scripts, the brand files, the compiler
tools/ the one check that runs before anything merges
Expand Down
Binary file removed assets/deed-v0.2.1-wasm32-unknown-unknown.wasm
Binary file not shown.
Binary file added assets/deed-v0.2.2-wasm32-unknown-unknown.wasm
Binary file not shown.
100 changes: 100 additions & 0 deletions assets/errors.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
// The error index, asked of the compiler rather than written here.
//
// `deed-explain` generates a page per diagnostic code from the doc comment
// above it and an example taken out of a test that already had to exist. The
// artifact carries all of them, so this page cannot document a code the
// compiler does not have, or miss one it does.

const TAG = "v0.2.2";
const VERSION = "0.2.2";
const WASM_URL = `../assets/deed-${TAG}-wasm32-unknown-unknown.wasm`;

const STATUS = document.getElementById("status");
const FILTER = document.getElementById("filter");
const COUNT = document.getElementById("count");
const CODES = document.getElementById("codes");

function esc(text) {
return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
}

function render(pages) {
CODES.innerHTML = pages
.map(
(page) => `
<section class="diagnostic" id="${page.code}">
<h2><a href="#${page.code}">${page.code}</a> <span class="d-gutter">${esc(page.name)}</span></h2>
<p>${esc(page.text).replace(/\n\n/g, "</p><p>").replace(/\n/g, " ")}</p>
${
page.example
? `<pre class="code"><code>${esc(page.example)}</code></pre>
<p class="from d-gutter">from ${esc(page.example_source ?? "")}</p>`
: `<p class="from d-gutter">No example could be lifted from a test for this one.</p>`
}
</section>`,
)
.join("");
}

async function load() {
let wasm;
try {
const module = await WebAssembly.instantiateStreaming(fetch(WASM_URL), {});
wasm = module.instance.exports;
} catch (error) {
STATUS.innerHTML = `<span class="d-error">The compiler did not load, so there is nothing to list. (${error})</span>`;
return;
}

const decoder = new TextDecoder();
const read = () => {
const ptr = wasm.deed_result_ptr();
const len = wasm.deed_result_len();
const text = decoder.decode(new Uint8Array(wasm.memory.buffer).slice(ptr, ptr + len));
wasm.deed_free(ptr, len);
return text;
};

wasm.deed_version();
const reported = read();
if (reported !== VERSION) {
STATUS.innerHTML = `<span class="d-error">This page pinned ${VERSION} and the module says ${reported}, so it is not being used.</span>`;
return;
}

wasm.deed_explain();
const pages = read()
.split("\n")
.filter((line) => line.trim() !== "")
.map((line) => JSON.parse(line));

STATUS.innerHTML =
`Deed ${esc(reported)}, ` +
`<a href="https://github.com/deed-lang/deed/releases/tag/${TAG}">${TAG}</a>, ` +
`asked in this tab.`;

FILTER.disabled = false;
const show = () => {
const wanted = FILTER.value.trim().toLowerCase();
const shown = wanted
? pages.filter((page) =>
`${page.code} ${page.name} ${page.text}`.toLowerCase().includes(wanted),
)
: pages;
COUNT.textContent = wanted
? `${shown.length} of ${pages.length} codes`
: `${pages.length} codes`;
render(shown);
};

FILTER.addEventListener("input", show);
show();

// A link to a code should land on it, and the list did not exist when the
// browser tried the first time.
if (location.hash) {
document.getElementById(location.hash.slice(1))?.scrollIntoView();
}
}

load();
126 changes: 119 additions & 7 deletions assets/play.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@
// release, because a release asset cannot be fetched from a browser at all:
// both the download URL and the API one redirect to a host that sends no
// `Access-Control-Allow-Origin`. See decisions/2026-07-31-no-build-step.md.
const TAG = "v0.2.1";
const VERSION = "0.2.1";
const TAG = "v0.2.2";
const VERSION = "0.2.2";
const WASM_URL = `../assets/deed-${TAG}-wasm32-unknown-unknown.wasm`;

const SOURCE = document.getElementById("source");
Expand All @@ -23,6 +23,8 @@ const STATUS = document.getElementById("status");
const EXAMPLE = document.getElementById("example");
const SUMMARY = document.getElementById("summary");
const STOP = document.getElementById("stop");
const HIGHLIGHT = document.getElementById("highlight");
const GUTTER = document.getElementById("gutter");
const VERBS = Array.from(document.querySelectorAll("[data-verb]"));

const encoder = new TextEncoder();
Expand All @@ -33,6 +35,7 @@ const decoder = new TextDecoder();
let worker = null;
let pending = null;
let nextId = 1;
let ready = false;

// Long enough that nothing anyone types by hand hits it, short enough that a
// frozen page is not what a mistake looks like.
Expand Down Expand Up @@ -62,6 +65,7 @@ function spawn() {
// replacement to load the module again.
function stop(why, deliberate = false) {
worker.terminate();
ready = false;
if (pending) {
const settle = pending;
pending = null;
Expand Down Expand Up @@ -285,24 +289,126 @@ function render(verb, json, source) {
async function run(verb) {
const source = SOURCE.value;
try {
OUTPUT.innerHTML = render(verb, await ask(verb, source), source);
const answer = await ask(verb, source);
OUTPUT.innerHTML = render(verb, answer, source);
if (verb === "deed_check" || verb === "deed_fmt") markLines(answer, SOURCE.value);
if (verb === "deed_fmt") paint();
} catch (error) {
// A stop is an answer the reader asked for, not a failure to answer.
if (error.stopped) return;
OUTPUT.innerHTML = span("d-error", `the compiler could not answer: ${error.message}`);
}
}

// The colouring, from the compiler's own lexer rather than from a grammar
// written here. `deed_tokens` classifies every byte range but whitespace, so
// the gaps between ranges are exactly the whitespace and get copied through.
//
// The textarea sits on top with transparent text, so what anyone reads is
// this layer and what anyone types is that one. They have to agree on every
// font property or the caret drifts, which is why the CSS sets both from the
// same block.
async function paint() {
const source = SOURCE.value;
let classified;
try {
classified = await ask("deed_tokens", source);
} catch {
// Colouring is the part that can be missing. The editor still works.
return;
}
if (SOURCE.value !== source) return;

let out = "";
let at = 0;
for (const line of classified.split("\n")) {
if (line.trim() === "") continue;
const { class: kind, start, end } = JSON.parse(line);
out += esc(source.slice(at, start));
out += `<span class="t-${kind}">${esc(source.slice(start, end))}</span>`;
at = end;
}
out += esc(source.slice(at));

// A trailing newline collapses in a `pre`, and the caret can sit after it.
HIGHLIGHT.innerHTML = out + "\n";
drawGutter(source);
}

// The line numbers, and which lines the compiler had something to say about.
let marked = new Map();

function drawGutter(source) {
const lines = source.split("\n").length;
let out = "";
for (let n = 1; n <= lines; n++) {
const severity = marked.get(n);
out += severity ? `<b class="has-${severity}">${n}</b>\n` : `${n}\n`;
}
GUTTER.innerHTML = out;
}

function markLines(answer, source) {
marked = new Map();
for (const line of answer.split("\n")) {
if (line.trim() === "") continue;
const item = JSON.parse(line);
if (item.kind !== "diagnostic") continue;
const { file, span } = item.diagnostic.primary;
if (file !== "main.deed") continue;
// An error on a line beats a warning on the same one.
const severity = item.diagnostic.severity === "warning" ? "warning" : "error";
if (severity === "error" || !marked.has(span.startLine)) {
marked.set(span.startLine, severity);
}
}
drawGutter(source);
}

// Three layers scrolling as one. The gutter follows vertically only: it has
// no columns to scroll past.
SOURCE.addEventListener("scroll", () => {
HIGHLIGHT.scrollTop = SOURCE.scrollTop;
HIGHLIGHT.scrollLeft = SOURCE.scrollLeft;
GUTTER.scrollTop = SOURCE.scrollTop;
});

// `check` is the fast one and the one the language is about, so it runs while
// you type rather than waiting to be asked. Only when nothing else is in
// flight: a `run` that is still going is a better use of the compiler than a
// `check` of a program that is being edited anyway.
//
// Colouring is on a shorter fuse than checking, because it is answering a
// question about the text rather than about the program, and text that stays
// grey while you type reads as broken.
let painting = null;
let typing = null;

// Both of these can find the compiler busy with the other one. Re-arming
// rather than returning is the difference between "later" and "never": an
// earlier version dropped the check whenever a paint was still in flight, so
// the output pane kept answering about the program before last.
//
// "Busy" and "not there" are separate questions for the same reason. The verbs
// are disabled in both cases, so reading the buttons would have made a
// temporary state look permanent.
function schedule(which, delay) {
clearTimeout(which === paint ? painting : typing);
const timer = setTimeout(() => {
if (!ready) return;
if (pending) return schedule(which, 100);
which();
}, delay);
if (which === paint) painting = timer;
else typing = timer;
}

const check = () => run("deed_check").then(paint);

SOURCE.addEventListener("input", () => {
clearTimeout(typing);
typing = setTimeout(() => {
if (!pending && worker && !VERBS[0].disabled) run("deed_check");
}, 500);
drawGutter(SOURCE.value);
schedule(paint, 150);
schedule(check, 500);
});

STOP.addEventListener("click", () => {
Expand Down Expand Up @@ -336,7 +442,9 @@ function arrived(data) {
`Deed ${esc(reported)}, ` +
`<a href="https://github.com/deed-lang/deed/releases/tag/${TAG}">${TAG}</a>, ` +
`running in this tab.`;
ready = true;
running(false);
paint();
}

function load() {
Expand Down Expand Up @@ -434,6 +542,8 @@ async function loadFromLink() {
if (version !== VERSION) {
SHARED.textContent = `This program was written against ${version} and the page is running ${VERSION}, so it may not say the same thing.`;
}
marked = new Map();
paint();
return true;
}

Expand Down Expand Up @@ -472,6 +582,8 @@ async function loadExamples() {
OUTPUT.textContent = "Press Check, Run, Test or Format.";
const response = await fetch(`../examples/${encodeURIComponent(file)}`);
SOURCE.value = await response.text();
marked = new Map();
paint();
});
}

Expand Down
Loading