From 938bbb5c4307851fe2ec6c40823ab13b9f6ee674 Mon Sep 17 00:00:00 2001 From: Lilleman auf Larv Date: Mon, 5 Oct 2026 09:41:20 +0200 Subject: [PATCH] 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 --- README.md | 2 +- inline.go | 8 ++++++++ name_test.go | 6 ++++++ namefence.go | 36 +++++++++++++++++++++++++----------- reference.go | 8 -------- todo.md | 6 +++--- 6 files changed, 43 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index a56cd27..612a022 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/inline.go b/inline.go index fee06a0..4392e2a 100644 --- a/inline.go +++ b/inline.go @@ -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 +} diff --git a/name_test.go b/name_test.go index e0807fb..1cc0eb8 100644 --- a/name_test.go +++ b/name_test.go @@ -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) + } +} diff --git a/namefence.go b/namefence.go index 443ca17..bcb63e4 100644 --- a/namefence.go +++ b/namefence.go @@ -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 { diff --git a/reference.go b/reference.go index 11eccc7..af1ecca 100644 --- a/reference.go +++ b/reference.go @@ -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 -} diff --git a/todo.md b/todo.md index d5290a0..6aacf6f 100644 --- a/todo.md +++ b/todo.md @@ -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.