Move the seeded randomness and the {seq()} counters into internal/drawstate

This commit is contained in:
2026-10-05 11:43:45 +02:00
committed by Lillem4n
parent 4a680f03b3
commit 2d9c315e75
14 changed files with 107 additions and 76 deletions
+11 -9
View File
@@ -7,6 +7,8 @@ import (
"math" "math"
"strconv" "strconv"
"strings" "strings"
"github.com/larvit/fejkdata/internal/drawstate"
) )
// maxLen caps sample output lengths (hex, nanoid, base64, digits, upper, lower) // maxLen caps sample output lengths (hex, nanoid, base64, digits, upper, lower)
@@ -40,19 +42,19 @@ var builtins = withTransforms(map[string]builtin{
"lower": {arity: 1, checkArgs: posIntArg, prep: chars("abcdefghijklmnopqrstuvwxyz")}, "lower": {arity: 1, checkArgs: posIntArg, prep: chars("abcdefghijklmnopqrstuvwxyz")},
"base64": {arity: 1, checkArgs: posIntArg, prep: func(a []string) callFn { "base64": {arity: 1, checkArgs: posIntArg, prep: func(a []string) callFn {
n := atoi(a[0]) n := atoi(a[0])
return func(s *generatorState, _ string, _ []string) string { return func(s *drawstate.State, _ string, _ []string) string {
return base64.StdEncoding.EncodeToString(randBytes(s, n)) return base64.StdEncoding.EncodeToString(randBytes(s, n))
} }
}}, }},
"int": {arity: 2, checkArgs: intRangeArgs, prep: func(a []string) callFn { "int": {arity: 2, checkArgs: intRangeArgs, prep: func(a []string) callFn {
lo, span := atoi(a[0]), atoi(a[1])-atoi(a[0])+1 lo, span := atoi(a[0]), atoi(a[1])-atoi(a[0])+1
return func(s *generatorState, _ string, _ []string) string { return strconv.Itoa(lo + s.IntN(span)) } return func(s *drawstate.State, _ string, _ []string) string { return strconv.Itoa(lo + s.IntN(span)) }
}, prints: DataTypeInteger, proveNumber: func(token string, prints DataType, a []string) proven { }, prints: DataTypeInteger, proveNumber: func(token string, prints DataType, a []string) proven {
return printing(token, prints, bounded(float64(atoi(a[0])), float64(atoi(a[1])), true)) return printing(token, prints, bounded(float64(atoi(a[0])), float64(atoi(a[1])), true))
}}, }},
"float": {arity: 3, checkArgs: floatArgs, prep: func(a []string) callFn { "float": {arity: 3, checkArgs: floatArgs, prep: func(a []string) callFn {
lo, hi, dp := atof(a[0]), atof(a[1]), atoi(a[2]) lo, hi, dp := atof(a[0]), atof(a[1]), atoi(a[2])
return func(s *generatorState, _ string, _ []string) string { return func(s *drawstate.State, _ string, _ []string) string {
return formatFloat(lo+s.Float64()*(hi-lo), dp) return formatFloat(lo+s.Float64()*(hi-lo), dp)
} }
}, prints: DataTypeNumber, proveNumber: func(token string, _ DataType, a []string) proven { }, prints: DataTypeNumber, proveNumber: func(token string, _ DataType, a []string) proven {
@@ -60,7 +62,7 @@ var builtins = withTransforms(map[string]builtin{
}}, }},
"iban": {arity: 1, checkArgs: ibanArg, prep: func(a []string) callFn { "iban": {arity: 1, checkArgs: ibanArg, prep: func(a []string) callFn {
cc := a[0] cc := a[0]
return func(s *generatorState, _ string, _ []string) string { return iban(s, cc) } return func(s *drawstate.State, _ string, _ []string) string { return iban(s, cc) }
}}, }},
"date": {arity: -1, checkArgs: dateArgs, prep: datePrep}, "date": {arity: -1, checkArgs: dateArgs, prep: datePrep},
"time": {arity: -1, checkArgs: timeArg, prep: timePrep}, "time": {arity: -1, checkArgs: timeArg, prep: timePrep},
@@ -73,8 +75,8 @@ var builtins = withTransforms(map[string]builtin{
if len(a) == 1 { if len(a) == 1 {
key = a[0] key = a[0]
} }
return func(s *generatorState, _ string, _ []string) string { return func(s *drawstate.State, _ string, _ []string) string {
return strconv.FormatUint(s.next(key), 10) return strconv.FormatUint(s.Next(key), 10)
} }
}, prints: DataTypeInteger, proveNumber: func(token string, prints DataType, _ []string) proven { }, prints: DataTypeInteger, proveNumber: func(token string, prints DataType, _ []string) proven {
return printing(token, prints, bounded(1, math.MaxInt64, true)) return printing(token, prints, bounded(1, math.MaxInt64, true))
@@ -86,18 +88,18 @@ var builtins = withTransforms(map[string]builtin{
// sample of n characters drawn from an alphabet. // sample of n characters drawn from an alphabet.
func derive(f func(emitted string) string) func([]string) callFn { func derive(f func(emitted string) string) func([]string) callFn {
return func([]string) callFn { return func([]string) callFn {
return func(_ *generatorState, emitted string, _ []string) string { return f(emitted) } return func(_ *drawstate.State, emitted string, _ []string) string { return f(emitted) }
} }
} }
func sample(f func(rng) string) func([]string) callFn { func sample(f func(rng) string) func([]string) callFn {
return func([]string) callFn { return func([]string) callFn {
return func(s *generatorState, _ string, _ []string) string { return f(s) } return func(s *drawstate.State, _ string, _ []string) string { return f(s) }
} }
} }
func chars(alphabet string) func([]string) callFn { func chars(alphabet string) func([]string) callFn {
return func(a []string) callFn { return func(a []string) callFn {
n := atoi(a[0]) n := atoi(a[0])
return func(s *generatorState, _ string, _ []string) string { return randChars(s, n, alphabet) } return func(s *drawstate.State, _ string, _ []string) string { return randChars(s, n, alphabet) }
} }
} }
+3 -1
View File
@@ -7,6 +7,8 @@ import (
"strconv" "strconv"
"strings" "strings"
"unicode" "unicode"
"github.com/larvit/fejkdata/internal/drawstate"
) )
// calcNode is a parsed expression node. It evaluates over the operand values expand // calcNode is a parsed expression node. It evaluates over the operand values expand
@@ -235,7 +237,7 @@ func calcPrep(args []string) callFn {
} }
placed := indexVars(expr, at) placed := indexVars(expr, at)
dp := calcDecimals(args) dp := calcDecimals(args)
return func(_ *generatorState, _ string, operands []string) string { return func(_ *drawstate.State, _ string, operands []string) string {
return formatFloat(placed.eval(operands), dp) return formatFloat(placed.eval(operands), dp)
} }
} }
+15 -28
View File
@@ -6,11 +6,12 @@ import (
"errors" "errors"
"fmt" "fmt"
"io/fs" "io/fs"
"math/rand/v2"
"os" "os"
"reflect" "reflect"
"sort" "sort"
"sync" "sync"
"github.com/larvit/fejkdata/internal/drawstate"
) )
// MaxRepeat caps a repeat, and caps the renders nested repeats multiply to along // MaxRepeat caps a repeat, and caps the renders nested repeats multiply to along
@@ -26,27 +27,15 @@ var ErrNoData = errors.New("no data: WithoutShippedData needs at least one WithD
// It is safe for concurrent use; a seeded sequence is reproducible only when drawn // It is safe for concurrent use; a seeded sequence is reproducible only when drawn
// from one goroutine. // from one goroutine.
type Generator struct { type Generator struct {
// mu guards rand, records, structs and root, which gains a shipped category on the // mu guards draws, records, structs and root, which gains a shipped category on the
// first call reaching it. // first call reaching it.
mu sync.Mutex mu sync.Mutex
rand *generatorState draws *drawstate.State
root folder // the categories as the node a path walks from, owned here so a walk allocates none root folder // the categories as the node a path walks from, owned here so a walk allocates none
records map[node]recordShape records map[node]recordShape
structs map[reflect.Type]structResult structs map[reflect.Type]structResult
} }
// generatorState is one generator's draw state, kept across its renders: the seeded rng plus
// the {seq()} counters.
type generatorState struct {
*rand.Rand
counters map[string]uint64
}
func (s *generatorState) next(key string) uint64 {
s.counters[key]++
return s.counters[key]
}
type config struct { type config struct {
seed uint64 seed uint64
seeded bool seeded bool
@@ -97,11 +86,11 @@ func New(opts ...Option) (*Generator, error) {
if err != nil { if err != nil {
return nil, fmt.Errorf("fejkdata: %w", err) return nil, fmt.Errorf("fejkdata: %w", err)
} }
rng, err := newRand(c.seed, c.seeded) seed, err := c.drawSeed()
if err != nil { if err != nil {
return nil, fmt.Errorf("fejkdata: %w", err) return nil, fmt.Errorf("fejkdata: %w", err)
} }
return &Generator{rand: rng, root: root}, nil return &Generator{draws: drawstate.New(seed), root: root}, nil
} }
// load is the tree New starts from. // load is the tree New starts from.
@@ -231,18 +220,16 @@ func join(prefix, name string) string {
// randomBytes seeds an unseeded generator. // randomBytes seeds an unseeded generator.
var randomBytes = crand.Read var randomBytes = crand.Read
func newRand(seed uint64, seeded bool) (*generatorState, error) { // drawSeed is the seed WithSeed gave, or one read from the system's entropy.
var r *rand.Rand func (c config) drawSeed() (uint64, error) {
if seeded { if c.seeded {
r = rand.New(rand.NewPCG(seed, seed^0x9e3779b97f4a7c15)) return c.seed, nil
} else {
var b [16]byte
if _, err := randomBytes(b[:]); err != nil {
return nil, fmt.Errorf("seeding from crypto/rand: %w", err)
}
r = rand.New(rand.NewPCG(binary.LittleEndian.Uint64(b[:8]), binary.LittleEndian.Uint64(b[8:])))
} }
return &generatorState{Rand: r, counters: map[string]uint64{}}, nil var b [8]byte
if _, err := randomBytes(b[:]); err != nil {
return 0, fmt.Errorf("seeding from crypto/rand: %w", err)
}
return binary.LittleEndian.Uint64(b[:]), nil
} }
func internalError(format string, a ...any) string { func internalError(format string, a ...any) string {
+4 -2
View File
@@ -3,6 +3,8 @@ package fejkdata
import ( import (
"fmt" "fmt"
"strings" "strings"
"github.com/larvit/fejkdata/internal/drawstate"
) )
// scanUnit is one unit of a scanned format string: a literal rune or the body of a // scanUnit is one unit of a scanned format string: a literal rune or the body of a
@@ -119,7 +121,7 @@ func parseFormat(format string) ([]formatToken, error) {
} }
// builtin is a format-string function invoked as {name(args)}. It receives the // builtin is a format-string function invoked as {name(args)}. It receives the
// generatorState, the output emitted so far in the current expansion (for derivations // draw state, the output emitted so far in the current expansion (for derivations
// such as a checksum over preceding digits), and the values of the operands it named // such as a checksum over preceding digits), and the values of the operands it named
// (calc and the transforms name them). All must stay pure over (rng, emitted, args) so // (calc and the transforms name them). All must stay pure over (rng, emitted, args) so
// seeded output is reproducible; seq advances per-generator counter state, which is // seeded output is reproducible; seq advances per-generator counter state, which is
@@ -461,7 +463,7 @@ func checkSegments(a arm) error {
// callFn is a builtin prepared for one call site: its args already parsed. It reads the // callFn is a builtin prepared for one call site: its args already parsed. It reads the
// output emitted so far in the current expansion (a derivation's payload) and the // output emitted so far in the current expansion (a derivation's payload) and the
// values of the operands it named, which expand read for it. // values of the operands it named, which expand read for it.
type callFn func(s *generatorState, emitted string, operands []string) string type callFn func(s *drawstate.State, emitted string, operands []string) string
// op is one compiled unit of a format string: a literal run, a name read, // op is one compiled unit of a format string: a literal run, a name read,
// or a builtin already prepared with its args. // or a builtin already prepared with its args.
+1 -1
View File
@@ -20,7 +20,7 @@ type Template struct {
func (t *Template) Fake() string { func (t *Template) Fake() string {
t.g.mu.Lock() t.g.mu.Lock()
defer t.g.mu.Unlock() defer t.g.mu.Unlock()
return renderOnce(t.g.rand, t.n) return renderOnce(t.g.draws, t.n)
} }
// NewTemplate compiles an inline template — a format string or a JSON value — and // NewTemplate compiles an inline template — a format string or a JSON value — and
+22
View File
@@ -0,0 +1,22 @@
// Package drawstate holds what a generator draws through: the seeded randomness and the
// {seq()} counters.
package drawstate
import "math/rand/v2"
// State is one generator's draw state, kept across its renders.
type State struct {
*rand.Rand
counters map[string]uint64
}
// New builds the state a seed reproduces.
func New(seed uint64) *State {
return &State{Rand: rand.New(rand.NewPCG(seed, seed^0x9e3779b97f4a7c15)), counters: map[string]uint64{}}
}
// Next advances the counter key names, the one {seq()} or {seq(key)} reads, and returns it.
func (s *State) Next(key string) uint64 {
s.counters[key]++
return s.counters[key]
}
+4 -2
View File
@@ -4,6 +4,8 @@ import (
"fmt" "fmt"
"strings" "strings"
"time" "time"
"github.com/larvit/fejkdata/internal/drawstate"
) )
const dayLayout = "2006-01-02" const dayLayout = "2006-01-02"
@@ -135,13 +137,13 @@ func datePrep(a []string) callFn {
panic(internalError("builtin arg %q reached prep unvalidated: %v", a[1], err)) panic(internalError("builtin arg %q reached prep unvalidated: %v", a[1], err))
} }
layout, start, span := layoutOf(a[2]), from.Unix(), int(to.Unix()-from.Unix())+86400 layout, start, span := layoutOf(a[2]), from.Unix(), int(to.Unix()-from.Unix())+86400
return func(s *generatorState, _ string, _ []string) string { return func(s *drawstate.State, _ string, _ []string) string {
return time.Unix(start+int64(s.IntN(span)), 0).UTC().Format(layout) return time.Unix(start+int64(s.IntN(span)), 0).UTC().Format(layout)
} }
} }
func timePrep(a []string) callFn { func timePrep(a []string) callFn {
layout := layoutOf(a[0]) layout := layoutOf(a[0])
return func(s *generatorState, _ string, _ []string) string { return func(s *drawstate.State, _ string, _ []string) string {
return time.Unix(int64(s.IntN(86400)), 0).UTC().Format(layout) return time.Unix(int64(s.IntN(86400)), 0).UTC().Format(layout)
} }
} }
+5 -3
View File
@@ -3,6 +3,8 @@ package fejkdata
import ( import (
"fmt" "fmt"
"strings" "strings"
"github.com/larvit/fejkdata/internal/drawstate"
) )
// splitPath splits a dotted path into segments, a selector — [key or name] after a // splitPath splits a dotted path into segments, a selector — [key or name] after a
@@ -430,7 +432,7 @@ func probePath(n node, tail []string, steps []pathStep) ([]pathStep, error) {
// For a read through a name, memo keeps the variant drawn at each of levels, so paths sharing a // For a read through a name, memo keeps the variant drawn at each of levels, so paths sharing a
// prefix share it. It returns the leaf and the pins its row is in: a step down after a step up draws // prefix share it. It returns the leaf and the pins its row is in: a step down after a step up draws
// afresh, into pins of its own, which memo shares with every path stepping down there. // afresh, into pins of its own, which memo shares with every path stepping down there.
func drawSteps(s *generatorState, n node, steps []pathStep, pins *pinSet, memo *drawMemo, levels []string) (node, *pinSet) { func drawSteps(s *drawstate.State, n node, steps []pathStep, pins *pinSet, memo *drawMemo, levels []string) (node, *pinSet) {
var climbed *table var climbed *table
for _, st := range steps { for _, st := range steps {
if c, ok := n.(*choice); ok { if c, ok := n.(*choice); ok {
@@ -458,7 +460,7 @@ func drawSteps(s *generatorState, n node, steps []pathStep, pins *pinSet, memo *
return n, pins return n, pins
} }
func (t *table) drawStep(s *generatorState, st pathStep, pins *pinSet) node { func (t *table) drawStep(s *drawstate.State, st pathStep, pins *pinSet) node {
switch st.kind { switch st.kind {
case stepSelect: case stepSelect:
if err := pins.pinRow(t, st.row); err != nil { if err := pins.pinRow(t, st.row); err != nil {
@@ -480,7 +482,7 @@ func (t *table) drawStep(s *generatorState, st pathStep, pins *pinSet) node {
panic(internalError("drawStep has no case for step kind %d", st.kind)) panic(internalError("drawStep has no case for step kind %d", st.kind))
} }
func drawVariant(s *generatorState, c *choice, memo *drawMemo, levels []string, at int) node { func drawVariant(s *drawstate.State, c *choice, memo *drawMemo, levels []string, at int) node {
if memo == nil { if memo == nil {
return resolveChoice(s, c) return resolveChoice(s, c)
} }
+8 -6
View File
@@ -1,5 +1,7 @@
package fejkdata package fejkdata
import "github.com/larvit/fejkdata/internal/drawstate"
// namedPick is one draw of a name: the variant drawn at each level a read of it addresses, the // namedPick is one draw of a name: the variant drawn at each level a read of it addresses, the
// value each read there produced, keyed by the path from the name, and the table rows they // value each read there produced, keyed by the path from the name, and the table rows they
// pinned. // pinned.
@@ -135,7 +137,7 @@ func (sc renderScope) frameOf(scope *nameScope) *pickFrame {
return nil return nil
} }
func readName(s *generatorState, sc renderScope, a arm) readValue { func readName(s *drawstate.State, sc renderScope, a arm) readValue {
f := sc.frameOf(a.named.scope) f := sc.frameOf(a.named.scope)
if f == nil { if f == nil {
panic(internalError("name %q is read where no frame of its scope renders", a.named.name)) panic(internalError("name %q is read where no frame of its scope renders", a.named.name))
@@ -155,7 +157,7 @@ func readName(s *generatorState, sc renderScope, a arm) readValue {
} }
// drawRowOf draws the row a render of t reads: inside the pick's rows where t renders as part of one. // drawRowOf draws the row a render of t reads: inside the pick's rows where t renders as part of one.
func (sc renderScope) drawRowOf(s *generatorState, t *table) int { func (sc renderScope) drawRowOf(s *drawstate.State, t *table) int {
if sc.pick != nil { if sc.pick != nil {
return t.drawIn(s, &sc.pick.pins) return t.drawIn(s, &sc.pick.pins)
} }
@@ -170,7 +172,7 @@ func (sc renderScope) keeps(a arm) bool {
// readUnder reads a of t, which renders as part of the pick sc.pick at sc.pickKey, where a read of // readUnder reads a of t, which renders as part of the pick sc.pick at sc.pickKey, where a read of
// the name addresses it: once per pick, by its path from the name. // the name addresses it: once per pick, by its path from the name.
func readUnder(s *generatorState, t *template, sc renderScope, a arm) readValue { func readUnder(s *drawstate.State, t *template, sc renderScope, a arm) readValue {
p, key := sc.pick, underKey(sc.pickKey, a.path) p, key := sc.pick, underKey(sc.pickKey, a.path)
if r, done := p.memo.value[key]; done { if r, done := p.memo.value[key]; done {
return r return r
@@ -194,7 +196,7 @@ func underKey(prefix, path string) string {
// draw draws the path key names under p, its variant at key kept too, returning the leaf and the // draw draws the path key names under p, its variant at key kept too, returning the leaf and the
// pins its row is in. // pins its row is in.
func (p *namedPick) draw(s *generatorState, head node, steps []pathStep, levels []string, key string) (node, *pinSet) { func (p *namedPick) draw(s *drawstate.State, head node, steps []pathStep, levels []string, key string) (node, *pinSet) {
leaf, pins := drawSteps(s, head, steps, &p.pins, &p.memo, levels) leaf, pins := drawSteps(s, head, steps, &p.pins, &p.memo, levels)
if c, isChoice := leaf.(*choice); isChoice { if c, isChoice := leaf.(*choice); isChoice {
leaf = p.memo.variantOf(s, c, key) leaf = p.memo.variantOf(s, c, key)
@@ -203,7 +205,7 @@ func (p *namedPick) draw(s *generatorState, head node, steps []pathStep, levels
} }
// renderAt renders leaf, drawn at key into pins, as part of p, and keeps what it rendered. // renderAt renders leaf, drawn at key into pins, as part of p, and keeps what it rendered.
func (p *namedPick) renderAt(s *generatorState, leaf node, pins *pinSet, key string, sc renderScope) readValue { func (p *namedPick) renderAt(s *drawstate.State, leaf node, pins *pinSet, key string, sc renderScope) readValue {
sc = sc.at(leaf, pins) sc = sc.at(leaf, pins)
sc.pick, sc.pickKey = p, key sc.pick, sc.pickKey = p, key
r := renderLeaf(s, leaf, sc) r := renderLeaf(s, leaf, sc)
@@ -215,7 +217,7 @@ func (p *namedPick) renderAt(s *generatorState, leaf node, pins *pinSet, key str
} }
// variantOf is the variant of c drawn at level, drawn now where none was. // variantOf is the variant of c drawn at level, drawn now where none was.
func (m *drawMemo) variantOf(s *generatorState, c *choice, level string) node { func (m *drawMemo) variantOf(s *drawstate.State, c *choice, level string) node {
n, drew := m.variant[level] n, drew := m.variant[level]
if !drew { if !drew {
n = resolveChoice(s, c) n = resolveChoice(s, c)
+7 -5
View File
@@ -7,6 +7,8 @@ import (
"strings" "strings"
"unicode" "unicode"
"unicode/utf8" "unicode/utf8"
"github.com/larvit/fejkdata/internal/drawstate"
) )
// Column is one rendered column of a record. Value is the rendered text, which a // Column is one rendered column of a record. Value is the rendered text, which a
@@ -133,7 +135,7 @@ func (f *Generator) FakeRecord(path string) (*Record, error) {
var frames frameStack var frames frameStack
sc := renderScope{frames: &frames} sc := renderScope{frames: &frames}
if t, isTable := n.(*table); isTable { if t, isTable := n.(*table); isTable {
if n, sc.row, err = tableRecord(f.rand, t, tail); err != nil { if n, sc.row, err = tableRecord(f.draws, t, tail); err != nil {
return nil, fmt.Errorf("fejkdata: %s: %w", path, err) return nil, fmt.Errorf("fejkdata: %s: %w", path, err)
} }
} else if len(tail) > 0 { } else if len(tail) > 0 {
@@ -147,12 +149,12 @@ func (f *Generator) FakeRecord(path string) (*Record, error) {
if shape.err != nil { if shape.err != nil {
return nil, fmt.Errorf("fejkdata: %s %w", path, shape.err) return nil, fmt.Errorf("fejkdata: %s %w", path, shape.err)
} }
return renderRecord(f.rand, shape.template, shape.columns, sc), nil return renderRecord(f.draws, shape.template, shape.columns, sc), nil
} }
// tableRecord walks a path's tail from a table to the table whose row is the record, and that // tableRecord walks a path's tail from a table to the table whose row is the record, and that
// row, selected or drawn. // row, selected or drawn.
func tableRecord(s *generatorState, t *table, tail []string) (node, renderedRow, error) { func tableRecord(s *drawstate.State, t *table, tail []string) (node, renderedRow, error) {
n, pins, err := descend(s, t, tail) n, pins, err := descend(s, t, tail)
if err != nil { if err != nil {
return nil, renderedRow{}, err return nil, renderedRow{}, err
@@ -204,7 +206,7 @@ func (t *RecordTemplate) Fake() *Record {
t.g.mu.Lock() t.g.mu.Lock()
defer t.g.mu.Unlock() defer t.g.mu.Unlock()
var frames frameStack var frames frameStack
return renderRecord(t.g.rand, t.template, t.columns, renderScope{frames: &frames}) return renderRecord(t.g.draws, t.template, t.columns, renderScope{frames: &frames})
} }
// NewRecordTemplate compiles an inline record — a JSON object with a format and // NewRecordTemplate compiles an inline record — a JSON object with a format and
@@ -260,7 +262,7 @@ func recordOf(n node) (*template, []Column, error) {
// renderRecord draws each column once, in the name order recordOf fixed, as one render, so the // renderRecord draws each column once, in the name order recordOf fixed, as one render, so the
// columns read one pick of each name; a table's columns read sc's row. // columns read one pick of each name; a table's columns read sc's row.
func renderRecord(s *generatorState, t *template, columns []Column, sc renderScope) *Record { func renderRecord(s *drawstate.State, t *template, columns []Column, sc renderScope) *Record {
if mark := sc.renderFrame(t); mark >= 0 { if mark := sc.renderFrame(t); mark >= 0 {
defer sc.frames.pop(mark) defer sc.frames.pop(mark)
} }
+14 -12
View File
@@ -3,10 +3,12 @@ package fejkdata
import ( import (
"fmt" "fmt"
"strings" "strings"
"github.com/larvit/fejkdata/internal/drawstate"
) )
// rng is the randomness a builtin sample draws from; *rand.Rand satisfies it. The // rng is the randomness a builtin sample draws from; *rand.Rand satisfies it. The
// render path takes the concrete *generatorState instead, which keeps the draws of the walk // render path takes the concrete *drawstate.State instead, which keeps the draws of the walk
// drawing through it off the heap. // drawing through it off the heap.
type rng interface { type rng interface {
IntN(n int) int IntN(n int) int
@@ -25,7 +27,7 @@ func (f *Generator) Fake(path string) (string, error) {
return "", fmt.Errorf("fejkdata: %w", err) return "", fmt.Errorf("fejkdata: %w", err)
} }
f.loadShippedAt(segments) f.loadShippedAt(segments)
n, pins, err := descend(f.rand, &f.root, segments) n, pins, err := descend(f.draws, &f.root, segments)
if err != nil { if err != nil {
return "", fmt.Errorf("fejkdata: %s: %w", path, err) return "", fmt.Errorf("fejkdata: %s: %w", path, err)
} }
@@ -34,11 +36,11 @@ func (f *Generator) Fake(path string) (string, error) {
} }
var frames frameStack var frames frameStack
sc, _ := renderScope{frames: &frames}.at(n, &pins).enter(n, nil) sc, _ := renderScope{frames: &frames}.at(n, &pins).enter(n, nil)
return render(f.rand, n, sc), nil return render(f.draws, n, sc), nil
} }
// descend walks a caller's path to the node it names, returning the rows its leaf renders in. // descend walks a caller's path to the node it names, returning the rows its leaf renders in.
func descend(s *generatorState, root node, segments []string) (node, pinSet, error) { func descend(s *drawstate.State, root node, segments []string) (node, pinSet, error) {
// docs/decisions.md#a-path-is-walked-once-without-drawing-before-it-is-walked-for-real // docs/decisions.md#a-path-is-walked-once-without-drawing-before-it-is-walked-for-real
var buf [16]pathStep var buf [16]pathStep
steps, err := probePath(root, segments, buf[:0]) steps, err := probePath(root, segments, buf[:0])
@@ -51,7 +53,7 @@ func descend(s *generatorState, root node, segments []string) (node, pinSet, err
} }
// renderOnce renders n as one render, over frames of its own. // renderOnce renders n as one render, over frames of its own.
func renderOnce(s *generatorState, n node) string { func renderOnce(s *drawstate.State, n node) string {
var frames frameStack var frames frameStack
return render(s, n, renderScope{frames: &frames}) return render(s, n, renderScope{frames: &frames})
} }
@@ -59,7 +61,7 @@ func renderOnce(s *generatorState, n node) string {
// render evaluates a compiled node to a string. compile validates every node up // render evaluates a compiled node to a string. compile validates every node up
// front, so rendering a compiled tree cannot fail. // front, so rendering a compiled tree cannot fail.
// A child this switch renders is one renderEdges must list too, or the fences miss it. // A child this switch renders is one renderEdges must list too, or the fences miss it.
func render(s *generatorState, n node, sc renderScope) string { func render(s *drawstate.State, n node, sc renderScope) string {
switch n := n.(type) { switch n := n.(type) {
case *choice: case *choice:
return render(s, pick(s, n), sc) return render(s, pick(s, n), sc)
@@ -94,7 +96,7 @@ func render(s *generatorState, n node, sc renderScope) string {
// renderRepeat renders each iteration of t inside the name scopes rendering t, where a name bound // renderRepeat renders each iteration of t inside the name scopes rendering t, where a name bound
// outside t keeps its pick, and a name t binds picks again. // outside t keeps its pick, and a name t binds picks again.
func renderRepeat(s *generatorState, t *template, sc renderScope) string { func renderRepeat(s *drawstate.State, t *template, sc renderScope) string {
var b strings.Builder var b strings.Builder
b.Grow(t.repeat * (t.compiled.grow + len(t.separator))) b.Grow(t.repeat * (t.compiled.grow + len(t.separator)))
for i := 0; i < t.repeat; i++ { for i := 0; i < t.repeat; i++ {
@@ -114,14 +116,14 @@ func renderRepeat(s *generatorState, t *template, sc renderScope) string {
// pick selects one item. Uniform choices are O(1); weighted choices are an // pick selects one item. Uniform choices are O(1); weighted choices are an
// O(log n) search over precomputed cumulative weights. // O(log n) search over precomputed cumulative weights.
func pick(s *generatorState, c *choice) node { func pick(s *drawstate.State, c *choice) node {
if c.cum == nil { if c.cum == nil {
return c.items[s.IntN(len(c.items))] return c.items[s.IntN(len(c.items))]
} }
return c.items[pickCum(s, c.cum)] return c.items[pickCum(s, c.cum)]
} }
func expand(s *generatorState, t *template, sc renderScope) string { func expand(s *drawstate.State, t *template, sc renderScope) string {
var b strings.Builder var b strings.Builder
b.Grow(t.compiled.grow) b.Grow(t.compiled.grow)
for i := range t.compiled.ops { for i := range t.compiled.ops {
@@ -148,7 +150,7 @@ func expand(s *generatorState, t *template, sc renderScope) string {
// readField renders one arm of a token. A read of a name, or of a level a read of the name // readField renders one arm of a token. A read of a name, or of a level a read of the name
// rendering addresses, is kept in that name's pick; every other read draws afresh, so {word} // rendering addresses, is kept in that name's pick; every other read draws afresh, so {word}
// {word} draws twice and {p.a} {p.b} reads two draws of p. // {word} draws twice and {p.a} {p.b} reads two draws of p.
func readField(s *generatorState, t *template, sc renderScope, a arm) readValue { func readField(s *drawstate.State, t *template, sc renderScope, a arm) readValue {
switch { switch {
case a.kind == namedRead: case a.kind == namedRead:
return readName(s, sc, a) return readName(s, sc, a)
@@ -170,7 +172,7 @@ func readField(s *generatorState, t *template, sc renderScope, a arm) readValue
// renderLeaf draws and renders what a read lands on: null on a null item, or on a column that only // renderLeaf draws and renders what a read lands on: null on a null item, or on a column that only
// reads one reference or name whose read drew null. // reads one reference or name whose read drew null.
func renderLeaf(s *generatorState, n node, sc renderScope) readValue { func renderLeaf(s *drawstate.State, n node, sc renderScope) readValue {
n = resolveChoice(s, n) n = resolveChoice(s, n)
switch leaf := n.(type) { switch leaf := n.(type) {
case *nullItem: case *nullItem:
@@ -185,7 +187,7 @@ func renderLeaf(s *generatorState, n node, sc renderScope) readValue {
// resolveChoice resolves a choice to one variant. Nested choices unwrap too: a draw is one value, // resolveChoice resolves a choice to one variant. Nested choices unwrap too: a draw is one value,
// not another set to pick from. // not another set to pick from.
func resolveChoice(s *generatorState, n node) node { func resolveChoice(s *drawstate.State, n node) node {
for c, ok := n.(*choice); ok; c, ok = n.(*choice) { for c, ok := n.(*choice); ok; c, ok = n.(*choice) {
n = pick(s, c) n = pick(s, c)
} }
+4 -2
View File
@@ -8,6 +8,8 @@ import (
"slices" "slices"
"strconv" "strconv"
"strings" "strings"
"github.com/larvit/fejkdata/internal/drawstate"
) )
// FakeStruct fills the struct v points to. Each exported field tagged `fake:"…"` is a // FakeStruct fills the struct v points to. Each exported field tagged `fake:"…"` is a
@@ -27,7 +29,7 @@ func (f *Generator) FakeStruct(v any) error {
if err != nil { if err != nil {
return fmt.Errorf("fejkdata: %w", err) return fmt.Errorf("fejkdata: %w", err)
} }
shape.fill(f.rand, p.Elem()) shape.fill(f.draws, p.Elem())
return nil return nil
} }
@@ -316,7 +318,7 @@ func (k columnKind) holds(v proven) bool {
} }
// fill draws the record into v's tagged fields, then each nested struct as a record of its own. // fill draws the record into v's tagged fields, then each nested struct as a record of its own.
func (s *structShape) fill(state *generatorState, v reflect.Value) { func (s *structShape) fill(state *drawstate.State, v reflect.Value) {
if s.record != nil { if s.record != nil {
var frames frameStack var frames frameStack
for i, c := range renderRecord(state, s.record, s.columns, renderScope{frames: &frames}).columns { for i, c := range renderRecord(state, s.record, s.columns, renderScope{frames: &frames}).columns {
+6 -4
View File
@@ -7,6 +7,8 @@ import (
"strconv" "strconv"
"strings" "strings"
"sync" "sync"
"github.com/larvit/fejkdata/internal/drawstate"
) )
// table is a category whose rows come from a TSV beside it: the header names the // table is a category whose rows come from a TSV beside it: the header names the
@@ -470,20 +472,20 @@ func (t *table) builtLookup() *rowLookup {
return &t.lookup return &t.lookup
} }
func (t *table) drawRow(s *generatorState) int { func (t *table) drawRow(s *drawstate.State) int {
if t.cum == nil { if t.cum == nil {
return s.IntN(t.rowCount()) return s.IntN(t.rowCount())
} }
return pickCum(s, t.cum) return pickCum(s, t.cum)
} }
func pickCum(s *generatorState, cum []float64) int { func pickCum(s *drawstate.State, cum []float64) int {
x := s.Float64() * cum[len(cum)-1] x := s.Float64() * cum[len(cum)-1]
return min(sort.Search(len(cum), func(i int) bool { return cum[i] > x }), len(cum)-1) // x can round up to the total return min(sort.Search(len(cum), func(i int) bool { return cum[i] > x }), len(cum)-1) // x can round up to the total
} }
// drawUnder picks a row among those linked to parent row pr. // drawUnder picks a row among those linked to parent row pr.
func (t *table) drawUnder(s *generatorState, pr int) int { func (t *table) drawUnder(s *drawstate.State, pr int) int {
lookup := t.builtLookup() lookup := t.builtLookup()
k := t.parentT.cell(pr, t.parentT.keyIndex) k := t.parentT.cell(pr, t.parentT.keyIndex)
rows := lookup.rowsByParent[k] rows := lookup.rowsByParent[k]
@@ -496,7 +498,7 @@ func (t *table) drawUnder(s *generatorState, pr int) int {
// drawIn is the row of t a path or a named pick reads: the one pinned in p, else one drawn inside the nearest // drawIn is the row of t a path or a named pick reads: the one pinned in p, else one drawn inside the nearest
// pinned ancestor — its parent drawn inside that first where the ancestor is further up — or // pinned ancestor — its parent drawn inside that first where the ancestor is further up — or
// over the whole table, and pinned with its ancestors. // over the whole table, and pinned with its ancestors.
func (t *table) drawIn(s *generatorState, p *pinSet) int { func (t *table) drawIn(s *drawstate.State, p *pinSet) int {
if r, ok := p.pinned(t); ok { if r, ok := p.pinned(t); ok {
return r return r
} }
+3 -1
View File
@@ -4,6 +4,8 @@ import (
"fmt" "fmt"
"strings" "strings"
"unicode" "unicode"
"github.com/larvit/fejkdata/internal/drawstate"
) )
// transforms are the builtins that rewrite one operand's value; they nest, so // transforms are the builtins that rewrite one operand's value; they nest, so
@@ -65,7 +67,7 @@ func transformPrep(outer func(string) string) func([]string) callFn {
if err != nil { if err != nil {
panic(internalError("transform arg %q reached prep unvalidated: %v", a[0], err)) panic(internalError("transform arg %q reached prep unvalidated: %v", a[0], err))
} }
return func(_ *generatorState, _ string, operands []string) string { return func(_ *drawstate.State, _ string, operands []string) string {
v := operands[0] v := operands[0]
for i := len(chain) - 1; i >= 0; i-- { for i := len(chain) - 1; i >= 0; i-- {
v = chain[i](v) v = chain[i](v)