diff --git a/README.md b/README.md index 9811035..df5dd38 100644 --- a/README.md +++ b/README.md @@ -627,7 +627,8 @@ tokens add cost in proportion to the output. its columns from a struct instead, because a Go caller has already written that schema: the fields name the columns and their types are the datatypes, so a tag says only what to draw, and a `datatype` in it would be a second spelling of the - type. + type. The Go type is a struct column's one datatype, so its items need not agree on + one among themselves; each must only hold that type. - **A struct's records follow Go's field access, and compile on first use.** The fields an embedded struct promotes are the struct's own — `e.First`, as `encoding/json` and SQL mappers read them — so they are columns of its record and diff --git a/datatype.go b/datatype.go index 3b9d3e2..ec05352 100644 --- a/datatype.go +++ b/datatype.go @@ -64,18 +64,41 @@ func datatypeOf(m map[string]any, pos position) (DataType, error) { return 0, fmt.Errorf(`datatype takes "integer", "number" or "boolean", got %q`, name) } -// checkColumns rejects a record column whose items hold different datatypes. -func checkColumns(path string, n node) error { - t, ok := n.(*template) - if !ok || !t.record { - return nil - } - for _, name := range recordColumns(t) { - if _, err := columnDatatype(t.fields[name]); err != nil { +// checkColumns rejects a record column whose items hold different datatypes, checking a column +// after the columns its items read, so a column read is named before its readers. +func checkColumns(s nodeScope) error { + checked := map[node]bool{} + var check func(path, name string, column node) error + check = func(path, name string, column node) error { + if checked[column] { + return nil + } + checked[column] = true + items, _ := columnItems(column) + for _, it := range items { + if r := it.readsColumn; r != nil { + if err := check(r.a.key[1:], r.a.tail[0], r.column); err != nil { + return err + } + } + } + if _, err := columnDatatype(column); err != nil { return fmt.Errorf("%s: field %q: %w", path, name, err) } + return nil } - return nil + return s(func(path string, n node) error { + t, ok := n.(*template) + if !ok || !t.record { + return nil + } + for _, name := range recordColumns(t) { + if err := check(path, name, t.fields[name]); err != nil { + return err + } + } + return nil + }) } // columnDatatype is the datatype a column's items hold. They must agree, since a @@ -136,13 +159,15 @@ func columnItems(n node) (items []*template, nullable bool) { // disagreement names the fix for two items of one column holding different datatypes. func disagreement(a *template, da DataType, b *template, db DataType) error { - bare, want := b, da + bare, typed, want := b, a, da if da == DataTypeString { - bare, want = a, db + bare, typed, want = a, b, db } switch { case da != DataTypeString && db != DataTypeString: return fmt.Errorf("its items hold %s and %s; a column holds one datatype", da, db) + case typed.datatype == DataTypeString && (&valueProof{}).columnItem(bare).not[want] != "": + return fmt.Errorf(`item %q is not %s, the datatype item %q takes from the column it reads; to read that column as text, write {"format":"{text}","text":%q}`, bare.format, dataTypeNouns[want], typed.format, typed.format) case bare.fromString: // an object may carry a weight, which this spelling would drop return fmt.Errorf(`item %q declares no datatype, and a column holds one; write it as {"format":%q,"datatype":%q}`, bare.format, bare.format, want) } diff --git a/graph.go b/graph.go index 86b51c7..be5a593 100644 --- a/graph.go +++ b/graph.go @@ -177,14 +177,19 @@ func inlineScope(n node, label string) nodeScope { return func(fn func(path string, m node) error) error { return eachNode(n, label, fn) } } -// checkScope runs the per-node fences over a scope, each over the whole scope +// checkScope runs every fence over a scope: checkColumns, then checkRenders. +func checkScope(s nodeScope) error { + if err := checkColumns(s); err != nil { + return err + } + return checkRenders(s) +} + +// checkRenders runs the per-node fences over a scope, each over the whole scope // before the next, so which of several broken nodes is reported does not depend on // the walk. It runs after checkNoCycles, whose guarantee is what lets the walks // terminate. -func checkScope(s nodeScope) error { - if err := s(checkColumns); err != nil { - return err - } +func checkRenders(s nodeScope) error { mem := reachMemo{} if err := s(func(path string, n node) error { return repeatCheck(path, n, mem) }); err != nil { return err diff --git a/inline.go b/inline.go index 763f985..23b592b 100644 --- a/inline.go +++ b/inline.go @@ -32,7 +32,7 @@ func (f *Generator) NewTemplate(input string) (*Template, error) { if err != nil { return nil, fmt.Errorf("fejkdata: %w", err) } - if err := bindInline(n, "template", f.categories); err != nil { + if err := bindInline(n, "template", f.categories, checkScope); err != nil { return nil, fmt.Errorf("fejkdata: %w", err) } return &Template{g: f, n: n}, nil @@ -133,14 +133,14 @@ func inputValue(input string) (any, error) { return raw, nil } -// bindInline links an inline node's references against root and runs the fences over it, -// naming its nodes from label. -func bindInline(n node, label string, root map[string]node) error { +// bindInline links an inline node's references against root and runs check over it, naming its +// nodes from label. +func bindInline(n node, label string, root map[string]node, check func(nodeScope) error) error { scope := inlineScope(n, label) if err := linkNodeRefs(scope, root); err != nil { return err } - return checkScope(scope) + return check(scope) } // linkNodeRefs binds the references in an inline node's templates against the diff --git a/record.go b/record.go index d1f8fab..b630526 100644 --- a/record.go +++ b/record.go @@ -217,7 +217,7 @@ func recordOf(n node) (*template, []Column, error) { } columns := make([]Column, len(names)) for i, name := range names { - datatype, _ := columnDatatype(t.fields[name]) // checkColumns refused a column whose items disagree + datatype, _ := columnDatatype(t.fields[name]) // checkColumns refused items that disagree wherever DataType is read columns[i] = Column{Name: name, DataType: datatype} } return t, columns, nil diff --git a/struct.go b/struct.go index 3b60e03..11d6dad 100644 --- a/struct.go +++ b/struct.go @@ -258,7 +258,7 @@ func (s *structShape) compileRecord(root map[string]node, t reflect.Type, label if err != nil { return fmt.Errorf("%s: %w", label, err) } - if err := bindInline(n, label, root); err != nil { + if err := bindInline(n, label, root, checkRenders); err != nil { return err } record, columns, err := recordOf(n)