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
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
20 changes: 12 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -48,18 +51,19 @@ 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

```bash
# 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
Expand Down Expand Up @@ -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
```

Expand Down
37 changes: 27 additions & 10 deletions docs/needs.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
11 changes: 6 additions & 5 deletions spool.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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" }
2 changes: 1 addition & 1 deletion src/canvas.tw
Original file line number Diff line number Diff line change
Expand Up @@ -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] = [
" ", "▘", "▝", "▀",
"▖", "▌", "▞", "▛",
"▗", "▚", "▐", "▜",
Expand Down
32 changes: 16 additions & 16 deletions src/heatmap.tw
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -40,29 +40,29 @@ 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
while row < m.rows {
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)), " ")
}
Expand All @@ -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)))
}
19 changes: 9 additions & 10 deletions src/scale.tw
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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] {
Expand Down
4 changes: 2 additions & 2 deletions src/sparkline.tw
Original file line number Diff line number Diff line change
Expand Up @@ -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 "" }
Expand Down
6 changes: 3 additions & 3 deletions src/svg.tw
Original file line number Diff line number Diff line change
Expand Up @@ -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) +
"<text x=\"" + str(b.left) + "\" y=\"18\" fill=\"" + ACCENT + "\" font-size=\"13\">" +
escape(title) + "</text>\n"
Expand All @@ -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 + "<rect x=\"" + str(b.left + col * cw) + "\" y=\"" + str(b.top + row * chh) +
"\" width=\"" + str(cw) + "\" height=\"" + str(chh) + "\" fill=\"" + ramp_hex(t) + "\"/>\n"
col = col + 1
Expand All @@ -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 }
Expand Down
4 changes: 2 additions & 2 deletions src/theme.tw
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading