diff --git a/README.md b/README.md index fd304b7..e49141c 100644 --- a/README.md +++ b/README.md @@ -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, diff --git a/calc.go b/calc.go index b6e7fd7..95599c6 100644 --- a/calc.go +++ b/calc.go @@ -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) { diff --git a/calc_test.go b/calc_test.go index 1411171..2fb017b 100644 --- a/calc_test.go +++ b/calc_test.go @@ -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)) diff --git a/format.go b/format.go index 4013b37..b4069a0 100644 --- a/format.go +++ b/format.go @@ -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 } diff --git a/inline.go b/inline.go index e4bfde6..9beddcf 100644 --- a/inline.go +++ b/inline.go @@ -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) diff --git a/name.go b/name.go index 5c6ae05..23e1382 100644 --- a/name.go +++ b/name.go @@ -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 { diff --git a/name_test.go b/name_test.go index d8799b0..7cc6656 100644 --- a/name_test.go +++ b/name_test.go @@ -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"}, diff --git a/namefence.go b/namefence.go index 5e677d4..b02d95b 100644 --- a/namefence.go +++ b/namefence.go @@ -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 diff --git a/reference.go b/reference.go index 6b81f82..530f368 100644 --- a/reference.go +++ b/reference.go @@ -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 } diff --git a/todo.md b/todo.md index 011c02a..4aaa488 100644 --- a/todo.md +++ b/todo.md @@ -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. diff --git a/transform.go b/transform.go index 17d7f4e..9330c50 100644 --- a/transform.go +++ b/transform.go @@ -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 } diff --git a/valueproof.go b/valueproof.go index b77501a..e4e89f9 100644 --- a/valueproof.go +++ b/valueproof.go @@ -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"