244 lines
7.5 KiB
Go
244 lines
7.5 KiB
Go
package fejkdata
|
|
|
|
import (
|
|
"fmt"
|
|
"sort"
|
|
"strings"
|
|
)
|
|
|
|
// heldCheck rejects a format reading one held draw two ways, and every route to a held
|
|
// name except the readers of its draw. An expansion holds one draw of that name;
|
|
// anything else that renders it draws again, and the two disagree.
|
|
// docs/decisions.md#the-expansion-hold-and-the-renders-draws-are-two-fences
|
|
func heldCheck(path string, n node) error {
|
|
t, ok := n.(*template)
|
|
if !ok || len(t.compiled.held) == 0 {
|
|
return nil
|
|
}
|
|
if err := checkTemplateHold(t); err != nil {
|
|
return fmt.Errorf("%s: %w", t.site.label(path), err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func checkTemplateHold(t *template) error {
|
|
if err := checkNoOverlap(t.compiled); err != nil {
|
|
return err
|
|
}
|
|
if err := checkNoRepeatedRead(t.compiled); err != nil {
|
|
return err
|
|
}
|
|
readers := pathHeadReaders(t.compiled)
|
|
for _, name := range heldNames(t) {
|
|
if err := checkNameHeld(t, name, readers); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// heldNames lists a template's held names, operands' first, then paths', each
|
|
// in name order: a level read both ways is reported by the operand's fence.
|
|
func heldNames(t *template) []string {
|
|
names := make([]string, 0, len(t.compiled.held))
|
|
for name := range t.compiled.held {
|
|
names = append(names, name)
|
|
}
|
|
sort.Slice(names, func(i, j int) bool {
|
|
pi := t.compiled.held[names[i]].path != ""
|
|
pj := t.compiled.held[names[j]].path != ""
|
|
if pi != pj {
|
|
return !pi
|
|
}
|
|
return names[i] < names[j]
|
|
})
|
|
return names
|
|
}
|
|
|
|
// heldNodes is what the hold of name answers for: a path pins the levels it
|
|
// passes through and the leaf it lands on, an operand exactly the value its render
|
|
// produces.
|
|
func heldNodes(t *template, name string, readers []reader) map[node]bool {
|
|
held := map[node]bool{}
|
|
if t.compiled.held[name].path == "" {
|
|
operandDraw(t.head(name), held)
|
|
return held
|
|
}
|
|
for _, r := range readers {
|
|
if r.a.head == name {
|
|
cover(r.a.cover, held, false)
|
|
}
|
|
}
|
|
return held
|
|
}
|
|
|
|
// checkNameHeld rejects every route to what name's hold pins except the readers
|
|
// holding it. 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.
|
|
func checkNameHeld(t *template, name string, readers []reader) error {
|
|
held := heldNodes(t, name, readers)
|
|
if len(held) == 0 {
|
|
return nil // a fixed head holds nothing to reach
|
|
}
|
|
path := t.compiled.held[name].path
|
|
seen := map[node]bool{}
|
|
for _, e := range renderEdges(t) {
|
|
if e.read.head == name || !renders(e.to, held, seen) {
|
|
continue
|
|
}
|
|
if path != "" {
|
|
return fmt.Errorf("%s reaches %q by a second route, where {%s} holds it to one draw; name the fields you want instead", e.reached(), name, path)
|
|
}
|
|
call, operand := operandReader(t, name)
|
|
if e.operand != "" {
|
|
return fmt.Errorf("%s renders %q, which {%s} holds to one draw for this expansion; stop %s reaching %q", e.reached(), name, call, e.reached(), name)
|
|
}
|
|
return fmt.Errorf("%s renders %q, which {%s} holds to one draw for this expansion; drop %s, or write {%s} in this format to read that draw", e.reached(), name, call, e.reached(), operand)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// operandReader spells the call whose operand holds name, and the operand, as written.
|
|
func operandReader(t *template, name string) (call, operand string) {
|
|
for _, o := range t.compiled.ops {
|
|
for _, a := range o.operands {
|
|
if a.head == name && !a.isRefPath() {
|
|
return o.body, a.spelling
|
|
}
|
|
}
|
|
}
|
|
return "", ""
|
|
}
|
|
|
|
// cover collects a level and everything contained in it. A fixed string outside a
|
|
// choice is left out — it cannot disagree with itself — but inside one each
|
|
// variant carries its own, so there it counts.
|
|
func cover(n node, into map[node]bool, inChoice bool) {
|
|
if isFixed(n) && !inChoice {
|
|
return
|
|
}
|
|
into[n] = true
|
|
_, isChoice := n.(*choice)
|
|
for _, c := range contained(n) {
|
|
cover(c.node, into, inChoice || isChoice)
|
|
}
|
|
}
|
|
|
|
// operandDraw collects what one held draw of an operand answers for: the operand
|
|
// and what rendering it settles inside itself. The builtin renders its operand
|
|
// whole, so that draw fixes every value the render produced, and a second route to
|
|
// any of them disagrees with it.
|
|
//
|
|
// The walk stops at a reference edge, which is where the operand's own value ends
|
|
// and a shared source begins: two names referencing one category are two draws, the
|
|
// same rule {word} {word} follows.
|
|
func operandDraw(n node, into map[node]bool) {
|
|
if isFixed(n) {
|
|
return
|
|
}
|
|
if into[n] {
|
|
return
|
|
}
|
|
into[n] = true
|
|
for _, e := range renderEdges(n) {
|
|
if e.readsRef() {
|
|
continue
|
|
}
|
|
operandDraw(e.to, into)
|
|
}
|
|
}
|
|
|
|
// isFixed is a string that varies nothing: fixed text with no fields to read into.
|
|
func isFixed(n node) bool {
|
|
t, ok := n.(*template)
|
|
if !ok {
|
|
return false
|
|
}
|
|
_, fixed := t.fixedText()
|
|
return fixed && len(t.fields) == 0
|
|
}
|
|
|
|
// renders reports whether rendering n can reach anything in want, following the
|
|
// same edges expand does.
|
|
func renders(n node, want, seen map[node]bool) bool {
|
|
if want[n] {
|
|
return true
|
|
}
|
|
if seen[n] {
|
|
return false
|
|
}
|
|
seen[n] = true
|
|
for _, e := range renderEdges(n) {
|
|
if renders(e.to, want, seen) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// checkNoOverlap rejects a format that both renders a sibling level and reads a path
|
|
// into it — {p} beside {p.first}, {p.addr} beside {p.addr.city}. The path reads the
|
|
// level's held draw while rendering the level expands it afresh, so their values would
|
|
// disagree.
|
|
// docs/decisions.md#a-bare-reference-draws-each-time-a-reference-path-is-held
|
|
func checkNoOverlap(c formatOps) error {
|
|
names := pathHeadReaders(c)
|
|
// Stable over one format-order scan, so two readers of one name (a token and a
|
|
// calc operand both naming "p") are reported as the format writes them.
|
|
sort.SliceStable(names, func(i, j int) bool { return names[i].a.path < names[j].a.path })
|
|
for i, level := range names {
|
|
for _, path := range names[i+1:] {
|
|
if strings.HasPrefix(path.a.path, level.a.path+".") {
|
|
return fmt.Errorf("%s renders a level that {%s} reads a path into; name the fields you want instead", level.label, path.a.spelling)
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// reader is one way a format reaches a path head, and how to name it.
|
|
type reader struct {
|
|
a arm
|
|
label string
|
|
}
|
|
|
|
// pathHeadReaders lists every way a format reaches a sibling path head, in the order the
|
|
// format writes them. An operand renders its field, so it names a level exactly
|
|
// as a token does; one scan finds both, which is what puts them in one order.
|
|
func pathHeadReaders(c formatOps) []reader {
|
|
var names []reader
|
|
for _, o := range c.ops {
|
|
for _, a := range o.operands {
|
|
if c.held[a.head].path != "" {
|
|
names = append(names, reader{a, fmt.Sprintf("%s operand %q", o.fn, a.spelling)})
|
|
}
|
|
}
|
|
for _, a := range o.arms {
|
|
if c.held[a.head].path != "" {
|
|
names = append(names, reader{a, "token {" + a.spelling + "}"})
|
|
}
|
|
}
|
|
}
|
|
return names
|
|
}
|
|
|
|
// checkNoRepeatedRead rejects a bare token repeated on a held name: {w} {w} beside
|
|
// {uppercase(w)} would read one draw twice, where {w} {w} alone draws twice. The
|
|
// error names the single-token spelling.
|
|
// docs/decisions.md#a-bare-reference-draws-each-time-a-reference-path-is-held
|
|
func checkNoRepeatedRead(c formatOps) error {
|
|
count := map[string]int{}
|
|
for _, o := range c.ops {
|
|
for _, a := range o.arms {
|
|
h, held := c.held[a.head]
|
|
if len(a.tail) > 0 || !held {
|
|
continue
|
|
}
|
|
if count[a.head]++; count[a.head] > 1 {
|
|
return fmt.Errorf("token {%s} is repeated, and %s holds %q to one draw per expansion; write {%s} once", a.spelling, h.holder, a.head, a.spelling)
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|