Take the datatype and null of the column a lone reference reads, and check a column's datatypes once its references are linked
Tests / vet + fmt + tests (pull_request) Successful in 1m3s
Tests / vet + fmt + tests (pull_request) Successful in 1m3s
This commit is contained in:
@@ -310,6 +310,12 @@ order.id: datatype integer: {digits(3)} prints text, not an integer
|
||||
order.id: datatype integer: "1{digits(2)}" is not one value; write one literal or one {int()}, {float()}, {seq()} or {calc()}, or read one
|
||||
```
|
||||
|
||||
A column of one reference alone to another record's column — `"score": "{/src.score}"`,
|
||||
or a struct field tagged `src.score` — is that column: it takes the column's datatype
|
||||
and is null, or nil, where the column is. A `datatype` of its own types a string
|
||||
column's values, and over a typed column is refused naming the datatype it takes. Any
|
||||
other read renders the column's text, a null as `""`.
|
||||
|
||||
A typed column's `{calc()}` must be proven to print a number: each operand a number
|
||||
literal, an `{int()}`, `{float()}`, `{seq()}` or `{digits()}` call, a calc, or a read of
|
||||
such values, whose bounds keep every divisor from zero and the result within `1e300`.
|
||||
@@ -663,6 +669,11 @@ tokens add cost in proportion to the output.
|
||||
- **A typed column holds one value, not composed text.** Its bounds come from a
|
||||
literal or a call's arguments, so a load error names a real value, a range check is
|
||||
one comparison, and `1{digits(2)}` is a second spelling of `{int(100,199)}`.
|
||||
- **A column of one reference alone is the column it reads.** `{/src.score}` renders
|
||||
exactly what `src.score` draws, so it takes that column's datatype and null rather
|
||||
than restating them, and a `datatype` restating a typed column is a second spelling.
|
||||
Over a string column a `datatype` still types the values — the one way to type a
|
||||
column someone else wrote.
|
||||
- **A typed column's calc is refused unless proven.** Operand bounds must keep each
|
||||
divisor from zero and the result finite; what they cannot show is refused rather
|
||||
than trusted, since a bare `NaN` breaks the JSON and SQL it lands in.
|
||||
|
||||
+51
-14
@@ -64,22 +64,55 @@ func datatypeOf(m map[string]any, pos position) (DataType, error) {
|
||||
return 0, fmt.Errorf(`datatype takes "integer", "number" or "boolean", got %q`, name)
|
||||
}
|
||||
|
||||
// columnDatatype is the datatype a column's items declare. They must agree, since a
|
||||
// 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 {
|
||||
return fmt.Errorf("%s: field %q: %w", path, name, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// columnDatatype is the datatype a column's items hold. They must agree, since a
|
||||
// column holds one; a column only ever null is a string.
|
||||
func columnDatatype(n node) (DataType, error) {
|
||||
items, _ := columnItems(n)
|
||||
if len(items) == 0 {
|
||||
return DataTypeString, nil
|
||||
}
|
||||
first := itemDatatype(items[0])
|
||||
for _, t := range items[1:] {
|
||||
if t.datatype != items[0].datatype {
|
||||
return items[0].datatype, disagreement(items[0], t)
|
||||
if d := itemDatatype(t); d != first {
|
||||
return first, disagreement(items[0], first, t, d)
|
||||
}
|
||||
}
|
||||
return items[0].datatype, nil
|
||||
return first, nil
|
||||
}
|
||||
|
||||
// columnItems is a column's template items, its choices unwrapped, and whether one is null.
|
||||
// itemDatatype is the datatype a column item declares, else that of the column it reads whole.
|
||||
func itemDatatype(t *template) DataType {
|
||||
if t.datatype != DataTypeString {
|
||||
return t.datatype
|
||||
}
|
||||
return readDatatype(t)
|
||||
}
|
||||
|
||||
// readDatatype is the datatype of the column t reads whole; a string when it reads none.
|
||||
func readDatatype(t *template) DataType {
|
||||
if t.inherits == nil {
|
||||
return DataTypeString
|
||||
}
|
||||
d, _ := columnDatatype(t.inherits) // checkColumns refuses that column where it sits
|
||||
return d
|
||||
}
|
||||
|
||||
// columnItems is a column's template items, its choices unwrapped, and whether one is null
|
||||
// or reads whole a column that can be.
|
||||
func columnItems(n node) (items []*template, nullable bool) {
|
||||
var collect func(node)
|
||||
collect = func(n node) {
|
||||
@@ -90,6 +123,10 @@ func columnItems(n node) (items []*template, nullable bool) {
|
||||
}
|
||||
case *template:
|
||||
items = append(items, n)
|
||||
if n.inherits != nil {
|
||||
_, inherited := columnItems(n.inherits)
|
||||
nullable = nullable || inherited
|
||||
}
|
||||
case *null:
|
||||
nullable = true
|
||||
}
|
||||
@@ -98,17 +135,17 @@ func columnItems(n node) (items []*template, nullable bool) {
|
||||
return items, nullable
|
||||
}
|
||||
|
||||
// disagreement names the fix for two items of one column declaring different datatypes.
|
||||
func disagreement(a, b *template) error {
|
||||
typed, bare := a, b
|
||||
if typed.datatype == DataTypeString {
|
||||
typed, bare = b, a
|
||||
// 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
|
||||
if da == DataTypeString {
|
||||
bare, want = a, db
|
||||
}
|
||||
switch {
|
||||
case bare.datatype != DataTypeString:
|
||||
return fmt.Errorf("its items declare %s and %s; a column holds one datatype", a.datatype, b.datatype)
|
||||
case da != DataTypeString && db != DataTypeString:
|
||||
return fmt.Errorf("its items declare %s and %s; a column holds one datatype", da, db)
|
||||
case bare.fields == nil: // a JSON string; an object, which may carry a weight, has a fields map
|
||||
return fmt.Errorf(`item %q declares no datatype, and a column holds one; write it as {"format":%q,"datatype":%q}`, bare.format, bare.format, typed.datatype)
|
||||
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)
|
||||
}
|
||||
return fmt.Errorf(`an item declares no datatype beside one declaring %s; a column holds one, so give it "datatype": %q`, typed.datatype, typed.datatype)
|
||||
return fmt.Errorf(`item %q declares no datatype beside one declaring %s; a column holds one, so give it "datatype": %q`, bare.format, want, want)
|
||||
}
|
||||
|
||||
@@ -182,6 +182,9 @@ func inlineScope(n node, label string) nodeScope {
|
||||
// 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
|
||||
}
|
||||
mem := reachMemo{}
|
||||
if err := s(func(path string, n node) error { return repeatCheck(path, n, mem) }); err != nil {
|
||||
return err
|
||||
|
||||
@@ -253,11 +253,20 @@ func checkNoRepeatedRead(format string, c formatOps, refs map[string]refBinding)
|
||||
}
|
||||
|
||||
// draws is what an expansion has already drawn for its held names: the variant each
|
||||
// was drawn as, so every path under it reads one row, and the value each read, by
|
||||
// its one spelling, so the same read written twice reads one value.
|
||||
// was drawn as, so every path under it reads one row, the value each read, by its one
|
||||
// spelling, so the same read written twice reads one value, and the columns drawn null,
|
||||
// so a read of one whole is null too.
|
||||
type draws struct {
|
||||
variant map[string]node
|
||||
value map[string]string
|
||||
nulls map[node]bool
|
||||
}
|
||||
|
||||
func (d *draws) drewNull(column node) {
|
||||
if d.nulls == nil {
|
||||
d.nulls = map[node]bool{}
|
||||
}
|
||||
d.nulls[column] = true
|
||||
}
|
||||
|
||||
// readField renders one arm of a token. An arm's key is a sibling field or a
|
||||
@@ -299,7 +308,7 @@ func readField(s *session, t *template, held, refScope *draws, a arm) string {
|
||||
}
|
||||
return []node{n}, nil
|
||||
},
|
||||
leaf: func(n node) error { v = render(s, n, refScope); return nil },
|
||||
leaf: func(n node) error { v, _ = renderColumn(s, n, refScope); return nil },
|
||||
})
|
||||
d.value[a.path] = v
|
||||
return v
|
||||
|
||||
@@ -59,6 +59,9 @@ type template struct {
|
||||
// held is every name drawn once per expansion: the bound levels above, plus the
|
||||
// siblings a {calc()} reads. nil when the format holds nothing (see expand).
|
||||
held map[string]bool
|
||||
// inherits is the column a format of one reference alone reads, taking its datatype and null.
|
||||
inherits node
|
||||
record bool // compiled at the top without a repeat, so its fields are record columns
|
||||
}
|
||||
|
||||
func (*template) isNode() {}
|
||||
@@ -244,7 +247,7 @@ func compileTemplate(m map[string]any, pos position) (node, error) {
|
||||
if err := checkTokens(o.format, fields); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
t := &template{format: o.format, fields: fields, repeat: o.repeat, separator: o.separator, datatype: o.datatype}
|
||||
t := &template{format: o.format, fields: fields, repeat: o.repeat, separator: o.separator, datatype: o.datatype, record: fieldPos == inColumn}
|
||||
if err := t.compileFormat(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -307,9 +310,6 @@ func compileFields(m map[string]any, pos position) (map[string]node, error) {
|
||||
return nil, fmt.Errorf("field %w", err)
|
||||
}
|
||||
n, err := compileAt(m[k], pos)
|
||||
if err == nil && pos == inColumn {
|
||||
_, err = columnDatatype(n)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("field %q: %w", k, err)
|
||||
}
|
||||
|
||||
@@ -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]) // compile refused a column whose items disagree
|
||||
datatype, _ := columnDatatype(t.fields[name]) // checkColumns refused a column whose items disagree
|
||||
columns[i] = Column{Name: name, DataType: datatype}
|
||||
}
|
||||
return t, columns, nil
|
||||
@@ -300,16 +300,27 @@ func renderRecord(s *session, t *template, columns []Column) *Record {
|
||||
scope := &draws{variant: map[string]node{}, value: map[string]string{}}
|
||||
r := &Record{columns: append([]Column(nil), columns...)}
|
||||
for i := range r.columns {
|
||||
n := drawn(s, t.fields[r.columns[i].Name])
|
||||
if _, isNull := n.(*null); isNull {
|
||||
r.columns[i].Null = true
|
||||
} else {
|
||||
r.columns[i].Value = render(s, n, scope)
|
||||
}
|
||||
r.columns[i].Value, r.columns[i].Null = renderColumn(s, t.fields[r.columns[i].Name], scope)
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
// renderColumn draws a column and reports whether the draw is null: a null item, or an item
|
||||
// reading whole a column that scope drew null.
|
||||
func renderColumn(s *session, column node, scope *draws) (string, bool) {
|
||||
n := drawn(s, column)
|
||||
value, isNull := "", true
|
||||
if _, drewNull := n.(*null); !drewNull {
|
||||
value = render(s, n, scope)
|
||||
t, _ := n.(*template)
|
||||
isNull = t != nil && t.inherits != nil && scope != nil && scope.nulls[t.inherits]
|
||||
}
|
||||
if isNull && scope != nil {
|
||||
scope.drewNull(column)
|
||||
}
|
||||
return value, isNull
|
||||
}
|
||||
|
||||
// recordColumns is the sorted non-reference field names — the columns a record
|
||||
// projects. A {/path} binding is carried in fields under its root path, so only a
|
||||
// name that is not a reference is a column.
|
||||
|
||||
@@ -107,9 +107,24 @@ func linkTemplateRefs(folder []string, path string, t *template, root map[string
|
||||
if err := t.compileFormat(); err != nil {
|
||||
return fmt.Errorf("%s: %w", path, err)
|
||||
}
|
||||
t.inherits = columnRead(t)
|
||||
return nil
|
||||
}
|
||||
|
||||
// columnRead is the column t reads whole: its format is one reference alone, reading a field of
|
||||
// a record, so what that column draws is what t draws.
|
||||
func columnRead(t *template) node {
|
||||
if t.repeat != 1 || len(t.ops) != 1 || t.ops[0].kind != 'f' || len(t.ops[0].arms) != 1 {
|
||||
return nil
|
||||
}
|
||||
a := t.ops[0].arms[0]
|
||||
target, isTemplate := t.fields[a.key].(*template)
|
||||
if !isRef(a.name) || !isTemplate || !target.record || len(a.tail) != 1 {
|
||||
return nil
|
||||
}
|
||||
return target.fields[a.tail[0]]
|
||||
}
|
||||
|
||||
// eachTemplate calls fn once per template, with the folder its category sits in
|
||||
// and the dot path reaching it, folders and names in sorted order.
|
||||
func eachTemplate(root map[string]node, fn func(folder []string, path string, t *template) error) error {
|
||||
|
||||
@@ -269,9 +269,6 @@ func (s *structShape) compileRecord(root map[string]node, t reflect.Type, label
|
||||
s.fields = make([][]int, len(columns))
|
||||
for i, c := range columns {
|
||||
sf, _ := t.FieldByName(c.Name)
|
||||
if c.DataType != DataTypeString {
|
||||
return fmt.Errorf("%s.%s: its Go type %s sets the datatype; drop \"datatype\"", label, c.Name, sf.Type)
|
||||
}
|
||||
if err := proof.checkField(label+"."+c.Name, sf.Type, record.fields[c.Name]); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -318,10 +315,15 @@ func (k columnKind) holds(v proven) bool {
|
||||
return v.lo >= k.lo && v.hi <= k.hi
|
||||
}
|
||||
|
||||
// checkField rejects a column some render of which a field of Go type ft cannot hold: a null
|
||||
// outside a pointer, or a value its kind's datatype or range refuses.
|
||||
// checkField rejects a column a field of Go type ft cannot fill: a datatype, which the Go type
|
||||
// sets, a null outside a pointer, or a value its kind's datatype or range refuses.
|
||||
func (p *valueProof) checkField(label string, ft reflect.Type, column node) error {
|
||||
items, nullable := columnItems(column)
|
||||
for _, it := range items {
|
||||
if it.datatype != DataTypeString {
|
||||
return fmt.Errorf("%s: its Go type %s sets the datatype; drop \"datatype\"", label, ft)
|
||||
}
|
||||
}
|
||||
elem := ft
|
||||
if ft.Kind() == reflect.Pointer {
|
||||
elem = ft.Elem()
|
||||
@@ -333,7 +335,7 @@ func (p *valueProof) checkField(label string, ft reflect.Type, column node) erro
|
||||
return nil
|
||||
}
|
||||
for _, it := range items {
|
||||
v := p.of(it)
|
||||
v := p.columnItem(it)
|
||||
if reason := v.not[kind.datatype]; reason != "" {
|
||||
return fmt.Errorf("%s (%s): %s", label, ft, reason)
|
||||
}
|
||||
|
||||
@@ -17,11 +17,6 @@ The record API lands first, so the data update can use it.
|
||||
- `code` and `symbol` sibling fields reading `currency`, as `{code} {symbol}` → a matching pair
|
||||
- `{a} & {b}`, each reading `person` → one person, or two when `a` and `b` name different groups
|
||||
- two bare `{/sv_SE.word}` → two words
|
||||
- Reference inheritance — settle whether a column that is exactly one reference to
|
||||
another record's column, like `{/src.score}`, takes that column's datatype and
|
||||
null. Today a null there writes `""`, a `*T` struct field reading it gets `""`
|
||||
rather than nil, and a typed column reading it is refused. Settle before draw
|
||||
groups and the data update.
|
||||
|
||||
### Data
|
||||
|
||||
|
||||
@@ -23,24 +23,38 @@ type valueProof struct {
|
||||
memo map[node]proven
|
||||
}
|
||||
|
||||
// checkDatatype rejects a typed column 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 declaring a datatype over the typed column it reads whole.
|
||||
func (p *valueProof) checkDatatype(path string, n node) error {
|
||||
t, ok := n.(*template)
|
||||
if !ok || t.datatype == DataTypeString {
|
||||
return nil
|
||||
}
|
||||
if err := p.prove(t, t.datatype); err != nil {
|
||||
return fmt.Errorf("%s: %w", path, err)
|
||||
if d := readDatatype(t); d != DataTypeString {
|
||||
return fmt.Errorf(`%s: %s takes datatype %s from the column it reads; drop "datatype"`, path, t.format, d)
|
||||
}
|
||||
if reason := p.columnItem(t).not[t.datatype]; reason != "" {
|
||||
return fmt.Errorf("%s: datatype %s: %s", path, t.datatype, reason)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// prove reports why some render of n is not text of datatype d.
|
||||
func (p *valueProof) prove(n node, d DataType) error {
|
||||
if reason := p.of(n).not[d]; reason != "" {
|
||||
return fmt.Errorf("datatype %s: %s", d, reason)
|
||||
// columnItem proves a column item: what it renders, or, reading a column whole, that column's
|
||||
// items, whose nulls it draws as null rather than rendering them.
|
||||
func (p *valueProof) columnItem(t *template) proven {
|
||||
if t.inherits == nil {
|
||||
return p.of(t)
|
||||
}
|
||||
return nil
|
||||
var v proven
|
||||
items, _ := columnItems(t.inherits)
|
||||
for i, it := range items {
|
||||
if w := p.columnItem(it); i == 0 {
|
||||
v = w
|
||||
} else {
|
||||
v = v.or(w)
|
||||
}
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
func (p *valueProof) of(n node) proven {
|
||||
@@ -66,7 +80,13 @@ func (p *valueProof) of(n node) proven {
|
||||
func (p *valueProof) unite(nodes []node) proven {
|
||||
v := p.of(nodes[0])
|
||||
for _, n := range nodes[1:] {
|
||||
w := p.of(n)
|
||||
v = v.or(p.of(n))
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
// or is what a proof knows of a render that is either v or w.
|
||||
func (v proven) or(w proven) proven {
|
||||
v.lo, v.hi, v.nonZero = min(v.lo, w.lo), max(v.hi, w.hi), min(v.nonZero, w.nonZero)
|
||||
v.integral = v.integral && w.integral
|
||||
if v.notOperand == "" {
|
||||
@@ -77,7 +97,6 @@ func (p *valueProof) unite(nodes []node) proven {
|
||||
v.not[d] = w.not[d]
|
||||
}
|
||||
}
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user