Rename ftoken to scanUnit and session to generatorState

This commit is contained in:
2026-09-29 12:31:52 +02:00
committed by Lillem4n
parent aa2890b479
commit 14194a775d
14 changed files with 56 additions and 59 deletions
+9 -9
View File
@@ -40,19 +40,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 *session, _ string, _ []string) string {
return func(s *generatorState, _ 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 *session, _ string, _ []string) string { return strconv.Itoa(lo + s.IntN(span)) }
return func(s *generatorState, _ 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 *session, _ string, _ []string) string {
return func(s *generatorState, _ string, _ []string) string {
return formatFloat(lo+s.Float64()*(hi-lo), dp)
}
}, prints: DataTypeNumber, proveNumber: func(token string, _ DataType, a []string) proven {
@@ -60,12 +60,12 @@ var builtins = withTransforms(map[string]builtin{
}},
"iban": {arity: 1, checkArgs: ibanArg, prep: func(a []string) callFn {
cc := a[0]
return func(s *session, _ string, _ []string) string { return iban(s, cc) }
return func(s *generatorState, _ string, _ []string) string { return iban(s, cc) }
}},
"date": {arity: -1, checkArgs: dateArgs, prep: datePrep},
"time": {arity: -1, checkArgs: timeArg, prep: timePrep},
"calc": {arity: -1, checkArgs: checkCalc, prep: calcPrep, operands: calcOperands},
// seq is the one stateful builtin: a per-session counter from 1, advancing on
// seq is the one stateful builtin: a per-generator counter from 1, advancing on
// each call. An optional name selects an independent counter; no name uses the
// default one. Deterministic by construction, so seeded output stays stable.
"seq": {arity: -1, checkArgs: seqArg, prep: func(a []string) callFn {
@@ -73,7 +73,7 @@ var builtins = withTransforms(map[string]builtin{
if len(a) == 1 {
key = a[0]
}
return func(s *session, _ string, _ []string) string {
return func(s *generatorState, _ string, _ []string) string {
return strconv.FormatUint(s.next(key), 10)
}
}, prints: DataTypeInteger, proveNumber: func(token string, prints DataType, _ []string) proven {
@@ -86,18 +86,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(_ *session, emitted string, _ []string) string { return f(emitted) }
return func(_ *generatorState, emitted string, _ []string) string { return f(emitted) }
}
}
func sample(f func(rng) string) func([]string) callFn {
return func([]string) callFn {
return func(s *session, _ string, _ []string) string { return f(s) }
return func(s *generatorState, _ 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 *session, _ string, _ []string) string { return randChars(s, n, alphabet) }
return func(s *generatorState, _ string, _ []string) string { return randChars(s, n, alphabet) }
}
}
+5 -5
View File
@@ -102,10 +102,10 @@ func TestBuiltinChecksums(t *testing.T) {
}
}
// TestBuiltinSeqPerSession pins seq's contract: a counter from 1, advancing on
// TestBuiltinSeqPerGenerator pins seq's contract: a counter from 1, advancing on
// each call, named counters independent, and the whole thing scoped to one
// session so a fresh Generator restarts at 1.
func TestBuiltinSeqPerSession(t *testing.T) {
// generatorState so a fresh Generator restarts at 1.
func TestBuiltinSeqPerGenerator(t *testing.T) {
f := engine(1)
for i := 1; i <= 5; i++ {
if got := mustRender(t, f, `"{seq()}"`); got != strconv.Itoa(i) {
@@ -122,9 +122,9 @@ func TestBuiltinSeqPerSession(t *testing.T) {
if got := mustRender(t, engine(2), `{"format":"{seq()}","repeat":3,"separator":","}`); got != "1,2,3" {
t.Fatalf("repeat seq = %q, want 1,2,3", got)
}
// ...and a fresh session restarts from 1.
// ...and a fresh generator restarts from 1.
if got := mustRender(t, engine(2), `"{seq()}"`); got != "1" {
t.Fatalf("new session seq = %q, want 1", got)
t.Fatalf("new generator seq = %q, want 1", got)
}
}
+1 -1
View File
@@ -199,7 +199,7 @@ func calcPrep(args []string) callFn {
}
placed := indexVars(expr, at)
dp := calcDecimals(args)
return func(_ *session, _ string, operands []string) string {
return func(_ *generatorState, _ string, operands []string) string {
return formatFloat(placed.eval(operands), dp)
}
}
+6 -6
View File
@@ -34,20 +34,20 @@ type Generator struct {
// table's row lookup, built under its sync.Once, so List, NewTemplate and
// NewRecordTemplate read it without mu.
mu sync.Mutex
rand *session
rand *generatorState
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
}
// session is one generator's mutable render state: the seeded rng plus the {seq()}
// generatorState is one generator's state across its renders: the seeded rng plus the {seq()}
// counters.
type session struct {
type generatorState struct {
*rand.Rand
counters map[string]uint64
}
func (s *session) next(key string) uint64 {
func (s *generatorState) next(key string) uint64 {
s.counters[key]++
return s.counters[key]
}
@@ -228,7 +228,7 @@ func join(prefix, name string) string {
// randomBytes seeds an unseeded generator.
var randomBytes = crand.Read
func newRand(seed uint64, seeded bool) (*session, error) {
func newRand(seed uint64, seeded bool) (*generatorState, error) {
var r *rand.Rand
if seeded {
r = rand.New(rand.NewPCG(seed, seed^0x9e3779b97f4a7c15))
@@ -239,7 +239,7 @@ func newRand(seed uint64, seeded bool) (*session, error) {
}
r = rand.New(rand.NewPCG(binary.LittleEndian.Uint64(b[:8]), binary.LittleEndian.Uint64(b[8:])))
}
return &session{Rand: r, counters: map[string]uint64{}}, nil
return &generatorState{Rand: r, counters: map[string]uint64{}}, nil
}
func internalError(format string, a ...any) string {
+8 -8
View File
@@ -5,9 +5,9 @@ import (
"strings"
)
// ftoken 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
// {…} token.
type ftoken struct {
type scanUnit struct {
isToken bool // a {…} body, else a literal rune
char rune
body string
@@ -16,10 +16,10 @@ type ftoken struct {
// eachToken scans a format string once and calls fn for each unit, the single
// source of truth for how braces are read: "{{" and "}}" are literal braces, a "{"
// opens a token that must reach its "}", and a lone "}" is an error.
func eachToken(format string, fn func(ftoken) error) error {
func eachToken(format string, fn func(scanUnit) error) error {
rs := []rune(format)
for i := 0; i < len(rs); i++ {
var t ftoken
var t scanUnit
switch c := rs[i]; c {
case '{':
if i+1 < len(rs) && rs[i+1] == '{' {
@@ -85,7 +85,7 @@ func parseFormat(format string) ([]formatToken, error) {
lit.Reset()
}
}
err := eachToken(format, func(t ftoken) error {
err := eachToken(format, func(t scanUnit) error {
if !t.isToken {
lit.WriteRune(t.char)
return nil
@@ -110,11 +110,11 @@ func parseFormat(format string) ([]formatToken, error) {
}
// builtin is a format-string function invoked as {name(args)}. It receives the
// session (its rng, and the {seq()} counters), the output emitted so far in the
// generatorState (its rng, and the {seq()} counters), 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-session counter state, which is itself deterministic. arity is the exact arg
// per-generator counter state, which is itself deterministic. arity is the exact arg
// count, or -1 for variadic (then checkArgs does all the validation).
type builtin struct {
arity int
@@ -366,7 +366,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 *session, emitted string, operands []string) string
type callFn func(s *generatorState, 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. compile builds these so render never
+2 -2
View File
@@ -135,13 +135,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 *session, _ string, _ []string) string {
return func(s *generatorState, _ 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 *session, _ string, _ []string) string {
return func(s *generatorState, _ string, _ []string) string {
return time.Unix(int64(s.IntN(86400)), 0).UTC().Format(layout)
}
}
+1 -1
View File
@@ -353,7 +353,7 @@ func (w *pathProbe) readRow(t *table, tail []string, r tableRoute) error {
// Where pathCheck walks every variant of a choice, it walks the one drawn; at a table it selects
// as pathCheck does and draws the row the route asks for.
type pathDraw struct {
s *session
s *generatorState
pins *pinSet
held *hold
a *arm
+2 -2
View File
@@ -76,7 +76,7 @@ func spellPins(p *pinSet) string {
return "row " + strings.Join(rows, ", ")
}
func renderRoot(s *session, t *template, trace renderTrace) {
func renderRoot(s *generatorState, t *template, trace renderTrace) {
set := holdSet{trace: trace}
sc := renderScope{set: &set}
switch {
@@ -96,7 +96,7 @@ func renderRoot(s *session, t *template, trace renderTrace) {
}
}
func renderRecordRoot(s *session, t *template, columns []Column, trace renderTrace) {
func renderRecordRoot(s *generatorState, t *template, columns []Column, trace renderTrace) {
set := holdSet{trace: trace}
sc := renderScope{set: &set}
if t.site.row == formatRow {
+2 -2
View File
@@ -152,7 +152,7 @@ func (f *Generator) FakeRecord(path string) (*Record, error) {
// tableRecord walks a path's tail from a table to the table whose row is the record,
// pinning the rows it selects or draws.
func tableRecord(s *session, t *table, tail []string, sc renderScope) (node, error) {
func tableRecord(s *generatorState, t *table, tail []string, sc renderScope) (node, error) {
n, err := descend(s, t, tail, sc)
if err != nil {
return nil, err
@@ -261,7 +261,7 @@ func recordOf(n node) (*template, []Column, error) {
// renderRecord draws each column once, in the name order recordOf fixed, as one render
// over sc's hold; a table's columns read the row pinned there.
func renderRecord(s *session, t *template, columns []Column, sc renderScope) *Record {
func renderRecord(s *generatorState, t *template, columns []Column, sc renderScope) *Record {
sc = sc.in(t)
if t.site.row == formatRow {
sc = sc.at(t.site.table.rowNode, &sc.groupHold().pins)
+12 -12
View File
@@ -6,7 +6,7 @@ import (
)
// rng is the randomness a builtin sample draws from; *rand.Rand satisfies it. The
// render path takes the concrete *session instead, which keeps the hold set of the walk
// render path takes the concrete *generatorState instead, which keeps the hold set of the walk
// drawing through it off the heap.
type rng interface {
IntN(n int) int
@@ -38,7 +38,7 @@ func (f *Generator) Fake(path string) (string, error) {
// descend walks a caller's path to the node it names, pinning in sc the rows it
// selects or draws.
func descend(s *session, root node, segments []string, sc renderScope) (node, error) {
func descend(s *generatorState, root node, segments []string, sc renderScope) (node, error) {
// docs/decisions.md#a-path-is-walked-once-without-drawing-before-it-is-walked-for-real
if _, err := (&pathProbe{}).walk(root, segments); err != nil {
return nil, err
@@ -47,7 +47,7 @@ func descend(s *session, root node, segments []string, sc renderScope) (node, er
}
// renderOnce renders n as one render, over a hold set of its own.
func renderOnce(s *session, n node) string {
func renderOnce(s *generatorState, n node) string {
var set holdSet
return render(s, n, renderScope{set: &set})
}
@@ -56,7 +56,7 @@ func renderOnce(s *session, n node) string {
// front, so rendering a compiled tree cannot fail. sc holds the reference draws the
// render shares; each repeat iteration renders over a hold set of its own.
// A child this switch renders is one renderEdges must list too, or the fences miss it.
func render(s *session, n node, sc renderScope) string {
func render(s *generatorState, n node, sc renderScope) string {
switch n := n.(type) {
case *choice:
return render(s, pick(s, n), sc)
@@ -99,7 +99,7 @@ func render(s *session, n node, sc renderScope) string {
// render's loop, its hold set would move to the heap.
//
//go:noinline
func expandAnew(s *session, t *template) string {
func expandAnew(s *generatorState, t *template) string {
var set holdSet
return expand(s, t, renderScope{set: &set})
}
@@ -107,7 +107,7 @@ func expandAnew(s *session, t *template) string {
// 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.
func pick(s *session, c *choice) node {
func pick(s *generatorState, c *choice) node {
if c.cum == nil {
return c.items[s.IntN(len(c.items))]
}
@@ -116,7 +116,7 @@ func pick(s *session, c *choice) node {
// expand renders a template's compiled ops. compile validated every token, so this
// cannot fail.
func expand(s *session, t *template, sc renderScope) string {
func expand(s *generatorState, t *template, sc renderScope) string {
var b strings.Builder
b.Grow(t.compiled.grow)
// One draw per held name, for this expansion only: a nested template and each
@@ -157,7 +157,7 @@ func expand(s *session, t *template, sc renderScope) string {
// reads — is drawn once and kept in held, so {place.postal-code} and {place.locality}
// read one row, either read twice gives one value, and a shown operand is the operand
// computed. Every other name is drawn afresh, so {word} {word} still draws twice.
func readField(s *session, t *template, held *hold, sc renderScope, a arm) readValue {
func readField(s *generatorState, t *template, held *hold, sc renderScope, a arm) readValue {
if isRef(a.head) && len(a.tail) > 0 {
return readReference(s, t, sc, a)
}
@@ -178,7 +178,7 @@ func readField(s *session, t *template, held *hold, sc renderScope, a arm) readV
// readReference reads a reference path, kept in the render's hold for its group, so its draw
// spans the render.
func readReference(s *session, t *template, sc renderScope, a arm) readValue {
func readReference(s *generatorState, t *template, sc renderScope, a arm) readValue {
if sc.set.trace != nil {
traceRead(sc.set.trace, t, sc, a)
}
@@ -186,7 +186,7 @@ func readReference(s *session, t *template, sc renderScope, a arm) readValue {
return readHeld(s, t, &group.hold, &group.pins, sc, a)
}
func readHeld(s *session, t *template, held *hold, pins *pinSet, sc renderScope, a arm) readValue {
func readHeld(s *generatorState, t *template, held *hold, pins *pinSet, sc renderScope, a arm) readValue {
if r, done := held.value[a.path]; done {
return r
}
@@ -215,7 +215,7 @@ func traceRead(trace renderTrace, t *template, sc renderScope, a arm) {
// renderLeaf draws and renders what a read lands on: null on a null item, or on a column of one
// reference alone whose read drew null.
func renderLeaf(s *session, n node, sc renderScope) readValue {
func renderLeaf(s *generatorState, n node, sc renderScope) readValue {
n = resolveChoice(s, n)
switch leaf := n.(type) {
case *nullItem:
@@ -231,7 +231,7 @@ func renderLeaf(s *session, n node, sc renderScope) readValue {
// resolveChoice resolves a choice to one variant, so a held head is a concrete node the
// rest of the expansion shares. Nested choices unwrap too: a draw is one value, not
// another set to pick from.
func resolveChoice(s *session, n node) node {
func resolveChoice(s *generatorState, n node) node {
for c, ok := n.(*choice); ok; c, ok = n.(*choice) {
n = pick(s, c)
}
+3 -3
View File
@@ -349,10 +349,10 @@ func (p *valueProof) checkField(label string, ft reflect.Type, column node) erro
}
// fill draws the record into v's tagged fields, then each nested struct as a record of its own.
func (s *structShape) fill(sess *session, v reflect.Value) {
func (s *structShape) fill(state *generatorState, v reflect.Value) {
if s.record != nil {
var set holdSet
for i, c := range renderRecord(sess, s.record, s.columns, renderScope{set: &set}).columns {
for i, c := range renderRecord(state, s.record, s.columns, renderScope{set: &set}).columns {
setColumn(fieldAt(v, s.fieldIndexes[i]), c)
}
}
@@ -364,7 +364,7 @@ func (s *structShape) fill(sess *session, v reflect.Value) {
}
field = field.Elem()
}
n.shape.fill(sess, field)
n.shape.fill(state, field)
}
}
+4 -4
View File
@@ -472,20 +472,20 @@ func (t *table) builtLookup() *rowLookup {
}
// drawRow picks a row over the whole table.
func (t *table) drawRow(s *session) int {
func (t *table) drawRow(s *generatorState) int {
if t.cum == nil {
return s.IntN(t.rowCount())
}
return pickCum(s, t.cum)
}
func pickCum(s *session, cum []float64) int {
func pickCum(s *generatorState, 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 *session, pr int) int {
func (t *table) drawUnder(s *generatorState, pr int) int {
lookup := t.builtLookup()
k := t.parentT.cell(pr, t.parentT.keyIndex)
rows := lookup.rowsByParent[k]
@@ -498,7 +498,7 @@ func (t *table) drawUnder(s *session, pr int) int {
// drawIn is the render's row of t: 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 *session, p *pinSet) int {
func (t *table) drawIn(s *generatorState, p *pinSet) int {
if r, ok := p.pinned(t); ok {
return r
}
-3
View File
@@ -31,9 +31,6 @@ on 2026-09-29 a third time (Navigation 7.0, Locality 5.6, Shape 6.3, Self-suffic
This round, in order:
- Rename `ftoken` to `scanUnit`, beside `formatToken`, and `session` to
`generatorState`: three seats confused the token types, and both architects read
`session` as per-render state.
- State at `pinSet.pin` that its caller proved r sits inside the nearest pinned
ancestor, which `pinRow` and `drawIn` do.
- Delete the prose two or more seats found empty: `loadData`'s comment, which also names
+1 -1
View File
@@ -70,7 +70,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(_ *session, _ string, operands []string) string {
return func(_ *generatorState, _ string, operands []string) string {
v := operands[0]
for i := len(chain) - 1; i >= 0; i-- {
v = chain[i](v)