Hold a calc operand's draw for the expansion
A field the format rendered and a calc read was drawn twice, so the operand shown could differ from the operand computed — the same disagreement the dotted-path rule already fences, in its plainest spelling. The README carried it as a caveat, which is what a rule like this exists to remove. A calc operand now joins the names an expansion holds, so it is drawn once and every later read of it — the calc, and any bare token spelling it — sees that draw. The hold stays per expansion: each repeat iteration and each nested template draws its own. expand reads a calc's operands before the call and hands over their values, so a builtin takes (emitted, operands) rather than the sibling fields, and the evaluator indexes that slice instead of walking the node tree. That is what keeps the draws a local. Handing them to a builtin instead lets them escape through an indirect call, which put two maps on the heap for every held format, calc or not: BenchmarkBound went 56 B/5 allocs -> 744 B/10. Measured against main: Bound 359 -> 349 ns at 56 B/5 allocs, unchanged; Calc 416 -> 577 ns and one more alloc, which is what the correlation costs. Validation is unchanged: t.bound still carries only the levels a dotted token reads, so the overlap fences fence exactly what they did. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+10
-10
@@ -31,35 +31,35 @@ var builtins = map[string]builtin{
|
|||||||
"ulid": {arity: 0, prep: generate(ulid)},
|
"ulid": {arity: 0, prep: generate(ulid)},
|
||||||
"nanoid": {arity: 1, check: posIntArg, prep: func(a []string) callFn {
|
"nanoid": {arity: 1, check: posIntArg, prep: func(a []string) callFn {
|
||||||
n := atoi(a[0])
|
n := atoi(a[0])
|
||||||
return func(s *session, _ string, _ map[string]node) string { return nanoid(s, n) }
|
return func(s *session, _ string, _ []string) string { return nanoid(s, n) }
|
||||||
}},
|
}},
|
||||||
"hex": {arity: 1, check: posIntArg, prep: func(a []string) callFn {
|
"hex": {arity: 1, check: posIntArg, prep: func(a []string) callFn {
|
||||||
n := atoi(a[0])
|
n := atoi(a[0])
|
||||||
return func(s *session, _ string, _ map[string]node) string { return randHex(s, n) }
|
return func(s *session, _ string, _ []string) string { return randHex(s, n) }
|
||||||
}},
|
}},
|
||||||
"base64": {arity: 1, check: posIntArg, prep: func(a []string) callFn {
|
"base64": {arity: 1, check: posIntArg, prep: func(a []string) callFn {
|
||||||
n := atoi(a[0])
|
n := atoi(a[0])
|
||||||
return func(s *session, _ string, _ map[string]node) string {
|
return func(s *session, _ string, _ []string) string {
|
||||||
return base64.StdEncoding.EncodeToString(randBytes(s, n))
|
return base64.StdEncoding.EncodeToString(randBytes(s, n))
|
||||||
}
|
}
|
||||||
}},
|
}},
|
||||||
"int": {arity: 2, check: intRangeArgs, prep: func(a []string) callFn {
|
"int": {arity: 2, check: 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 *session, _ string, _ map[string]node) string { return strconv.Itoa(lo + s.IntN(span)) }
|
return func(s *session, _ string, _ []string) string { return strconv.Itoa(lo + s.IntN(span)) }
|
||||||
}},
|
}},
|
||||||
"float": {arity: 3, check: floatArgs, prep: func(a []string) callFn {
|
"float": {arity: 3, check: floatArgs, prep: func(a []string) callFn {
|
||||||
lo, _ := strconv.ParseFloat(a[0], 64)
|
lo, _ := strconv.ParseFloat(a[0], 64)
|
||||||
hi, _ := strconv.ParseFloat(a[1], 64)
|
hi, _ := strconv.ParseFloat(a[1], 64)
|
||||||
dp := atoi(a[2])
|
dp := atoi(a[2])
|
||||||
return func(s *session, _ string, _ map[string]node) string {
|
return func(s *session, _ string, _ []string) string {
|
||||||
return strconv.FormatFloat(lo+s.Float64()*(hi-lo), 'f', dp, 64)
|
return strconv.FormatFloat(lo+s.Float64()*(hi-lo), 'f', dp, 64)
|
||||||
}
|
}
|
||||||
}},
|
}},
|
||||||
"iban": {arity: 1, check: ibanArg, prep: func(a []string) callFn {
|
"iban": {arity: 1, check: ibanArg, prep: func(a []string) callFn {
|
||||||
cc := a[0]
|
cc := a[0]
|
||||||
return func(s *session, _ string, _ map[string]node) string { return iban(s, cc) }
|
return func(s *session, _ string, _ []string) string { return iban(s, cc) }
|
||||||
}},
|
}},
|
||||||
// calc is the one builtin that reads the sibling fields (to render its operands);
|
// calc is the one builtin that names operands (expand reads them for it);
|
||||||
// every other ignores them. 1 or 2 args: the expression and an optional decimals.
|
// every other ignores them. 1 or 2 args: the expression and an optional decimals.
|
||||||
"calc": {arity: -1, check: checkCalc, prep: calcPrep},
|
"calc": {arity: -1, check: checkCalc, prep: calcPrep},
|
||||||
// seq is the one stateful builtin: a per-session counter from 1, advancing on
|
// seq is the one stateful builtin: a per-session counter from 1, advancing on
|
||||||
@@ -70,7 +70,7 @@ var builtins = map[string]builtin{
|
|||||||
if len(a) == 1 {
|
if len(a) == 1 {
|
||||||
key = a[0]
|
key = a[0]
|
||||||
}
|
}
|
||||||
return func(s *session, _ string, _ map[string]node) string {
|
return func(s *session, _ string, _ []string) string {
|
||||||
return strconv.FormatUint(s.next(key), 10)
|
return strconv.FormatUint(s.next(key), 10)
|
||||||
}
|
}
|
||||||
}},
|
}},
|
||||||
@@ -81,13 +81,13 @@ var builtins = map[string]builtin{
|
|||||||
// lifts that one function into the prep every registry entry supplies.
|
// lifts that one function into the prep every registry entry supplies.
|
||||||
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(_ *session, emitted string, _ map[string]node) string { return f(emitted) }
|
return func(_ *session, emitted string, _ []string) string { return f(emitted) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func generate(f func(rng) string) func([]string) callFn {
|
func generate(f func(rng) string) func([]string) callFn {
|
||||||
return func([]string) callFn {
|
return func([]string) callFn {
|
||||||
return func(s *session, _ string, _ map[string]node) string { return f(s) }
|
return func(s *session, _ string, _ []string) string { return f(s) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,42 +9,48 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// calc is the {calc(expr[, dp])} token: an arithmetic expression over number
|
// calc is the {calc(expr[, dp])} token: an arithmetic expression over number
|
||||||
// literals and sibling-field names (rendered, then parsed as numbers), with
|
// literals and sibling-field names, with + - * /, unary minus and parentheses. It is
|
||||||
// + - * /, unary minus and parentheses. It is a registry builtin like any other
|
// a registry builtin like any other {name(args)} function — the one that names
|
||||||
// {name(args)} function — the one whose check and prep read the sibling fields (to
|
// operands, which expand reads for it (once per expansion, so the value computed is
|
||||||
// render its operands). The value prints in minimal decimal form, or rounded to dp
|
// the value the format showed) and hands over as strings. The value prints in
|
||||||
// when given. A field that doesn't render to a number becomes NaN, which
|
// minimal decimal form, or rounded to dp when given. An operand that isn't a number
|
||||||
// propagates and prints as "NaN" — visible, never a render error. The expression is
|
// becomes NaN, which propagates and prints as "NaN" — visible, never a render error.
|
||||||
// parsed once at New into an AST every render shares and none mutates, so render
|
// The expression is parsed once at New into an AST every render shares and none
|
||||||
// cannot fail and must not carry per-render state.
|
// mutates, so render cannot fail and must not carry per-render state.
|
||||||
|
|
||||||
// calcNode is a parsed expression node.
|
// calcNode is a parsed expression node. It evaluates over the operand values expand
|
||||||
|
// read, so the evaluator touches neither the rng nor the node tree.
|
||||||
type calcNode interface {
|
type calcNode interface {
|
||||||
eval(s *session, fields map[string]node) float64
|
eval(operands []string) float64
|
||||||
}
|
}
|
||||||
|
|
||||||
type calcNum float64 // a number literal
|
type calcNum float64 // a number literal
|
||||||
type calcVar string // a sibling-field name
|
type calcVar string // a sibling-field name, before indexVars places it
|
||||||
|
type calcIdx int // an operand, by its position in the values expand read
|
||||||
type calcNeg struct{ x calcNode }
|
type calcNeg struct{ x calcNode }
|
||||||
type calcBin struct { // a + - * / b
|
type calcBin struct { // a + - * / b
|
||||||
op byte
|
op byte
|
||||||
l, r calcNode
|
l, r calcNode
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n calcNum) eval(*session, map[string]node) float64 { return float64(n) }
|
func (n calcNum) eval([]string) float64 { return float64(n) }
|
||||||
|
|
||||||
func (n calcVar) eval(s *session, fields map[string]node) float64 {
|
func (n calcIdx) eval(operands []string) float64 {
|
||||||
v, err := strconv.ParseFloat(strings.TrimSpace(render(s, fields[string(n)])), 64)
|
v, err := strconv.ParseFloat(strings.TrimSpace(operands[n]), 64)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return math.NaN() // a non-numeric operand stays visible, never an error
|
return math.NaN() // a non-numeric operand stays visible, never an error
|
||||||
}
|
}
|
||||||
return v
|
return v
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n calcNeg) eval(s *session, fields map[string]node) float64 { return -n.x.eval(s, fields) }
|
// eval on an unplaced name cannot happen: calcPrep runs indexVars over every
|
||||||
|
// expression it compiles, so only a calcIdx reaches a render.
|
||||||
|
func (n calcVar) eval([]string) float64 { return math.NaN() }
|
||||||
|
|
||||||
func (n calcBin) eval(s *session, fields map[string]node) float64 {
|
func (n calcNeg) eval(operands []string) float64 { return -n.x.eval(operands) }
|
||||||
l, r := n.l.eval(s, fields), n.r.eval(s, fields)
|
|
||||||
|
func (n calcBin) eval(operands []string) float64 {
|
||||||
|
l, r := n.l.eval(operands), n.r.eval(operands)
|
||||||
switch n.op {
|
switch n.op {
|
||||||
case '+':
|
case '+':
|
||||||
return l + r
|
return l + r
|
||||||
@@ -82,19 +88,39 @@ func checkCalc(fields map[string]node, args []string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// calcPrep parses the expression and decimals once, at compile time. checkCalc
|
// calcPrep parses the expression and decimals once, at compile time, and places each
|
||||||
// proved both valid, so neither step here can fail; dp -1 prints the minimal form.
|
// operand name at the position expand will read it into. checkCalc proved both args
|
||||||
|
// valid, so no step here can fail; dp -1 prints the minimal form.
|
||||||
func calcPrep(args []string) callFn {
|
func calcPrep(args []string) callFn {
|
||||||
expr, _ := parseCalc(args[0])
|
expr, _ := parseCalc(args[0])
|
||||||
|
at := make(map[string]int)
|
||||||
|
for i, name := range calcVars(expr) {
|
||||||
|
at[name] = i
|
||||||
|
}
|
||||||
|
placed := indexVars(expr, at)
|
||||||
dp := -1
|
dp := -1
|
||||||
if len(args) == 2 {
|
if len(args) == 2 {
|
||||||
dp = atoi(args[1])
|
dp = atoi(args[1])
|
||||||
}
|
}
|
||||||
return func(s *session, _ string, fields map[string]node) string {
|
return func(_ *session, _ string, operands []string) string {
|
||||||
return strconv.FormatFloat(expr.eval(s, fields), 'f', dp, 64)
|
return strconv.FormatFloat(placed.eval(operands), 'f', dp, 64)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// indexVars replaces each operand name with its position in the values expand reads.
|
||||||
|
// Both sides take that order from calcVars, so they cannot drift.
|
||||||
|
func indexVars(n calcNode, at map[string]int) calcNode {
|
||||||
|
switch n := n.(type) {
|
||||||
|
case calcVar:
|
||||||
|
return calcIdx(at[string(n)])
|
||||||
|
case calcNeg:
|
||||||
|
return calcNeg{indexVars(n.x, at)}
|
||||||
|
case calcBin:
|
||||||
|
return calcBin{n.op, indexVars(n.l, at), indexVars(n.r, at)}
|
||||||
|
}
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
|
||||||
// calcOperands lists the sibling-field names every {calc(...)} token in a format
|
// calcOperands lists the sibling-field names every {calc(...)} token in a format
|
||||||
// reads, so cycle detection sees the field edges calc renders through (a function
|
// reads, so cycle detection sees the field edges calc renders through (a function
|
||||||
// token otherwise carries no field edge).
|
// token otherwise carries no field edge).
|
||||||
@@ -124,19 +150,29 @@ func calcTokenOperands(body string) []string {
|
|||||||
return calcVars(expr)
|
return calcVars(expr)
|
||||||
}
|
}
|
||||||
|
|
||||||
// calcVars lists the field names an expression references, for the existence
|
// calcVars lists the distinct field names an expression reads, in the order it first
|
||||||
// check in checkCalc.
|
// names each. That order is the contract between expand, which reads the operands
|
||||||
|
// into a slice, and indexVars, which places each name at its position in it.
|
||||||
func calcVars(n calcNode) []string {
|
func calcVars(n calcNode) []string {
|
||||||
|
var out []string
|
||||||
|
seen := map[string]bool{}
|
||||||
|
var walk func(calcNode)
|
||||||
|
walk = func(n calcNode) {
|
||||||
switch n := n.(type) {
|
switch n := n.(type) {
|
||||||
case calcVar:
|
case calcVar:
|
||||||
return []string{string(n)}
|
if name := string(n); !seen[name] {
|
||||||
case calcNeg:
|
seen[name] = true
|
||||||
return calcVars(n.x)
|
out = append(out, name)
|
||||||
case calcBin:
|
|
||||||
return append(calcVars(n.l), calcVars(n.r)...)
|
|
||||||
default:
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
case calcNeg:
|
||||||
|
walk(n.x)
|
||||||
|
case calcBin:
|
||||||
|
walk(n.l)
|
||||||
|
walk(n.r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
walk(n)
|
||||||
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
// calcParser is a recursive-descent parser over the expression runes, threading
|
// calcParser is a recursive-descent parser over the expression runes, threading
|
||||||
|
|||||||
@@ -47,9 +47,12 @@ type template struct {
|
|||||||
ops []op // format compiled once (see compileOps); what expand walks
|
ops []op // format compiled once (see compileOps); what expand walks
|
||||||
grow int // minimum output size, to size the render buffer
|
grow int // minimum output size, to size the render buffer
|
||||||
// bound maps each field the format addresses by dotted path to one path token
|
// bound maps each field the format addresses by dotted path to one path token
|
||||||
// reading it: drawn once per expansion (see compileOps and expand), and the
|
// reading it, which is the half of an overlap the fences name. nil when the
|
||||||
// token names the other half of an overlap. nil when the format takes no path.
|
// format takes no path.
|
||||||
bound map[string]string
|
bound map[string]string
|
||||||
|
// held is every name drawn once per expansion: the bound levels above, plus the
|
||||||
|
// siblings a {calc()} reads. nil when the format holds nothing (see expand).
|
||||||
|
held map[string]bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (*template) isNode() {}
|
func (*template) isNode() {}
|
||||||
@@ -160,7 +163,7 @@ func compileTemplate(m map[string]any) (node, error) {
|
|||||||
if err := checkTokens(format, t.fields); err != nil {
|
if err := checkTokens(format, t.fields); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
t.ops, t.grow, t.bound = compileOps(format)
|
t.ops, t.grow, t.bound, t.held = compileOps(format)
|
||||||
if err := checkNoOverlap(format, t.bound); err != nil {
|
if err := checkNoOverlap(format, t.bound); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -137,13 +137,14 @@ func classChar(s *session, c rune) byte {
|
|||||||
func expand(s *session, t *template) string {
|
func expand(s *session, t *template) string {
|
||||||
var b strings.Builder
|
var b strings.Builder
|
||||||
b.Grow(t.grow)
|
b.Grow(t.grow)
|
||||||
// One draw per bound head, held for this expansion only: a nested template and
|
// One draw per held name, for this expansion only: a nested template and each
|
||||||
// each repeat iteration get their own, since each is its own expansion.
|
// repeat iteration get their own, since each is its own expansion. held stays a
|
||||||
var bound *draws
|
// local: nothing hands it to a builtin, so it and its maps never leave the stack.
|
||||||
if len(t.bound) > 0 {
|
var held *draws
|
||||||
bound = &draws{
|
if len(t.held) > 0 {
|
||||||
variant: make(map[string]node, len(t.bound)),
|
held = &draws{
|
||||||
value: make(map[string]string, len(t.bound)),
|
variant: make(map[string]node, len(t.held)),
|
||||||
|
value: make(map[string]string, len(t.held)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for i := range t.ops {
|
for i := range t.ops {
|
||||||
@@ -154,57 +155,66 @@ func expand(s *session, t *template) string {
|
|||||||
case 'c':
|
case 'c':
|
||||||
b.WriteByte(classChar(s, o.r))
|
b.WriteByte(classChar(s, o.r))
|
||||||
case 'f':
|
case 'f':
|
||||||
b.WriteString(resolve(s, o.arms, t, bound))
|
b.WriteString(readField(s, t, held, o.arms[s.IntN(len(o.arms))]))
|
||||||
case 'b':
|
case 'b':
|
||||||
b.WriteString(o.call(s, b.String(), t.fields)) // b.String() is the output so far
|
// A calc's operands are read here, in the order its expression first
|
||||||
|
// names them, so the value it computes is the value the format showed.
|
||||||
|
var operands []string
|
||||||
|
if len(o.operands) > 0 {
|
||||||
|
operands = make([]string, len(o.operands))
|
||||||
|
for j, name := range o.operands {
|
||||||
|
operands[j] = readField(s, t, held, arm{name: name, key: name})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
b.WriteString(o.call(s, b.String(), operands)) // b.String() is the output so far
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return b.String()
|
return b.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
// draws is what an expansion has already drawn for its bound heads: the variant
|
// draws is what an expansion has already drawn for its held names: the variant each
|
||||||
// each head was drawn as, so every path under it reads one row, and the value each
|
// was drawn as, so every path under it reads one row, and the value each read, so
|
||||||
// path read, so the same path read twice reads one value.
|
// the same name read twice reads one value.
|
||||||
type draws struct {
|
type draws struct {
|
||||||
variant map[string]node
|
variant map[string]node
|
||||||
value map[string]string
|
value map[string]string
|
||||||
}
|
}
|
||||||
|
|
||||||
// resolve renders one field alternation: the '|' alternatives, one picked at
|
// readField renders one arm of a token. An arm's key is a sibling field or a
|
||||||
// random. An arm's key is a sibling field or a {..path} reference, which linkRefs
|
// {..path} reference, which linkRefs bound into fields too. A name the expansion
|
||||||
// bound into fields too. A field the format addresses by dotted path is bound: it
|
// holds — a level some token addresses by dotted path, or a sibling a {calc()}
|
||||||
// is drawn once for the expansion, so {place.postal-code} and {place.locality}
|
// reads — is drawn once and kept, so {place.postal-code} and {place.locality} read
|
||||||
// read one row and either read twice gives one value. checkTokens, checkPath and
|
// one row, either read twice gives one value, and a shown operand is the operand
|
||||||
// linkRefs prove every step, so this cannot fail.
|
// computed. Every other name is drawn afresh, so {word} {word} still draws twice.
|
||||||
func resolve(s *session, arms []arm, t *template, bound *draws) string {
|
// checkTokens, checkPath and linkRefs prove every step, so this cannot fail.
|
||||||
a := arms[s.IntN(len(arms))]
|
func readField(s *session, t *template, held *draws, a arm) string {
|
||||||
if _, isBound := t.bound[a.key]; !isBound {
|
if !t.held[a.key] {
|
||||||
return render(s, t.fields[a.key])
|
return render(s, t.fields[a.key])
|
||||||
}
|
}
|
||||||
if v, read := bound.value[a.name]; read {
|
if v, read := held.value[a.name]; read {
|
||||||
return v
|
return v
|
||||||
}
|
}
|
||||||
n, drew := bound.variant[a.key]
|
n, drew := held.variant[a.key]
|
||||||
if !drew {
|
if !drew {
|
||||||
n = drawn(s, t.fields[a.key])
|
n = drawn(s, t.fields[a.key])
|
||||||
bound.variant[a.key] = n
|
held.variant[a.key] = n
|
||||||
}
|
}
|
||||||
// Hold the draw at every level passed through, so two paths sharing a prefix
|
// Hold the draw at every level passed through, so two paths sharing a prefix
|
||||||
// share it.
|
// share it.
|
||||||
for i, seg := range a.tail {
|
for i, seg := range a.tail {
|
||||||
if i < len(a.steps) {
|
if i < len(a.steps) {
|
||||||
held, drew := bound.variant[a.steps[i]]
|
step, drew := held.variant[a.steps[i]]
|
||||||
if !drew {
|
if !drew {
|
||||||
held = drawn(s, child(n, seg))
|
step = drawn(s, child(n, seg))
|
||||||
bound.variant[a.steps[i]] = held
|
held.variant[a.steps[i]] = step
|
||||||
}
|
}
|
||||||
n = held
|
n = step
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
n = child(n, seg)
|
n = child(n, seg)
|
||||||
}
|
}
|
||||||
v := render(s, n)
|
v := render(s, n)
|
||||||
bound.value[a.name] = v
|
held.value[a.name] = v
|
||||||
return v
|
return v
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+32
-12
@@ -60,8 +60,8 @@ func eachToken(format string, fn func(ftoken) error) 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
|
||||||
// session (its rng, and the {seq()} counters), the output emitted so far in the
|
// session (its rng, and the {seq()} counters), the output emitted so far in the
|
||||||
// current expansion (for derivations such as a checksum over preceding digits),
|
// current expansion (for derivations such as a checksum over preceding digits), and
|
||||||
// the sibling fields (only calc reads them, to render its operands), and its args.
|
// the values of the operands it named (only calc names any).
|
||||||
// Almost all are pure over (rng, emitted, args) — no wall-clock, no crypto/rand —
|
// Almost all are pure over (rng, emitted, args) — no wall-clock, no crypto/rand —
|
||||||
// so seeding stays reproducible; a time-based id derives its time from the rng.
|
// so seeding stays reproducible; a time-based id derives its time from the rng.
|
||||||
// seq is the one exception: it advances per-session counter state, which is itself
|
// seq is the one exception: it advances per-session counter state, which is itself
|
||||||
@@ -183,7 +183,7 @@ func checkNoRepeatedArm(body string, names []string) error {
|
|||||||
|
|
||||||
// fieldTokens returns the field and reference names a format renders via {name}
|
// fieldTokens returns the field and reference names a format renders via {name}
|
||||||
// or {a|..b} tokens (function tokens, which carry no field edges, are excluded).
|
// or {a|..b} tokens (function tokens, which carry no field edges, are excluded).
|
||||||
// These are exactly the child nodes expand's resolve recurses into.
|
// These are exactly the child nodes expand recurses into, through readField.
|
||||||
func fieldTokens(format string) []string {
|
func fieldTokens(format string) []string {
|
||||||
var names []string
|
var names []string
|
||||||
_ = eachToken(format, func(t ftoken) error {
|
_ = eachToken(format, func(t ftoken) error {
|
||||||
@@ -303,8 +303,10 @@ func splitArms(body string) []arm {
|
|||||||
return arms
|
return arms
|
||||||
}
|
}
|
||||||
|
|
||||||
// callFn is a builtin bound to one call site: its args already parsed.
|
// callFn is a builtin bound to one call site: its args already parsed. It reads the
|
||||||
type callFn func(s *session, emitted string, fields map[string]node) string
|
// 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
|
||||||
|
|
||||||
// op is one compiled unit of a format string: a literal run, a class char, a field
|
// op is one compiled unit of a format string: a literal run, a class char, a field
|
||||||
// alternation, or a builtin already bound to its args. compile builds these so
|
// alternation, or a builtin already bound to its args. compile builds these so
|
||||||
@@ -315,19 +317,32 @@ type op struct {
|
|||||||
r rune // kind 'c'
|
r rune // kind 'c'
|
||||||
arms []arm // kind 'f': the '|' alternatives, split into key and path once
|
arms []arm // kind 'f': the '|' alternatives, split into key and path once
|
||||||
call callFn
|
call callFn
|
||||||
|
// operands names the sibling fields a {calc()} reads, in the order its
|
||||||
|
// expression first names them. expand reads them before the call, so the
|
||||||
|
// evaluator never touches the node tree. nil for every other builtin.
|
||||||
|
operands []string
|
||||||
}
|
}
|
||||||
|
|
||||||
// compileOps turns a format string into ops, and returns the smallest output it can
|
// compileOps turns a format string into ops, and returns the smallest output it can
|
||||||
// produce (literals plus one byte per class char) to size the render buffer, plus
|
// produce (literals plus one byte per class char) to size the render buffer, plus
|
||||||
// the fields the format addresses by dotted path — each drawn once per expansion,
|
// two sets. bound is the levels the format addresses by dotted path, each mapped to
|
||||||
// so every token reading one sees the same row (see expand). bound is nil when the
|
// the first path reading it, which is what the overlap fences name. held is every
|
||||||
// format takes no path, so data that uses none carries no render-time cost.
|
// name drawn once per expansion — those levels, plus the siblings a {calc()} reads,
|
||||||
// Call checkTokens first: it is what proves the scan and every token are valid.
|
// so an operand shown is the operand computed. Both are nil when the format needs
|
||||||
func compileOps(format string) ([]op, int, map[string]string) {
|
// neither, so data that uses neither carries no render-time cost. Call checkTokens
|
||||||
|
// first: it is what proves the scan and every token are valid.
|
||||||
|
func compileOps(format string) ([]op, int, map[string]string, map[string]bool) {
|
||||||
var ops []op
|
var ops []op
|
||||||
var lit strings.Builder
|
var lit strings.Builder
|
||||||
var bound map[string]string
|
var bound map[string]string
|
||||||
|
var held map[string]bool
|
||||||
grow := 0
|
grow := 0
|
||||||
|
hold := func(name string) {
|
||||||
|
if held == nil {
|
||||||
|
held = map[string]bool{}
|
||||||
|
}
|
||||||
|
held[name] = true
|
||||||
|
}
|
||||||
flush := func() {
|
flush := func() {
|
||||||
if lit.Len() > 0 {
|
if lit.Len() > 0 {
|
||||||
grow += lit.Len()
|
grow += lit.Len()
|
||||||
@@ -346,7 +361,11 @@ func compileOps(format string) ([]op, int, map[string]string) {
|
|||||||
case 'b':
|
case 'b':
|
||||||
flush()
|
flush()
|
||||||
if name, args, ok := funcCall(t.body); ok {
|
if name, args, ok := funcCall(t.body); ok {
|
||||||
ops = append(ops, op{kind: 'b', call: builtins[name].prep(args)})
|
operands := calcTokenOperands(t.body)
|
||||||
|
for _, operand := range operands {
|
||||||
|
hold(operand) // a calc renders its operand, so the expansion holds that draw
|
||||||
|
}
|
||||||
|
ops = append(ops, op{kind: 'b', call: builtins[name].prep(args), operands: operands})
|
||||||
} else {
|
} else {
|
||||||
arms := splitArms(t.body)
|
arms := splitArms(t.body)
|
||||||
for _, a := range arms {
|
for _, a := range arms {
|
||||||
@@ -357,6 +376,7 @@ func compileOps(format string) ([]op, int, map[string]string) {
|
|||||||
if _, named := bound[a.key]; !named {
|
if _, named := bound[a.key]; !named {
|
||||||
bound[a.key] = a.name // the first path reading it, for error messages
|
bound[a.key] = a.name // the first path reading it, for error messages
|
||||||
}
|
}
|
||||||
|
hold(a.key)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ops = append(ops, op{kind: 'f', arms: arms})
|
ops = append(ops, op{kind: 'f', arms: arms})
|
||||||
@@ -365,5 +385,5 @@ func compileOps(format string) ([]op, int, map[string]string) {
|
|||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
flush()
|
flush()
|
||||||
return ops, grow, bound
|
return ops, grow, bound, held
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user