Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1e4921cdf3 |
@@ -240,16 +240,16 @@ hyphenated field can't be an operand.
|
|||||||
{ "format": "{net} x {qty} = {calc(net * qty, 2)}", "net": ["19.99", "5.00"], "qty": ["3", "7"] }
|
{ "format": "{net} x {qty} = {calc(net * qty, 2)}", "net": ["19.99", "5.00"], "qty": ["3", "7"] }
|
||||||
```
|
```
|
||||||
|
|
||||||
Renders e.g. `19.99 x 3 = 59.97`. A non-numeric operand yields `NaN` and a
|
Renders e.g. `19.99 x 3 = 59.97`: an operand is drawn once per expansion, so the
|
||||||
|
operand shown is the operand computed. A non-numeric operand yields `NaN` and a
|
||||||
division by zero `Inf`; both print rather than fail.
|
division by zero `Inf`; both print rather than fail.
|
||||||
|
|
||||||
### Transforms
|
### Transforms
|
||||||
|
|
||||||
`{lowercase(x)}`, `{uppercase(x)}` and `{ascii(x)}` rewrite the value of `x` — a
|
`{lowercase(x)}`, `{uppercase(x)}` and `{ascii(x)}` rewrite the value of `x` — a
|
||||||
field, a path or a `..path` — and nest. `ascii` folds Latin letters (`Åsa Öberg`
|
field, a path or a `..path` — and nest. `ascii` folds Latin letters (`Åsa Öberg`
|
||||||
→ `Asa Oberg`) and drops any other non-ASCII rune. `x` is held
|
→ `Asa Oberg`) and drops any other non-ASCII rune. Like a calc operand, `x` is
|
||||||
([One draw, one spelling](#one-draw-one-spelling)), so an email built from a name
|
drawn once per expansion, so an email built from a name matches the name beside it:
|
||||||
matches the name beside it:
|
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{ "format": "{p.first} {p.last} <{lowercase(ascii(p.first))}.{lowercase(ascii(p.last))}@example.com>",
|
{ "format": "{p.first} {p.last} <{lowercase(ascii(p.first))}.{lowercase(ascii(p.last))}@example.com>",
|
||||||
@@ -282,9 +282,9 @@ through a chain.
|
|||||||
|
|
||||||
### Correlated fields
|
### Correlated fields
|
||||||
|
|
||||||
`{name.field}` reads a path into a sibling, which holds the sibling to one draw
|
`{name.field}` reads a path into a sibling, and a sibling read that way is drawn
|
||||||
([One draw, one spelling](#one-draw-one-spelling)), so several tokens read one
|
**once per expansion**, so several tokens read one row — a locality and the
|
||||||
row — a locality and the postal code that really covers it:
|
postal code that really covers it:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{ "format": "{street} {int(1,99)}\n{place.postal-code} {place.locality}",
|
{ "format": "{street} {int(1,99)}\n{place.postal-code} {place.locality}",
|
||||||
@@ -297,9 +297,11 @@ row — a locality and the postal code that really covers it:
|
|||||||
|
|
||||||
Renders e.g. `Kungsgatan 35` / `176 99 Stockholm`, never a Stockholm code beside
|
Renders e.g. `Kungsgatan 35` / `176 99 Stockholm`, never a Stockholm code beside
|
||||||
Tranås; each row's `weight` says how often it appears. A path is held at every
|
Tranås; each row's `weight` says how often it appears. A path is held at every
|
||||||
level it passes through: `{p.geo.town.name} {p.geo.town.zip}` share the town.
|
level it passes through (`{p.geo.town.name} {p.geo.town.zip}` share the town),
|
||||||
Every variant of a choice on the path must carry the rest of it, so a row missing
|
one path read twice reads one value, and a field no path addresses is drawn each
|
||||||
a field is named at load:
|
time (`{word} {word}` differs). The hold lasts one expansion: each `repeat`
|
||||||
|
iteration and each nested template draws again. Every variant of a choice on the
|
||||||
|
path must carry the rest of it, so a row missing a field is named at load:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
token {place.postal-code}: field "place": not every variant of this 2-way choice carries "postal-code"; all carry [locality]
|
token {place.postal-code}: field "place": not every variant of this 2-way choice carries "postal-code"; all carry [locality]
|
||||||
@@ -310,17 +312,14 @@ The sub-fields stay addressable — `Fake("address.place.locality")` renders, an
|
|||||||
|
|
||||||
### One draw, one spelling
|
### One draw, one spelling
|
||||||
|
|
||||||
A name any token reads as a path (`{p.first}`) or as an operand (`{calc(net * 2)}`,
|
A format may not both **render** a level and **read a path into** it — `{p}`
|
||||||
`{uppercase(w)}`) is drawn **once per expansion**, and every other route to it —
|
beside `{p.first}`, `{..cat.net}` beside `{calc(net * 2)}`, or `{q}` beside
|
||||||
a bare `{p}`, a second bare `{w}`, `{..cat.net}`, a nested template rendering
|
`{p.first}` where `q` renders `{..cat.p.last}` — because the render draws afresh
|
||||||
`{..cat.p.last}`, at any depth — is a load error naming the spelling to use. A
|
while the path reads the held draw, and the two would disagree. Wherever the
|
||||||
name nothing reads that way is drawn each time: `{word} {word}` differs. An
|
second route sits, it is a load error naming the spelling to use:
|
||||||
expansion is one render of one format, so each `repeat` iteration and each nested
|
|
||||||
template draws again.
|
|
||||||
|
|
||||||
```text
|
```text
|
||||||
token {p} renders a level that {p.first} reads a path into; name the fields you want instead
|
token {p} renders a level that {p.first} reads a path into; name the fields you want instead
|
||||||
token {w} is repeated, and uppercase operand "w" holds "w" to one draw per expansion; write {w} once
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Performance
|
### Performance
|
||||||
@@ -348,11 +347,6 @@ tokens add cost in proportion to the output.
|
|||||||
- **The shipped data is embedded, not discovered.** A directory a machine happens
|
- **The shipped data is embedded, not discovered.** A directory a machine happens
|
||||||
to have would make `--seed 42` machine-dependent. Data still lives in `data/`
|
to have would make `--seed 42` machine-dependent. Data still lives in `data/`
|
||||||
as JSON; `--data-path` layers over it.
|
as JSON; `--data-path` layers over it.
|
||||||
- **A bare reference draws each time; a reference path is held.** `{..p} {..p}`
|
|
||||||
is two draws, as `{word} {word}` is, while `{..p.first}` beside a nested template
|
|
||||||
rendering `{..p.first}` is a load error: a bare token is by contract an
|
|
||||||
independent draw, a path pins its level, and any route into a pinned level from
|
|
||||||
another expansion could show another row.
|
|
||||||
- **Samples say what they emit, transforms what they do.** `{upper(2)}` is two
|
- **Samples say what they emit, transforms what they do.** `{upper(2)}` is two
|
||||||
letters, `{uppercase(x)}` is `x` upper-cased; one name for both would turn on
|
letters, `{uppercase(x)}` is `x` upper-cased; one name for both would turn on
|
||||||
whether the argument looks like a number.
|
whether the argument looks like a number.
|
||||||
|
|||||||
+3
-2
@@ -692,7 +692,7 @@ func TestBoundPathIsReachableByFake(t *testing.T) {
|
|||||||
func TestRepeatedBareTokenOfAHeldNameIsRejected(t *testing.T) {
|
func TestRepeatedBareTokenOfAHeldNameIsRejected(t *testing.T) {
|
||||||
for src, want := range map[string]string{
|
for src, want := range map[string]string{
|
||||||
`{"format":"{w} {w} {uppercase(w)}","w":["a","b"]}`: "write {w} once",
|
`{"format":"{w} {w} {uppercase(w)}","w":["a","b"]}`: "write {w} once",
|
||||||
`{"format":"{w} {w|x} {uppercase(w)}","w":["a","b"],"x":["c","d"]}`: "write {w} once",
|
`{"format":"{w} {w|x} {uppercase(w)}","w":["a","b"],"x":["c"]}`: "write {w} once",
|
||||||
`{"format":"{n} + {n} = {calc(n * 2)}","n":["1","2"]}`: "write {n} once",
|
`{"format":"{n} + {n} = {calc(n * 2)}","n":["1","2"]}`: "write {n} once",
|
||||||
`{"format":"{p.a} {q} {q} {lowercase(q)}","p":{"format":"{a}","a":"1"},"q":["A","B"]}`: "write {q} once",
|
`{"format":"{p.a} {q} {q} {lowercase(q)}","p":{"format":"{a}","a":"1"},"q":["A","B"]}`: "write {q} once",
|
||||||
} {
|
} {
|
||||||
@@ -704,8 +704,9 @@ func TestRepeatedBareTokenOfAHeldNameIsRejected(t *testing.T) {
|
|||||||
for _, ok := range []string{
|
for _, ok := range []string{
|
||||||
`{"format":"{w} {w}","w":["a","b"]}`,
|
`{"format":"{w} {w}","w":["a","b"]}`,
|
||||||
`{"format":"{w} {uppercase(w)}","w":["a","b"]}`,
|
`{"format":"{w} {uppercase(w)}","w":["a","b"]}`,
|
||||||
`{"format":"{net} x {qty} = {calc(net * qty, 2)}","net":["19.99","5.00"],"qty":["3","7"]}`,
|
`{"format":"{net} x {qty} = {calc(net * qty, 2)}","net":["19.99"],"qty":["3","7"]}`,
|
||||||
`{"format":"{uppercase(w)} {uppercase(w)}","w":["a","b"]}`,
|
`{"format":"{uppercase(w)} {uppercase(w)}","w":["a","b"]}`,
|
||||||
|
`{"format":"{w} {x} {x}","w":["a","b"],"x":["c","d"],"y":"{uppercase(w)}"}`,
|
||||||
} {
|
} {
|
||||||
if _, err := compile(parse(t, ok)); err != nil {
|
if _, err := compile(parse(t, ok)); err != nil {
|
||||||
t.Errorf("compile(%s) = %v, want it accepted", ok, err)
|
t.Errorf("compile(%s) = %v, want it accepted", ok, err)
|
||||||
|
|||||||
+4
-4
@@ -140,16 +140,16 @@ func TestCalcOperandReadsTheExpansionsDraw(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TestCalcOperandSharesOneDraw pins the reach of that hold: the draw belongs to the
|
// TestCalcOperandSharesOneDraw pins the reach of that hold: the draw belongs to the
|
||||||
// expansion, not to the calc, so the bare token rendering the same name reads it too.
|
// expansion, not to the calc, so a bare token rendering the same name reads it too.
|
||||||
func TestCalcOperandSharesOneDraw(t *testing.T) {
|
func TestCalcOperandSharesOneDraw(t *testing.T) {
|
||||||
dir := writeData(t, map[string]string{
|
dir := writeData(t, map[string]string{
|
||||||
"same": `{"format":"{w} {calc(w)}","w":["1","2","3","4","5"]}`,
|
"same": `{"format":"{w} {w} {calc(w)}","w":["1","2","3","4","5"]}`,
|
||||||
})
|
})
|
||||||
f := newGenerator(t, dir, WithSeed(5))
|
f := newGenerator(t, dir, WithSeed(5))
|
||||||
for i := 0; i < 200; i++ {
|
for i := 0; i < 200; i++ {
|
||||||
got := fake(t, f, "same")
|
got := fake(t, f, "same")
|
||||||
if p := strings.Fields(got); len(p) != 2 || p[0] != p[1] {
|
if p := strings.Fields(got); len(p) != 3 || p[0] != p[1] || p[0] != p[2] {
|
||||||
t.Fatalf("same = %q, want one value twice", got)
|
t.Fatalf("same = %q, want one value three times", got)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -87,17 +87,13 @@ func compileString(s string) (node, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
t := &template{format: s, repeat: 1}
|
t := &template{format: s, repeat: 1}
|
||||||
if err := t.compileFormat(); err != nil {
|
t.compileFormat()
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return t, nil
|
return t, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// compileFormat compiles the format into ops once every field is in place, and
|
// compileFormat compiles the format into ops once every field is in place.
|
||||||
// applies the fences that need the compiled reads.
|
func (t *template) compileFormat() {
|
||||||
func (t *template) compileFormat() error {
|
t.ops, t.grow, t.bound, t.held = compileOps(t.format, t.refs)
|
||||||
c := compileOps(t.format, t.refs)
|
|
||||||
t.ops, t.grow, t.bound, t.held = c.ops, c.grow, c.bound, c.held
|
|
||||||
t.fixed = true
|
t.fixed = true
|
||||||
for _, o := range t.ops {
|
for _, o := range t.ops {
|
||||||
if o.kind != 'l' {
|
if o.kind != 'l' {
|
||||||
@@ -107,10 +103,6 @@ func (t *template) compileFormat() error {
|
|||||||
if t.fixed && len(t.ops) == 1 {
|
if t.fixed && len(t.ops) == 1 {
|
||||||
t.lit = t.ops[0].lit
|
t.lit = t.ops[0].lit
|
||||||
}
|
}
|
||||||
if err := checkNoOverlap(t.format, t.bound, t.refs); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return checkNoRepeatedRead(t.format, c, t.refs)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func compileChoice(items []any) (node, error) {
|
func compileChoice(items []any) (node, error) {
|
||||||
@@ -226,7 +218,8 @@ 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
|
||||||
}
|
}
|
||||||
if err := t.compileFormat(); err != nil {
|
t.compileFormat()
|
||||||
|
if err := checkNoOverlap(format, t.bound, nil); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return t, nil
|
return t, nil
|
||||||
|
|||||||
+2
-1
@@ -46,7 +46,8 @@ func linkRefs(root map[string]node) error {
|
|||||||
t.fields[key] = target
|
t.fields[key] = target
|
||||||
t.refs[name] = key
|
t.refs[name] = key
|
||||||
}
|
}
|
||||||
if err := t.compileFormat(); err != nil {
|
t.compileFormat()
|
||||||
|
if err := checkNoOverlap(t.format, t.bound, t.refs); err != nil {
|
||||||
return fmt.Errorf("%s: %w", path, err)
|
return fmt.Errorf("%s: %w", path, err)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
+45
-88
@@ -363,69 +363,38 @@ type op struct {
|
|||||||
operands []arm
|
operands []arm
|
||||||
}
|
}
|
||||||
|
|
||||||
// formatOps is a compiled format: its ops, the size of its literal text (to size
|
// compileOps turns a format string into ops, and returns the size of its literal
|
||||||
// the render buffer), and the names drawn once per expansion. bound maps each level
|
// text to size the render buffer, plus two sets. bound is the levels the format
|
||||||
// a path reads into to the first such path; held is every such level plus the
|
// addresses by dotted path, each mapped to the first path reading it, which is what
|
||||||
// fields an operand reads; holder maps each held name to the first reader holding
|
// the overlap fences name. held is every name drawn once per expansion — those
|
||||||
// it, for error messages. The maps are nil when the format holds nothing, so data
|
// levels, plus the fields an operand reads, so an operand shown is the operand
|
||||||
// that holds nothing carries no render-time cost.
|
// computed. Both are nil when the format needs neither, so data that uses neither
|
||||||
type formatOps struct {
|
// carries no render-time cost. Call checkTokens first: it is what proves the scan
|
||||||
ops []op
|
// and every token are valid.
|
||||||
grow int
|
func compileOps(format string, refs map[string]string) ([]op, int, map[string]string, map[string]bool) {
|
||||||
bound map[string]string
|
var ops []op
|
||||||
held map[string]bool
|
|
||||||
holder map[string]string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *formatOps) hold(a arm, label string) {
|
|
||||||
if c.held == nil {
|
|
||||||
c.held = map[string]bool{}
|
|
||||||
c.holder = map[string]string{}
|
|
||||||
}
|
|
||||||
c.held[a.key] = true
|
|
||||||
if _, named := c.holder[a.key]; !named {
|
|
||||||
c.holder[a.key] = label
|
|
||||||
}
|
|
||||||
if len(a.tail) > 0 {
|
|
||||||
if c.bound == nil {
|
|
||||||
c.bound = map[string]string{}
|
|
||||||
}
|
|
||||||
if _, named := c.bound[a.key]; !named {
|
|
||||||
c.bound[a.key] = a.name
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *formatOps) function(body string, refs map[string]string) {
|
|
||||||
name, args, _ := funcCall(body)
|
|
||||||
var operands []arm
|
|
||||||
for _, operand := range tokenOperands(body) {
|
|
||||||
a := splitArm(operand, refs)
|
|
||||||
c.hold(a, fmt.Sprintf("%s operand %q", name, operand))
|
|
||||||
operands = append(operands, a)
|
|
||||||
}
|
|
||||||
c.ops = append(c.ops, op{kind: 'b', call: builtins[name].prep(args), operands: operands})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *formatOps) field(body string, refs map[string]string) {
|
|
||||||
arms := splitArms(body, refs)
|
|
||||||
for _, a := range arms {
|
|
||||||
if len(a.tail) > 0 {
|
|
||||||
c.hold(a, "token {"+a.name+"}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
c.ops = append(c.ops, op{kind: 'f', arms: arms})
|
|
||||||
}
|
|
||||||
|
|
||||||
// compileOps compiles a format string. Call checkTokens first: it is what proves
|
|
||||||
// the scan and every token are valid.
|
|
||||||
func compileOps(format string, refs map[string]string) formatOps {
|
|
||||||
var c formatOps
|
|
||||||
var lit strings.Builder
|
var lit strings.Builder
|
||||||
|
var bound map[string]string
|
||||||
|
var held map[string]bool
|
||||||
|
grow := 0
|
||||||
|
hold := func(a arm) {
|
||||||
|
if held == nil {
|
||||||
|
held = map[string]bool{}
|
||||||
|
}
|
||||||
|
held[a.key] = true
|
||||||
|
if len(a.tail) > 0 {
|
||||||
|
if bound == nil {
|
||||||
|
bound = map[string]string{}
|
||||||
|
}
|
||||||
|
if _, named := bound[a.key]; !named {
|
||||||
|
bound[a.key] = a.name // the first path reading it, for error messages
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
flush := func() {
|
flush := func() {
|
||||||
if lit.Len() > 0 {
|
if lit.Len() > 0 {
|
||||||
c.grow += lit.Len()
|
grow += lit.Len()
|
||||||
c.ops = append(c.ops, op{kind: 'l', lit: lit.String()})
|
ops = append(ops, op{kind: 'l', lit: lit.String()})
|
||||||
lit.Reset()
|
lit.Reset()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -435,38 +404,26 @@ func compileOps(format string, refs map[string]string) formatOps {
|
|||||||
lit.WriteRune(t.r)
|
lit.WriteRune(t.r)
|
||||||
case 'b':
|
case 'b':
|
||||||
flush()
|
flush()
|
||||||
if _, _, isFunc := funcCall(t.body); isFunc {
|
if name, args, ok := funcCall(t.body); ok {
|
||||||
c.function(t.body, refs)
|
var operands []arm
|
||||||
|
for _, operand := range tokenOperands(t.body) {
|
||||||
|
a := splitArm(operand, refs)
|
||||||
|
hold(a) // the builtin renders its operand, so the expansion holds that draw
|
||||||
|
operands = append(operands, a)
|
||||||
|
}
|
||||||
|
ops = append(ops, op{kind: 'b', call: builtins[name].prep(args), operands: operands})
|
||||||
} else {
|
} else {
|
||||||
c.field(t.body, refs)
|
arms := splitArms(t.body, refs)
|
||||||
|
for _, a := range arms {
|
||||||
|
if len(a.tail) > 0 {
|
||||||
|
hold(a)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ops = append(ops, op{kind: 'f', arms: arms})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
flush()
|
flush()
|
||||||
return c
|
return ops, grow, bound, held
|
||||||
}
|
|
||||||
|
|
||||||
// checkNoRepeatedRead rejects a bare token repeated on a held name: {w} {w} beside
|
|
||||||
// {uppercase(w)} would read one draw twice, where {w} {w} alone draws twice. The
|
|
||||||
// error names the single-token spelling.
|
|
||||||
func checkNoRepeatedRead(format string, c formatOps, refs map[string]string) error {
|
|
||||||
count := map[string]int{}
|
|
||||||
return eachToken(format, func(t ftoken) error {
|
|
||||||
if t.kind != 'b' {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if _, _, isFunc := funcCall(t.body); isFunc {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
for _, a := range splitArms(t.body, refs) {
|
|
||||||
if len(a.tail) > 0 || !c.held[a.key] {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if count[a.key]++; count[a.key] > 1 {
|
|
||||||
return fmt.Errorf("token {%s} is repeated, and %s holds %q to one draw per expansion; write {%s} once", a.name, c.holder[a.key], a.key, a.name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user