From b4f48a6ed28d6ec8491d2dd15d2eebfc6d615629 Mon Sep 17 00:00:00 2001 From: onatozmenn Date: Sat, 1 Aug 2026 20:27:44 +0300 Subject: [PATCH] Offer twelve programs, and stop promising Run it cannot keep The picker listed all twenty-eight and exactly one of them could be started here. Twenty-one are libraries, which the page already said. The other six have a `main` and want the filesystem, and for those the page turned Run on, let it be pressed, and answered with a list of capabilities instead of anything about the program. So `needs` is recorded next to `runs`, both asked of the artifact, and the note says which of the two it is. The rest of it is that the corpus is not a menu. About half is one language feature at a time, written so the compiler's own tests have something to read, and scrolling past `sink`, `names` and `diverge` shows a visitor the inside of a test suite. Twelve programs a person would recognise, `hello` first because it is the one that starts. The other sixteen are still here and still open by name. Three line kinds the pinned build sends were falling through to the default arm and printing as raw JSON: `summary`, `property` and `refused`. All three arrived with 0.2.3. --- README.md | 33 +++++++++++++----- assets/play.js | 84 ++++++++++++++++++++++++++++++++++++--------- examples/index.json | 81 +++++++++++++++++++++++++++++++++++++++++++ tools/artifact.mjs | 47 ++++++++++++++++++++++--- tools/check.mjs | 34 +++++++++++++----- tools/examples.mjs | 16 ++++++--- 6 files changed, 252 insertions(+), 43 deletions(-) diff --git a/README.md b/README.md index 3ef0b04..474adb6 100644 --- a/README.md +++ b/README.md @@ -25,8 +25,10 @@ Concretely, that rules out a few things that would otherwise be tempting: 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 - refusal was still produced by running it. + file. Which twelve of them the picker offers is a choice made here, and it is + the only one: choosing what to show is not writing it, and every file is + still served. The landing page's program is the exception, and it is short + and its refusal was still produced by running it. The split exists because the two repositories have different constraints. The compiler's has no dependencies on purpose and its tests are strict in ways @@ -85,17 +87,30 @@ $ rm examples/greeting.deed examples/todo.txt $ node tools/examples.mjs ``` -That last step rewrites `examples/index.json`, which is the list the picker -reads. Everything in it comes from somewhere else: each summary is the comment -at the top of the file, and whether an example has a `main` to run is what the -pinned artifact answered when asked. Seven of the twenty-eight do; the picker -turns Run off for the other twenty-one and says why, rather than letting -somebody press it and be refused. +That last step rewrites `examples/index.json`, which is what the picker reads. +Everything in it comes from somewhere else: each summary is the comment at the +top of the file, and the rest is what the pinned artifact answered when asked. + +What it was asked matters, because "can this be run" has two halves here. +Twenty-one of the twenty-eight have no `main` at all. Six of the remaining +seven have one and want the filesystem, which a page does not have, so `needs` +records the capabilities they asked for. That leaves exactly one example this +page can start, and Run is off for the other twenty-seven with the reason +beside it, rather than letting somebody press it and be refused. + +The picker shows twelve of the twenty-eight, and that list is in +`tools/artifact.mjs` because both tools need it. The corpus is not a menu: +about half of it is one language feature at a time, written so the compiler's +own tests have something to read, and a visitor scrolling past `sink`, `names` +and `diverge` is being shown the inside of a test suite. The other sixteen stay +here, still asked about, still reachable by name. Forgetting that step is caught rather than shipped. `tools/check.mjs` asks the same artifact the same questions and fails if the committed answers differ, which is also what stops a generated file being edited by hand: it looks -exactly like a generated file. +exactly like a generated file. It also fails when the picker names a file the +corpus no longer has, so a promoted or deleted example shortens the menu +loudly. `greeting.deed` is left out because it imports two other modules and this page hands the compiler one file. That is the only one: the other twenty-eight were diff --git a/assets/play.js b/assets/play.js index 71182e2..2b4d577 100644 --- a/assets/play.js +++ b/assets/play.js @@ -123,11 +123,15 @@ function running(yes) { } } -// Twenty-two of the twenty-nine examples are libraries: functions and `test` -// blocks with no `main` to enter through. Run is the wrong button for those, -// and finding that out by pressing it and being refused is a bad way to be -// told. `examples/index.json` records the answer the pinned artifact gave for -// each file, so the button is off before it is reached for. +// Most of the corpus cannot be started here, for two different reasons, and +// pressing Run to find that out is a bad way to be told. Twenty-one are +// libraries with no `main` to enter through. Six more have one and want the +// filesystem, which this page does not have, so they would answer with a list +// of capabilities rather than with anything about the program. +// +// `examples/index.json` records both answers from the pinned artifact, so the +// button is off before it is reached for, and the note says which of the two +// it is. // // It goes back on the moment the text is edited, because then it is no longer // the file that was asked about. @@ -301,7 +305,7 @@ function render(verb, json, source) { ? renderDiagnostic(item.diagnostic, source) : span("d-error", item.message), ); - // Twenty-two of the twenty-nine examples are libraries: a `module` of + // Twenty-one of the twenty-eight examples are libraries: a `module` of // functions and `test` blocks, with no `main` to enter through. The // compiler is right and the page was unhelpful, offering Run for all // of them and then leaving the reader with a refusal and no next step. @@ -321,6 +325,37 @@ function render(verb, json, source) { renderDiagnostic(item.diagnostic, source), ); break; + // A property is generated from a contract rather than written by + // somebody, so it says where it came from and carries its seed. A run + // you cannot reproduce is a rumour, and the seed is how you reproduce it. + case "property": + out.push( + (item.passed + ? `${span("d-ok", "pass")} ${esc(item.function)}` + : `${span("d-error", "fail")} ${esc(item.function)}`) + + span("d-note", ` its contract, ${item.cases} cases, seed ${esc(item.seed)}`) + + (item.passed ? "" : `\n${renderDiagnostic(item.diagnostic, source)}`), + ); + break; + // Silence means "well formed" on the check verb, so a test run says how + // it came out rather than leaving an empty console to mean two things. + case "summary": + out.push( + span( + item.failed ? "d-error" : "d-ok", + `${item.passed} passed, ${item.failed} failed`, + ), + ); + break; + // The compiler refuses to run a program that does not check, because + // running one answers a question nobody asked. Check is where the + // reasons are, so the page offers it rather than repeating them here. + case "refused": + out.push( + span("d-error", item.message) + + `\n `, + ); + break; case "capability": out.push(span("d-warning", item.message)); break; @@ -616,8 +651,15 @@ async function loadFromLink() { // tests, and the summary under the picker is the comment at the top of the // file rather than a description written here. // -// One file is missing: `greeting.deed` imports two other modules, and this -// page hands the compiler one file. It is left out rather than shown failing. +// The picker shows twelve of the twenty-eight. The rest are the corpus doing +// its other job, one language feature at a time so the compiler's tests have +// something to read, and a menu of those is a menu of somebody else's test +// suite. `shown` in the index carries the choice and its order; every file is +// still here and still opens by name. +// +// One file is missing entirely: `greeting.deed` imports two other modules, and +// this page hands the compiler one file. It is left out rather than shown +// failing. async function loadExamples() { let index; try { @@ -629,8 +671,11 @@ async function loadExamples() { } const summaries = new Map(); - for (const entry of index.examples) { - summaries.set(entry.file, entry); + for (const entry of index.examples) summaries.set(entry.file, entry); + + for (const entry of index.examples + .filter((entry) => entry.shown >= 0) + .sort((a, b) => a.shown - b.shown)) { const option = document.createElement("option"); option.value = entry.file; option.textContent = entry.file.replace(/\.deed$/, ""); @@ -651,16 +696,23 @@ async function loadExamples() { const response = await fetch(`../examples/${encodeURIComponent(file)}`); SOURCE.value = await response.text(); marked = new Map(); - thisIsRunnable( - entry?.runs !== false, - entry?.runs === false - ? `A library: no \`main\` to run, ${entry.tests} tests to press Test on.` - : "", - ); + thisIsRunnable(entry ? entry.runs && entry.needs.length === 0 : true, whyNot(entry)); paint(); }); } +// What to say instead of Run, which is two different sentences: a library has +// no way in, and a program that wants the filesystem has one and cannot use it +// here. +function whyNot(entry) { + if (!entry || (entry.runs && entry.needs.length === 0)) return ""; + if (!entry.runs) return `A library: no \`main\` to run, ${entry.tests} tests to press Test on.`; + return ( + `Wants ${entry.needs.join(", ")}, which this page has no filesystem for. ` + + `Its ${entry.tests} tests still run.` + ); +} + load(); loadExamples(); loadFromLink(); diff --git a/examples/index.json b/examples/index.json index 27dcea6..59ca38a 100644 --- a/examples/index.json +++ b/examples/index.json @@ -3,170 +3,251 @@ "examples": [ { "file": "calculator.deed", + "shown": 2, "summary": "A small expression language, written in Deed: split into tokens, parse by precedence, evaluate. Tokens are space separated on purpose, to keep character-by-character lexing out of the way of the part this file is actually about, which is precedence and associativity.", "runs": false, + "needs": [], "tests": 3 }, { "file": "closures.deed", + "shown": -1, "summary": "Closures, the two holes they used to open, and the boundary they can now cross.", "runs": false, + "needs": [], "tests": 4 }, { "file": "config.deed", + "shown": -1, "summary": "A program that reads a file it was given access to, and cannot read one it was not. `deed run examples/config.deed --dir examples`", "runs": true, + "needs": [ + "Io.open", + "Io.read" + ], "tests": 0 }, { "file": "counter.deed", + "shown": 7, "summary": "A Deed program that actually runs. `deed test examples/counter.deed` executes the tests at the bottom.", "runs": false, + "needs": [], "tests": 10 }, { "file": "diverge.deed", + "shown": -1, "summary": "Non-termination, which is something a function does, so it goes in the row.", "runs": false, + "needs": [], "tests": 1 }, { "file": "generator.deed", + "shown": -1, "summary": "A generator pattern over a Yield effect.", "runs": false, + "needs": [], "tests": 3 }, { "file": "generic_types.deed", + "shown": -1, "summary": "Generic types, and the shortcut they were there to cover for.", "runs": false, + "needs": [], "tests": 5 }, { "file": "generics.deed", + "shown": -1, "summary": "Generic functions, and the thing they make possible.", "runs": false, + "needs": [], "tests": 5 }, { "file": "hello.deed", + "shown": 0, "summary": "A Deed program with a `main`. `deed run examples/hello.deed` executes it.", "runs": true, + "needs": [], "tests": 0 }, { "file": "journal.deed", + "shown": -1, "summary": "Writing a file, which until now the language could not do at all.", "runs": true, + "needs": [ + "Io.epoch", + "Io.list", + "Io.make", + "Io.read", + "Io.save" + ], "tests": 3 }, { "file": "json.deed", + "shown": 4, "summary": "A small JSON-shaped value, encoded to text. No decoder: reading JSON back needs a real tokenizer with string-escape handling and a wall this file does not climb, unlike `examples/calculator.deed`'s space-separated shortcut. Left for whoever wants that wall next.", "runs": false, + "needs": [], "tests": 2 }, { "file": "kv_store.deed", + "shown": -1, "summary": "A small key-value store: `key=value` lines, saved and loaded through `Io.save`/`Io.read` the way `journal.deed` and `todo.deed` already do. The parsing and the table-building are pure and tested here; `main` is the untested glue that hands them a real `Dir`, the same split every other example with a `main` already uses.", "runs": true, + "needs": [ + "Io.read", + "Io.save" + ], "tests": 3 }, { "file": "lists.deed", + "shown": -1, "summary": "Holding more than one of something, which until now the language could not do at all.", "runs": false, + "needs": [], "tests": 5 }, { "file": "logs.deed", + "shown": 9, "summary": "A program that reads a directory of logs and reports what is in them.", "runs": true, + "needs": [ + "Io.list", + "Io.read" + ], "tests": 13 }, { "file": "markdown.deed", + "shown": 3, "summary": "Markdown to plain text, three rules: a line starting `# ` becomes an upper-cased heading, a line starting `- ` becomes an indented bullet, and `**` markers are stripped wherever they appear. Nothing here needs a real Markdown parser (no nesting, no inline links); one line in, one line out, which is as far as `logs.deed`-style line-at-a-time text handling goes before it would need one.", "runs": false, + "needs": [], "tests": 4 }, { "file": "names.deed", + "shown": -1, "summary": "The other side of an import. `deed check examples/` sees this file and `greeting.deed` at the same time, which is what makes the `use` in that file mean something.", "runs": false, + "needs": [], "tests": 1 }, { "file": "proven.deed", + "shown": 11, "summary": "The Proven tier, which is the part of this language that is supposed to be interesting.", "runs": false, + "needs": [], "tests": 5 }, { "file": "ranking.deed", + "shown": -1, "summary": "design/02-syntax.md's stated falsification test for \"no traits\": write a program needing a generic sort over a user type, or needing to print a `T`, and see whether it is unwritable rather than merely uglier with a passed function.", "runs": false, + "needs": [], "tests": 1 }, { "file": "scheduler.deed", + "shown": -1, "summary": "A cooperative round-robin scheduler, written entirely in Deed.", "runs": false, + "needs": [], "tests": 2 }, { "file": "sink.deed", + "shown": -1, "summary": "An effect and a handler, declared here and used from `greeting.deed`.", "runs": false, + "needs": [], "tests": 0 }, { "file": "stack_machine.deed", + "shown": 5, "summary": "A tiny stack machine: push a number, or combine the top two with an operator. Four instructions, and the interesting part is `applied`, which is one function for all three binary operators because the operator itself is the parameter, the same shape `logs.deed` uses to take a walk's question as an argument.", "runs": false, + "needs": [], "tests": 4 }, { "file": "strings.deed", + "shown": -1, "summary": "Strings, which until recently you could hold and not use.", "runs": false, + "needs": [], "tests": 7 }, { "file": "tic_tac_toe.deed", + "shown": 1, "summary": "Tic-tac-toe: a 9-cell board, a move, a winner check over the eight lines that can win, and a text render. The board is a plain `List` rather than a record with nine fields, because every operation here walks it by position and a record would need the same nine-way match `placed` and `rendered` are written once to avoid.", "runs": false, + "needs": [], "tests": 3 }, { "file": "todo.deed", + "shown": 10, "summary": "A real program, written to find out what is missing rather than to show off what is there. `deed run examples/todo.deed --dir examples`", "runs": true, + "needs": [ + "Io.args", + "Io.read", + "Io.remove", + "Io.save" + ], "tests": 15 }, { "file": "transfer.deed", + "shown": -1, "summary": "The running example, and two of the fourteen design documents work through it: design/02-syntax.md and design/03-effects.md.", "runs": false, + "needs": [], "tests": 3 }, { "file": "tree.deed", + "shown": 6, "summary": "A binary search tree, written in Deed.", "runs": false, + "needs": [], "tests": 9 }, { "file": "using_list.deed", + "shown": 8, "summary": "Using the list library, which is the point of it existing.", "runs": false, + "needs": [], "tests": 7 }, { "file": "workers.deed", + "shown": -1, "summary": "A concurrent program written to find the walls.", "runs": true, + "needs": [ + "Io.list", + "Io.open", + "Io.read", + "Io.save" + ], "tests": 5 } ] diff --git a/tools/artifact.mjs b/tools/artifact.mjs index 85e788c..76b9de0 100644 --- a/tools/artifact.mjs +++ b/tools/artifact.mjs @@ -7,6 +7,35 @@ import { readFile } from "node:fs/promises"; +// What the picker offers, in this order. +// +// The corpus is twenty-eight files and it is not a menu: about half of it is +// one language feature at a time, written so the compiler's own tests have +// something to read, and a visitor scrolling past `sink`, `names` and +// `diverge` is being shown the inside of a test suite. So the picker is a +// dozen programs a person would recognise, and the rest stay on disk, still +// asked about here and still reachable by name. +// +// `hello` is first because it is the only one this page can start. The others +// have tests, which is the button that works for them. +// +// It lives beside the code that asks the compiler because both tools need it: +// one writes the order into the index and the other checks it is still there. +export const SHOWN = [ + "hello.deed", + "tic_tac_toe.deed", + "calculator.deed", + "markdown.deed", + "json.deed", + "stack_machine.deed", + "tree.deed", + "counter.deed", + "using_list.deed", + "logs.deed", + "todo.deed", + "proven.deed", +]; + export async function open(path) { const { instance } = await WebAssembly.instantiate(await readFile(path), {}); const wasm = instance.exports; @@ -48,18 +77,26 @@ export async function open(path) { return read(); } - // The three things `examples/index.json` says about a file. Two of them are - // answers from the module and the third is the file's own opening comment, - // so none of it is a description written here. + // What `examples/index.json` says about a file. All of it is either an + // answer from the module or the file's own opening comment, so none of it is + // a description written here. function describe(source) { // Asked rather than guessed. Looking for `fn main` in the text would be a // small parser written here, which is the thing this repository does not do. - const runs = !lines("deed_run", source).some( + const run = lines("deed_run", source); + const runs = !run.some( (item) => item.kind === "result" && item.ok === false && /no `main`/.test(item.message ?? ""), ); + // The capabilities this page cannot offer. A `main` is not enough to press + // Run on: the command line has a filesystem behind it and a page does not, + // so six of these have an entry point and still cannot start here. + const needs = run + .filter((item) => item.kind === "capability") + .map((item) => (item.message ?? "").match(/`([^`]+)`/)?.[1]) + .filter(Boolean); const tests = lines("deed_test", source).filter((item) => item.kind === "test").length; - return { summary: summaryOf(source), runs, tests }; + return { summary: summaryOf(source), runs, needs, tests }; } return { call, lines, version, describe }; diff --git a/tools/check.mjs b/tools/check.mjs index fbf58a6..ae3779b 100644 --- a/tools/check.mjs +++ b/tools/check.mjs @@ -8,7 +8,7 @@ import { readFile, readdir, stat } from "node:fs/promises"; import { join, dirname, resolve, relative } from "node:path"; -import { open } from "./artifact.mjs"; +import { open, SHOWN } from "./artifact.mjs"; const root = resolve(import.meta.dirname, ".."); const problems = []; @@ -153,17 +153,18 @@ if (index.tag !== tag) { complain("examples/index.json", `says ${index.tag} and the page pins ${tag}`); } -// `summary`, `runs` and `tests` are answers rather than descriptions, and the -// page reads all three: Run is turned off from `runs`, and the note beside it -// counts `tests`. They are derived again here, from the same artifact and the -// same code that wrote them, because a generated file that somebody edited by -// hand looks exactly like a generated file. +// `summary`, `runs`, `needs` and `tests` are answers rather than descriptions, +// and the page reads all four: Run is turned off from `runs` and `needs`, and +// the note beside it counts `tests` and names the capabilities. They are +// derived again here, from the same artifact and the same code that wrote +// them, because a generated file that somebody edited by hand looks exactly +// like a generated file. if (deed) { for (const entry of index.examples) { if (!present.has(entry.file)) continue; const answer = deed.describe(await readFile(join(root, "examples", entry.file), "utf8")); - for (const field of ["summary", "runs", "tests"]) { - if (entry[field] !== answer[field]) { + for (const field of ["summary", "runs", "needs", "tests"]) { + if (JSON.stringify(entry[field]) !== JSON.stringify(answer[field])) { complain( "examples/index.json", `${entry.file} says ${field} is ${JSON.stringify(entry[field])} and the artifact ` + @@ -174,6 +175,23 @@ if (deed) { } } +// The picker's dozen. `shown` is the position in `SHOWN` and -1 for the rest, +// so a file leaving the corpus takes its place in the picker with it and says +// so here rather than shortening the menu quietly. +for (const file of SHOWN) { + if (!present.has(file)) complain("tools/artifact.mjs", `the picker names ${file}, which is not here`); +} +for (const entry of index.examples) { + const expected = SHOWN.indexOf(entry.file); + if (entry.shown !== expected) { + complain( + "examples/index.json", + `${entry.file} says shown is ${entry.shown} and the picker's list says ${expected}. ` + + "Regenerate with `node tools/examples.mjs`.", + ); + } +} + // This site names one release, and the pin is which one. Everything else that // says a version is prose: the filenames on the install page, the `deed 0.2.2` // under each `--version`, and the version a share link carries. diff --git a/tools/examples.mjs b/tools/examples.mjs index 4110636..46fb3a5 100644 --- a/tools/examples.mjs +++ b/tools/examples.mjs @@ -13,7 +13,7 @@ import { readFile, readdir, writeFile } from "node:fs/promises"; import { join, resolve } from "node:path"; -import { open } from "./artifact.mjs"; +import { open, SHOWN } from "./artifact.mjs"; const root = resolve(import.meta.dirname, ".."); @@ -33,7 +33,13 @@ const names = (await readdir(join(root, "examples"))) const examples = []; for (const file of names) { const source = await readFile(join(root, "examples", file), "utf8"); - examples.push({ file, ...deed.describe(source) }); + examples.push({ file, shown: SHOWN.indexOf(file), ...deed.describe(source) }); +} + +const missing = SHOWN.filter((file) => !names.includes(file)); +if (missing.length) { + console.error(`the picker names ${missing.join(", ")}, which the corpus does not have`); + process.exit(1); } await writeFile( @@ -42,8 +48,8 @@ await writeFile( "utf8", ); -const programs = examples.filter((e) => e.runs).length; +const startable = examples.filter((e) => e.runs && e.needs.length === 0).length; console.log( - `${examples.length} examples at ${tag}: ${programs} with a \`main\`, ` + - `${examples.length - programs} libraries.`, + `${examples.length} examples at ${tag}, ${SHOWN.length} in the picker: ` + + `${startable} the page can start, ${examples.length - startable} it can only check and test.`, );