From a7d77bd3adbca012b874786a65dff85c02efd6e1 Mon Sep 17 00:00:00 2001 From: Lilleman auf Larv Date: Tue, 29 Sep 2026 10:11:12 +0200 Subject: [PATCH] Rename arm.key to head, give renderScope.row its own type, and value.go to valueproof.go --- README.md | 2 +- datatype.go | 2 +- drawfence.go | 4 +-- format.go | 66 +++++++++++++++++++-------------------- graph.go | 4 +-- hold.go | 13 +++++--- hold_test.go | 2 +- holdfence.go | 38 +++++++++++----------- node.go | 12 +++---- readsurvey.go | 2 +- readsurvey_test.go | 2 +- reference.go | 2 +- render.go | 12 +++---- table.go | 4 +-- table_test.go | 4 +-- todo.md | 3 -- value.go => valueproof.go | 2 +- 17 files changed, 88 insertions(+), 86 deletions(-) rename value.go => valueproof.go (99%) diff --git a/README.md b/README.md index c9cae0b..5867104 100644 --- a/README.md +++ b/README.md @@ -896,7 +896,7 @@ checksum.go the check characters a derivation appends, and the IBAN they sit transform.go the builtins that rewrite an operand's value, and the ASCII folding calc.go the {calc()} arithmetic evaluator: parser, eval, validation datatype.go column datatypes: DataType, where datatype and null may sit, a column's datatype -value.go the value proof: what a typed column or calc operand holds, checked at load +valueproof.go the value proof: what a typed column or calc operand holds, checked at load data.go the load path: fs.FS folders/files -> merged namespace tree, and the bind pipeline every entry point runs cmd/fejkdata/ the fejkdata CLI data/ shipped data (JSON, and a TSV per table), embedded at build: locale folders, geo, misc diff --git a/datatype.go b/datatype.go index 6837775..9930975 100644 --- a/datatype.go +++ b/datatype.go @@ -77,7 +77,7 @@ func checkColumns(s nodeScope) error { items, _ := columnItems(column) for _, it := range items { if r := it.link.readsColumn; r != nil { - if err := check(r.a.key[1:], r.a.tail[0], r.column); err != nil { + if err := check(r.a.head[1:], r.a.tail[0], r.column); err != nil { return err } } diff --git a/drawfence.go b/drawfence.go index 6080817..325695e 100644 --- a/drawfence.go +++ b/drawfence.go @@ -90,7 +90,7 @@ func checkOwnFamily(t *template) error { seen[n] = true for _, e := range renderEdges(n) { if e.readsRef() { - if head, isTable := n.(*template).head(e.read.key).(*table); isTable && head.familyRoot() == own.familyRoot() { + if head, isTable := n.(*template).head(e.read.head).(*table); isTable && head.familyRoot() == own.familyRoot() { return e, head, true } } @@ -183,7 +183,7 @@ func readsTable(n node, a arm) bool { if !isTemplate { return false } - _, isTable := t.head(a.key).(*table) + _, isTable := t.head(a.head).(*table) return isTable } diff --git a/format.go b/format.go index f232179..d9849c5 100644 --- a/format.go +++ b/format.go @@ -246,25 +246,25 @@ func checkArm(name string, fields map[string]node, wholeToken bool) error { if err := checkSegments(a); err != nil { return err } - field, ok := fields[a.key] + field, ok := fields[a.head] if !ok { - if a.key == "" { + if a.head == "" { return fmt.Errorf("a name is never empty, so this token can name no field") } - if isOption(a.key) { - return fmt.Errorf("%q is an option and can never be a field", a.key) + if isOption(a.head) { + return fmt.Errorf("%q is an option and can never be a field", a.head) } if len(fields) == 0 { hint := "" if wholeToken && hintableRef(name) { hint = fmt.Sprintf(" — write {/%s} to reference the data", name) } - return fmt.Errorf("no field %q; a token names a sibling field, and this template has none%s", a.key, hint) + return fmt.Errorf("no field %q; a token names a sibling field, and this template has none%s", a.head, hint) } - return fmt.Errorf("no field %q", a.key) + return fmt.Errorf("no field %q", a.head) } - if err := checkPath(field, a.tail, a.key); err != nil { - return fmt.Errorf("field %q: %w", a.key, err) + if err := checkPath(field, a.tail, a.head); err != nil { + return fmt.Errorf("field %q: %w", a.head, err) } return nil } @@ -302,45 +302,45 @@ func checkNoRepeatedArm(body string, names []string) error { return nil } -// arm is one alternative of a {a|b} token or one operand, split into the key +// arm is one alternative of a {a|b} token or one operand, split into the head // `template.head` resolves and the tail of a dotted path into it. A non-empty tail // is what makes the arm a held draw: its head is drawn once per expansion (see // compileOps). type arm struct { spelling string // as written, for messages - key string + head string tail []string - levels []string // the key at each level the tail passes through, the arm's key first - path string // key and tail, the one path every way of writing this read shares + levels []string // the path at each level the tail passes through, the head first + path string // head and tail, the one path every way of writing this read shares } -// splitArm splits one name into key and tail. refs maps a reference to what +// splitArm splits one name into head and tail. refs maps a reference to what // linkRefs resolved it to; before linking, a reference is whole. func splitArm(name string, refs map[string]refBinding) arm { if isRef(name) { b, linked := refs[name] if !linked || len(b.tail) == 0 { - key := name + head := name if linked { - key = b.key + head = b.key } - return arm{spelling: name, key: key, path: key} + return arm{spelling: name, head: head, path: head} } return pathArm(name, b.key, b.tail) } segs, err := splitPath(name) if err != nil || len(segs) == 1 { - return arm{spelling: name, key: name, path: name} + return arm{spelling: name, head: name, path: name} } return pathArm(name, segs[0], segs[1:]) } -func pathArm(name, key string, segs []string) arm { - levels := []string{key} +func pathArm(name, head string, segs []string) arm { + levels := []string{head} for i := 0; i < len(segs)-1; i++ { - levels = append(levels, key+"."+strings.Join(segs[:i+1], ".")) + levels = append(levels, head+"."+strings.Join(segs[:i+1], ".")) } - return arm{spelling: name, key: key, tail: segs, levels: levels, path: key + "." + strings.Join(segs, ".")} + return arm{spelling: name, head: head, tail: segs, levels: levels, path: head + "." + strings.Join(segs, ".")} } // checkSegments rejects an unfinished path: "{a.}" and "{a..b}" each have a @@ -350,7 +350,7 @@ func checkSegments(a arm) error { if len(a.tail) == 0 { return nil } - if a.key == "" { + if a.head == "" { return fmt.Errorf("path has an empty segment") } for _, seg := range a.tail { @@ -379,13 +379,13 @@ type op struct { } // formatOps is a compiled format: its ops, the size of its literal text, and the -// names it holds. pathKeys maps each key a path starts from to the first such path; -// held is every such key plus the fields an operand reads; holder maps each held +// names it holds. pathHeads maps each head a path starts from to the first such path; +// held is every such head plus the fields an operand reads; holder maps each held // name to the first reader holding it, for error messages. type formatOps struct { ops []op grow int - pathKeys map[string]string + pathHeads map[string]string held map[string]bool holder map[string]string heldLocal bool // some held name is not a reference path, so expand makes a hold @@ -396,19 +396,19 @@ func (c *formatOps) holdName(a arm, label string) { c.held = map[string]bool{} c.holder = map[string]string{} } - c.held[a.key] = true - if !isRef(a.key) || len(a.tail) == 0 { + c.held[a.head] = true + if !isRef(a.head) || len(a.tail) == 0 { c.heldLocal = true } - if _, named := c.holder[a.key]; !named { - c.holder[a.key] = label + if _, named := c.holder[a.head]; !named { + c.holder[a.head] = label } if len(a.tail) > 0 { - if c.pathKeys == nil { - c.pathKeys = map[string]string{} + if c.pathHeads == nil { + c.pathHeads = map[string]string{} } - if _, named := c.pathKeys[a.key]; !named { - c.pathKeys[a.key] = a.spelling + if _, named := c.pathHeads[a.head]; !named { + c.pathHeads[a.head] = a.spelling } } } diff --git a/graph.go b/graph.go index bbb226b..d99fb26 100644 --- a/graph.go +++ b/graph.go @@ -109,7 +109,7 @@ type renderEdge struct { operand string } -func (e renderEdge) readsRef() bool { return isRef(e.read.key) } +func (e renderEdge) readsRef() bool { return isRef(e.read.head) } // reached names an edge as the author spelled it. func (e renderEdge) reached() string { @@ -133,7 +133,7 @@ func renderEdges(n node) []renderEdge { case *template: var es []renderEdge add := func(a arm, operand string) { - c := n.head(a.key) + c := n.head(a.head) if c == nil { return } diff --git a/hold.go b/hold.go index a97fb26..dfaf116 100644 --- a/hold.go +++ b/hold.go @@ -41,7 +41,12 @@ type holdSet struct { type renderScope struct { set *holdSet group string - row tablePin + row renderedRow +} + +type renderedRow struct { + t *table + index int } // in is the scope t renders in: its draw group where it names one, else its caller's. @@ -57,9 +62,9 @@ func (sc renderScope) in(t *template) renderScope { func (sc renderScope) at(n node, pins *pinSet) renderScope { switch n := n.(type) { case *tableRow: - sc.row = tablePin{n.t, pins.mustRow(n.t)} + sc.row = renderedRow{n.t, pins.mustRow(n.t)} case *tableColumn: - sc.row = tablePin{n.t, pins.mustRow(n.t)} + sc.row = renderedRow{n.t, pins.mustRow(n.t)} } return sc } @@ -69,7 +74,7 @@ func (sc renderScope) rowOf(t *table) int { if sc.row.t != t { panic(internalError("a column of %s renders in a scope holding no row of it", t.segment)) } - return sc.row.row + return sc.row.index } func (sc renderScope) groupHold() *groupHold { diff --git a/hold_test.go b/hold_test.go index 5121c13..b3621d5 100644 --- a/hold_test.go +++ b/hold_test.go @@ -761,7 +761,7 @@ func TestRepeatedBareTokenOfAHeldNameIsRejected(t *testing.T) { } func TestReadFieldPanicsOnAPathItCannotHold(t *testing.T) { - a := arm{spelling: "w.x", key: "w", tail: []string{"x"}, path: "w.x"} + a := arm{spelling: "w.x", head: "w", tail: []string{"x"}, path: "w.x"} for name, format := range map[string]string{ "unheld arm with a path": `{"format":"{w}","w":{"format":"{x}","x":"1"}}`, "held arm with no hold": `{"format":"{w.x}","w":{"format":"{x}","x":"1"}}`, diff --git a/holdfence.go b/holdfence.go index 9343523..534e547 100644 --- a/holdfence.go +++ b/holdfence.go @@ -15,9 +15,9 @@ func heldCheck(path string, n node) error { if !ok || len(t.compiled.held) == 0 { return nil } - readers := pathKeyReaders(t.compiled.ops, t.compiled.pathKeys) + readers := pathHeadReaders(t.compiled.ops, t.compiled.pathHeads) for _, name := range heldNames(t) { - if _, isPath := t.compiled.pathKeys[name]; isPath && isRef(name) { + if _, isPath := t.compiled.pathHeads[name]; isPath && isRef(name) { continue // held for the render: drawFence compares every read of it, by path, across the render and its groups } if err := checkNameHeld(t, name, readers); err != nil { @@ -36,8 +36,8 @@ func heldNames(t *template) []string { names = append(names, name) } sort.Slice(names, func(i, j int) bool { - _, pi := t.compiled.pathKeys[names[i]] - _, pj := t.compiled.pathKeys[names[j]] + _, pi := t.compiled.pathHeads[names[i]] + _, pj := t.compiled.pathHeads[names[j]] if pi != pj { return !pi } @@ -51,12 +51,12 @@ func heldNames(t *template) []string { // produces. func heldNodes(t *template, name string, readers []reader) map[node]bool { held := map[node]bool{} - if _, isPath := t.compiled.pathKeys[name]; !isPath { + if _, isPath := t.compiled.pathHeads[name]; !isPath { operandDraw(t.head(name), held) return held } for _, r := range readers { - if r.a.key == name { + if r.a.head == name { coverPath(t.head(name), r.a.tail, held) } } @@ -71,10 +71,10 @@ func checkNameHeld(t *template, name string, readers []reader) error { if len(held) == 0 { return nil // a fixed head holds nothing to reach } - reader, isPath := t.compiled.pathKeys[name] + reader, isPath := t.compiled.pathHeads[name] seen := map[node]bool{} for _, e := range renderEdges(t) { - if e.read.key == name || !renders(e.to, held, seen) { + if e.read.head == name || !renders(e.to, held, seen) { continue } if isPath { @@ -89,7 +89,7 @@ func checkNameHeld(t *template, name string, readers []reader) error { func operandReader(t *template, name string) string { for _, o := range t.compiled.ops { for _, a := range o.operands { - if a.key == name { + if a.head == name { return o.fn } } @@ -175,8 +175,8 @@ func renders(n node, want, seen map[node]bool) bool { // disagree. Reads are compared in sorted order, so which pair is reported does not // depend on where the tokens sit. // docs/decisions.md#a-bare-reference-draws-each-time-a-reference-path-is-held -func checkNoOverlap(ops []op, pathKeys map[string]string) error { - names := pathKeyReaders(ops, pathKeys) +func checkNoOverlap(ops []op, pathHeads map[string]string) error { + names := pathHeadReaders(ops, pathHeads) // Stable over one format-order scan, so two readers of one name (a token and a // calc operand both naming "p") are reported as the format writes them. sort.SliceStable(names, func(i, j int) bool { return names[i].a.path < names[j].a.path }) @@ -190,25 +190,25 @@ func checkNoOverlap(ops []op, pathKeys map[string]string) error { return nil } -// reader is one way a format reaches a path key, and how to name it. +// reader is one way a format reaches a path head, and how to name it. type reader struct { a arm label string } -// pathKeyReaders lists every way a format reaches a sibling path key, in the order the +// pathHeadReaders lists every way a format reaches a sibling path head, in the order the // format writes them. An operand renders its field, so it names a level exactly // as a token does; one scan finds both, which is what puts them in one order. -func pathKeyReaders(ops []op, pathKeys map[string]string) []reader { +func pathHeadReaders(ops []op, pathHeads map[string]string) []reader { var names []reader for _, o := range ops { for _, a := range o.operands { - if _, isPathKey := pathKeys[a.key]; isPathKey && !isRef(a.key) { + if _, isPathHead := pathHeads[a.head]; isPathHead && !isRef(a.head) { names = append(names, reader{a, fmt.Sprintf("%s operand %q", o.fn, a.spelling)}) } } for _, a := range o.arms { - if _, isPathKey := pathKeys[a.key]; isPathKey && !isRef(a.key) { + if _, isPathHead := pathHeads[a.head]; isPathHead && !isRef(a.head) { names = append(names, reader{a, "token {" + a.spelling + "}"}) } } @@ -224,11 +224,11 @@ func checkNoRepeatedRead(c formatOps) error { count := map[string]int{} for _, o := range c.ops { for _, a := range o.arms { - if len(a.tail) > 0 || !c.held[a.key] { + if len(a.tail) > 0 || !c.held[a.head] { 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.spelling, c.holder[a.key], a.key, a.spelling) + if count[a.head]++; count[a.head] > 1 { + return fmt.Errorf("token {%s} is repeated, and %s holds %q to one draw per expansion; write {%s} once", a.spelling, c.holder[a.head], a.head, a.spelling) } } } diff --git a/node.go b/node.go index 77ab03d..fc53753 100644 --- a/node.go +++ b/node.go @@ -81,12 +81,12 @@ type templateLink struct { func (*template) isNode() {} -// head is the node an arm's key names: a sibling field, or a reference's category. -func (t *template) head(key string) node { - if isRef(key) { - return t.link.refHeads[key] +// head is the node an arm's head names: a sibling field, or a reference's category. +func (t *template) head(name string) node { + if isRef(name) { + return t.link.refHeads[name] } - return t.fields[key] + return t.fields[name] } // compile converts parsed JSON — a category or an inline template — into a node tree, @@ -229,7 +229,7 @@ func (t *template) fixedText() (string, bool) { // compiled reads. func compileFormat(toks []formatToken, refs map[string]refBinding) (formatOps, error) { c := compileOps(toks, refs) - if err := checkNoOverlap(c.ops, c.pathKeys); err != nil { + if err := checkNoOverlap(c.ops, c.pathHeads); err != nil { return c, err } return c, checkNoRepeatedRead(c) diff --git a/readsurvey.go b/readsurvey.go index e2e94e6..8f5106e 100644 --- a/readsurvey.go +++ b/readsurvey.go @@ -143,7 +143,7 @@ func (s *readSurvey) walk(n node, at surveyAt) { func (s *readSurvey) edge(from node, e renderEdge, at surveyAt) { if e.readsRef() { a := e.read - tr := tableReadOf(from.(*template).head(a.key), a, e.to) + tr := tableReadOf(from.(*template).head(a.head), a, e.to) if k := (readKey{at.group, a.path, s.pinsID(&at.pins), s.pinsID(&at.wholePins)}); !s.read[k] { s.read[k] = true s.reads = append(s.reads, pathRead{at, a, tr}) diff --git a/readsurvey_test.go b/readsurvey_test.go index bcf71e2..5e67d68 100644 --- a/readsurvey_test.go +++ b/readsurvey_test.go @@ -35,7 +35,7 @@ func wantGathered(t *testing.T, tables map[string]*table, label string, gathered } seen := map[traceKey]bool{} trace := func(group, table string, row int, a arm) { - if !isRef(a.key) { + if !isRef(a.head) { return } r := pathRead{at: surveyAt{group: group}, a: a} diff --git a/reference.go b/reference.go index 668b1db..03e2902 100644 --- a/reference.go +++ b/reference.go @@ -146,7 +146,7 @@ func columnReadOf(t *template, link templateLink) *columnRead { return nil } a := splitArm(name, link.refs) - target, isTemplate := link.refHeads[a.key].(*template) + target, isTemplate := link.refHeads[a.head].(*template) if !isTemplate || !target.isRecord || len(a.tail) != 1 { return nil } diff --git a/render.go b/render.go index c2433e8..b8821e4 100644 --- a/render.go +++ b/render.go @@ -62,7 +62,7 @@ func render(s *session, n node, sc renderScope) string { case *nullItem: return "" case *table: - sc.row = tablePin{n, n.drawRow(s)} + sc.row = renderedRow{n, n.drawRow(s)} return expand(s, n.formatTemplate, sc) case *tableRow: return expand(s, n.t.formatTemplate, sc) @@ -159,17 +159,17 @@ func expand(s *session, t *template, sc renderScope) string { // 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 { - if isRef(a.key) && len(a.tail) > 0 { + if isRef(a.head) && len(a.tail) > 0 { return readReference(s, t, sc, a) } if sc.set.trace != nil { traceRead(sc.set.trace, t, sc, a) } - if !t.compiled.held[a.key] { + if !t.compiled.held[a.head] { if len(a.tail) > 0 { - panic(internalError("%q reads a path into %q, which the expansion does not hold", a.spelling, a.key)) + panic(internalError("%q reads a path into %q, which the expansion does not hold", a.spelling, a.head)) } - return readValue{text: render(s, t.head(a.key), sc)} + return readValue{text: render(s, t.head(a.head), sc)} } if held == nil { panic(internalError("%q reads a name the expansion holds, with no hold to keep it in", a.spelling)) @@ -191,7 +191,7 @@ func readHeld(s *session, t *template, held *hold, pins *pinSet, sc renderScope, if r, done := held.value[a.path]; done { return r } - leaf := drawPath(t.head(a.key), a.tail, a.key, &pathDraw{s: s, held: held, pins: pins, a: &a}) + leaf := drawPath(t.head(a.head), a.tail, a.head, &pathDraw{s: s, held: held, pins: pins, a: &a}) if pins != nil { sc = sc.at(leaf, pins) } diff --git a/table.go b/table.go index a5c70e8..feee148 100644 --- a/table.go +++ b/table.go @@ -330,8 +330,8 @@ func (t *table) compileRowFormat(format string) error { } for _, name := range tok.names { a := splitArm(name, nil) - if _, ok := t.col[a.key]; !ok && !isRef(a.key) && a.key != "" { - return fmt.Errorf("format names no column %q of %s; the columns are %v", a.key, t.file, t.header) + if _, ok := t.col[a.head]; !ok && !isRef(a.head) && a.head != "" { + return fmt.Errorf("format names no column %q of %s; the columns are %v", a.head, t.file, t.header) } } } diff --git a/table_test.go b/table_test.go index 8a3d656..22c03f6 100644 --- a/table_test.go +++ b/table_test.go @@ -1063,7 +1063,7 @@ func TestProbeReportsTheTablesADrawPins(t *testing.T) { {"region[12].name", "region=region[12]"}, } { a := splitArm(c.path, nil) - head := f.root.children[a.key].(*table) + head := f.root.children[a.head].(*table) tr := tableReadOf(head, a, nil) var sels []string for _, s := range tr.sels { @@ -1075,7 +1075,7 @@ func TestProbeReportsTheTablesADrawPins(t *testing.T) { probed, drawn := tr.pins, tr.drawn for i := 0; i < 20; i++ { var pins pinSet - drawPath(head, a.tail, a.key, &pathDraw{s: f.rand, pins: &pins}) + drawPath(head, a.tail, a.head, &pathDraw{s: f.rand, pins: &pins}) want := map[*table]bool{} pins.each(func(tbl *table, _ int) { if _, selected := probed.pinned(tbl); !selected { diff --git a/todo.md b/todo.md index 9607b20..e042842 100644 --- a/todo.md +++ b/todo.md @@ -28,9 +28,6 @@ ranking `readSurvey.walk` hardest and all seven the unit they would least modify This round, in order: -- Rename `arm.key` to `head`, as `template.head` reads it, leaving `key` the table's key - column; give `renderScope.row` a type that is not `tablePin`, since it pins nothing; - and name `value.go` by the `valueProof` it holds. - Delete the prose the seats found empty: "kept whole on purpose" (`format.go`, `calc.go`), the second copy of the concrete-session rationale (`table.drawRow`, `pick`), the doc comments restating `isSelector`, `selectorOf`, diff --git a/value.go b/valueproof.go similarity index 99% rename from value.go rename to valueproof.go index ef82019..9c46f5b 100644 --- a/value.go +++ b/valueproof.go @@ -164,7 +164,7 @@ func (p *valueProof) proveTemplate(t *template) proven { case o.kind == nameRead: var leaves []node for _, a := range o.arms { - leaves = append(leaves, pathLeaves(t.head(a.key), a.tail)...) + leaves = append(leaves, pathLeaves(t.head(a.head), a.tail)...) } return p.proveUnion(leaves) case name == "calc":