diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a01aea8..2658f7d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -84,10 +84,10 @@ jobs: # would have. Pinned rather than floating: a green run should mean this # code passed against a known compiler, not against whatever was newest # that morning. - - name: Install twill v1.9.0 + - name: Install twill v1.12.0 run: | curl -fsSL -o twill \ - https://github.com/twill-lang/twill/releases/download/v1.9.0/twill-v1.9.0-linux-amd64 + https://github.com/twill-lang/twill/releases/download/v1.12.0/twill-v1.12.0-linux-amd64 chmod +x twill ./twill --version diff --git a/CHANGELOG.md b/CHANGELOG.md index 8f4b757..09c3778 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,22 @@ cannot point at. Seventeen commits since `v0.1.0`, all of them either following twill's releases or fixing what following them exposed. +### The glyph tables are `const`, and a low and a high come back as a tuple + +`docs/needs.md` entries 9 and 13 were open and twill had delivered both. `HEX`, +`QUADRANTS`, `DENSITY` and `LEVELS`, with the two ASCII fallbacks, are declared +`const` (twill 1.10), so an assignment through any of them in its own file is +refused by the checker. The guarantee does not yet cross an import, and the +needs entry says so rather than claiming more. + +`pad_degenerate` and `range_of` return `(F64, F64)` (twill 1.12) and their +callers destructure the pair. The `Span` and `Range` structs are gone; they were +two of the four single-use type names entry 13 complained about. The heatmap +functions that took a `Range` take `lo` and `hi`. + +The minimum twill is 1.12.0 now, in `spool.toml`, in CI and in the README. The +six suites pass unchanged, which is the point: nothing a test could see moved. + ### The minimum twill is now 1.7.0, and it is load-bearing `src/theme.tw:33` and `src/svg.tw:35` dispatch the palette index on integer diff --git a/README.md b/README.md index ddfcf9d..f2baf7c 100644 --- a/README.md +++ b/README.md @@ -26,9 +26,12 @@ here executed and this section said so. It runs now: the 6 test suites under `tests/` pass, and CI runs them against a released twill on every push rather than gating on the prose in this file. -**twill 1.7.0 is the minimum, and it is a real floor rather than a cautious -one.** `src/theme.tw:33` and `src/svg.tw:35` dispatch on a palette index using -integer literals as `match` patterns: +**twill 1.12.0 is the minimum, and it is a real floor rather than a cautious +one.** `src/scale.tw` and `src/heatmap.tw` return a low and a high as a tuple, +`(F64, F64)`, which arrived in 1.12.0, and the four glyph tables are declared +`const`, which arrived in 1.10.0. The floor before that was 1.7.0, for the same +kind of reason: `src/theme.tw:33` and `src/svg.tw:35` dispatch on a palette +index using integer literals as `match` patterns: ```rust match i % 4 { @@ -48,8 +51,9 @@ line 22: in import "theme.tw": line 33:5: expected identifier but found "0" line 4: in import "../src/svg.tw": line 35:5: expected identifier but found "0" ``` -`docs/needs.md` is the list of what this library asked the language for. It now -records which of those 1.7 delivered and which are still open. +`docs/needs.md` is the list of what this library asked the language for. It +records which of those 1.7, 1.9, 1.10 and 1.12 delivered and which are still +open. ## Getting started @@ -57,9 +61,9 @@ records which of those 1.7 delivered and which are still open. # Assets: linux-amd64, linux-arm64, darwin-amd64, darwin-arm64, # windows-amd64.exe. curl -fsSL -o twill \ - https://github.com/twill-lang/twill/releases/download/v1.7.1/twill-v1.7.1-linux-amd64 + https://github.com/twill-lang/twill/releases/download/v1.12.0/twill-v1.12.0-linux-amd64 chmod +x twill -./twill --version # Twill 1.7.1 +./twill --version # Twill 1.12.0 git clone https://github.com/twill-lang/weft && cd weft ../twill test tests @@ -287,7 +291,7 @@ tests/ six suites, collected by `twill test`. harness.tw holds the one now. bars, heatmap, sparkline and theme have no file of their own; the first and third are covered from chart_test.tw and the other two are not covered at all -docs/needs.md what weft asked the language for, and what 1.7 delivered +docs/needs.md what weft asked the language for, and what each release delivered examples/ loss.tw, the training loop above ``` diff --git a/docs/needs.md b/docs/needs.md index 4575abc..11587c1 100644 --- a/docs/needs.md +++ b/docs/needs.md @@ -178,10 +178,20 @@ rather than per frame, and that is not a property to rely on. **Needs:** a `const`, or `let` at the top level being read-only **Used by:** `src/canvas.tw` (`QUADRANTS`), `src/theme.tw` (`DENSITY`), `src/sparkline.tw` (`LEVELS`), `src/svg.tw` (`HEX`) -**Status:** **open.** There is no `const` in 1.7.1: `const K: I64 = 1` is a -syntax error at the top level and inside a function. `Arr` has reference -semantics and `let` binds a handle, so every one of these glyph tables is -writable by any importer. +**Status: delivered in twill 1.10.0 for the declaring file, and taken up.** +All four tables, and the two ASCII fallbacks beside `DENSITY` and `LEVELS`, are +declared `const`. Within the file that declares one, an assignment through the +name, or to an element of it, is refused by `twill check`. What 1.10 does not +do, and says so in its language guide, is cross a file boundary: the checker +reads one file, so an importer that writes `svg.HEX = "x"` is still accepted +today. That is the half this entry actually wanted, and twill's own roadmap +names it as the next step for `const`. The tables are declared the way the +guarantee will be read, so nothing in weft changes when it lands. + +What it said while it was open: there is no `const` in 1.7.1; `const K: I64 = +1` is a syntax error at the top level and inside a function. `Arr` has +reference semantics and `let` binds a handle, so every one of these glyph +tables is writable by any importer. These are lookup tables. A library whose palette can be reassigned by a caller, accidentally or otherwise, has no way to keep the promise the theme file makes @@ -240,9 +250,16 @@ collects, so it stayed. **Needs:** tuples, or destructuring a returned struct **Used by:** `src/scale.tw` (`Span`), `src/heatmap.tw` (`Range`), `src/canvas.tw` (`Cp` in twill's own `width.tw`, for the same reason) -**Status:** **open.** `(1, 2)` is a syntax error in 1.7.1. Not in the design, -and a struct is the stated answer. - -Every function that computes a low and a high declares a two-field struct to hand -them back. It works, and it puts four single-use type names in a library that has -eleven real ones. Low priority: this is a readability complaint, not a wall. +**Status: delivered in twill 1.12.0, and taken up.** `pad_degenerate` in +`src/scale.tw` and `range_of` in `src/heatmap.tw` return `(F64, F64)`, and +their callers bind the pair with `let (lo, hi) = ...`. `Span` and `Range` are +deleted; the three heatmap functions that took a `Range` take `lo` and `hi` +instead, because a tuple is destructured or passed on whole and a parameter +that is read by part wants two names. twill's own changelog for 1.12 cites this +entry's four type names as one of the reasons the feature exists. + +What it said while it was open: `(1, 2)` is a syntax error in 1.7.1, not in the +design, and a struct is the stated answer. Every function that computes a low +and a high declares a two-field struct to hand them back. It works, and it puts +four single-use type names in a library that has eleven real ones. Low priority: +this is a readability complaint, not a wall. diff --git a/spool.toml b/spool.toml index f33e363..b1e7b6c 100644 --- a/spool.toml +++ b/spool.toml @@ -7,9 +7,10 @@ entry = "src/chart.tw" # modules are `std/term/...` as of twill 1.7 and are compiled into the binary, # and a working checkout has no `twill_modules/` in it. # -# The entry stays because the floor is real. `src/theme.tw` and `src/svg.tw` -# match on integer literals, which 1.6.7 cannot parse, so `^1.7.0` is the -# minimum this code actually compiles under rather than the newest release at -# the time of writing. See the note in README.md and docs/needs.md. +# The entry stays because the floor is real. `src/scale.tw` and +# `src/heatmap.tw` return a tuple, which arrived in 1.12.0, and the glyph +# tables are `const`, which arrived in 1.10.0, so `^1.12.0` is the minimum this +# code actually compiles under rather than the newest release at the time of +# writing. See the note in README.md and docs/needs.md. [dependencies] -twill = { version = "^1.9.0", git = "https://github.com/twill-lang/twill" } +twill = { version = "^1.12.0", git = "https://github.com/twill-lang/twill" } diff --git a/src/canvas.tw b/src/canvas.tw index 3f9ee9f..191121b 100644 --- a/src/canvas.tw +++ b/src/canvas.tw @@ -200,7 +200,7 @@ fn bit_index(res: Resolution, cx: I64, cy: I64) -> I64 = match res { Resolution.Ascii => 0, } -let QUADRANTS: Arr[Str] = [ +const QUADRANTS: Arr[Str] = [ " ", "▘", "▝", "▀", "▖", "▌", "▞", "▛", "▗", "▚", "▐", "▜", diff --git a/src/heatmap.tw b/src/heatmap.tw index 69c2764..2d6ee49 100644 --- a/src/heatmap.tw +++ b/src/heatmap.tw @@ -29,9 +29,9 @@ fn matrix(rows: I64, cols: I64, values: Arr[F64]) -> Matrix = fn at(m: Matrix, r: I64, c: I64) -> F64 = m.values[r * m.cols + c] -struct Range { lo: F64, hi: F64 } - -fn range_of(m: Matrix) -> Range { +# The low and the high of the matrix, as a tuple. There used to be a `Range` +# struct for this pair; it had one constructor site and no name anyone used. +fn range_of(m: Matrix) -> (F64, F64) { let lo = m.values[0] let hi = m.values[0] let i: I64 = 1 @@ -40,11 +40,11 @@ fn range_of(m: Matrix) -> Range { hi = f.max_f(hi, m.values[i]) i = i + 1 } - Range { lo: lo, hi: hi } + (lo, hi) } fn render(title: Str, m: Matrix, caps: cp.Caps) -> Str { - let r = range_of(m) + let (lo, hi) = range_of(m) let out = "" if len(title) > 0 { out = out + ch.line_of(caps, ansi.paint(caps, th.title_style(), title)) } let row: I64 = 0 @@ -52,17 +52,17 @@ fn render(title: Str, m: Matrix, caps: cp.Caps) -> Str { let s = "" let c: I64 = 0 while c < m.cols { - s = s + cell(caps, at(m, row, c), r) + s = s + cell(caps, at(m, row, c), lo, hi) c = c + 1 } out = out + ch.line_of(caps, s) row = row + 1 } - out + scale_bar(caps, r) + out + scale_bar(caps, lo, hi) } -fn cell(caps: cp.Caps, v: F64, r: Range) -> Str { - let t = normalise(v, r) +fn cell(caps: cp.Caps, v: F64, lo: F64, hi: F64) -> Str { + let t = normalise(v, lo, hi) if th.gradients_ok(caps) { return ansi.paint(caps, ansi.on(ansi.plain_style(), th.ramp_at(t)), " ") } @@ -73,23 +73,23 @@ fn cell(caps: cp.Caps, v: F64, r: Range) -> Str { # Position in the range, in thousandths, clamped. Thousandths rather than a # float because the ramp is interpolated in integer channel space, and mixing # the two representations is how a ramp ends up off by one shade at the top. -fn normalise(v: F64, r: Range) -> I64 { - let span = r.hi - r.lo +fn normalise(v: F64, lo: F64, hi: F64) -> I64 { + let span = hi - lo if span <= 0.0 { return 500 } - let t = i64((v - r.lo) / span * 1000.0) + let t = i64((v - lo) / span * 1000.0) if t < 0 { 0 } else if t > 1000 { 1000 } else { t } } # A key, always. A heatmap without one is a picture of a matrix rather than a # reading of it, and the two ends are the only numbers most readers take away. -fn scale_bar(caps: cp.Caps, r: Range) -> Str { +fn scale_bar(caps: cp.Caps, lo: F64, hi: F64) -> Str { let bar = "" let i: I64 = 0 while i < 20 { - bar = bar + cell(caps, r.lo + (r.hi - r.lo) * f64(i) / 19.0, r) + bar = bar + cell(caps, lo + (hi - lo) * f64(i) / 19.0, lo, hi) i = i + 1 } ch.line_of(caps, - ansi.paint(caps, th.label_style(), f.compact(r.lo) + " ") + bar + - ansi.paint(caps, th.label_style(), " " + f.compact(r.hi))) + ansi.paint(caps, th.label_style(), f.compact(lo) + " ") + bar + + ansi.paint(caps, th.label_style(), " " + f.compact(hi))) } diff --git a/src/scale.tw b/src/scale.tw index d8d1ed8..ef6bd8c 100644 --- a/src/scale.tw +++ b/src/scale.tw @@ -88,10 +88,10 @@ fn ceil_div(v: F64, step: F64) -> I64 { # labels readable in the first place. fn axis(lo: F64, hi: F64, target: I64) -> Axis { let n = if target < 2 { 2 } else { target } - let padded = pad_degenerate(lo, hi) - let step = nice_step((padded.hi - padded.lo) / f64(n - 1)) - let a = floor_to(padded.lo, step) - let b = ceil_to(padded.hi, step) + let (plo, phi) = pad_degenerate(lo, hi) + let step = nice_step((phi - plo) / f64(n - 1)) + let a = floor_to(plo, step) + let b = ceil_to(phi, step) Axis { lo: a, hi: b, @@ -101,15 +101,14 @@ fn axis(lo: F64, hi: F64, target: I64) -> Axis { } } -struct Span { lo: F64, hi: F64 } - # A series of identical values has no range to scale. Give it one proportional # to its own magnitude, so a flat line at 1e6 does not get a range of 1 and a -# flat line at 1e-6 does not get a range of 1 either. -fn pad_degenerate(lo: F64, hi: F64) -> Span { - if hi > lo { return Span { lo: lo, hi: hi } } +# flat line at 1e-6 does not get a range of 1 either. Returns the pair as a +# tuple: the low and the high have no name between them worth a struct. +fn pad_degenerate(lo: F64, hi: F64) -> (F64, F64) { + if hi > lo { return (lo, hi) } let mag = f.max_f(f.abs_f(lo), 1.0) - Span { lo: lo - mag * 0.05, hi: hi + mag * 0.05 } + (lo - mag * 0.05, hi + mag * 0.05) } fn tick_values(lo: F64, hi: F64, step: F64) -> Arr[F64] { diff --git a/src/sparkline.tw b/src/sparkline.tw index 4407d64..547db1a 100644 --- a/src/sparkline.tw +++ b/src/sparkline.tw @@ -19,8 +19,8 @@ import "fmtnum.tw" as f # Eighth-blocks, low to high. The empty cell is not used: a value at the bottom # of the range is still a measurement and drawing it as a blank makes it look # like missing data, which is the other thing a sparkline is used to show. -let LEVELS: Arr[Str] = ["▁", "▂", "▃", "▄", "▅", "▆", "▇", "█"] -let LEVELS_ASCII: Arr[Str] = ["_", "_", ".", ".", "-", "-", "^", "^"] +const LEVELS: Arr[Str] = ["▁", "▂", "▃", "▄", "▅", "▆", "▇", "█"] +const LEVELS_ASCII: Arr[Str] = ["_", "_", ".", ".", "-", "-", "^", "^"] fn render(caps: cp.Caps, ys: Arr[F64]) -> Str { if len(ys) == 0 { return "" } diff --git a/src/svg.tw b/src/svg.tw index 600a351..c0d886c 100644 --- a/src/svg.tw +++ b/src/svg.tw @@ -254,7 +254,7 @@ fn bar_svg(title: Str, bs: Arr[bars.Bar], b: Box) -> Str { } fn heatmap_svg(title: Str, m: hm.Matrix, b: Box) -> Str { - let r = hm.range_of(m) + let (lo, hi) = hm.range_of(m) let out = header(b) + background(b) + "" + escape(title) + "\n" @@ -264,7 +264,7 @@ fn heatmap_svg(title: Str, m: hm.Matrix, b: Box) -> Str { while row < m.rows { let col: I64 = 0 while col < m.cols { - let t = hm.normalise(hm.at(m, row, col), r) + let t = hm.normalise(hm.at(m, row, col), lo, hi) out = out + "\n" col = col + 1 @@ -284,7 +284,7 @@ fn ramp_hex(t: I64) -> Str { fn mix(r0: I64, g0: I64, b0: I64, r1: I64, g1: I64, b1: I64, t: I64) -> Str = "#" + hex2(r0 + (r1 - r0) * t / 1000) + hex2(g0 + (g1 - g0) * t / 1000) + hex2(b0 + (b1 - b0) * t / 1000) -let HEX: Str = "0123456789ABCDEF" +const HEX: Str = "0123456789ABCDEF" fn hex2(v: I64) -> Str { let x = if v < 0 { 0 } else if v > 255 { 255 } else { v } diff --git a/src/theme.tw b/src/theme.tw index 99b6b09..50bb4cf 100644 --- a/src/theme.tw +++ b/src/theme.tw @@ -66,8 +66,8 @@ fn ramp_at(t_x1000: I64) -> col.Rgb = col.ramp3_at(deep(), teal(), accent(), t_x # Density glyphs, dark to light, for the tiers where the ramp is not available. # Five steps because a reader can order five shades of one glyph family reliably # and cannot order eight. -let DENSITY: Arr[Str] = [" ", "░", "▒", "▓", "█"] -let DENSITY_ASCII: Arr[Str] = [" ", ".", ":", "+", "#"] +const DENSITY: Arr[Str] = [" ", "░", "▒", "▓", "█"] +const DENSITY_ASCII: Arr[Str] = [" ", ".", ":", "+", "#"] fn density(c: cp.Caps, t_x1000: I64) -> Str { let i = clamp_i(t_x1000 * 5 / 1000, 0, 4)