Answer architecture round 1: exempt only a calc operand from the read-once refusal, run the name passes from one list, read every operand's leaves, show a name operand in a README example, and plan a path through a name as an operand
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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
@@ -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))
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user