Skip to main content

sva_cli/
help.rs

1// Concern: the `--help` page, each flag's default printed from its own constant | Non-concern: parsing those flags (args/), the JSON a subcommand answers (output.rs) | IO: () -> the page
2
3use sva_core::DEFAULT_STORE_BYTES;
4use sva_engine::{
5    DEFAULT_BUFFER_SECS, DEFAULT_CELL_SECS, DEFAULT_FLOOR_DB, DEFAULT_FRAME_SECS,
6    DEFAULT_LEDGER_DEPTH, DEFAULT_MAX_PAIRS, DEFAULT_MAX_PEAKS, DEFAULT_MODULATION_WINDOW_SECS,
7    DEFAULT_NOVELTY_WINDOW_SECS, DEFAULT_OVERSAMPLE, DEFAULT_PERIODICITY_WINDOW_SECS,
8    DEFAULT_SAMPLE_RATE, DEFAULT_SMOOTH_SECS, PSYCHOACOUSTIC_V1, READINGS, Reads,
9    self_similarity::MAX_CELLS,
10};
11
12/// Read off the constants the parser itself defaults to, so a printed default cannot drift
13/// from the one a render actually uses.
14pub fn help_text() -> String {
15    let bits = PSYCHOACOUSTIC_V1.precision_bits;
16    let store_gb = DEFAULT_STORE_BYTES >> 30;
17    let representations = representations();
18    format!(
19        r#"USAGE:
20  sva-cli (render | analyze | lint | trace | builtins | outline | new) [arguments]
21
22DESCRIPTION:
23  A composition is a directory of node files, each one closed-form expression in
24  `t` or `f` on one line, or a score placing notes in time. sva-cli reads that
25  composition and prints what it is and what it sounds like, as JSON on stdout.
26  `render`, `lint` and `trace` read the current directory; `new` writes beside it
27  and `analyze` reads a file.
28
29SCORES:
30  A score is data: JSON Lines, one event per line, sorted by `at`. A file whose
31  first line that is neither blank nor a `;` comment starts with `{{` is a
32  score; its name has no extension, and `@score/bass` reads it like any node.
33  A `;` line is a comment, so a score opens with its `; Models: ...` line like
34  any file. Each line plays the node it names, a sound, another score or a
35  control curve, shifted to its time, and the score is their sum, one closed
36  form. `b` is a bar, so "0.25b" is a quarter of a bar, against
37  `variables/bpm` and `variables/meter`:
38    {{"at": "0b", "node": "@../instruments/bass", "deg": 1, "len": "0.25b", "vel": 0.9}}
39    {{"at": "0.25b", "node": "@../instruments/bass", "deg": 5, "cutoff": "900hz"}}
40  is @../instruments/bass(t, f0=deg(1), len=0.25b, vel=0.9) +
41  @../instruments/bass(t - 0.25b, f0=deg(5), cutoff=900hz).
42    at     required: a time string, "0.25b" or "0.5s", or constant arithmetic
43           over times, "2b + 1b/3" for a triplet; a bare number has no unit and
44           is refused. One time, and one score, counts in bars or in seconds.
45    node   required: an `@ref` string, resolved from the score's own directory.
46           A line writing `sound`, this field's old name, refuses.
47    len    a time string as `at` takes, bound as the node's `len`.
48    deg    a scale degree, a whole number counted from 1, the tonic: sugar for
49           "f0": "deg(n)". In a seven-row scale 8 is the tonic an octave up, 0
50           the seventh degree below it and -6 the tonic an octave down. A line
51           writing `f0` and `deg` refuses.
52    other  any other field binds the node's parameter of that name: a number,
53           or a constant in a string: one literal ("0.03s", "-6db", "900hz",
54           "A1") or arithmetic over literals and `deg(n)`, as "0.8*0.9".
55  Any field may be a list. A line listing N items is N events, event i taking
56  item i of every list and a single value going to all N; lists of different
57  lengths refuse. So a chord with a voice per note is one line:
58    {{"at": "0b", "node": "@../instruments/keys", "deg": [1, 3, 5],
59      "vel": [0.8, 0.5, 0.6], "len": ["1b", "0.5b", "1b"]}}
60  and a list of `at` is a run of one node, its items in order.
61  `deg(n)` is the tonic in `variables/key` times degree n's ratio in the scale
62  its `scale = @../scales/<name>` line names, in a score value or in any
63  equation. A chromatic note is a degree times a ratio and an octave is a
64  factor of 2: "f0": "deg(7)*1st" raises the seventh, "f0": "deg(5)/2" is the
65  fifth an octave down, and both follow every edit of the key or its scale. A
66  scale file holds one row per degree in cents above the tonic, ascending, the
67  last the period it repeats at: `scales/major` is 200 400 500 700 900 1100
68  1200. Editing `variables/key`'s note transposes every score and editing its
69  `scale` line changes mode. By convention a sound's `vel` runs 0..1 and its
70  mix level is a constant in its own equation, so a score's dynamics survive a
71  change of mix. A value reading `t` or a `@ref`, a field the node does not
72  read, a parameter it reads that nothing binds, and a `deg` with no scale
73  refuse before any audio, each naming the score's line and column. Keep a
74  score phrase-sized and place it from an equation: `@score/verse(t - 8b)`,
75  `repeat(@score/riff-1b, 4)`.
76
77  A score binds constants; what moves is a score of control curves, an
78  automation lane, read by an equation. A riser sweeping a lowpass open over
79  bars 21-24:
80    curves/close   len = 1b
81                   crop(1 - t/len, 0s, len)
82    auto/riser     {{"at": "20b", "node": "@../curves/close", "len": "4b"}}
83    master-fx      crop(lowpass(sample(@master),
84                     cutoff=20khz*pow(0.02, @auto/riser(t))), 0s, 32b)
85  An fx throw is the same lane gating a send: `@fx/echo(t, x=sample(@lead) *
86  @auto/throw(t))`.
87
88RENDER:
89  sva-cli render '<expression>' --representation <list> [--until '<condition>']
90                 [--bits <n>] [--rate <hz>] [--threads <n>] [--cache <path|none>]
91                 [--confirm]
92
93  Renders one expression, in the grammar a node file's body uses, and prints one
94  reading per representation under `data.representations`. `@path` reads a node
95  from the current directory and `@/abs/path` one anywhere; there is no default
96  target. Every reading states its `source` (exact or measured), the
97  conformance profile it ran under, and its rate.
98
99  The target's own ref may read an interval: `@piano4([0, 2b], f0=C4,
100  vel=0.5, release=1s)`, where `release` is the instrument's own parameter. Its ends are the language's literals (`s`, `ms`, `b`,
101  `sp`, or a bare `0`); `[a, inf)` and `[a,)` leave the end open; the other
102  arguments are the node's own named ones. The start trims the output alone:
103  history before it is still computed, so a loop or a filter carries the state
104  it had there. With no interval the render starts at 0, earlier only where a
105  crop reaches before it, and its end is open. Inside an expression a window is
106  a `crop`. A target that samples its one ref read, `sample(@piano4([0, 2b],
107  f0=C4))`, reads that ref's interval and binds and the same node values.
108
109  Every node is computed over its support met with what reads it, and nowhere
110  else: outside its support a node is exactly zero. A closed interval renders
111  exactly its length, nothing cut. An open one ends where the root's support
112  does: a crop's end, the exact underflow of an `exp`, a clamp holding a factor
113  at zero; or earlier, where a proven upper bound on the root's magnitude stays
114  under the profile's silence threshold (-120 dBFS) from a sample on. A root
115  with no such bound (a held sine, a loop) is never cut. The label's `pruned`
116  states the threshold as `db` and the root's cut, `from` the sample the render
117  is treated as silent from. A stream's term leaves `@notes` where its support
118  ends. An open interval over a root whose support never ends and is never cut
119  (a held sine, a physical solver) refuses as `render.no_end`. A short-time
120  transform reads its input whole, and refuses one with no end as
121  `engine.unbounded_extent`. A form in `f` with no dual in `t` has no samples
122  and refuses as its dual does, `cast.left_algebra`.
123
124  A render is a stream pulled to its end: a closed form, a short-time
125  transform and what either reads are computed over their whole extent first,
126  and every other node block by block. `--until '<condition>'` stops the render
127  at the first sample the condition holds at, or at the interval's end,
128  whichever is first. It is checked as each block is pulled, so no node driven
129  block by block runs past the block it holds in. A condition compares (`<`,
130  `<=`, `>`, `>=`) `t`, `envelope(t)` (the RMS over every channel of the
131  `envelope` representation's frame holding `t`, framed by its own `frame`
132  where one is asked) and literals, joined by `and`/`or`.
133
134  `--representation <list>` takes a comma list of readings and may repeat. Each
135  is a call in the language's own syntax, its options its named arguments:
136  `spectrum(peaks=8, frame=50ms)`, `ledger(depth=3, brief=1)`. An entry
137  followed by `=<path>` writes a file instead, listed under `written`:
138  `samples=<path>.wav` writes audio, encoded as `--bits` says; any other path
139  takes the reading's JSON, uncapped. A path that already holds a file refuses
140  unless `--confirm` is written. `samples=alsa:<device>` plays the samples on
141  that ALSA device (`alsa:default`, `alsa:hw:1`, `alsa:plughw:1,0`) as each
142  block is pulled, in real time, through `aplay` from alsa-utils; Linux only,
143  anywhere else it refuses as `destination.unsupported_platform`. It sends
144  32-bit floats, or integer PCM where `--bits` is 16 or under. A device that
145  cannot take the format refuses with aplay's reason, and one aplay says plays
146  another rate than the render's stops there as `destination.rate_inexact`.
147  `samples(buffer=<t>)` is how much the device holds ahead of what it plays.
148  Once it has played out, `data.played` states the device, its seconds, rate,
149  channels, format, the buffer asked and the one granted, and every underrun
150  aplay reported, with what it cannot see.
151  `lines`, `atoms`, `derivative`, `bindings`, `arguments`, and a closed form's
152  `spectrum`, `envelope` and `pitch`, read the expression and ignore the range.
153
154  A closed form's `spectrum` and `pitch` are its exact lines, so a term under a
155  crop or an envelope, which has a width and is no line, refuses them; so do
156  `spectrum(frame=)` and `envelope(frame=)`, as a closed form has no frames. To
157  measure any of them frame by frame, a reader samples the target's ref,
158  `sample(@x([0, 1s]))`, and an author writes `sample(...)` inside the node.
159  A closed form with no exact envelope, such as a crop of a decay, refuses
160  `envelope` with the reason rather than measuring it unasked; `sample(...)`
161  asks for the measured one. A measured `spectrum` is one spectrum: every
162  `frame`-long window across the range, averaged. `pitch` is framed, one entry
163  per `frame`; for the spectrum at one instant, read a range one frame long.
164  A measured `envelope` is framed too, and reads every channel: a frame's
165  `rms` is the root mean square of all its channels' samples together, its
166  `peak` the largest magnitude in any channel.
167
168  `--bits <n>` is the precision every sample is written to, from 2 to 52: the
169  point where a series is truncated, and the encoding of a `.wav`, integer PCM
170  at n bits up to 16 and 32-bit float above. `--rate <hz>` (default
171  {DEFAULT_SAMPLE_RATE}) is the one rate a render samples at: the target is read
172  at its instants, and every node at the instants its reader asks for. A
173  closed form is exact at any instant, and so is a continuous loop, one
174  constant delay at a gain under 1 over a closed form such as
175  `x + 0.5*self(t - 17ms)`, which is its series. A filter, a discrete loop, a
176  solver and `rand` step at the rate asked for, where `1sp` is one step, so an
177  `sp` count or `self[idx(t) - 1]` means one sample at whatever rate is asked.
178  `rand` draws once per step, keyed by that step's index; a key between steps
179  reads the step nearest it, ties to even.
180
181  `--threads <n>` (default: every core the machine reports) is the most
182  threads a render computes on; `1` computes every value in turn on one.
183  Values none of the others reads, and the blocks of a value that holds no
184  state, compute apart, to the same bits on any number of threads.
185
186  A discrete loop reads its own past only by index; `self(t - d)` in one
187  refuses as `type.discrete_self_at_time`, naming what made it discrete. A
188  read `@x(k*t - d)` steps `x` at `k` times the step, every input it reads,
189  `sp` and `idx` with it, and rounds `d` to the nearest sample of that step,
190  ties to even: shifts are rounded to the nearest sample so placements share
191  one cached value; timing is exact to half a sample, and the label's
192  `moved_s` states the most any read moved. A node is computed once
193  per step however many reads ask for it. A node that holds state read at a
194  time that moves refuses as `type.stateful_warp`, naming what
195  holds its state; `@x[idx(...)]` reads its nearest step instead, as `p[idx(...)]`
196  does a signal passed in as parameter `p`, and any `idx`, as
197  `idx(t - 5ms - 2ms*sin(2*pi*t))`, is read sample by sample. An edit to a
198  stream plays from the next sample on.
199
200  A render keeps in a store on disk its target and, under it, each node of
201  samples that holds state (a filter, a discrete loop or a solver in it) or
202  that two or more nodes read. Nothing else is kept. A closed form, `sample`
203  of one and `rand` have a value at any instant and are computed where read.
204  A node without state that one node alone reads, however many times, is
205  computed again by that reader from what the node itself reads: no other
206  reader could ask for it, and without state it restarts at any sample, where
207  a filter or a loop must run from its start. So a stage worth keeping is a
208  node of its own that ends in a filter or a loop, or one two nodes read. A
209  kept value is named by what it computes: never its file's name, directory,
210  comments, spacing, or the order its named arguments are written in, so a
211  renamed or moved copy, a constant lifted into a default, an expression split
212  into a file of its own or written back inline, and two addends swapped all
213  read one entry. The next render of anything that reads the same value reads
214  it back, bit for bit, and computes nothing under it. The store is on by
215  default, at `$XDG_CACHE_HOME/sva`, else `~/.cache/sva`; `--cache <path>`
216  moves it (a `/dev/shm` path keeps it in memory) and `--cache none` turns it
217  off. It holds at most {store_gb} GB, the least recently used values going
218  first, and a store another build of sva-cli wrote is emptied when opened. A
219  render stages what it computes beside the store as it goes, and the store
220  itself is written once, when the render ends, fails, or is stopped by SIGINT
221  or SIGTERM.
222
223  `ledger` prints one row per node under the target. A row's `share` is the
224  part of its reader's own energy that row accounts for, so one reader's refs
225  sum to 1; a ref no addend isolates, such as one factor of a product, prints
226  `null`. Each ref carrying a share is collapsed once on its own, so a ledger
227  costs one collapse per attributed ref beyond the render, and `depth` bounds
228  how many, counted in refs from one file to another. Every row is computed
229  whole over the range, however late a reader reads it. `brief=1` keeps only
230  the rows that clipped, `skim=1` drops the wider fields.
231
232  `arguments` renders nothing: for every instance under the target it prints
233  each builtin call's named arguments as the numbers the call was lowered
234  with, a solver's whole parameter set with `written: false` on each default
235  it filled in, and the operand each `min`/`max` `chosen` where a call folds
236  one to a number: in named arguments and a solver's, modal bank's or
237  `noise`'s positionals. One inside a filter's or cast's positional or
238  `rand`'s key or seed is not listed. `at` spans are bytes of the instance's
239  own body, as `sva-cli outline` counts them.
240
241REPRESENTATIONS:
242  Each one a `--representation` call, its arguments in parentheses, every
243  default under DEFAULTS. `exact` is read off the written math, `measured` off
244  samples.
245
246{representations}
247
248ANALYZE:
249  sva-cli analyze <file.wav> --representation <list> [--confirm]
250
251  Runs the same readings over a whole external `.wav` at its own rate, never
252  resampled. Only the readings a buffer answers alone apply, each listed under
253  REPRESENTATIONS as `measured` or `exact off a closed form, measured off
254  samples`; the rest need the graph behind it.
255
256LINT:
257  sva-cli lint ['<expression>'] [--format <json|text>]
258
259  Checks the current directory without rendering a sample. With no target it
260  checks every file under its own rules, and types every entry point, which
261  types every file it reaches. With a target, in render's grammar, it checks
262  the files that target reaches, and prints the interval a render of it reads.
263  Either refuses a type error as `render` does.
264
265  A score's `;` lines are free comments and its events no body, so the comment
266  and length checks pass it by; a file of two or more lines that is neither a
267  score nor a scale refuses as `multiline-body` before any check runs.
268
269  Every check prints one `data.diagnostics` item. `warning` exits 0, `error`
270  exits non-zero, so branch on the verdict and never on whether the array is
271  empty. No flag downgrades an error.
272
273  error    missing-comment       no `;` comment line
274           multiline-comment     more than one
275           malformed-comment     not four ` | ` fields, `Models:` `Neglects:`
276                                 `IO: <in> -> <out>` `Tags: <tag>[, <tag>...]`
277           long-comment-block    a `;` block over 1000 characters, the line-1
278                                 doc comment's own run exempted
279           long-expression-body  a body over 10000 characters, a backstop rather
280                                 than a complexity budget
281           literal-sample-rate   a written rate where `sp` belongs
282           arity                 a builtin called without an argument it reads,
283                                 or with one it does not, as `rand(seed=k)`
284  warning  key-is-not-a-pitch    `variables/key` holds neither a note name nor
285                                 a number of hertz
286           parameter-has-no-default
287                                 a node reads a parameter no `name = value` line
288                                 binds, so a bare `@node` cannot play
289           support-never-ends    a node's support, cut where its bound falls
290                                 under the silence threshold, has no end for a
291                                 bare render to stop at
292
293TRACE:
294  sva-cli trace <node|expression>
295
296  Prints one node's position without rendering audio: what it reads (`down`, one
297  hop), everything that reads it (`up`, transitively to an entry point), each
298  beside the expression doing the reading, the node that made it discrete, and
299  the feedback loop it sits in, if any. A score is shown as its line count, its
300  first and last `at` and the nodes it plays, never the sum it lowers to. An
301  interval the target reads is ignored.
302
303BUILTINS:
304  sva-cli builtins
305
306  Prints the whole callable and syntactic vocabulary: every builtin with its
307  arity and named arguments, each argument's `meaning`, `unit`, the model
308  `part` it sets and whether it `moves` with `t` (a filter's cutoff, q and gain;
309  every other named argument is one number, refused when it names none), each
310  positional's meaning where the model states one, unit suffixes, the note-name
311  grammar, reserved identifiers, special call shapes, and what has no operator
312  at all.
313
314OUTLINE:
315  sva-cli outline <expression>
316
317  Prints the parse tree the engine builds from one expression, each node with
318  the byte `span` it was written in: calls by `name` with positional and named
319  `args`, operators by `op`, refs by `path` with their `binds`, refs and `self`
320  by how they `read`, `time` or `index`, literals by `value` and `unit`, names
321  by `name`. A node the parser supplies itself, the `0` of a prefix minus or the
322  `t` of a bare `@ref`, has `written: false`. A score's text prints `score`
323  instead of `outline`: one item per event, a line listing N items N of them,
324  with its `line`, `span`, `at`, `node`, `deg` and `args`. Reads no
325  composition.
326
327NEW:
328  sva-cli new <name> [--idempotency-key <key>]
329
330  Writes a starter composition at ./<name>, and refuses if that directory
331  exists. `--idempotency-key <key>` records the key beside the composition, so a
332  retry under the same key succeeds identically while the tree still holds what
333  was written. Any other key, or an edited tree, refuses.
334
335EXAMPLES:
336  sva-cli new song1 && cd song1
337  sva-cli render '@master' --representation samples=/tmp/song1.wav
338  sva-cli render '@master([0, 8b])' --representation 'ledger(depth=2),loudness'
339  sva-cli render '@voice/note([0, inf), f0=C4, len=2s)' \
340    --until 'envelope(t) < -60db and t > 1s' --representation samples=/tmp/note.wav
341  sva-cli render 'sample(@chord/home)' --representation 'spectrum(peaks=8)' --rate 48000
342  sva-cli render 'sample(@voice/note([0, 2s], f0=C4))' --representation pitch
343  sva-cli render '@master' \
344    --representation 'samples(buffer=100ms)=alsa:default,loudness'
345  sva-cli lint
346  sva-cli lint '@master'
347  sva-cli trace score/phrase
348  sva-cli builtins
349
350OUTPUT:
351  {{"status": "success", "data": {{"target": "@master([0, 8b])", "sample_rate":
352  {DEFAULT_SAMPLE_RATE}, "bits": {bits}, "profile": "psychoacoustic-v1",
353  "interval": {{"start_secs":
354  0, "end_secs": 16}}, "label": {{...}}, "written": {{"items": [...]}},
355  "representations": {{"ledger": {{...}}}}, "diagnostics": {{"items": []}}}},
356  "meta": {{"request_id": "req_...", "timestamp": 1700000000}}}}
357
358  `interval` is null where no reading read samples. An error adds "error":
359  {{"code", "message", "details": {{"count", "codes"}}}}. Success or error,
360  every response carries every finding in full at "data": {{"diagnostics":
361  {{"items": [{{"code", "severity", "message", "location", "help"}}],
362  "pagination": {{"count", "has_more", "next_cursor"}}}}}}, empty where it found
363  none.
364
365  Every collection carries that same {{items, pagination}} pair. `count` is the
366  whole reading's, `has_more` says `items` holds less than that, and
367  `next_cursor` is the interval start, as `0.5s`, the rest is read from. A
368  reading written to a file is written whole, uncapped. This page is an
369  envelope of its own, at "data": {{"help"}}.
370
371DEFAULTS:
372  --rate <hz>          the one rate a render samples at, and `1sp`.
373                       Default {DEFAULT_SAMPLE_RATE}.
374  --bits <n>           the precision every sample is written to. Default {bits},
375                       the `psychoacoustic-v1` profile's own.
376  ledger(depth=<n>)    how deep below its target a `ledger` walks.
377                       Default {DEFAULT_LEDGER_DEPTH}.
378  (peaks=<n>)          peaks a `spectrum` keeps, notes a `pitch`, formants a
379                       `formants`, stripes and novelty peaks a `self_similarity`
380                       lists per view. Default {DEFAULT_MAX_PEAKS}.
381  alias(oversample=<n>) the multiple `alias` re-renders at to hear what folded.
382                       Default {DEFAULT_OVERSAMPLE}.
383  (frame=<t>)          the step a framed reading advances by, in seconds.
384                       Default {DEFAULT_FRAME_SECS}, except `spectrum`, which sizes
385                       its own transform to the range unless it is set.
386  (floor=<db>)         the amplitude, as `-40db`, under which a harmonic reading
387                       or `self_similarity` drops a partial, and under which
388                       `self_similarity` counts a band level as zero.
389                       Default {DEFAULT_FLOOR_DB}db.
390  voice_leading(norm=<l1|l2>) how a voice leading's moves add up. Default l1.
391  dissonance(pairs=<n>) the most partial pairs one frame sums before it refuses.
392                       Default {DEFAULT_MAX_PAIRS}.
393  periodicity(window=<t>) the span of onset strength one window autocorrelates,
394                       in seconds, at least twice the longest period it reads.
395                       Default {DEFAULT_PERIODICITY_WINDOW_SECS}.
396  periodicity(curve=<0|1>) `1` adds every window's height at every lag.
397                       Default 0.
398  modulation(window=<t>) the span of band envelope one window transforms, in
399                       seconds, at least twice the slowest period it reads.
400                       Default {DEFAULT_MODULATION_WINDOW_SECS}.
401  modulation(bpm=<n>)  a tempo, as `140`, adding each rate's period in beats.
402                       Default none.
403  self_similarity(cell=<t>) the side of one matrix cell, in seconds, a whole
404                       number of frames; the target may span at most
405                       {MAX_CELLS} cells. Default {DEFAULT_CELL_SECS}.
406  self_similarity(smooth=<t>) the length each diagonal is averaged over
407                       before stripes are picked, in seconds.
408                       Default {DEFAULT_SMOOTH_SECS}.
409  self_similarity(window=<t>) the whole width of the novelty kernel, in
410                       seconds. Default {DEFAULT_NOVELTY_WINDOW_SECS}.
411  self_similarity(curve=<0|1>) `1` adds every cell's chroma and band levels and
412                       each view's matrix. Default 0.
413  bindings(node=@<path>) the instance whose bindings are read; required.
414  samples(buffer=<t>)  how much an `alsa:` device holds ahead of what it plays,
415                       in seconds. Default {DEFAULT_BUFFER_SECS}; only a device
416                       has one.
417  ledger(brief, skim)  `0` unless set `1`.
418  --format <json|text> how `lint` prints its findings: the envelope, or one
419                       terminal line each, colored where stdout is a terminal.
420                       The same objects either way. Default json.
421  --cache <path|none>  where the store lives. Default `$XDG_CACHE_HOME/sva`, else
422                       `~/.cache/sva`, holding at most {store_gb} GB; `none` keeps
423                       no store. A store that fails fails no render: it answers
424                       as without one, warned why in `diagnostics`.
425  --confirm            replaces a destination that already holds a file. Without
426                       it a path already taken refuses as `conflict` and nothing
427                       is written.
428
429ENVIRONMENT:
430  SVA_LOG=debug        `render` logs, once it ends, what it asked of each value
431                       to stderr; stdout and the samples are unchanged. Any
432                       other value, or none, logs nothing. One line per second
433                       of output, then one per node and a total:
434                         sva-cache pass hit=0 miss=190 ... reused=0 cum-hit=0.0%
435                         sva-cache t=1.022s hit=1 miss=2 ... cum-hit=0.5%
436                         sva-cache node hit=1 miss=0 ... reused=3 <node>
437                         sva-cache total hit=1 miss=198 ... hit-rate=0.5% ...
438                       `pass` is what was looked up before the first block.
439                       Each value is looked up once: a `hit` was answered by
440                       the store, `prefix` found a stored run up to a switch,
441                       `miss` computed it, `new` held it in memory, where the
442                       store takes what it keeps from. Each read of a value
443                       past its first is `reused`.
444
445EXIT CODES:
446  0  success (error.code absent)
447  1  internal_error (a destination could not be written)
448  3  validation_error (bad arguments, a composition that failed to parse, or a
449     render the engine refused)
450  4  conflict (a name `new` would overwrite, or a destination already holding a
451     file, without `--confirm`)
452  24 not_found (a `.wav` file, or a node this composition does not define)
453
454VERB ALIASES:
455  validate = lint, list = builtins, create = new, show = trace, from the `cli`
456  standard's own verb list. `render` and `analyze` take a reading, which that
457  list has no word for, so they keep their own names.
458
459SEE ALSO:
460  sva-cli --version    Show version information"#
461    )
462}
463
464/// One entry per reading, off the one table: its call, what it answers off, and its line.
465fn representations() -> String {
466    READINGS
467        .iter()
468        .map(|row| {
469            let keys: Vec<&str> = row.takes.iter().map(|arg| arg.key()).collect();
470            let call = match keys[..] {
471                [] => row.name.to_string(),
472                _ => format!("{}({})", row.name, keys.join(", ")),
473            };
474            let source = match row.reads {
475                Reads::ClosedForm | Reads::Structure => "exact",
476                Reads::Either => "exact off a closed form, measured off samples",
477                Reads::Samples => "measured",
478                Reads::Graph => "measured, and the graph behind the samples",
479            };
480            format!("  {call:<27}{source}\n      {}", row.help)
481        })
482        .collect::<Vec<_>>()
483        .join("\n")
484}