Struct-filling #14

Merged
lilleman merged 10 commits from struct-filling into main 2026-09-15 17:52:29 +02:00
4 changed files with 57 additions and 48 deletions
Showing only changes of commit e4ebfdb28d - Show all commits
+4 -4
View File
@@ -538,10 +538,10 @@ tokens add cost in proportion to the output.
whole — though only a leading one could collide — keeps one simple name rule whole — though only a leading one could collide — keeps one simple name rule
instead of a leading-position special case. The JSON string is what makes the instead of a leading-position special case. The JSON string is what makes the
library's own advice reachable: the error for an object holding only a format library's own advice reachable: the error for an object holding only a format
names `"…"`, and that spelling has to work where it is printed. One reference alone, names `"…"`, and that spelling has to work where it is printed. An argument or struct
`{/users}`, is refused naming the path `users`: both render the same text, and only tag of one reference alone, `{/users}`, is refused naming the path `users`: both
the path names a record. `IsTemplate` exports the rule, so the CLI, struct tags and render the same text, and only the path names a record. `IsTemplate` exports the
any other caller read one. rule, so the CLI, struct tags and any other caller read one.
- **An inline template skips the cycle fence, and only that one.** `New` proves the - **An inline template skips the cycle fence, and only that one.** `New` proves the
loaded tree acyclic, an inline node is a finite tree of its own, and nothing in loaded tree acyclic, an inline node is a finite tree of its own, and nothing in
the tree can reference it, so no render of it reaches itself. Every other fence the tree can reference it, so no render of it reaches itself. Every other fence
+1 -1
View File
@@ -418,7 +418,7 @@ const (
func classify(arg string) (argKind, error) { func classify(arg string) (argKind, error) {
inline, err := fejkdata.IsTemplate(arg) inline, err := fejkdata.IsTemplate(arg)
if inline { if inline {
return argTemplate, err return argTemplate, nil
} }
return argPath, err return argPath, err
} }
+12 -7
View File
@@ -72,8 +72,8 @@ func isTemplate(arg string) (bool, error) {
if i := strings.IndexAny(arg, `[]}"`); i >= 0 { if i := strings.IndexAny(arg, `[]}"`); i >= 0 {
return false, fmt.Errorf("%q holds a %q, which no path may, and it is not valid JSON, so it names no template either", arg, arg[i:i+1]) return false, fmt.Errorf("%q holds a %q, which no path may, and it is not valid JSON, so it names no template either", arg, arg[i:i+1])
} }
if len(arg) > 1 && strings.HasPrefix(arg, "/") { if path := strings.TrimLeft(arg, "/"); path != arg && path != "" {
return false, fmt.Errorf("path %s starts with /, and every path starts at the root already; write %s", arg, arg[1:]) return false, fmt.Errorf("path %s starts with /, and every path starts at the root already; write %s", arg, path)
} }
return false, nil return false, nil
} }
@@ -82,14 +82,19 @@ func isJSONStart(arg string) bool {
return strings.HasPrefix(arg, "[") || strings.HasPrefix(arg, `"`) return strings.HasPrefix(arg, "[") || strings.HasPrefix(arg, `"`)
} }
// loneReference is the path a template spells when it is one reference token and nothing else. // loneReference is the path a template spells when the value it holds — a format string, a
// JSON string, or an object holding only a format — is one reference token and nothing else.
func loneReference(arg string) (string, bool) { func loneReference(arg string) (string, bool) {
format := arg var raw any
if strings.HasPrefix(arg, `"`) && json.Unmarshal([]byte(arg), &format) != nil { if json.Unmarshal([]byte(arg), &raw) != nil {
return "", false raw = arg
} }
if m, isObject := raw.(map[string]any); isObject && len(m) == 1 {
raw = m["format"]
}
format, isString := raw.(string)
var units []ftoken var units []ftoken
if eachToken(format, func(t ftoken) error { units = append(units, t); return nil }) != nil || len(units) != 1 { if !isString || eachToken(format, func(t ftoken) error { units = append(units, t); return nil }) != nil || len(units) != 1 {
return "", false return "", false
} }
body := units[0].body body := units[0].body
+35 -31
View File
@@ -31,12 +31,15 @@ func (f *Generator) FakeStruct(v any) error {
return nil return nil
} }
// structResult is what compiling a struct type settled: its shape, or why it cannot be filled.
type structResult struct { type structResult struct {
shape *structShape shape *structShape
err error err error
} }
// maxStructRecords caps the records one struct type fills, which pointers between struct
// types multiply along every path.
const maxStructRecords = 1 << 10
// structShapeOf compiles a struct type once and remembers the answer. Callers hold the // structShapeOf compiles a struct type once and remembers the answer. Callers hold the
// generator's lock. // generator's lock.
func (f *Generator) structShapeOf(t reflect.Type) (*structShape, error) { func (f *Generator) structShapeOf(t reflect.Type) (*structShape, error) {
@@ -47,7 +50,8 @@ func (f *Generator) structShapeOf(t reflect.Type) (*structShape, error) {
if label == "" { if label == "" {
label = "struct" label = "struct"
} }
shape, err := compileStruct(f.categories, t, label, map[reflect.Type]bool{}) sc := &structCompile{root: f.categories, visiting: map[reflect.Type]bool{}, records: maxStructRecords}
shape, err := sc.record(t, label)
if err == nil && shape.empty() { if err == nil && shape.empty() {
err = fmt.Errorf("%s has no fake tags, so nothing to fill", t) err = fmt.Errorf("%s has no fake tags, so nothing to fill", t)
} }
@@ -75,29 +79,38 @@ type nestedStruct struct {
func (s *structShape) empty() bool { return s.record == nil && len(s.nested) == 0 } func (s *structShape) empty() bool { return s.record == nil && len(s.nested) == 0 }
// structCompile is what compiling one struct type shares across the records it reaches: the
// loaded tree, the types compiling or embedded above, so a pointer back to one is left alone
// rather than filled without end, and how many more records it may build.
type structCompile struct {
root map[string]node
visiting map[reflect.Type]bool
records int
}
// structFields gathers what one struct type fills: its tagged fields, those its embedded // structFields gathers what one struct type fills: its tagged fields, those its embedded
// structs promote included, as the tags of one record, and its named struct fields as nested // structs promote included, as the tags of one record, and its named struct fields as nested
// records. visiting holds the types compiling or embedded above, so a pointer back to one is // records.
// left alone rather than filled without end.
type structFields struct { type structFields struct {
root map[string]node *structCompile
t reflect.Type t reflect.Type
label string label string
visiting map[reflect.Type]bool
tags map[string]any tags map[string]any
shape *structShape shape *structShape
} }
// compileStruct compiles struct type t, naming its fields from label. func (sc *structCompile) record(t reflect.Type, label string) (*structShape, error) {
func compileStruct(root map[string]node, t reflect.Type, label string, visiting map[reflect.Type]bool) (*structShape, error) { if sc.records--; sc.records < 0 {
visiting[t] = true return nil, fmt.Errorf(`%s: the struct fields reach more than %d records; leave a pointer unfilled with fake:"-"`, label, maxStructRecords)
defer delete(visiting, t) }
c := &structFields{root: root, t: t, label: label, visiting: visiting, tags: map[string]any{}, shape: &structShape{}} sc.visiting[t] = true
defer delete(sc.visiting, t)
c := &structFields{structCompile: sc, t: t, label: label, tags: map[string]any{}, shape: &structShape{}}
if err := c.walk(t, nil); err != nil { if err := c.walk(t, nil); err != nil {
return nil, err return nil, err
} }
if len(c.tags) > 0 { if len(c.tags) > 0 {
if err := c.shape.compileRecord(root, t, label, c.tags); err != nil { if err := c.shape.compileRecord(sc.root, t, label, c.tags); err != nil {
return nil, err return nil, err
} }
} }
@@ -170,7 +183,6 @@ func fieldPath(t reflect.Type, index []int) string {
return strings.Join(names, ".") return strings.Join(names, ".")
} }
// embed gathers the fields an embedded struct promotes into c's record.
func (c *structFields) embed(sf reflect.StructField, elem reflect.Type) error { func (c *structFields) embed(sf reflect.StructField, elem reflect.Type) error {
c.visiting[elem] = true c.visiting[elem] = true
defer delete(c.visiting, elem) defer delete(c.visiting, elem)
@@ -179,14 +191,13 @@ func (c *structFields) embed(sf reflect.StructField, elem reflect.Type) error {
return err return err
} }
if sf.Type.Kind() == reflect.Pointer && !sf.IsExported() && (len(c.tags) > tags || len(c.shape.nested) > nested) { if sf.Type.Kind() == reflect.Pointer && !sf.IsExported() && (len(c.tags) > tags || len(c.shape.nested) > nested) {
return fmt.Errorf("%s.%s: an embedded pointer to an unexported type cannot be allocated, so the tags beneath it cannot fill; embed %s by value", c.label, fieldPath(c.t, sf.Index), elem) return fmt.Errorf("%s.%s: an unexported embedded pointer field cannot be set, so the tags beneath it cannot fill; embed %s by value", c.label, fieldPath(c.t, sf.Index), elem)
} }
return nil return nil
} }
// nest adds a named struct field, or a pointer to one, that carries tags as a record of its own.
func (c *structFields) nest(sf reflect.StructField, elem reflect.Type) error { func (c *structFields) nest(sf reflect.StructField, elem reflect.Type) error {
nested, err := compileStruct(c.root, elem, c.label+"."+sf.Name, c.visiting) nested, err := c.record(elem, c.label+"."+sf.Name)
if err != nil || nested.empty() { if err != nil || nested.empty() {
return err return err
} }
@@ -264,7 +275,7 @@ func (s *structShape) compileRecord(root map[string]node, t reflect.Type, label
} }
// columnKind is what a field of one Go kind holds: the datatype its text proves as, the range a // columnKind is what a field of one Go kind holds: the datatype its text proves as, the range a
// number of it stays in, and the kind to name when a value can pass that range. // number of it stays in, and the kind to name when a value is not proven within that range.
type columnKind struct { type columnKind struct {
datatype DataType datatype DataType
lo, hi float64 lo, hi float64
@@ -288,23 +299,16 @@ var columnKinds = map[reflect.Kind]columnKind{
reflect.Uint8: {DataTypeInteger, 0, math.MaxUint8, reflect.Int64}, reflect.Uint8: {DataTypeInteger, 0, math.MaxUint8, reflect.Int64},
} }
// past is the bound of v a field of this kind cannot hold, if either is. An integer prints // holds reports whether a field of this kind holds every value v proves. An integer prints
// whole, so its bounds round inward first. // whole, so its bounds round inward first.
func (k columnKind) past(v proven) (float64, bool) { func (k columnKind) holds(v proven) bool {
lo, hi := v.lo, v.hi
switch k.datatype { switch k.datatype {
case DataTypeString, DataTypeBoolean: case DataTypeString, DataTypeBoolean:
return 0, false return true
case DataTypeInteger: case DataTypeInteger:
lo, hi = math.Ceil(lo), math.Floor(hi) return math.Ceil(v.lo) >= k.lo && math.Floor(v.hi) <= k.hi
} }
switch { return v.lo >= k.lo && v.hi <= k.hi
case lo < k.lo:
return lo, true
case hi > k.hi:
return hi, true
}
return 0, false
} }
// checkField rejects a column some render of which a field of Go type ft cannot hold: a null // checkField rejects a column some render of which a field of Go type ft cannot hold: a null
@@ -326,8 +330,8 @@ func (p *valueProof) checkField(label string, ft reflect.Type, column node) erro
if reason := v.not[kind.datatype]; reason != "" { if reason := v.not[kind.datatype]; reason != "" {
return fmt.Errorf("%s (%s): %s", label, ft, reason) return fmt.Errorf("%s (%s): %s", label, ft, reason)
} }
if bound, over := kind.past(v); over { if !kind.holds(v) {
return fmt.Errorf("%s (%s): %q can reach %s, past %s; make it %s", label, ft, it.format, strconv.FormatFloat(bound, 'g', -1, 64), elem.Kind(), kind.wider) return fmt.Errorf("%s (%s): %q is not proven within %s; make it %s", label, ft, it.format, elem.Kind(), kind.wider)
} }
} }
return nil return nil