diff --git a/README.md b/README.md index ca131fe..3ef0b04 100644 --- a/README.md +++ b/README.md @@ -88,8 +88,8 @@ $ 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-nine do; the picker -turns Run off for the other twenty-two and says why, rather than letting +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. Forgetting that step is caught rather than shipped. `tools/check.mjs` asks the @@ -98,7 +98,7 @@ which is also what stops a generated file being edited by hand: it looks exactly like a generated file. `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-nine were +hands the compiler one file. That is the only one: the other twenty-eight were checked through the pinned artifact and every one of them is clean. ## Layout @@ -129,7 +129,7 @@ the release the pin names, and the example index still says what the compiler says. That last part is not a shape check. It loads the pinned artifact, asks it its -version, and asks it about all twenty-nine examples, because a committed wasm +version, and asks it about all twenty-eight examples, because a committed wasm is a file nothing here ever built: a truncated copy, or a different build wearing the right name, spells its filename correctly and passes everything else. It runs on every pull request too. diff --git a/assets/deed-v0.2.2-wasm32-unknown-unknown.wasm b/assets/deed-v0.2.2-wasm32-unknown-unknown.wasm deleted file mode 100644 index b57576e..0000000 Binary files a/assets/deed-v0.2.2-wasm32-unknown-unknown.wasm and /dev/null differ diff --git a/assets/deed-v0.2.3-wasm32-unknown-unknown.wasm b/assets/deed-v0.2.3-wasm32-unknown-unknown.wasm new file mode 100644 index 0000000..96e7a08 Binary files /dev/null and b/assets/deed-v0.2.3-wasm32-unknown-unknown.wasm differ diff --git a/assets/errors.js b/assets/errors.js index 54fb42e..b23de7f 100644 --- a/assets/errors.js +++ b/assets/errors.js @@ -5,8 +5,8 @@ // 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 TAG = "v0.2.3"; +const VERSION = "0.2.3"; const WASM_URL = `../assets/deed-${TAG}-wasm32-unknown-unknown.wasm`; const STATUS = document.getElementById("status"); diff --git a/assets/play.js b/assets/play.js index e79bcb5..71182e2 100644 --- a/assets/play.js +++ b/assets/play.js @@ -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.2"; -const VERSION = "0.2.2"; +const TAG = "v0.2.3"; +const VERSION = "0.2.3"; const WASM_URL = `../assets/deed-${TAG}-wasm32-unknown-unknown.wasm`; const SOURCE = document.getElementById("source"); diff --git a/examples/calendar.deed b/examples/calendar.deed deleted file mode 100644 index d0b5997..0000000 --- a/examples/calendar.deed +++ /dev/null @@ -1,60 +0,0 @@ -// A calendar on top of `Io.epoch`, which only gives milliseconds since 1970. -// design/04-capabilities.md says the language has no way to write one yet: -// this is that attempt, to find out whether that is still true. -// -// The algorithm is Howard Hinnant's `civil_from_days`: integer arithmetic -// only, no lookup table, no leap-year rule spelled out by name (it falls out -// of the same division and modulo every other line here uses). -// -// Only forward from 1970 is handled. A negative day count (a clock set -// before 1970) would need floor division where this uses the truncating -// division `/` already is, and nothing here needed that yet. -module examples/calendar - -record Date { - year: Int, - month: Int, - day: Int, -} - -fn days_since_epoch(milliseconds: Int) -> Int { - milliseconds / 86400000 -} - -fn date_of(milliseconds: Int) -> Date { - let z = days_since_epoch(milliseconds) + 719468 - let era = z / 146097 - let day_of_era = z - era * 146097 - let year_of_era = (day_of_era - day_of_era / 1460 + day_of_era / 36524 - day_of_era / 146096) / 365 - let year = year_of_era + era * 400 - let day_of_year = day_of_era - (365 * year_of_era + year_of_era / 4 - year_of_era / 100) - let mp = (5 * day_of_year + 2) / 153 - let day = day_of_year - (153 * mp + 2) / 5 + 1 - let month = if mp < 10 { - mp + 3 - } else { - mp - 9 - } - Date { - year: if month <= 2 { - year + 1 - } else { - year - }, - month, - day, - } -} - -test "the epoch itself is 1970-01-01" { - assert date_of(0) == Date { year: 1970, month: 1, day: 1 } -} - -test "a well known date, the turn of the millennium's next leap day" { - assert date_of(951868800000) == Date { year: 2000, month: 3, day: 1 } -} - -test "a leap day, and the day right after it" { - assert date_of(1709164800000) == Date { year: 2024, month: 2, day: 29 } - assert date_of(1709251200000) == Date { year: 2024, month: 3, day: 1 } -} diff --git a/examples/index.json b/examples/index.json index de74d52..27dcea6 100644 --- a/examples/index.json +++ b/examples/index.json @@ -1,5 +1,5 @@ { - "tag": "v0.2.2", + "tag": "v0.2.3", "examples": [ { "file": "calculator.deed", @@ -7,12 +7,6 @@ "runs": false, "tests": 3 }, - { - "file": "calendar.deed", - "summary": "A calendar on top of `Io.epoch`, which only gives milliseconds since 1970. design/04-capabilities.md says the language has no way to write one yet: this is that attempt, to find out whether that is still true.", - "runs": false, - "tests": 3 - }, { "file": "closures.deed", "summary": "Closures, the two holes they used to open, and the boundary they can now cross.", @@ -89,7 +83,7 @@ "file": "logs.deed", "summary": "A program that reads a directory of logs and reports what is in them.", "runs": true, - "tests": 11 + "tests": 13 }, { "file": "markdown.deed", diff --git a/examples/logs.deed b/examples/logs.deed index e4fa846..085fcf1 100644 --- a/examples/logs.deed +++ b/examples/logs.deed @@ -44,6 +44,8 @@ module examples/logs +use std/ratio.{percent_or, percent_text} + use std/table.{Entry, Table, or_else, set} // What this program counts: a key, and how many times it turned up. Naming it @@ -151,23 +153,40 @@ fn padded(text: String, width: Int) -> String { text + join(repeat(" ", width - length(text)), "") } -fn line_for(one: Entry) -> String { - padded(one.key, 16) + to_string(one.value) +// One row: the key, how many, and what share of the whole that is. +// +// The share is the thing this file could not write when it was first written. +// `design/fractional-values.md` refused every fractional number type and said +// the first real need would be a report wanting exact ratios that become text +// at the edge. This is that call, and it is answered by `std/ratio` rather than +// by the language: the percentage is an exact ratio right up until +// `percent_text` renders it, and the rounding is a decision this file can see. +fn line_for(one: Entry, total: Int) -> String { + let share = percent_or(percent_text(one.value, total, 1), "-") + padded(one.key, 16) + padded(to_string(one.value), 8) + share +} + +fn total_of(counts: Counts) -> Int { + for one in counts with sum = 0 { + sum + one.value + } } // The whole report, as lines. Pure, so a test can read it. fn report(lines: List) -> List { let levels = ranked(tallied(lines, level_of)) let sources = ranked(tallied(lines, source_of)) + let level_total = total_of(levels) + let source_total = total_of(sources) - let head = ["by level", padded("", 16) + "count"] + let head = ["by level", padded("", 16) + padded("count", 8) + "share"] let with_levels = for one in levels with out = head { - push(out, line_for(one)) + push(out, line_for(one, level_total)) } let with_gap = push(with_levels, "") let with_head = push(with_gap, "by source") for one in sources with out = with_head { - push(out, line_for(one)) + push(out, line_for(one, source_total)) } } @@ -275,6 +294,23 @@ test "an empty directory still reports its headings" { assert length(out) == 4 } +test "every row says what share of the whole it is" { + // Three lines, two of one level and one of the other, so the shares are + // `2/3` and `1/3`. Neither has a finite decimal expansion, which is the + // whole reason this column could not be written before `std/ratio`: the + // value stays exact and only the last step rounds. + let lines = ["d INFO ledger x", "d INFO ledger y", "d ERROR audit z"] + let out = report(lines) + + assert at(out, 2) == ok(padded("INFO", 16) + padded("2", 8) + "66.7") + assert at(out, 3) == ok(padded("ERROR", 16) + padded("1", 8) + "33.3") +} + +test "a share of a single line is the whole of it" { + let out = report(["d WARN cache x"]) + assert at(out, 2) == ok(padded("WARN", 16) + padded("1", 8) + "100.0") +} + test "appending walks one list onto the other" { assert appended(["a"], ["b", "c"]) == ["a", "b", "c"] assert appended([], []) == [] diff --git a/examples/scheduler.deed b/examples/scheduler.deed index f3be0af..ab82aa6 100644 --- a/examples/scheduler.deed +++ b/examples/scheduler.deed @@ -29,6 +29,25 @@ // - a standalone recursive function to drain the queue (ordinary `Diverge`) // // Nothing about the scheduler is language machinery. +// +// It is also not a library, and the reason is worth being exact about, because +// "concurrency is a library here" is the claim this file is usually read as +// making. What it shows is that a scheduler can be *written* in Deed. It cannot +// be *shipped*, and a program that wants one has to copy this file and edit it. +// +// The wall is one line above: `Schedule.fork` takes a +// `Fn() uses Schedule.fork, Schedule.yield, Log.note -> ()`, and that row names +// `Log.note` because these particular tasks log. A library cannot know what its +// tasks will perform, so the queue element type would have to be +// `Fn() uses r -> ()` for a row variable `r` belonging to the effect. Effects do +// not take row parameters: `effect Schedule` does not parse, and +// `DEED5008` separately confines a row variable to a function-typed parameter's +// row and the declaration's own `uses`, which a handler's `state` is neither. +// +// So the queue's element type has to name a concrete row, the row has to name +// every effect a task might perform, and both are things only the program +// knows. That is why `std/task` does not exist next to `std/list`, and it is a +// row-polymorphism gap rather than anything about concurrency. module examples/scheduler diff --git a/install/index.html b/install/index.html index fdb0b97..93d27a8 100644 --- a/install/index.html +++ b/install/index.html @@ -62,7 +62,7 @@

Install