Move the seeded randomness and the {seq()} counters into internal/drawstate
This commit is contained in:
+11
-9
@@ -7,6 +7,8 @@ import (
|
||||
"math"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/larvit/fejkdata/internal/drawstate"
|
||||
)
|
||||
|
||||
// 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")},
|
||||
"base64": {arity: 1, checkArgs: posIntArg, prep: func(a []string) callFn {
|
||||
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))
|
||||
}
|
||||
}},
|
||||
"int": {arity: 2, checkArgs: intRangeArgs, prep: func(a []string) callFn {
|
||||
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 {
|
||||
return printing(token, prints, bounded(float64(atoi(a[0])), float64(atoi(a[1])), true))
|
||||
}},
|
||||
"float": {arity: 3, checkArgs: floatArgs, prep: func(a []string) callFn {
|
||||
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)
|
||||
}
|
||||
}, 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 {
|
||||
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},
|
||||
"time": {arity: -1, checkArgs: timeArg, prep: timePrep},
|
||||
@@ -73,8 +75,8 @@ var builtins = withTransforms(map[string]builtin{
|
||||
if len(a) == 1 {
|
||||
key = a[0]
|
||||
}
|
||||
return func(s *generatorState, _ string, _ []string) string {
|
||||
return strconv.FormatUint(s.next(key), 10)
|
||||
return func(s *drawstate.State, _ string, _ []string) string {
|
||||
return strconv.FormatUint(s.Next(key), 10)
|
||||
}
|
||||
}, prints: DataTypeInteger, proveNumber: func(token string, prints DataType, _ []string) proven {
|
||||
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.
|
||||
func derive(f func(emitted string) string) 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 {
|
||||
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 {
|
||||
return func(a []string) callFn {
|
||||
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) }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,8 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"unicode"
|
||||
|
||||
"github.com/larvit/fejkdata/internal/drawstate"
|
||||
)
|
||||
|
||||
// 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)
|
||||
dp := calcDecimals(args)
|
||||
return func(_ *generatorState, _ string, operands []string) string {
|
||||
return func(_ *drawstate.State, _ string, operands []string) string {
|
||||
return formatFloat(placed.eval(operands), dp)
|
||||
}
|
||||
}
|
||||
|
||||
+15
-28
@@ -6,11 +6,12 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"math/rand/v2"
|
||||
"os"
|
||||
"reflect"
|
||||
"sort"
|
||||
"sync"
|
||||
|
||||
"github.com/larvit/fejkdata/internal/drawstate"
|
||||
)
|
||||
|
||||
// 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
|
||||
// from one goroutine.
|
||||
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.
|
||||
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
|
||||
records map[node]recordShape
|
||||
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 {
|
||||
seed uint64
|
||||
seeded bool
|
||||
@@ -97,11 +86,11 @@ func New(opts ...Option) (*Generator, error) {
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("fejkdata: %w", err)
|
||||
}
|
||||
rng, err := newRand(c.seed, c.seeded)
|
||||
seed, err := c.drawSeed()
|
||||
if err != nil {
|
||||
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.
|
||||
@@ -231,18 +220,16 @@ func join(prefix, name string) string {
|
||||
// randomBytes seeds an unseeded generator.
|
||||
var randomBytes = crand.Read
|
||||
|
||||
func newRand(seed uint64, seeded bool) (*generatorState, error) {
|
||||
var r *rand.Rand
|
||||
if seeded {
|
||||
r = rand.New(rand.NewPCG(seed, seed^0x9e3779b97f4a7c15))
|
||||
} 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:])))
|
||||
// drawSeed is the seed WithSeed gave, or one read from the system's entropy.
|
||||
func (c config) drawSeed() (uint64, error) {
|
||||
if c.seeded {
|
||||
return c.seed, nil
|
||||
}
|
||||
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 {
|
||||
|
||||
@@ -3,6 +3,8 @@ package fejkdata
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/larvit/fejkdata/internal/drawstate"
|
||||
)
|
||||
|
||||
// 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
|
||||
// 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
|
||||
// (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
|
||||
@@ -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
|
||||
// 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.
|
||||
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,
|
||||
// or a builtin already prepared with its args.
|
||||
|
||||
@@ -20,7 +20,7 @@ type Template struct {
|
||||
func (t *Template) Fake() string {
|
||||
t.g.mu.Lock()
|
||||
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
|
||||
|
||||
@@ -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,6 +4,8 @@ import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/larvit/fejkdata/internal/drawstate"
|
||||
)
|
||||
|
||||
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))
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
func timePrep(a []string) callFn {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@ package fejkdata
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/larvit/fejkdata/internal/drawstate"
|
||||
)
|
||||
|
||||
// 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
|
||||
// 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.
|
||||
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
|
||||
for _, st := range steps {
|
||||
if c, ok := n.(*choice); ok {
|
||||
@@ -458,7 +460,7 @@ func drawSteps(s *generatorState, n node, steps []pathStep, pins *pinSet, memo *
|
||||
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 {
|
||||
case stepSelect:
|
||||
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))
|
||||
}
|
||||
|
||||
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 {
|
||||
return resolveChoice(s, c)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
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
|
||||
// value each read there produced, keyed by the path from the name, and the table rows they
|
||||
// pinned.
|
||||
@@ -135,7 +137,7 @@ func (sc renderScope) frameOf(scope *nameScope) *pickFrame {
|
||||
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)
|
||||
if f == nil {
|
||||
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.
|
||||
func (sc renderScope) drawRowOf(s *generatorState, t *table) int {
|
||||
func (sc renderScope) drawRowOf(s *drawstate.State, t *table) int {
|
||||
if sc.pick != nil {
|
||||
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
|
||||
// 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)
|
||||
if r, done := p.memo.value[key]; done {
|
||||
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
|
||||
// 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)
|
||||
if c, isChoice := leaf.(*choice); isChoice {
|
||||
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.
|
||||
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.pick, sc.pickKey = p, key
|
||||
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.
|
||||
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]
|
||||
if !drew {
|
||||
n = resolveChoice(s, c)
|
||||
|
||||
@@ -7,6 +7,8 @@ import (
|
||||
"strings"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/larvit/fejkdata/internal/drawstate"
|
||||
)
|
||||
|
||||
// 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
|
||||
sc := renderScope{frames: &frames}
|
||||
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)
|
||||
}
|
||||
} else if len(tail) > 0 {
|
||||
@@ -147,12 +149,12 @@ func (f *Generator) FakeRecord(path string) (*Record, error) {
|
||||
if shape.err != nil {
|
||||
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
|
||||
// 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)
|
||||
if err != nil {
|
||||
return nil, renderedRow{}, err
|
||||
@@ -204,7 +206,7 @@ func (t *RecordTemplate) Fake() *Record {
|
||||
t.g.mu.Lock()
|
||||
defer t.g.mu.Unlock()
|
||||
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
|
||||
@@ -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
|
||||
// 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 {
|
||||
defer sc.frames.pop(mark)
|
||||
}
|
||||
|
||||
@@ -3,10 +3,12 @@ package fejkdata
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/larvit/fejkdata/internal/drawstate"
|
||||
)
|
||||
|
||||
// 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.
|
||||
type rng interface {
|
||||
IntN(n int) int
|
||||
@@ -25,7 +27,7 @@ func (f *Generator) Fake(path string) (string, error) {
|
||||
return "", fmt.Errorf("fejkdata: %w", err)
|
||||
}
|
||||
f.loadShippedAt(segments)
|
||||
n, pins, err := descend(f.rand, &f.root, segments)
|
||||
n, pins, err := descend(f.draws, &f.root, segments)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("fejkdata: %s: %w", path, err)
|
||||
}
|
||||
@@ -34,11 +36,11 @@ func (f *Generator) Fake(path string) (string, error) {
|
||||
}
|
||||
var frames frameStack
|
||||
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.
|
||||
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
|
||||
var buf [16]pathStep
|
||||
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.
|
||||
func renderOnce(s *generatorState, n node) string {
|
||||
func renderOnce(s *drawstate.State, n node) string {
|
||||
var frames frameStack
|
||||
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
|
||||
// front, so rendering a compiled tree cannot fail.
|
||||
// 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) {
|
||||
case *choice:
|
||||
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
|
||||
// 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
|
||||
b.Grow(t.repeat * (t.compiled.grow + len(t.separator)))
|
||||
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
|
||||
// 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 {
|
||||
return c.items[s.IntN(len(c.items))]
|
||||
}
|
||||
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
|
||||
b.Grow(t.compiled.grow)
|
||||
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
|
||||
// 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.
|
||||
func readField(s *generatorState, t *template, sc renderScope, a arm) readValue {
|
||||
func readField(s *drawstate.State, t *template, sc renderScope, a arm) readValue {
|
||||
switch {
|
||||
case a.kind == namedRead:
|
||||
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
|
||||
// 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)
|
||||
switch leaf := n.(type) {
|
||||
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,
|
||||
// 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) {
|
||||
n = pick(s, c)
|
||||
}
|
||||
|
||||
@@ -8,6 +8,8 @@ import (
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/larvit/fejkdata/internal/drawstate"
|
||||
)
|
||||
|
||||
// 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 {
|
||||
return fmt.Errorf("fejkdata: %w", err)
|
||||
}
|
||||
shape.fill(f.rand, p.Elem())
|
||||
shape.fill(f.draws, p.Elem())
|
||||
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.
|
||||
func (s *structShape) fill(state *generatorState, v reflect.Value) {
|
||||
func (s *structShape) fill(state *drawstate.State, v reflect.Value) {
|
||||
if s.record != nil {
|
||||
var frames frameStack
|
||||
for i, c := range renderRecord(state, s.record, s.columns, renderScope{frames: &frames}).columns {
|
||||
|
||||
@@ -7,6 +7,8 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/larvit/fejkdata/internal/drawstate"
|
||||
)
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
func (t *table) drawRow(s *generatorState) int {
|
||||
func (t *table) drawRow(s *drawstate.State) int {
|
||||
if t.cum == nil {
|
||||
return s.IntN(t.rowCount())
|
||||
}
|
||||
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]
|
||||
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.
|
||||
func (t *table) drawUnder(s *generatorState, pr int) int {
|
||||
func (t *table) drawUnder(s *drawstate.State, pr int) int {
|
||||
lookup := t.builtLookup()
|
||||
k := t.parentT.cell(pr, t.parentT.keyIndex)
|
||||
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
|
||||
// pinned ancestor — its parent drawn inside that first where the ancestor is further up — or
|
||||
// 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 {
|
||||
return r
|
||||
}
|
||||
|
||||
+3
-1
@@ -4,6 +4,8 @@ import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"unicode"
|
||||
|
||||
"github.com/larvit/fejkdata/internal/drawstate"
|
||||
)
|
||||
|
||||
// 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 {
|
||||
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]
|
||||
for i := len(chain) - 1; i >= 0; i-- {
|
||||
v = chain[i](v)
|
||||
|
||||
Reference in New Issue
Block a user