Compare commits
10 Commits
3f46e9c701
...
64a0d29496
| Author | SHA1 | Date | |
|---|---|---|---|
| 64a0d29496 | |||
| 138bd21d83 | |||
| e31e77b8a3 | |||
| 4b3dd011fc | |||
| 21e7af6c1f | |||
| 8d03ce34bb | |||
| cf3229cd4d | |||
| 39d59aec23 | |||
| f949550515 | |||
| 027d5c33fc |
@@ -238,8 +238,9 @@ option stays unset. `New` rejects an option that cannot take effect — a
|
|||||||
`separator` without a `repeat` above 1, a `weight` outside a choice — and a name
|
`separator` without a `repeat` above 1, a `weight` outside a choice — and a name
|
||||||
using a character the grammars reserve: `.` separates the segments of a path, `|`
|
using a character the grammars reserve: `.` separates the segments of a path, `|`
|
||||||
the arms of a token, `(` opens a function call and `}` ends the token, so a name
|
the arms of a token, `(` opens a function call and `}` ends the token, so a name
|
||||||
carrying one is a name no format could ever spell. That holds for a category, a
|
carrying one is a name no format could ever spell. An empty name goes the same
|
||||||
folder and a field alike.
|
way — it is no path segment at all, so `List` never offers it. That holds for a
|
||||||
|
category, a folder and a field alike.
|
||||||
|
|
||||||
**Functions.** A `{name()}` token calls a built-in function instead of rendering
|
**Functions.** A `{name()}` token calls a built-in function instead of rendering
|
||||||
a field. `{luhn()}` appends a Luhn check digit over the digits emitted **so far**
|
a field. `{luhn()}` appends a Luhn check digit over the digits emitted **so far**
|
||||||
@@ -365,9 +366,12 @@ the reference sits, including in a field the format renders.
|
|||||||
|
|
||||||
A `{calc()}` operand is held on its own terms too, so the same fence guards it:
|
A `{calc()}` operand is held on its own terms too, so the same fence guards it:
|
||||||
`{..cat.net} x 2 = {calc(net * 2, 2)}` names one field two ways and is a load
|
`{..cat.net} x 2 = {calc(net * 2, 2)}` names one field two ways and is a load
|
||||||
error, with no path token anywhere. What an operand's draw fixes is the value it
|
error, with no path token anywhere. A calc renders its operand whole, so the hold
|
||||||
renders, so a reference reaching *through* it is caught as well, while a sibling
|
pins what that render settled, following the operand's plain `{field}` tokens —
|
||||||
its format never renders is free — that sibling is no part of the value.
|
`{net.v}` and `{..cat.net.v}` are rejected alike. It stops at a `{..path}`, where
|
||||||
|
the operand's own value ends and a shared source begins: two names drawing from
|
||||||
|
one referenced category are two draws, as `{word} {word}` is, so two dice over
|
||||||
|
one `{..die}` are fine.
|
||||||
|
|
||||||
```text
|
```text
|
||||||
token {p} renders a level that {p.first} reads a path into; name the fields you want instead
|
token {p} renders a level that {p.first} reads a path into; name the fields you want instead
|
||||||
|
|||||||
+71
-13
@@ -148,25 +148,77 @@ func TestALevelRenderedOnlyByAPathTokenIsHeld(t *testing.T) {
|
|||||||
// shown is not the value computed. Two names that merely draw from one source are
|
// shown is not the value computed. Two names that merely draw from one source are
|
||||||
// two draws, as {word} {word} is — the rule everywhere else in the engine.
|
// two draws, as {word} {word} is — the rule everywhere else in the engine.
|
||||||
func TestACalcOperandIsHeldAgainstEveryRoute(t *testing.T) {
|
func TestACalcOperandIsHeldAgainstEveryRoute(t *testing.T) {
|
||||||
rejected := map[string]map[string]string{
|
rejected := map[string]struct {
|
||||||
|
files map[string]string
|
||||||
|
want string // the route the error names, spelled as the author wrote it
|
||||||
|
}{
|
||||||
"a reference beside the operand": {
|
"a reference beside the operand": {
|
||||||
"cat": `{"format":"{..cat.net} x 2 = {calc(net * 2, 2)}","net":["10.00","20.00"]}`,
|
map[string]string{
|
||||||
|
"cat": `{"format":"{..cat.net} x 2 = {calc(net * 2, 2)}","net":["10.00","20.00"]}`,
|
||||||
|
},
|
||||||
|
`{..cat.net} renders "net"`,
|
||||||
},
|
},
|
||||||
"a reference one level down": {
|
"a reference one level down": {
|
||||||
"cat": `{"format":"{calc(net * 2, 2)} {q}","net":["10.00","20.00"],` +
|
map[string]string{
|
||||||
`"q":{"format":"{..cat.net}"}}`,
|
"cat": `{"format":"{calc(net * 2, 2)} {q}","net":["10.00","20.00"],` +
|
||||||
|
`"q":{"format":"{..cat.net}"}}`,
|
||||||
|
},
|
||||||
|
`{q} renders "net"`,
|
||||||
},
|
},
|
||||||
// A reference ending at the operand binds the choice itself, not a variant,
|
// A reference ending at the operand binds the choice itself, not a variant,
|
||||||
// so wrapping the operand in one changes nothing.
|
// so wrapping the operand in one changes nothing.
|
||||||
"a reference to an operand wrapped in a choice": {
|
"a reference to an operand wrapped in a choice": {
|
||||||
"cat": `{"format":"{calc(n * 2, 2)} {q}","n":[{"format":"{v}","v":["1","2"]}],` +
|
map[string]string{
|
||||||
`"q":{"format":"{..cat.n}"}}`,
|
"cat": `{"format":"{calc(n * 2, 2)} {q}","n":[{"format":"{v}","v":["1","2"]}],` +
|
||||||
|
`"q":{"format":"{..cat.n}"}}`,
|
||||||
|
},
|
||||||
|
`{q} renders "n"`,
|
||||||
|
},
|
||||||
|
// The reaching route can be another operand, which is not a token — so the
|
||||||
|
// error must name it as one rather than invent a {b} the format never wrote.
|
||||||
|
"an operand reaching another operand": {
|
||||||
|
map[string]string{
|
||||||
|
"cat": `{"format":"{calc(a + b, 0)}","a":{"format":"{x}","x":["1","2"]},` +
|
||||||
|
`"b":{"format":"{..cat.a}"}}`,
|
||||||
|
},
|
||||||
|
`calc operand "b" renders "a"`,
|
||||||
|
},
|
||||||
|
// Reaching *into* the operand is the same shape as naming it: the draw the
|
||||||
|
// calc holds settled that value too. The token spelling {net.v} is already
|
||||||
|
// rejected, so the reference spelling has to be.
|
||||||
|
"a reference into the operand": {
|
||||||
|
map[string]string{
|
||||||
|
"cat": `{"format":"{calc(net * 2, 2)}|{..cat.net.v}",` +
|
||||||
|
`"net":{"format":"{v}","v":["10.00","20.00"]}}`,
|
||||||
|
},
|
||||||
|
`{..cat.net.v} renders "net"`,
|
||||||
|
},
|
||||||
|
"a reference into the operand one level down": {
|
||||||
|
map[string]string{
|
||||||
|
"cat": `{"format":"{calc(net * 2, 2)}|{q}","net":{"format":"{v}","v":["10.00","20.00"]},` +
|
||||||
|
`"q":{"format":"{..cat.net.v}"}}`,
|
||||||
|
},
|
||||||
|
`{q} renders "net"`,
|
||||||
|
},
|
||||||
|
// Each head is walked against its own seen set. Sharing one across heads
|
||||||
|
// would let the first head's walk mark the only route to the second's draw,
|
||||||
|
// and this violation sits on the later head in sorted order.
|
||||||
|
"a violation on the second of two held heads": {
|
||||||
|
map[string]string{
|
||||||
|
"cat": `{"format":"{calc(a + b, 0)} {w}","a":{"format":"{x}","x":["1","2"]},` +
|
||||||
|
`"b":{"format":"{y}","y":["3","4"]},"w":{"format":"{..cat.b}"}}`,
|
||||||
|
},
|
||||||
|
`{w} renders "b"`,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
for name, files := range rejected {
|
for name, c := range rejected {
|
||||||
_, err := New([]string{writeData(t, files)})
|
_, err := New([]string{writeData(t, c.files)})
|
||||||
if err == nil || !strings.Contains(err.Error(), "a {calc()} also reads") {
|
if err == nil || !strings.Contains(err.Error(), "a {calc()} also reads") {
|
||||||
t.Errorf("%s: New = %v, want the second route to the operand rejected", name, err)
|
t.Errorf("%s: New = %v, want the second route to the operand rejected", name, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !strings.Contains(err.Error(), c.want) {
|
||||||
|
t.Errorf("%s: New = %v, want it to name %q", name, err, c.want)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -200,6 +252,11 @@ func TestACalcOperandIsHeldAgainstEveryRoute(t *testing.T) {
|
|||||||
"a literal operand named twice": {
|
"a literal operand named twice": {
|
||||||
"cat": `{"format":"{calc(n * 2, 0)} {..cat.n}","n":"5"}`,
|
"cat": `{"format":"{calc(n * 2, 0)} {..cat.n}","n":"5"}`,
|
||||||
},
|
},
|
||||||
|
// One node reached twice while walking the operand: the walk must not
|
||||||
|
// revisit it, and the repeat is not a second route to anything.
|
||||||
|
"an operand that renders one field twice": {
|
||||||
|
"cat": `{"format":"{calc(v * 2, 0)}","v":{"format":"{a}{a}","a":["1","2"]}}`,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
for name, files := range accepted {
|
for name, files := range accepted {
|
||||||
if _, err := New([]string{writeData(t, files)}); err != nil {
|
if _, err := New([]string{writeData(t, files)}); err != nil {
|
||||||
@@ -599,12 +656,13 @@ func TestDeepPathsUnderOneHeadStayIndependentWhereTheyDiverge(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestEmptyPathSegmentIsRejected(t *testing.T) {
|
func TestEmptyPathSegmentIsRejected(t *testing.T) {
|
||||||
// "{a.}", "{.b}" and "{a..b}" are unfinished paths. A field really named ""
|
// "{a.}", "{.b}" and "{a..b}" are unfinished paths, and the segment naming
|
||||||
// would otherwise make them resolve, which reads as a typo that worked.
|
// nothing is reported as that rather than as a missing field. An empty name is
|
||||||
|
// rejected where it is authored, so no data can make these resolve.
|
||||||
rejected := map[string]string{
|
rejected := map[string]string{
|
||||||
"trailing dot": `{"format":"[{a.}]","a":{"format":"x","":["V"]}}`,
|
"trailing dot": `{"format":"[{a.}]","a":{"format":"x"}}`,
|
||||||
"leading dot": `{"format":"[{.b}]","":{"format":"{b}","b":["V"]}}`,
|
"leading dot": `{"format":"[{.b}]","a":{"format":"{b}","b":["V"]}}`,
|
||||||
"double dot": `{"format":"[{a..b}]","a":{"format":"x","":{"format":"{b}","b":["V"]}}}`,
|
"double dot": `{"format":"[{a..b}]","a":{"format":"x"}}`,
|
||||||
}
|
}
|
||||||
for name, file := range rejected {
|
for name, file := range rejected {
|
||||||
_, err := New([]string{writeData(t, map[string]string{"cat": file})})
|
_, err := New([]string{writeData(t, map[string]string{"cat": file})})
|
||||||
|
|||||||
+10
-3
@@ -48,9 +48,7 @@ var builtins = map[string]builtin{
|
|||||||
return func(s *session, _ string, _ []string) string { return strconv.Itoa(lo + s.IntN(span)) }
|
return func(s *session, _ string, _ []string) string { return strconv.Itoa(lo + s.IntN(span)) }
|
||||||
}},
|
}},
|
||||||
"float": {arity: 3, check: floatArgs, prep: func(a []string) callFn {
|
"float": {arity: 3, check: floatArgs, prep: func(a []string) callFn {
|
||||||
lo, _ := strconv.ParseFloat(a[0], 64)
|
lo, hi, dp := atof(a[0]), atof(a[1]), atoi(a[2])
|
||||||
hi, _ := strconv.ParseFloat(a[1], 64)
|
|
||||||
dp := atoi(a[2])
|
|
||||||
return func(s *session, _ string, _ []string) string {
|
return func(s *session, _ string, _ []string) string {
|
||||||
return strconv.FormatFloat(lo+s.Float64()*(hi-lo), 'f', dp, 64)
|
return strconv.FormatFloat(lo+s.Float64()*(hi-lo), 'f', dp, 64)
|
||||||
}
|
}
|
||||||
@@ -104,6 +102,15 @@ func atoi(s string) int {
|
|||||||
return n
|
return n
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// atof is atoi for a float arg, and reports an unvalidated one the same way.
|
||||||
|
func atof(s string) float64 {
|
||||||
|
f, err := strconv.ParseFloat(s, 64)
|
||||||
|
if err != nil {
|
||||||
|
panic(fmt.Sprintf("fakes: builtin arg %q reached prep unvalidated: %v", s, err))
|
||||||
|
}
|
||||||
|
return f
|
||||||
|
}
|
||||||
|
|
||||||
func randBytes(r rng, n int) []byte {
|
func randBytes(r rng, n int) []byte {
|
||||||
b := make([]byte, n)
|
b := make([]byte, n)
|
||||||
for i := range b {
|
for i := range b {
|
||||||
|
|||||||
@@ -193,3 +193,27 @@ func TestRegistryShapes(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestArgGuardsPanic pins the guards that report a builtin arg its check should
|
||||||
|
// have rejected. No data reaches them — checkFunc runs a builtin's check before
|
||||||
|
// compileOps ever calls prep — so they are exercised directly.
|
||||||
|
func TestArgGuardsPanic(t *testing.T) {
|
||||||
|
for name, call := range map[string]func(){
|
||||||
|
"atoi on an unvalidated arg": func() { atoi("nope") },
|
||||||
|
"atof on an unvalidated arg": func() { atof("nope") },
|
||||||
|
} {
|
||||||
|
mustPanic(t, name, call)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// mustPanic fails unless call panics, which is what separates a reported
|
||||||
|
// invariant break from a silently wrong value.
|
||||||
|
func mustPanic(t *testing.T, name string, call func()) {
|
||||||
|
t.Helper()
|
||||||
|
defer func() {
|
||||||
|
if recover() == nil {
|
||||||
|
t.Errorf("%s: no panic, want the invariant reported", name)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
call()
|
||||||
|
}
|
||||||
|
|||||||
@@ -103,6 +103,19 @@ func TestCalcTokenOperandsReadsOnlyACalc(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestCalcGuardsPanic pins the guards on calc's own invariants: checkCalc parsed
|
||||||
|
// the expression before prep sees it, and indexVars places every name calcVars
|
||||||
|
// read, so a break in either is reported rather than computed around.
|
||||||
|
func TestCalcGuardsPanic(t *testing.T) {
|
||||||
|
for name, call := range map[string]func(){
|
||||||
|
"prep on an expression that does not parse": func() { calcPrep([]string{"1 +"}) },
|
||||||
|
"an operand name never placed": func() { calcVar("n").eval(nil) },
|
||||||
|
"indexVars on a name it did not read": func() { indexVars(calcVar("n"), map[string]int{}) },
|
||||||
|
} {
|
||||||
|
mustPanic(t, name, call)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// --- one draw, one value: a calc operand reads the expansion's draw ---
|
// --- one draw, one value: a calc operand reads the expansion's draw ---
|
||||||
|
|
||||||
// TestCalcOperandReadsTheExpansionsDraw pins the one-draw rule for a calc operand.
|
// TestCalcOperandReadsTheExpansionsDraw pins the one-draw rule for a calc operand.
|
||||||
|
|||||||
+14
-1
@@ -120,6 +120,13 @@ func TestNewErrors(t *testing.T) {
|
|||||||
map[string]string{"a|b": `["1"]`},
|
map[string]string{"a|b": `["1"]`},
|
||||||
`category "a|b" contains "|"`,
|
`category "a|b" contains "|"`,
|
||||||
},
|
},
|
||||||
|
// An empty name is not a path segment, so List never offered it — while a
|
||||||
|
// bare {}, a trailing dot in Fake("a.") and a {..a.} reference all reached
|
||||||
|
// it. The engine accepted spellings it would never advertise.
|
||||||
|
"empty field name": {
|
||||||
|
map[string]string{"a": `{"format":"[{}]","":"VALUE"}`},
|
||||||
|
`field "" is empty`,
|
||||||
|
},
|
||||||
"folder name with a paren": {
|
"folder name with a paren": {
|
||||||
map[string]string{"a(b/cat": `["1"]`},
|
map[string]string{"a(b/cat": `["1"]`},
|
||||||
`folder "a(b" contains "("`,
|
`folder "a(b" contains "("`,
|
||||||
@@ -149,9 +156,15 @@ func TestNewErrors(t *testing.T) {
|
|||||||
map[string]string{"a": `{"format":"{..|..}"}`},
|
map[string]string{"a": `{"format":"{..|..}"}`},
|
||||||
"reference has no path",
|
"reference has no path",
|
||||||
},
|
},
|
||||||
|
// No field can be named "", so the token is told that rather than sent to
|
||||||
|
// name one — the fix "no field" points at is itself a load error.
|
||||||
"repeated empty arm": {
|
"repeated empty arm": {
|
||||||
map[string]string{"a": `{"format":"{|}"}`},
|
map[string]string{"a": `{"format":"{|}"}`},
|
||||||
`no field ""`,
|
"a name is never empty",
|
||||||
|
},
|
||||||
|
"bare empty token": {
|
||||||
|
map[string]string{"a": `{"format":"[{}]","x":["1"]}`},
|
||||||
|
"a name is never empty",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
for name, c := range rejected {
|
for name, c := range rejected {
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"math/rand/v2"
|
"math/rand/v2"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Fakes generates fake data from a loaded namespace tree. Create one with [New].
|
// Fakes generates fake data from a loaded namespace tree. Create one with [New].
|
||||||
@@ -93,14 +92,6 @@ func (f *Fakes) List() []string {
|
|||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
// addressable reports whether a name can be one segment of a dot path. A dot would
|
|
||||||
// split it into two segments and an empty name into none, so a path through such a
|
|
||||||
// name cannot be spelled — List must not offer one, and it must not count towards
|
|
||||||
// what a choice's variants share.
|
|
||||||
func addressable(name string) bool {
|
|
||||||
return name != "" && !strings.Contains(name, ".")
|
|
||||||
}
|
|
||||||
|
|
||||||
// paths lists the dot paths addressable from n, relative to it, where "" is n
|
// paths lists the dot paths addressable from n, relative to it, where "" is n
|
||||||
// itself. A group has no value of its own, so it contributes only its children's.
|
// itself. A group has no value of its own, so it contributes only its children's.
|
||||||
func paths(n node) []string {
|
func paths(n node) []string {
|
||||||
@@ -108,9 +99,6 @@ func paths(n node) []string {
|
|||||||
case *group:
|
case *group:
|
||||||
var out []string
|
var out []string
|
||||||
for _, name := range sortedNames(n.children) {
|
for _, name := range sortedNames(n.children) {
|
||||||
if !addressable(name) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
for _, p := range paths(n.children[name]) {
|
for _, p := range paths(n.children[name]) {
|
||||||
out = append(out, join(name, p))
|
out = append(out, join(name, p))
|
||||||
}
|
}
|
||||||
@@ -119,7 +107,7 @@ func paths(n node) []string {
|
|||||||
case *template:
|
case *template:
|
||||||
out := []string{""}
|
out := []string{""}
|
||||||
for _, name := range sortedNames(n.fields) {
|
for _, name := range sortedNames(n.fields) {
|
||||||
if isRef(name) || !addressable(name) { // a binding, or unreachable by path
|
if isRef(name) { // a binding, not a path segment
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
for _, p := range paths(n.fields[name]) {
|
for _, p := range paths(n.fields[name]) {
|
||||||
|
|||||||
@@ -266,6 +266,9 @@ var reservedList = strings.Join(strings.Split(reservedInName, ""), " ")
|
|||||||
// category or folder and a field go through it, so there is one answer to what a
|
// category or folder and a field go through it, so there is one answer to what a
|
||||||
// name may contain.
|
// name may contain.
|
||||||
func checkName(name string) error {
|
func checkName(name string) error {
|
||||||
|
if name == "" {
|
||||||
|
return fmt.Errorf("%q is empty, which is not a path segment, so List never offers it", name)
|
||||||
|
}
|
||||||
if i := strings.IndexAny(name, reservedInName); i >= 0 {
|
if i := strings.IndexAny(name, reservedInName); i >= 0 {
|
||||||
return fmt.Errorf("%q contains %q; a name may not use %s, which the dot path and {token} grammars reserve",
|
return fmt.Errorf("%q contains %q; a name may not use %s, which the dot path and {token} grammars reserve",
|
||||||
name, name[i:i+1], reservedList)
|
name, name[i:i+1], reservedList)
|
||||||
|
|||||||
+45
-21
@@ -65,10 +65,10 @@ func checkBoundLevelsHeld(root map[string]node) error {
|
|||||||
if isPath {
|
if isPath {
|
||||||
cover(t.fields[head], held)
|
cover(t.fields[head], held)
|
||||||
} else {
|
} else {
|
||||||
coverRendered(t.fields[head], held)
|
operandDraw(t.fields[head], held)
|
||||||
}
|
}
|
||||||
if len(held) == 0 {
|
if len(held) == 0 {
|
||||||
continue // a fixed string, which cannot disagree with itself
|
continue // an early out: a literal head holds nothing to reach
|
||||||
}
|
}
|
||||||
// One seen set across the edges: a node that cannot reach the level
|
// One seen set across the edges: a node that cannot reach the level
|
||||||
// cannot reach it by another route either, so it is walked once here.
|
// cannot reach it by another route either, so it is walked once here.
|
||||||
@@ -79,9 +79,9 @@ func checkBoundLevelsHeld(root map[string]node) error {
|
|||||||
}
|
}
|
||||||
if renders(e.to, held, seen) {
|
if renders(e.to, held, seen) {
|
||||||
if isPath {
|
if isPath {
|
||||||
return fmt.Errorf("%s: {%s} renders %q, which {%s} reads a path into; name the fields you want instead", path, e.label, head, reader)
|
return fmt.Errorf("%s: %s renders %q, which {%s} reads a path into; name the fields you want instead", path, e.reached(), head, reader)
|
||||||
}
|
}
|
||||||
return fmt.Errorf("%s: {%s} renders %q, which a {calc()} also reads; reach it one way so it is drawn once", path, e.label, head)
|
return fmt.Errorf("%s: %s renders %q, which a {calc()} also reads; reach it one way so it is drawn once", path, e.reached(), head)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -103,14 +103,17 @@ func cover(n node, into map[node]bool) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// coverRendered collects what one held draw of a calc operand answers for: what
|
// operandDraw collects what one held draw of a {calc()} operand answers for: the
|
||||||
// rendering it reaches, following the same edges expand does. An operand is only
|
// operand and what rendering it settles inside itself. A calc renders its operand
|
||||||
// ever rendered — checkNoOverlap rejects a path into one — so its draw fixes that
|
// whole, so that draw fixes every value the render produced, and a second route to
|
||||||
// value and nothing else. That is narrower than containment where a sibling the
|
// any of them disagrees with it.
|
||||||
// operand's format never renders cannot disagree with it, and wider where the
|
//
|
||||||
// operand renders through a reference, which is part of the value it produces.
|
// The walk stops at a {..path} edge, which is where the operand's own value ends
|
||||||
// Literals are left out for the reason cover leaves them out.
|
// and a shared source begins: two names referencing one category are two draws, the
|
||||||
func coverRendered(n node, into map[node]bool) {
|
// same rule {word} {word} follows. cover stops there too, by way of named, so both
|
||||||
|
// halves of the fence end at the same boundary. A literal is left out for the
|
||||||
|
// reason cover leaves one out.
|
||||||
|
func operandDraw(n node, into map[node]bool) {
|
||||||
if _, fixed := n.(literal); fixed {
|
if _, fixed := n.(literal); fixed {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -119,7 +122,10 @@ func coverRendered(n node, into map[node]bool) {
|
|||||||
}
|
}
|
||||||
into[n] = true
|
into[n] = true
|
||||||
for _, e := range renderEdges(n) {
|
for _, e := range renderEdges(n) {
|
||||||
coverRendered(e.to, into)
|
if isRef(e.label) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
operandDraw(e.to, into)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -267,11 +273,23 @@ func refTokens(format string) []string {
|
|||||||
return refs
|
return refs
|
||||||
}
|
}
|
||||||
|
|
||||||
// renderEdge is a child a node renders into, labelled by the token that reaches it
|
// renderEdge is a child a node renders into, labelled by what reaches it (a field
|
||||||
// (a field name, reference, or choice index) for a readable cycle report.
|
// name, reference, or choice index) for a readable cycle report. operand marks a
|
||||||
|
// label that is a {calc()} operand name rather than a token, so an error can name
|
||||||
|
// it the way the author wrote it.
|
||||||
type renderEdge struct {
|
type renderEdge struct {
|
||||||
to node
|
to node
|
||||||
label string
|
label string
|
||||||
|
operand bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// reached names an edge as the author spelled it, the vocabulary boundReaders uses
|
||||||
|
// for the sibling fence.
|
||||||
|
func (e renderEdge) reached() string {
|
||||||
|
if e.operand {
|
||||||
|
return fmt.Sprintf("calc operand %q", e.label)
|
||||||
|
}
|
||||||
|
return "{" + e.label + "}"
|
||||||
}
|
}
|
||||||
|
|
||||||
// renderEdges lists the children rendering n recurses into, mirroring expand: a
|
// renderEdges lists the children rendering n recurses into, mirroring expand: a
|
||||||
@@ -282,21 +300,27 @@ func renderEdges(n node) []renderEdge {
|
|||||||
case *choice:
|
case *choice:
|
||||||
es := make([]renderEdge, len(n.items))
|
es := make([]renderEdge, len(n.items))
|
||||||
for i, it := range n.items {
|
for i, it := range n.items {
|
||||||
es[i] = renderEdge{it, fmt.Sprintf("[%d]", i)}
|
es[i] = renderEdge{to: it, label: fmt.Sprintf("[%d]", i)}
|
||||||
}
|
}
|
||||||
return es
|
return es
|
||||||
case *template:
|
case *template:
|
||||||
var es []renderEdge
|
var es []renderEdge
|
||||||
for _, name := range append(fieldTokens(n.format), calcOperands(n.format)...) {
|
add := func(name string, operand bool) {
|
||||||
a := splitArm(name)
|
a := splitArm(name)
|
||||||
c, ok := n.fields[a.key]
|
c, ok := n.fields[a.key]
|
||||||
if !ok {
|
if !ok {
|
||||||
continue
|
return
|
||||||
}
|
}
|
||||||
for _, leaf := range pathLeaves(c, a.tail) {
|
for _, leaf := range pathLeaves(c, a.tail) {
|
||||||
es = append(es, renderEdge{leaf, name})
|
es = append(es, renderEdge{leaf, name, operand})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
for _, name := range fieldTokens(n.format) {
|
||||||
|
add(name, false)
|
||||||
|
}
|
||||||
|
for _, name := range calcOperands(n.format) {
|
||||||
|
add(name, true)
|
||||||
|
}
|
||||||
return es
|
return es
|
||||||
default:
|
default:
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -156,7 +156,8 @@ func expand(s *session, t *template) string {
|
|||||||
case 'f':
|
case 'f':
|
||||||
b.WriteString(readField(s, t, held, o.arms[s.IntN(len(o.arms))]))
|
b.WriteString(readField(s, t, held, o.arms[s.IntN(len(o.arms))]))
|
||||||
case 'b':
|
case 'b':
|
||||||
// Read in the order calcVars fixed, which is what op.operands holds.
|
// Read before the call, so the value a calc computes is the value the
|
||||||
|
// format showed. calcVars fixed the order op.operands holds.
|
||||||
var operands []string
|
var operands []string
|
||||||
if len(o.operands) > 0 {
|
if len(o.operands) > 0 {
|
||||||
operands = make([]string, len(o.operands))
|
operands = make([]string, len(o.operands))
|
||||||
|
|||||||
@@ -147,6 +147,9 @@ func checkTokens(format string, fields map[string]node) error {
|
|||||||
}
|
}
|
||||||
head, ok := fields[a.key]
|
head, ok := fields[a.key]
|
||||||
if !ok {
|
if !ok {
|
||||||
|
if a.key == "" {
|
||||||
|
return fmt.Errorf("token {%s}: a name is never empty, so this token can name no field", t.body)
|
||||||
|
}
|
||||||
if isOption(a.key) {
|
if isOption(a.key) {
|
||||||
return fmt.Errorf("token {%s}: %q is an option and can never be a field", t.body, a.key)
|
return fmt.Errorf("token {%s}: %q is an option and can never be a field", t.body, a.key)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user