One reference draw per render in both views: a repeat iteration draws anew, a group option draws apart, and one fence refuses what a render's draws cannot answer for
Tests / vet + fmt + tests (pull_request) Successful in 1m6s

This commit is contained in:
2026-09-15 22:55:42 +02:00
parent 4b31482a53
commit 246f99a319
10 changed files with 400 additions and 167 deletions
+21 -11
View File
@@ -27,7 +27,7 @@ func (f *Generator) Fake(path string) (string, error) {
if _, ok := n.(*group); ok {
return "", fmt.Errorf("fejkdata: %s names a folder, not a value", path)
}
return render(f.rand, n, nil), nil
return renderOnce(f.rand, n), nil
}
// descend walks named fields to the node a path names. It is the one render-side
@@ -50,20 +50,21 @@ func descend(s *session, root node, segments []string) (node, error) {
}
// render evaluates a compiled node to a string. compile validates every node up
// front, so rendering a compiled tree cannot fail. refScope carries the draws a
// reference shares beyond its own expansion; nil keeps every reference local.
func render(s *session, n node, refScope *draws) string {
// front, so rendering a compiled tree cannot fail. sc holds the reference draws the
// render shares; each repeat iteration renders over draws of its own.
func render(s *session, n node, sc drawScope) string {
switch n := n.(type) {
case *choice:
return render(s, pick(s, n), refScope)
return render(s, pick(s, n), sc)
case *null:
return ""
case *template:
sc = sc.in(n)
if n.repeat == 1 {
if n.fixed {
return n.lit
}
return expand(s, n, refScope)
return expand(s, n, sc)
}
var b strings.Builder
b.Grow(n.repeat * (n.grow + len(n.separator)))
@@ -71,7 +72,7 @@ func render(s *session, n node, refScope *draws) string {
if i > 0 {
b.WriteString(n.separator)
}
b.WriteString(expand(s, n, refScope))
b.WriteString(expandAnew(s, n, sc.group))
}
return b.String()
default:
@@ -79,6 +80,15 @@ func render(s *session, n node, refScope *draws) string {
}
}
// expandAnew expands one repeat iteration of t as a render of its own, in group. Inlined into
// render's loop, its draw set would move to the heap.
//
//go:noinline
func expandAnew(s *session, t *template, group string) string {
var set drawSet
return expand(s, t, drawScope{set: &set, group: group})
}
// pick selects one item. Uniform choices are O(1); weighted choices are an
// O(log n) search over precomputed cumulative weights. compile guarantees a
// non-empty choice and a finite positive total, so the index is always in range.
@@ -93,12 +103,12 @@ func pick(r rng, c *choice) node {
// expand renders a template's compiled ops. compile validated every token, so this
// cannot fail.
func expand(s *session, t *template, refScope *draws) string {
func expand(s *session, t *template, sc drawScope) string {
var b strings.Builder
b.Grow(t.grow)
// One draw per held name, for this expansion only: a nested template and each
// repeat iteration get their own, since each is its own expansion. A reference
// reads the caller's scope instead, whenever one was supplied.
// path reads the render's draws in sc instead.
var held *draws
if len(t.held) > 0 {
held = &draws{
@@ -112,7 +122,7 @@ func expand(s *session, t *template, refScope *draws) string {
case 'l':
b.WriteString(o.lit)
case 'f':
b.WriteString(readField(s, t, held, refScope, o.arms[s.IntN(len(o.arms))]).text)
b.WriteString(readField(s, t, held, sc, o.arms[s.IntN(len(o.arms))]).text)
case 'b':
// Read before the call, so the value a calc computes is the value the
// format showed. calcVars fixed the order op.operands holds.
@@ -120,7 +130,7 @@ func expand(s *session, t *template, refScope *draws) string {
if len(o.operands) > 0 {
operands = make([]string, len(o.operands))
for j, a := range o.operands {
operands[j] = readField(s, t, held, refScope, a).text
operands[j] = readField(s, t, held, sc, a).text
}
}
b.WriteString(o.call(s, b.String(), operands)) // b.String() is the output so far