Answer architecture round 1: one categoryPath, the walk's label named label, checkAncestors split from proveParent, and file items 115 and 116

This commit is contained in:
2026-10-05 21:10:49 +02:00
committed by Lillem4n
parent 8ffaa31e39
commit 54cb52b483
13 changed files with 106 additions and 84 deletions
+2 -2
View File
@@ -23,13 +23,13 @@ func checkCalcFields(args []string, fields map[string]node) error {
} }
// checkCalcNames holds each calc of t reading a name to the checks checkCalcFields makes of a field. // checkCalcNames holds each calc of t reading a name to the checks checkCalcFields makes of a field.
func checkCalcNames(path string, t *template) error { func checkCalcNames(label string, t *template) error {
for _, o := range t.compiled.ops { for _, o := range t.compiled.ops {
if o.Fn != builtinfunc.CalcName || !slices.ContainsFunc(o.operands, func(a arm) bool { return a.kind == namedRead }) { if o.Fn != builtinfunc.CalcName || !slices.ContainsFunc(o.operands, func(a arm) bool { return a.kind == namedRead }) {
continue continue
} }
if err := checkOperands(o.Args[0], builtinfunc.ParsedCalc(o.Args[0]), operandNodes(o)); err != nil { if err := checkOperands(o.Args[0], builtinfunc.ParsedCalc(o.Args[0]), operandNodes(o)); err != nil {
return fmt.Errorf("%s: token {%s}: %w", path, o.Body, err) return fmt.Errorf("%s: token {%s}: %w", label, o.Body, err)
} }
} }
return nil return nil
+8 -5
View File
@@ -68,7 +68,7 @@ func mergeChildren(dst, src map[string]node) {
// nodeScope is the set of nodes one validation pass covers: the categories one load // nodeScope is the set of nodes one validation pass covers: the categories one load
// binds, or a single inline node. // binds, or a single inline node.
type nodeScope func(fn func(path string, n node) error) error type nodeScope func(fn func(label string, n node) error) error
// categorySite is a loaded category and where it sits: the folder holding it, that // categorySite is a loaded category and where it sits: the folder holding it, that
// folder's path, and its own dot path. // folder's path, and its own dot path.
@@ -97,11 +97,14 @@ func categorySites(root *folder) []categorySite {
} }
func siteIn(dir []string, in *folder, name string) categorySite { func siteIn(dir []string, in *folder, name string) categorySite {
return categorySite{dir: dir, path: join(strings.Join(dir, "."), name), in: in, n: in.children[name]} return categorySite{dir: dir, path: categoryPath(dir, name), in: in, n: in.children[name]}
} }
// categoryPath is the dot path of the category name in the folder dir.
func categoryPath(dir []string, name string) string { return join(strings.Join(dir, "."), name) }
func sitesScope(sites []categorySite) nodeScope { func sitesScope(sites []categorySite) nodeScope {
return func(fn func(path string, n node) error) error { return func(fn func(label string, n node) error) error {
for _, s := range sites { for _, s := range sites {
if err := eachNode(s.n, s.path, fn); err != nil { if err := eachNode(s.n, s.path, fn); err != nil {
return err return err
@@ -112,7 +115,7 @@ func sitesScope(sites []categorySite) nodeScope {
} }
func inlineScope(n node, label string) nodeScope { func inlineScope(n node, label string) nodeScope {
return func(fn func(path string, m node) error) error { return eachNode(n, label, fn) } return func(fn func(label string, m node) error) error { return eachNode(n, label, fn) }
} }
// binding is one scope's way through bind, the one load pipeline; its fields are where // binding is one scope's way through bind, the one load pipeline; its fields are where
@@ -159,7 +162,7 @@ func (b binding) bind() error {
// first. // first.
func checkNodeFences(s nodeScope) error { func checkNodeFences(s nodeScope) error {
mem := renderCounts{} mem := renderCounts{}
if err := s(func(path string, n node) error { return repeatCheck(path, n, mem) }); err != nil { if err := s(func(label string, n node) error { return repeatCheck(label, n, mem) }); err != nil {
return err return err
} }
return s((&valueProof{}).checkDatatype) return s((&valueProof{}).checkDatatype)
+5 -5
View File
@@ -59,8 +59,8 @@ func datatypeOf(m map[string]any, pos position) (DataType, error) {
// after the columns its items read, so a column read is named before its readers. // after the columns its items read, so a column read is named before its readers.
func checkColumns(s nodeScope) error { func checkColumns(s nodeScope) error {
checked := map[node]bool{} checked := map[node]bool{}
var check func(path, name string, column node) error var check func(label, name string, column node) error
check = func(path, name string, column node) error { check = func(label, name string, column node) error {
if checked[column] { if checked[column] {
return nil return nil
} }
@@ -79,18 +79,18 @@ func checkColumns(s nodeScope) error {
first := columnDatatype(column) first := columnDatatype(column)
for _, it := range items[1:] { for _, it := range items[1:] {
if d := itemDatatype(it); d != first { if d := itemDatatype(it); d != first {
return fmt.Errorf("%s: field %q: %w", path, name, disagreement(items[0], first, it, d)) return fmt.Errorf("%s: field %q: %w", label, name, disagreement(items[0], first, it, d))
} }
} }
return nil return nil
} }
return s(func(path string, n node) error { return s(func(label string, n node) error {
t, ok := n.(*template) t, ok := n.(*template)
if !ok || !t.isRecord { if !ok || !t.isRecord {
return nil return nil
} }
for _, name := range sortedNames(t.fields) { for _, name := range sortedNames(t.fields) {
if err := check(path, name, t.fields[name]); err != nil { if err := check(label, name, t.fields[name]); err != nil {
return err return err
} }
} }
+20 -19
View File
@@ -8,28 +8,29 @@ import (
"github.com/larvit/fejkdata/internal/invariant" "github.com/larvit/fejkdata/internal/invariant"
) )
// eachNode visits n and every node contained within it once, passing the dot path // eachNode visits n and every node contained within it once, passing the label naming
// that reaches each, followed by a table cell's line. It never crosses a reference edge — a {/path} reference is // each in an error: the dot path reaching it, and a table cell's line after it. It never
// skipped — so a single inline node is walked on its own. // crosses a reference edge — a {/path} reference is skipped — so a single inline node is
func eachNode(n node, path string, fn func(path string, n node) error) error { // walked on its own.
func eachNode(n node, label string, fn func(label string, n node) error) error {
seen := map[node]bool{} seen := map[node]bool{}
var visit func(string, node) error var visit func(string, node) error
visit = func(path string, m node) error { visit = func(label string, m node) error {
if m == nil || seen[m] { if m == nil || seen[m] {
return nil return nil
} }
seen[m] = true seen[m] = true
if err := fn(path, m); err != nil { if err := fn(label, m); err != nil {
return err return err
} }
for _, c := range contained(m) { for _, c := range contained(m) {
if err := visit(c.labelUnder(path), c.node); err != nil { if err := visit(c.labelIn(label), c.node); err != nil {
return err return err
} }
} }
return nil return nil
} }
return visit(path, n) return visit(label, n)
} }
// namedNode is a contained child and the segment reaching it; a choice's items carry // namedNode is a contained child and the segment reaching it; a choice's items carry
@@ -40,12 +41,12 @@ type namedNode struct {
line int // a table cell's line in its rows file, else 0 line int // a table cell's line in its rows file, else 0
} }
// labelUnder is the path naming c in an error, a cell by its line. // labelIn is c's label inside the node labelled label.
func (c namedNode) labelUnder(path string) string { func (c namedNode) labelIn(label string) string {
if c.line > 0 { if c.line > 0 {
return fmt.Sprintf("%s, line %d", path, c.line) return fmt.Sprintf("%s, line %d", label, c.line)
} }
return join(path, c.name) return join(label, c.name)
} }
func contained(n node) []namedNode { func contained(n node) []namedNode {
@@ -164,9 +165,9 @@ func (m renderCounts) renderCount(n node) int {
// repeatCheck bounds the renders a repeat multiplies to along any root-to-leaf // repeatCheck bounds the renders a repeat multiplies to along any root-to-leaf
// path, so nested repeats cannot build what one repeat may not. // path, so nested repeats cannot build what one repeat may not.
// docs/decisions.md#the-repeat-cap-bounds-renders-not-bytes // docs/decisions.md#the-repeat-cap-bounds-renders-not-bytes
func repeatCheck(path string, n node, mem renderCounts) error { func repeatCheck(label string, n node, mem renderCounts) error {
if t, ok := n.(*template); ok && t.repeat > 1 && mem.renderCount(n) > MaxRepeat { if t, ok := n.(*template); ok && t.repeat > 1 && mem.renderCount(n) > MaxRepeat {
return fmt.Errorf("%s: repeat %d multiplies to %d renders along one path, above the maximum %d", path, t.repeat, mem.renderCount(n), MaxRepeat) return fmt.Errorf("%s: repeat %d multiplies to %d renders along one path, above the maximum %d", label, t.repeat, mem.renderCount(n), MaxRepeat)
} }
return nil return nil
} }
@@ -191,25 +192,25 @@ func checkNoCycles(s nodeScope) error {
return nil return nil
}) })
color := map[node]int{} color := map[node]int{}
var visit func(n node, path string) error var visit func(n node, label string) error
visit = func(n node, path string) error { visit = func(n node, label string) error {
if !inScope[n] { if !inScope[n] {
return nil // loaded before this scope and proven then, so it never reaches back into it return nil // loaded before this scope and proven then, so it never reaches back into it
} }
switch color[n] { switch color[n] {
case grey: case grey:
return fmt.Errorf("reference cycle: %s", path) return fmt.Errorf("reference cycle: %s", label)
case black: case black:
return nil return nil
} }
color[n] = grey color[n] = grey
for _, e := range renderEdges(n) { for _, e := range renderEdges(n) {
if err := visit(e.to, path+" -> "+e.label); err != nil { if err := visit(e.to, label+" -> "+e.label); err != nil {
return err return err
} }
} }
color[n] = black color[n] = black
return nil return nil
} }
return s(func(path string, n node) error { return visit(n, path) }) return s(func(label string, n node) error { return visit(n, label) })
} }
+7 -7
View File
@@ -104,7 +104,7 @@ func bindInline(root *folder, n node, label string, typedByGo bool) error {
// refuseFolderRefs refuses each reference in scope not written from the root, {/x}: an // refuseFolderRefs refuses each reference in scope not written from the root, {/x}: an
// inline node sits in no folder. // inline node sits in no folder.
func refuseFolderRefs(scope nodeScope) error { func refuseFolderRefs(scope nodeScope) error {
return scope(func(path string, m node) error { return scope(func(label string, m node) error {
t, ok := m.(*template) t, ok := m.(*template)
if !ok { if !ok {
return nil return nil
@@ -112,10 +112,10 @@ func refuseFolderRefs(scope nodeScope) error {
for _, name := range refTokens(t.tokens) { for _, name := range refTokens(t.tokens) {
sigil, rest, err := grammar.RefShape(name) sigil, rest, err := grammar.RefShape(name)
if err != nil { if err != nil {
return fmt.Errorf("%s: reference {%s}: %w", path, name, err) return fmt.Errorf("%s: reference {%s}: %w", label, name, err)
} }
if sigil != "/" { if sigil != "/" {
return fmt.Errorf("%s: reference {%s}: an inline template has no folder; write {/%s}", path, name, rest) return fmt.Errorf("%s: reference {%s}: an inline template has no folder; write {/%s}", label, name, rest)
} }
} }
return nil return nil
@@ -125,18 +125,18 @@ func refuseFolderRefs(scope nodeScope) error {
// linkNodeRefs binds the references in an inline node's templates against the // linkNodeRefs binds the references in an inline node's templates against the
// loaded tree. // loaded tree.
func linkNodeRefs(scope nodeScope, root map[string]node) error { func linkNodeRefs(scope nodeScope, root map[string]node) error {
if err := scope(func(path string, m node) error { if err := scope(func(label string, m node) error {
if t, ok := m.(*template); ok { if t, ok := m.(*template); ok {
return linkTemplate(nil, path, "", t, root) return linkTemplate(nil, label, "", t, root)
} }
return nil return nil
}); err != nil { }); err != nil {
return err return err
} }
for _, pass := range linkPasses { for _, pass := range linkPasses {
if err := scope(func(path string, m node) error { if err := scope(func(label string, m node) error {
if t, ok := m.(*template); ok { if t, ok := m.(*template); ok {
return pass(path, t) return pass(label, t)
} }
return nil return nil
}); err != nil { }); err != nil {
+3 -3
View File
@@ -255,14 +255,14 @@ func namedReads(t *template, fn func(o *op, a *arm) error) error {
// linkPasses run in order once every template is linked, each over every template before the next // linkPasses run in order once every template is linked, each over every template before the next
// starts: a pass reads what the one before filled in. // starts: a pass reads what the one before filled in.
var linkPasses = []func(path string, t *template) error{linkNames, linkColumnRead, checkNameReads, checkCalcNames} var linkPasses = []func(label string, t *template) error{linkNames, linkColumnRead, checkNameReads, checkCalcNames}
// linkNames compiles t's reads of a name as paths from the head its binding resolved to, once every // linkNames compiles t's reads of a name as paths from the head its binding resolved to, once every
// template is linked, so each binder has resolved what it binds. // template is linked, so each binder has resolved what it binds.
func linkNames(path string, t *template) error { func linkNames(label string, t *template) error {
return namedReads(t, func(o *op, a *arm) error { return namedReads(t, func(o *op, a *arm) error {
if err := linkName(t, a); err != nil { if err := linkName(t, a); err != nil {
return fmt.Errorf("%s: token {%s}: %w", path, o.Body, err) return fmt.Errorf("%s: token {%s}: %w", label, o.Body, err)
} }
return nil return nil
}) })
+4 -4
View File
@@ -11,22 +11,22 @@ import (
// checkNameReads refuses each binding of t that checkUses refuses, a read of a name inside the // checkNameReads refuses each binding of t that checkUses refuses, a read of a name inside the
// field bound to it, and a read {n} beside {n.w} where n's pick renders w twice: the pick keeps one // field bound to it, and a read {n} beside {n.w} where n's pick renders w twice: the pick keeps one
// draw of w. // draw of w.
func checkNameReads(path string, t *template) error { func checkNameReads(label string, t *template) error {
for _, tok := range t.tokens { for _, tok := range t.tokens {
if tok.Kind != grammar.NameBind { if tok.Kind != grammar.NameBind {
continue continue
} }
if err := t.nameScope.bindings[tok.Bound].checkUses(); err != nil { if err := t.nameScope.bindings[tok.Bound].checkUses(); err != nil {
return fmt.Errorf("%s: token {%s}: %w", path, tok.Body, err) return fmt.Errorf("%s: token {%s}: %w", label, tok.Body, err)
} }
} }
return namedReads(t, func(o *op, a *arm) error { return namedReads(t, func(o *op, a *arm) error {
if a.named.bindsField() && rendersInside(compilePath(a.named.head, a.named.tail).leaves, t) { if a.named.bindsField() && rendersInside(compilePath(a.named.head, a.named.tail).leaves, t) {
return fmt.Errorf("%s: token {%s}: name %q is read inside %q, the field bound to it; read the name outside that field", path, o.Body, a.named.name, a.named.ref) return fmt.Errorf("%s: token {%s}: name %q is read inside %q, the field bound to it; read the name outside that field", label, o.Body, a.named.name, a.named.ref)
} }
for _, leaf := range a.leaves { for _, leaf := range a.leaves {
if err := a.named.checkOnce(a.spelling, leaf, a.path); err != nil { if err := a.named.checkOnce(a.spelling, leaf, a.path); err != nil {
return fmt.Errorf("%s: token {%s}: %w", path, o.Body, err) return fmt.Errorf("%s: token {%s}: %w", label, o.Body, err)
} }
} }
return nil return nil
+1 -1
View File
@@ -101,7 +101,7 @@ func compile(v any) (node, error) {
func compileCategory(c datafiles.Category) (node, error) { func compileCategory(c datafiles.Category) (node, error) {
if m, ok := c.JSON.(map[string]any); ok { if m, ok := c.JSON.(map[string]any); ok {
if _, isTable := m["rows"]; isTable { if _, isTable := m["rows"]; isTable {
return compileTable(m, c.Name, join(strings.Join(c.Folders, "."), c.Name), c.ReadRows) return compileTable(m, c.Folders, c.Name, c.ReadRows)
} }
} }
if items, ok := c.JSON.([]any); ok { if items, ok := c.JSON.([]any); ok {
+14 -14
View File
@@ -39,13 +39,13 @@ func refSegments(name string, folder []string) ([]string, error) {
// so a reference sees the override-resolved tree. A reference's head binds into // so a reference sees the override-resolved tree. A reference's head binds into
// refHeads under its root path and its tail reads like a sibling path. // refHeads under its root path and its tail reads like a sibling path.
func linkRefs(sites []categorySite, root map[string]node) error { func linkRefs(sites []categorySite, root map[string]node) error {
if err := eachTemplate(sites, func(s categorySite, path string, t *template) error { if err := eachTemplate(sites, func(s categorySite, label string, t *template) error {
return linkTemplate(s.dir, path, s.path, t, root) return linkTemplate(s.dir, label, s.path, t, root)
}); err != nil { }); err != nil {
return err return err
} }
for _, pass := range linkPasses { for _, pass := range linkPasses {
if err := eachTemplate(sites, func(_ categorySite, path string, t *template) error { return pass(path, t) }); err != nil { if err := eachTemplate(sites, func(_ categorySite, label string, t *template) error { return pass(label, t) }); err != nil {
return err return err
} }
} }
@@ -54,8 +54,8 @@ func linkRefs(sites []categorySite, root map[string]node) error {
// linkTemplate resolves t's references in category, "" for an inline template, and // linkTemplate resolves t's references in category, "" for an inline template, and
// compiles its format against them. // compiles its format against them.
func linkTemplate(folder []string, path, category string, t *template, root map[string]node) error { func linkTemplate(folder []string, label, category string, t *template, root map[string]node) error {
link, err := t.resolveLink(folder, path, category, root) link, err := t.resolveLink(folder, label, category, root)
if err != nil { if err != nil {
return err return err
} }
@@ -92,7 +92,7 @@ func compileArm(t *template, a *arm) {
// resolveLink binds every reference t reads, refusing one to t's own category: // resolveLink binds every reference t reads, refusing one to t's own category:
// docs/decisions.md#a-category-never-references-itself-and-a-records-fences-run-at-load // docs/decisions.md#a-category-never-references-itself-and-a-records-fences-run-at-load
func (t *template) resolveLink(folder []string, path, category string, root map[string]node) (templateLink, error) { func (t *template) resolveLink(folder []string, label, category string, root map[string]node) (templateLink, error) {
var link templateLink var link templateLink
names := refTokens(t.tokens) names := refTokens(t.tokens)
if len(names) == 0 { if len(names) == 0 {
@@ -103,18 +103,18 @@ func (t *template) resolveLink(folder []string, path, category string, root map[
for _, name := range names { for _, name := range names {
segments, err := refSegments(name, folder) segments, err := refSegments(name, folder)
if err != nil { if err != nil {
return link, fmt.Errorf("%s: reference {%s}: %w", path, name, err) return link, fmt.Errorf("%s: reference {%s}: %w", label, name, err)
} }
categorySegs, target, tail, err := resolveCategory(root, segments) categorySegs, target, tail, err := resolveCategory(root, segments)
if err != nil { if err != nil {
return link, fmt.Errorf("%s: reference {%s}: %w", path, name, err) return link, fmt.Errorf("%s: reference {%s}: %w", label, name, err)
} }
head := "/" + strings.Join(categorySegs, ".") head := "/" + strings.Join(categorySegs, ".")
if category != "" && head == "/"+category { if category != "" && head == "/"+category {
return link, fmt.Errorf("%s: reference {%s}: names the category it sits in; read a sibling field as a path, or move the shared value into its own category and reference that", path, name) return link, fmt.Errorf("%s: reference {%s}: names the category it sits in; read a sibling field as a path, or move the shared value into its own category and reference that", label, name)
} }
if err := checkPathResolves(target, tail, head); err != nil { if err := checkPathResolves(target, tail, head); err != nil {
return link, fmt.Errorf("%s: reference {%s}: %w", path, name, err) return link, fmt.Errorf("%s: reference {%s}: %w", label, name, err)
} }
link.refHeads[head] = target link.refHeads[head] = target
link.refs[name] = refBinding{head, tail} link.refs[name] = refBinding{head, tail}
@@ -160,12 +160,12 @@ func linkColumnRead(_ string, t *template) error {
} }
// eachTemplate calls fn once per template of the categories, with the category it sits // eachTemplate calls fn once per template of the categories, with the category it sits
// in and the dot path reaching it. // in and eachNode's label for it.
func eachTemplate(sites []categorySite, fn func(s categorySite, path string, t *template) error) error { func eachTemplate(sites []categorySite, fn func(s categorySite, label string, t *template) error) error {
for _, s := range sites { for _, s := range sites {
if err := eachNode(s.n, s.path, func(path string, n node) error { if err := eachNode(s.n, s.path, func(label string, n node) error {
if t, isTemplate := n.(*template); isTemplate { if t, isTemplate := n.(*template); isTemplate {
return fn(s, path, t) return fn(s, label, t)
} }
return nil return nil
}); err != nil { }); err != nil {
+1 -1
View File
@@ -101,7 +101,7 @@ func loadShipped(root *folder, wanted []unloadedCategory) {
} }
site, err := u.load() site, err := u.load()
if err != nil { if err != nil {
panic(invariant.Broken("shipped %s: %v; after a change under data/, regenerate shippedindex.go", join(strings.Join(u.dir, "."), u.name), err)) panic(invariant.Broken("shipped %s: %v; after a change under data/, regenerate shippedindex.go", categoryPath(u.dir, u.name), err))
} }
sites = append(sites, site) sites = append(sites, site)
queue = append(queue, unloadedReads(root, u.dir, sitesScope([]categorySite{site}))...) queue = append(queue, unloadedReads(root, u.dir, sitesScope([]categorySite{site}))...)
+27 -19
View File
@@ -85,7 +85,7 @@ func isTableOption(name string) bool {
// token arm. // token arm.
const inSelector = `[]{}"|` const inSelector = `[]{}"|`
func compileTable(m map[string]any, segment, path string, readRows func(file string) (string, error)) (*table, error) { func compileTable(m map[string]any, dir []string, name string, readRows func(file string) (string, error)) (*table, error) {
o, err := readTableOptions(m) o, err := readTableOptions(m)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -94,7 +94,7 @@ func compileTable(m map[string]any, segment, path string, readRows func(file str
if err != nil { if err != nil {
return nil, err return nil, err
} }
t := &table{segment: segment, path: path, file: o.rows, keyIndex: -1, nameIndex: -1, weightIndex: -1, parentIndex: -1} t := &table{segment: name, path: categoryPath(dir, name), file: o.rows, keyIndex: -1, nameIndex: -1, weightIndex: -1, parentIndex: -1}
if err := t.parseRows(data); err != nil { if err := t.parseRows(data); err != nil {
return nil, fmt.Errorf("%s: %w", o.rows, err) return nil, fmt.Errorf("%s: %w", o.rows, err)
} }
@@ -404,8 +404,7 @@ func linkTables(sites []categorySite) error {
} }
// proveParent is the table among siblings that t's link column names, proved: it has a key, // proveParent is the table among siblings that t's link column names, proved: it has a key,
// every link cell is one, every row of it is linked to, no chain of parents closes, and no // every link cell is one, every row of it is linked to, and its ancestors pass checkAncestors.
// ancestor has a column named like t.
// docs/decisions.md#a-parent-row-with-no-child-row-is-a-load-error // docs/decisions.md#a-parent-row-with-no-child-row-is-a-load-error
func (t *table) proveParent(siblings map[string]node) (*table, error) { func (t *table) proveParent(siblings map[string]node) (*table, error) {
name := t.header[t.parentIndex] name := t.header[t.parentIndex]
@@ -418,21 +417,8 @@ func (t *table) proveParent(siblings map[string]node) (*table, error) {
case p.keyIndex < 0: case p.keyIndex < 0:
return nil, fmt.Errorf("parent %q has no key column to link to", name) return nil, fmt.Errorf("parent %q has no key column to link to", name)
} }
var ancestors []*table if err := t.checkAncestors(p, siblings); err != nil {
for q, seen := p, map[*table]bool{t: true}; q != nil; q, _ = siblings[q.header[q.parentIndex]].(*table) { return nil, err
if seen[q] {
return nil, fmt.Errorf("parent cycle: %s reaches itself through its parents", q.segment)
}
seen[q] = true
ancestors = append(ancestors, q)
if q.parentIndex < 0 {
break
}
}
for _, q := range ancestors {
if _, clash := q.col[t.segment]; clash {
return nil, fmt.Errorf("%q is named like a column of %q, its ancestor, so %s.%s could read either; rename one", t.segment, q.segment, q.segment, t.segment)
}
} }
linked := make(map[string]bool, p.rowCount()) linked := make(map[string]bool, p.rowCount())
for r := 0; r < t.rowCount(); r++ { for r := 0; r < t.rowCount(); r++ {
@@ -450,6 +436,28 @@ func (t *table) proveParent(siblings map[string]node) (*table, error) {
return p, nil return p, nil
} }
// checkAncestors proves the chain of parents from p, t's parent, closes nowhere, and holds no
// column named like t.
func (t *table) checkAncestors(p *table, siblings map[string]node) error {
var ancestors []*table
for q, seen := p, map[*table]bool{t: true}; q != nil; q, _ = siblings[q.header[q.parentIndex]].(*table) {
if seen[q] {
return fmt.Errorf("parent cycle: %s reaches itself through its parents", q.segment)
}
seen[q] = true
ancestors = append(ancestors, q)
if q.parentIndex < 0 {
break
}
}
for _, q := range ancestors {
if _, clash := q.col[t.segment]; clash {
return fmt.Errorf("%q is named like a column of %q, its ancestor, so %s.%s could read either; rename one", t.segment, q.segment, q.segment, t.segment)
}
}
return nil
}
func (t *table) builtLookup() *rowLookup { func (t *table) builtLookup() *rowLookup {
t.once.Do(func() { t.once.Do(func() {
if t.nameIndex >= 0 { if t.nameIndex >= 0 {
+11 -1
View File
@@ -6,7 +6,7 @@
`Bar = 9` `Bar = 9`
`Next ID = 115` `Next ID = 117`
| Goal | W | | Goal | W |
|---|---| |---|---|
@@ -118,6 +118,7 @@
| 13 | 0.3.0 | defect | **Place xlsx cells by their `r` reference in `data-import/xlsx.py`.** | 2 | 2 | 3 | 6 | 4 | 13.7 | | 13 | 0.3.0 | defect | **Place xlsx cells by their `r` reference in `data-import/xlsx.py`.** | 2 | 2 | 3 | 6 | 4 | 13.7 |
| 39 | 0.3.0 | | **Stop the `columnKinds` suggestion naming a field's own kind or a narrower one.** | 1 | 2 | 3 | 6 | 6 | 13.5 | | 39 | 0.3.0 | | **Stop the `columnKinds` suggestion naming a field's own kind or a narrower one.** | 1 | 2 | 3 | 6 | 6 | 13.5 |
| 51 | 0.3.0 | | **Spell a table one way across the errors that name it.** | 1 | 2 | 3 | 6 | 6 | 13.5 | | 51 | 0.3.0 | | **Spell a table one way across the errors that name it.** | 1 | 2 | 3 | 6 | 6 | 13.5 |
| 115 | 0.3.0 | | **Spell a rows file's line one way in every error, naming the file: today a cell reads `g.t.b, line 3`, a cell check `t.tsv: line 3, b` and a link `t.tsv line 3`.** | 1 | 2 | 3 | 6 | 6 | 13.5 |
| 10 | 0.3.0 | | **Test that every node kind reaches each switch over node kinds.** | 1 | 3 | 2 | 6 | 3.2 | 13.1 | | 10 | 0.3.0 | | **Test that every node kind reaches each switch over node kinds.** | 1 | 3 | 2 | 6 | 3.2 | 13.1 |
| 11 | 0.3.0 | | **Test that every `geo` tree holds the five table names and the `address` columns that port across countries.** | 1 | 2 | 3 | 5 | 4 | 13.0 | | 11 | 0.3.0 | | **Test that every `geo` tree holds the five table names and the `address` columns that port across countries.** | 1 | 2 | 3 | 5 | 4 | 13.0 |
| 35 | 0.3.0 | | **Add `{base64url(n)}` for JWT shapes.** | 1 | 2 | 4 | 5 | 8 | 13.0 | | 35 | 0.3.0 | | **Add `{base64url(n)}` for JWT shapes.** | 1 | 2 | 4 | 5 | 8 | 13.0 |
@@ -132,6 +133,7 @@
| 72 | 0.3.0 | principle | **Fail the merge gate when test coverage falls below the last recorded figure.** | 2 | 3 | 2 | 5 | 3 | 10.2 | | 72 | 0.3.0 | principle | **Fail the merge gate when test coverage falls below the last recorded figure.** | 2 | 3 | 2 | 5 | 3 | 10.2 |
| 43 | 0.3.0 | | **Call `templateError` a compile failure in its doc.** | 1 | 1 | 1 | 5 | 3.3 | 9.8 | | 43 | 0.3.0 | | **Call `templateError` a compile failure in its doc.** | 1 | 1 | 1 | 5 | 3.3 | 9.8 |
| 95 | 0.3.0 | principle | **Hold a struct's columns and the field each fills in one slice.** | 1 | 2 | 1 | 5 | 3.2 | 9.5 | | 95 | 0.3.0 | principle | **Hold a struct's columns and the field each fills in one slice.** | 1 | 2 | 1 | 5 | 3.2 | 9.5 |
| 116 | 0.3.0 | | **Turn a row index into its rows file's line in one function, where eleven sites add 2 today.** | 1 | 2 | 1 | 5 | 3.2 | 9.5 |
| 19 | 0.3.0 | | **Fill the 398 Swedish localities weighted 200 from SCB småorter.** | 2 | 4 | 4 | 6 | 14 | 9.2 | | 19 | 0.3.0 | | **Fill the 398 Swedish localities weighted 200 from SCB småorter.** | 2 | 4 | 4 | 6 | 14 | 9.2 |
| 56 | 0.3.0 | defect | **Build on a manual run of `test.yml`.** | 1 | 1 | 1 | 4 | 1 | 8.8 | | 56 | 0.3.0 | defect | **Build on a manual run of `test.yml`.** | 1 | 1 | 1 | 4 | 1 | 8.8 |
| 109 | 0.3.0 | principle | **Indent every script under `data-import/` with hard tabs, as `AGENTS.md` requires and `release-tooling/` does.** | 2 | 4 | 1 | 4 | 3 | 6.2 | | 109 | 0.3.0 | principle | **Indent every script under `data-import/` with hard tabs, as `AGENTS.md` requires and `release-tooling/` does.** | 2 | 4 | 1 | 4 | 3 | 6.2 |
@@ -421,6 +423,10 @@ An `int64` or `float64` field unproven in range is told to become itself, and a
`pinRow` names `t.path` and `mustRow` panics with `t.category`, so one table is `territory` and `misc.territory`, and the short spelling names no file where two folders hold that name. `pinRow` names `t.path` and `mustRow` panics with `t.category`, so one table is `territory` and `misc.territory`, and the short spelling names no file where two folders hold that name.
### 115. Spell a rows file's line one way in every error, naming the file: today a cell reads `g.t.b, line 3`, a cell check `t.tsv: line 3, b` and a link `t.tsv line 3`.
The cell's label names the category and column but not the TSV the line is in.
### 10. Test that every node kind reaches each switch over node kinds. ### 10. Test that every node kind reaches each switch over node kinds.
Today nine, from `render` to `columnItems`, must agree, and only `render` and `renderEdges` say so. Today nine, from `render` to `columnItems`, must agree, and only `render` and `renderEdges` say so.
@@ -461,6 +467,10 @@ Its doc says render failure, and it wraps what `NewTemplate` and `NewRecordTempl
`structShape.columns` and `structShape.fieldIndexes` pair up by index alone, against the technical principle "one owner per value". `structShape.columns` and `structShape.fieldIndexes` pair up by index alone, against the technical principle "one owner per value".
### 116. Turn a row index into its rows file's line in one function, where eleven sites add 2 today.
`namedNode`'s cell line in `graph.go` and ten sites in `table.go` each add the header line and the one-based count. Item 102 moves the rows into `internal/rows`; the function goes with them.
### 56. Build on a manual run of `test.yml`. ### 56. Build on a manual run of `test.yml`.
`workflow_dispatch` leaves `github.event.before` empty, so the diff compares `HEAD` with itself and skips `docker build`. `workflow_dispatch` leaves `github.event.before` empty, so the diff compares `HEAD` with itself and skips `docker build`.
+3 -3
View File
@@ -19,16 +19,16 @@ type valueProof struct {
// checkDatatype rejects a typed column item some render of which is not text of its datatype, // checkDatatype rejects a typed column item some render of which is not text of its datatype,
// and one restating the datatype of the column it is. // and one restating the datatype of the column it is.
// docs/decisions.md#a-column-that-only-reads-one-reference-or-name-is-the-column-it-reads // docs/decisions.md#a-column-that-only-reads-one-reference-or-name-is-the-column-it-reads
func (p *valueProof) checkDatatype(path string, n node) error { func (p *valueProof) checkDatatype(label string, n node) error {
t, ok := n.(*template) t, ok := n.(*template)
if !ok || t.datatype == DataTypeString { if !ok || t.datatype == DataTypeString {
return nil return nil
} }
if r := t.link.readsColumn; r != nil && columnDatatype(r.column) == t.datatype { if r := t.link.readsColumn; r != nil && columnDatatype(r.column) == t.datatype {
return fmt.Errorf(`%s: %s takes datatype %s from the column it reads; drop "datatype"`, path, t.format, t.datatype) return fmt.Errorf(`%s: %s takes datatype %s from the column it reads; drop "datatype"`, label, t.format, t.datatype)
} }
if reason := p.proveColumnItem(t).Not[t.datatype]; reason != "" { if reason := p.proveColumnItem(t).Not[t.datatype]; reason != "" {
return fmt.Errorf("%s: datatype %s: %s", path, t.datatype, reason) return fmt.Errorf("%s: datatype %s: %s", label, t.datatype, reason)
} }
return nil return nil
} }