Answer architecture round 2: refuse a self-read only inside what the binding lands on, fix the struct.go Layout line, move loneRef beside its caller, and widen item 83 to the link passes

This commit is contained in:
2026-10-05 09:41:20 +02:00
committed by Lillem4n
parent 1a15a139c8
commit 938bbb5c43
6 changed files with 43 additions and 23 deletions
+1 -1
View File
@@ -959,7 +959,7 @@ scope.go the scope a render reads names and rows through
path.go the dotted-path walks with their selectors, and proving a path resolves
render.go Fake, the recursive renderer (choices, format strings, expansions) and the reads its tokens make
record.go records: Record, the JSON/CSV/SQL serializers, and their entry points
struct.go structs: FakeStruct, fake tags, the one pick a struct's path tags into one category read, and a field's Go type as its column's datatype
struct.go structs: FakeStruct, fake tags as one record's columns, and a field's Go type as its column's datatype
inline.go inline templates: Template, NewTemplate, FakeTemplate, IsTemplate, and their compile and link
format.go the {token} grammar: scanning, tokens, operands, validation, compiling a format
pins.go the pin set: the table rows one path or one named pick fixes
+8
View File
@@ -206,3 +206,11 @@ func linkNodeRefs(scope nodeScope, root map[string]node) error {
}
return nil
}
// loneRef is the reference a format of one reference token and nothing else reads.
func loneRef(toks []formatToken) (string, bool) {
if len(toks) != 1 || toks[0].kind != nameRead || len(toks[0].names) != 1 || !isRef(toks[0].names[0]) {
return "", false
}
return toks[0].names[0], true
}
+6
View File
@@ -417,3 +417,9 @@ func TestANameReadAloneIsTheRecordColumnItReads(t *testing.T) {
t.Errorf("NewRecordTemplate = %v, want a name-read column accepted inline", err)
}
}
func TestAFieldBindingReadOutsideWhatItBindsLoads(t *testing.T) {
if _, err := engine(1).NewTemplate(`{"format":"{x.y as n}{n}{n}{x}","x":{"format":"{z}","y":["a","b"],"z":"{n}"}}`); err != nil {
t.Errorf("NewTemplate = %v, want a read of n from x.z, outside x.y, accepted", err)
}
}
+25 -11
View File
@@ -1,7 +1,6 @@
package fejkdata
import (
"errors"
"fmt"
"slices"
"strings"
@@ -19,7 +18,7 @@ func checkNameReads(path string, t *template) error {
}
}
return namedReads(t, func(o *op, a *arm) error {
if a.named.bindsField() && rendersInside(a.named.head, t) {
if a.named.bindsField() && rendersInside(compilePath(a.named.head, a.named.tail).leaves, t) {
return fmt.Errorf("%s: token {%s}: name %q is read inside %q, the field it binds, which would render itself; read the name outside that field", t.site.label(path), o.body, a.named.name, a.named.ref)
}
for _, leaf := range a.leaves {
@@ -31,18 +30,33 @@ func checkNameReads(path string, t *template) error {
})
}
// rendersInside reports whether t sits in what n contains.
func rendersInside(n node, t *template) bool {
return eachNode(n, "", func(_ string, m node) error {
if m == t {
return errFound
// rendersInside reports whether t sits in what one of nodes contains.
func rendersInside(nodes []node, t *template) bool {
seen := map[node]bool{}
var inside func(n node) bool
inside = func(n node) bool {
if n == node(t) {
return true
}
return nil
}) != nil
if seen[n] {
return false
}
seen[n] = true
for _, c := range contained(n) {
if inside(c.node) {
return true
}
}
return false
}
for _, n := range nodes {
if inside(n) {
return true
}
}
return false
}
var errFound = errors.New("found")
// checkUses refuses a binding read once at a spot the bound spelling can stand: that spelling
// draws the same way without the name. A field stands only beside its binding.
func (b *nameBinding) checkUses() error {
-8
View File
@@ -251,11 +251,3 @@ func refTokens(toks []formatToken) []string {
}
return refs
}
// loneRef is the reference a format of one reference token and nothing else reads.
func loneRef(toks []formatToken) (string, bool) {
if len(toks) != 1 || toks[0].kind != nameRead || len(toks[0].names) != 1 || !isRef(toks[0].names[0]) {
return "", false
}
return toks[0].names[0], true
}
+3 -3
View File
@@ -131,7 +131,7 @@
| 95 | 0.3.0 | principle | **Hold a struct's columns and the field each fills in one slice.** | 1 | 2 | 1 | 5 | 3.2 | 9.5 |
| 19 | 0.3.0 | | **Fill the 398 Swedish localities weighted 200 from SCB småorter.** | 2 | 4 | 4 | 6 | 14.1 | 9.2 |
| 56 | 0.3.0 | defect | **Build on a manual run of `test.yml`.** | 1 | 1 | 1 | 4 | 1 | 8.8 |
| 83 | 0.3.0 | principle | **Read a name binding's head and tail from its binder's link, and delete `linkBindings`.** | 4 | 3 | 1 | 6 | 3.2 | 8.1 |
| 83 | 0.3.0 | principle | **Read a name binding's head and tail from its binder's link, and have each link pass return what it compiles, so `linkBindings` goes and no pass reads what an earlier one wrote in place.** | 4 | 4 | 1 | 6 | 3.2 | 7.8 |
| 63 | 0.4.0 | | **Pair a street with its exact postnummer.** | 3 | 6 | 5 | 8 | 4.1 | 19.1 |
| 62 | 0.4.0 | | **Add `{btc()}` and `{eth()}`.** | 2 | 4 | 4 | 6 | 4 | 15.2 |
| 59 | 0.4.0 | | **Promise in `Generator`'s godoc that its renders run one at a time.** | 1 | 1 | 5 | 5 | 12 | 13.2 |
@@ -446,9 +446,9 @@ Its doc says render failure, and it wraps what `NewTemplate` and `NewRecordTempl
`workflow_dispatch` leaves `github.event.before` empty, so the diff compares `HEAD` with itself and skips `docker build`.
### 83. Read a name binding's head and tail from its binder's link, and delete `linkBindings`.
### 83. Read a name binding's head and tail from its binder's link, and have each link pass return what it compiles, so `linkBindings` goes and no pass reads what an earlier one wrote in place.
`linkBindings` writes `nameBinding.head` and `tail` in `linkRefs`'s first pass and `linkName` reads them in its second, so swapping the passes hands `linkName` a nil head. Technical principle: one owner per value.
`linkBindings` writes `nameBinding.head` and `tail` in `linkRefs`'s first pass and `linkName` reads them in its second, so swapping the passes hands `linkName` a nil head. `linkPasses` does the same: `linkColumnRead` reads the `arm.named` that `linkNames` wrote, and `checkNameReads` the arms both filled. Technical principle: one owner per value.
### 63. Pair a street with its exact postnummer.