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)