Make great documentation goal 1.2, and plan the README's reorganisation and the nested-template gaps #33

Open
lilleman wants to merge 966 commits from worktree-todo-relative-dates-exact-counts into main
12 changed files with 76 additions and 43 deletions
Showing only changes of commit 94cf741706 - Show all commits
+15 -7
View File
@@ -677,16 +677,23 @@ the SQL example above.
`{calc(expr)}` evaluates `+ - * /`, parentheses and unary minus over number
literals, sibling field names and [names](#names), each rendered then read as a number;
a second argument rounds to that many decimals. A name is the operand `{k}` prints:
`{/n as k}{k} x 2 = {calc(k * 2)}` doubles the pick it shows. A hyphen is always
subtraction, so a hyphenated field or name can't be an operand.
a second argument rounds to that many decimals. A hyphen is always subtraction, so a
hyphenated field or name can't be an operand.
```json
{ "format": "{net} x {qty} = {calc(net * qty, 2)}", "net": ["19.99", "5.00"], "qty": ["3", "7"] }
```
Renders e.g. `19.99 x 3 = 59.97`. A result that rounds to zero prints unsigned — `0`,
`0.00` — as `{float()}`'s does. An operand that can never be a number (`"abc"`,
Renders e.g. `19.99 x 3 = 59.97`. A name is the operand `{lat}` prints, so the calc computes
from the pick shown:
```json
"{/misc.coordinate.lat as lat}{lat}° is {calc(lat * 60, 0)} arcminutes"
```
Renders e.g. `5.338477° is 320 arcminutes`.
A result that rounds to zero prints unsigned — `0`, `0.00` — as `{float()}`'s does. An operand that can never be a number (`"abc"`,
or a choice of such) is rejected at load, as is a division by a constant zero
(`1/0`, or a fixed `"0"` field); an operand that sometimes is not a number yields
`NaN`, and a division by one that is not constant `Inf` — both print rather than
@@ -771,8 +778,9 @@ Refused at `New`, each error naming what to write instead:
- a binding of anything but a reference;
- a binding in a choice's item, which every other item would leave unbound;
- a binding nothing reads, or a binding of a whole category read once whole where it is
bound, which the bare reference spells: `{/word as w}{w}` is `{/word}`, while
`{/n as k}{calc(k * 2)}` stands, since a calc operand is no reference;
bound, which the bare reference spells: `{/word as w}{w}` is `{/word}`. A calc's
operand cannot be a reference, so there the name is the one spelling:
`{/n as k}{calc(k * 2)}` loads;
- a path through a name that selects a row;
- `{n}` beside `{n.w}` where `n`'s category renders `w` twice, as in `{w}-{w}`;
- reads of a name in two draw groups, or inside a `repeat` nested where the name is bound,
+13 -14
View File
@@ -91,22 +91,21 @@ func checkCalcNames(path string, t *template) error {
if o.fn != "calc" || !slices.ContainsFunc(o.operands, func(a arm) bool { return a.kind == namedRead }) {
continue
}
if err := checkOperands(o.args[0], parsedCalc(o.args[0]), operandNodes(t, o)); err != nil {
if err := checkOperands(o.args[0], parsedCalc(o.args[0]), operandNodes(o)); err != nil {
return fmt.Errorf("%s: token {%s}: %w", t.site.label(path), o.body, err)
}
}
return nil
}
// operandNodes lists every node each operand of o may render: its field, or each node its name's
// path lands on.
func operandNodes(t *template, o op) func(name string) []node {
// operandNodes lists every node each operand of o may render, once linkNames compiled the names.
func operandNodes(o op) func(name string) []node {
return func(name string) []node {
i := slices.IndexFunc(o.operands, func(a arm) bool { return a.head == name })
if a := o.operands[i]; a.kind == namedRead {
return a.leaves
a := o.operands[slices.IndexFunc(o.operands, func(a arm) bool { return a.head == name })]
if len(a.leaves) == 0 {
panic(internalError("calc operand %q was read before it was compiled", name))
}
return []node{t.fields[name]}
return a.leaves
}
}
@@ -114,7 +113,7 @@ func operandNodes(t *template, o op) func(name string) []node {
// operand lists every node an operand may render, nil while that is unknown.
func checkOperands(text string, expr calcNode, operand func(name string) []node) error {
for _, name := range calcVars(expr) {
if rendered, never := neverNumber(operand(name)); never {
if rendered, never := noneNumeric(operand(name)); never {
return fmt.Errorf("calc(%q): operand %q is never a number: it renders %q", text, name, rendered)
}
}
@@ -208,13 +207,13 @@ func neverNumeric(n node) (text string, never bool) {
return lit, true
}
case *choice:
return neverNumber(n.items)
return noneNumeric(n.items)
}
return "", false
}
// neverNumber reports nodes no render of which is a number, and none where there are none.
func neverNumber(nodes []node) (text string, never bool) {
// noneNumeric reports nodes no render of which is a number, and none where there are none.
func noneNumeric(nodes []node) (text string, never bool) {
for _, n := range nodes {
t, nodeNever := neverNumeric(n)
if !nodeNever {
@@ -420,8 +419,8 @@ func (p *calcParser) number() (calcNode, error) {
return calcNum(v), nil
}
// ident reads a field name: a letter or '_', then letters, digits or '_'. A '-'
// is always the minus operator, so a hyphenated field name can't be an operand.
// ident reads a field or name: a letter or '_', then letters, digits or '_'. A '-'
// is always the minus operator, so a hyphenated one can't be an operand.
func (p *calcParser) ident() (calcNode, error) {
start := p.pos
for p.pos < len(p.rs) {
+1 -2
View File
@@ -317,7 +317,7 @@ func TestCalcReadsANameItReadsNowhereElse(t *testing.T) {
}
}
func TestCalcRefusesANameNeverANumber(t *testing.T) {
func TestCalcRefusesABadOperand(t *testing.T) {
for card, want := range map[string]string{
`"{/word as w}{w}{calc(w * 2)}"`: `operand "w" is never a number: it renders "def"`,
`"{/zero as z}{z}{calc(1 / z)}"`: `divides by z, which is always zero`,
@@ -338,7 +338,6 @@ func TestCalcRefusesANameNeverANumber(t *testing.T) {
func TestATypedColumnProvesACalcOverAName(t *testing.T) {
dir := writeData(t, map[string]string{
"n": `"{int(1,9)}"`,
"word": `["1","x"]`,
"order": `{"format":"{/n as k}{k}","double":{"format":"{calc(k * 2)}","datatype":"integer"}}`,
})
f := newGenerator(t, dir, WithSeed(1))
+7 -3
View File
@@ -133,6 +133,9 @@ type builtin struct {
// operands names the fields the call reads, which expand renders for it; nil
// for a builtin that reads none.
operands func(args []string) []string
// refOperands says an operand may be a reference, so a name read once there has the bare
// reference as a second spelling.
refOperands bool
// proveNumber bounds the number a call's text reads as, token its body, and says which
// datatypes that text is not; set it where every render reads as a finite number, which
// makes the call a calc operand, and leave it nil otherwise.
@@ -215,10 +218,11 @@ func parseChecked(format string, fields map[string]node) ([]formatToken, []unbou
// unboundRead is a token reading a head no field holds, which only a name bound around the
// template can answer; err is the refusal where none does. tail is the path read into the name,
// "" for the name itself, operand marks a builtin's read of it, and body is the token's.
// "" for the name itself, operand marks a builtin's read of it, noRef one where no reference can
// stand, and body is the token's.
type unboundRead struct {
head, tail, body string
operand bool
operand, noRef bool
err error
}
@@ -277,7 +281,7 @@ func checkReads(t formatToken, fields map[string]node, operands bool) ([]unbound
return nil, err
}
a := splitArm(name, nil)
unbound = append(unbound, unboundRead{head: a.head, tail: joinSegments(a.tail), body: t.body, operand: operands, err: err})
unbound = append(unbound, unboundRead{head: a.head, tail: joinSegments(a.tail), body: t.body, operand: operands, noRef: operands && !builtins[t.fn].refOperands, err: err})
}
return unbound, nil
}
+1 -1
View File
@@ -200,7 +200,7 @@ func linkNodeRefs(scope nodeScope, root map[string]node) error {
}); err != nil {
return err
}
for _, pass := range []func(string, *template) error{linkNames, checkNameReads, checkCalcNames} {
for _, pass := range namePasses {
if err := scope(func(path string, m node) error {
if t, ok := m.(*template); ok {
return pass(path, t)
+9 -5
View File
@@ -34,11 +34,11 @@ type nameBinding struct {
}
// nameUse is one read of a name: the path it reads into the name, "" for the name itself; whether
// a builtin reads it as an operand; the draw group its template renders in; and whether it sits in
// a repeat nested inside the name's scope.
// a builtin reads it as an operand, and whether that operand can be no reference; the draw group
// its template renders in; and whether it sits in a repeat nested inside the name's scope.
type nameUse struct {
tail, group string
operand, nested bool
tail, group string
operand, noRef, nested bool
}
func (sc *nameScope) lookup(name string) *nameBinding {
@@ -184,7 +184,7 @@ func resolveReads(n node, where, group string, scopes []*nameScope) error {
if b == nil {
return unresolved(where, u, t.nameScope, scopes)
}
b.uses = append(b.uses, nameUse{tail: u.tail, group: group, operand: u.operand, nested: t.nameScope != b.scope})
b.uses = append(b.uses, nameUse{tail: u.tail, group: group, operand: u.operand, noRef: u.noRef, nested: t.nameScope != b.scope})
}
return nil
}
@@ -250,6 +250,10 @@ func namedReads(t *template, fn func(o *op, a *arm) error) error {
return nil
}
// namePasses run over every linked template in turn, each over all of them before the next: a pass
// reads what the one before it filled in.
var namePasses = []func(path string, t *template) error{linkNames, checkNameReads, checkCalcNames}
// linkNames compiles t's reads of a name as paths from the head its binding's reference names, once
// every template is linked, so each binder has resolved that reference.
func linkNames(path string, t *template) error {
+4
View File
@@ -204,6 +204,10 @@ func TestNameErrors(t *testing.T) {
"a binding names nothing"},
{"read once whole", map[string]string{"word": `["a","b"]`, "card": `{"format":"{/word as w}{x}","x":"{w}"}`},
`write {/word}`},
{"read once whole, as a transform's operand", map[string]string{"word": `["a","b"]`, "card": `"{/word as w}{uppercase(w)}"`},
`write /word where it is read`},
{"read once whole, as a calc's operand", map[string]string{"n": `["2","3"]`, "card": `"{/n as k}{calc(k * 2)}"`},
""},
{"rendered twice where a path reads it", map[string]string{"word": `{"format":"{w}-{w}","w":["a","b"]}`, "card": `"{/word as n}{n}|{n.w}"`},
`{n} renders field "w" twice`},
{"a binding in a cell", map[string]string{"word": `["a","b"]`, "t.json": `{"format":"{k}","rows":"t.tsv","key":"k"}`, "t.tsv": "k\tc\nx\t{/word as w}{w}{w}\ny\tz\n"},
+8 -4
View File
@@ -26,11 +26,15 @@ func checkNameReads(path string, t *template) error {
})
}
// checkUses refuses a binding of a category read once whole, outside a calc, which takes no reference, and reads of b in two draw groups,
// or inside a repeat, where what b names reads a reference path.
// checkUses refuses a binding of a category read once whole where a reference could stand. It
// refuses reads of b in two draw groups, or inside a repeat, where what b names reads a reference path.
func (b *nameBinding) checkUses() error {
if r := b.uses[0]; len(b.uses) == 1 && r.tail == "" && !r.nested && !r.operand && len(b.tail) == 0 {
return fmt.Errorf("name %q is read once, whole, which the bare reference draws the same way; write {%s} where it is read, and drop the token", b.name, b.ref)
if r := b.uses[0]; len(b.uses) == 1 && r.tail == "" && !r.nested && !r.noRef && len(b.tail) == 0 {
spelling := b.ref
if !r.operand {
spelling = "{" + spelling + "}"
}
return fmt.Errorf("name %q is read once, whole, which the bare reference draws the same way; write %s where it is read, and drop the token", b.name, spelling)
}
if !readsHeld(b.head, map[node]bool{}) {
return nil
+1 -1
View File
@@ -77,7 +77,7 @@ func linkRefs(sites []categorySite, root map[string]node) error {
}); err != nil {
return err
}
for _, pass := range []func(string, *template) error{linkNames, checkNameReads, checkCalcNames} {
for _, pass := range namePasses {
if err := eachTemplate(sites, func(_ categorySite, path string, t *template) error { return pass(path, t) }); err != nil {
return err
}
+12 -2
View File
@@ -6,7 +6,7 @@
`Bar = 9`
`Next ID = 91`
`Next ID = 93`
| Goal | W |
|---|---|
@@ -74,6 +74,7 @@
| 57 | 0.1.0 | | **Rewrite `CHANGELOG.md`'s `[Unreleased]` as what v0.1.0 holds.** | 1 | 2 | 6 | 6 | 1.1 | 22.5 |
| 34 | 0.1.0 | | **Add `{isin()}`, `{cusip()}`, `{aba()}` and `{vin()}`.** | 2 | 4 | 6 | 7 | 4 | 20.9 |
| 5 | 0.1.0 | | **Run the nine-seat comprehension panel after the draw restructure, and file what it names.** | 1 | 3 | 3 | 9 | 3 | 20.4 |
| 91 | 0.1.0 | | **Accept a path through a name as a `calc` operand, so `{calc(t.population * 2)}` computes from the row `{t}` prints.** | 3 | 4 | 6 | 7 | 4.1, 5.3 | 19.9 |
| 89 | 0.1.0 | | **Fail `New` without `WithSeed`, naming the option, and seed the CLI from the system.** | 3 | 3 | 6 | 7 | 10.1 | 16.0 |
| 90 | 0.1.0 | | **Restructure the code into folders named for what they hold: the public API stays in the root package, and the implementation moves into `internal/` packages.** | 6 | 9 | 3 | 9 | 3.2, 3.4 | 13.9 |
| 79 | 0.1.0 | decision | **Beside a `--data-path`, load in `New` the user's categories, the shipped ones they override or read, and the shipped ones whose reads reach an overridden one; load the rest on first reach.** | 6 | 6 | 7 | 8 | 13 | 12.9 |
@@ -87,6 +88,7 @@
| 52 | 0.2.0 | | **Name the file a link error comes from beside its category path, `sub.x (/d/a/sub/x.json): …`.** | 1 | 2 | 5 | 7 | 6 | 19.0 |
| 28 | 0.2.0 | decision | **Accept a middle name, and draw a `personnummer` whose sex matches a sex selected through a named pick.** | 4 | 4 | 6 | 7 | 4.1 | 18.9 |
| 24 | 0.2.0 | defect | **Stop `misc.territory[EH].tld` rendering `.eh`, the one shipped TLD `misc.tld` does not hold.** | 1 | 1 | 4 | 7 | 4 | 18.6 |
| 92 | 0.2.0 | | **Name the rename in a `calc`'s refusal of a hyphenated operand, which today reports `no field or name "calling"` for `{calc(calling-code * 2)}`.** | 1 | 2 | 5 | 6 | 6 | 17.5 |
| 53 | 0.2.0 | | **Let a table column carry a `datatype`, so `--format json` writes `"safe": true` and `--format sql` a boolean.** | 4 | 5 | 6 | 6 | 4 | 16.9 |
| 55 | 0.2.0 | | **Document `NewRecordTemplate`, `ErrNoColumns`, `ErrNoData` and `MaxRepeat` in the README's Library section.** | 1 | 2 | 6 | 7 | 12 | 16.8 |
| 18 | 0.2.0 | | **Give the address records one column set across countries: `region` and `municipality` as columns on `geo.SE.address` too.** | 4 | 4 | 6 | 6 | 5 | 16.6 |
@@ -229,7 +231,7 @@ Shape: T = table, t = template, c = choice.
- Deletes the token-order rules (`drawsApart`, `readFold`, `branches`, the pairwise replay), the expansion hold fence, `drawGroup`, the own-family fence and the cell-reference fences, and revises the decisions that point here. The remaining draw checks are names, paths through names, cycles and the repeat cap.
- Fixes on the way: `{/sel}|{/sel}` panics out of `Fake` today where two draws land on rows whose cells select different rows of another table; it must render.
- Rewrites the README's Computation, References, Draw group, Correlated fields, Linked tables, Names and "One draw, one spelling" sections; the Linked tables section says which pick's weights govern a family, since the bound row decides.
- Widens the refusal of a name read once to every binding, a path into it or a reference path bound included, since `{n.x}` read once is then `{/ref.x}`, and deletes the names fence over draw groups and nested repeats (`checkUses`, `readsHeld`).
- Keeps `{/n as k}{calc(k * 2)}` loading, since a calc operand cannot be a reference. Widens the refusal of a name read once to every binding, a path into it or a reference path bound included, since `{n.x}` read once is then `{/ref.x}`, and deletes the names fence over draw groups and nested repeats (`checkUses`, `readsHeld`).
- Say how a record's columns and a struct's tags read one pick, so the `first` and `sex` columns of one `sv_SE.person` record, or one struct's tags reading them, still describe one person.
- Re-pin seeded output and `testdata/shipped_shape.txt` in their own commits; the CHANGELOG names the grammar change.
@@ -245,6 +247,10 @@ Nothing has shipped, so "no longer paths", "where it used to fail" and "where it
Needs items 4 and 90. The decision "comprehension floor: every dimension and the overall at 7.0 or above; items 4 and 90 ship with no scoring run, and item 5 scores them" suspends every scoring run and panel until then.
### 91. Accept a path through a name as a `calc` operand, so `{calc(t.population * 2)}` computes from the row `{t}` prints.
A calc operand parses as a bare identifier, so `{/misc.territory as t}{calc(t.population * 2)}` fails with `unexpected ".population"`, and no binding can hold the column alone beside the row. Item 4 removes today's route, a field holding the reference path, which the expansion hold keeps for the render. Resolve the path through `linkName`'s leaves, as `operandNodes` does for a bare name.
### 89. Fail `New` without `WithSeed`, naming the option, and seed the CLI from the system.
Goal 10.1 has the caller supply the seed, and today `New` without `WithSeed` seeds itself. This item ships in 0.1.0 beside item 77, so `New` breaks once before anyone depends on it. Mark `WithSeed` required in the Library section's options table.
@@ -310,6 +316,10 @@ Needs item 4.
ISO 3166 reserves it for Western Sahara and the root zone has never been delegated it, so no resolver answers for it.
### 92. Name the rename in a `calc`'s refusal of a hyphenated operand, which today reports `no field or name "calling"` for `{calc(calling-code * 2)}`.
A hyphen is always subtraction in a calc, and the shipped data is full of hyphenated fields (`calling-code`, `birth-number`). The refusal names the first fragment, not the field the author meant.
### 53. Let a table column carry a `datatype`, so `--format json` writes `"safe": true` and `--format sql` a boolean.
Today only a JSON field takes one, so `misc.httpmethod`'s booleans are typed in Go and text everywhere else. Typing a shipped column changes what `json` and `sql` write, so the capability is a minor and applying it to `misc.httpmethod` is a major.
+1 -1
View File
@@ -19,7 +19,7 @@ func withTransforms(calls map[string]builtin) map[string]builtin {
if _, clash := calls[name]; clash {
panic(internalError("%s is registered as a builtin and a transform", name))
}
calls[name] = builtin{arity: 1, checkArgs: transformArg, prep: transformPrep(fn), operands: transformOperand}
calls[name] = builtin{arity: 1, checkArgs: transformArg, prep: transformPrep(fn), operands: transformOperand, refOperands: true}
}
return calls
}
+4 -3
View File
@@ -200,7 +200,7 @@ func (p *valueProof) proveTemplate(t *template) proven {
}
return p.proveUnion(leaves)
case name == "calc":
return p.proveCalc(t, body, args)
return p.proveCalc(o)
case builtins[name].proveNumber != nil:
return builtins[name].proveNumber(body, builtins[name].prints, args)
case isTransform:
@@ -209,9 +209,10 @@ func (p *valueProof) proveTemplate(t *template) proven {
return printing(body, DataTypeString, proven{notOperand: fmt.Sprintf("{%s} prints text, not a number", body)})
}
func (p *valueProof) proveCalc(t *template, body string, args []string) proven {
func (p *valueProof) proveCalc(o op) proven {
body, args := o.body, o.args
expr := parsedCalc(args[0])
nodes := operandNodes(t, t.compiled.ops[0])
nodes := operandNodes(o)
v, doubt := p.proveExpr(expr, func(name string) proven { return p.proveUnion(nodes(name)) })
if doubt == "" && !(magnitude(v) <= calcLimit) {
doubt = calcText(expr) + " is not proven within 1e300"