Reference sigils follow the filesystem: / the root, . this folder, .. the folder above
Tests / vet + fmt + tests (pull_request) Successful in 53s

This commit is contained in:
2026-09-02 18:26:02 +02:00
parent 5d1e567b8d
commit 1bf5470c6f
7 changed files with 169 additions and 78 deletions
+22 -18
View File
@@ -135,10 +135,10 @@ func checkTokens(format string, fields map[string]node) error {
names := strings.Split(t.body, "|")
for _, name := range names {
if isRef(name) {
if name == refPrefix {
return fmt.Errorf("token {%s}: reference has no path", t.body)
if _, _, err := refShape(name); err != nil {
return fmt.Errorf("token {%s}: %w", t.body, err)
}
continue // a root reference; its target is checked at New (see linkRefs)
continue // its target is checked at New (see linkRefs)
}
if err := checkArm(name, fields); err != nil {
return fmt.Errorf("token {%s}: %w", t.body, err)
@@ -230,7 +230,7 @@ func fieldTokens(format string) []string {
}
// arm is one alternative of a {a|b} token or one operand, split into the key
// naming the node in a template's fields (a sibling field, or the "..path" head a
// naming the node in a template's fields (a sibling field, or the head a
// reference is bound under) and the tail of a dotted path into it. A non-empty
// tail is what makes the arm a bound draw: its head is drawn once per expansion
// (see compileOps).
@@ -241,15 +241,19 @@ type arm struct {
steps []string // key per level passed through; the head and leaf hold their own
}
// splitArm splits one name into key and tail. refs maps a reference to the head
// linkRefs bound it under; before linking, a reference is whole.
func splitArm(name string, refs map[string]string) arm {
// splitArm splits one name into key and tail. refs maps a reference to what
// linkRefs bound it to; before linking, a reference is whole.
func splitArm(name string, refs map[string]refBinding) arm {
if isRef(name) {
key, bound := refs[name]
if !bound || key == name {
return arm{name: name, key: name}
b, bound := refs[name]
if !bound || len(b.tail) == 0 {
key := name
if bound {
key = b.key
}
return arm{name: name, key: key}
}
return pathArm(name, key, strings.Split(name[len(key)+1:], "."))
return pathArm(name, b.key, b.tail)
}
head, tail, dotted := strings.Cut(name, ".")
if !dotted {
@@ -271,7 +275,7 @@ func pathArm(name, key string, segs []string) arm {
// level's held draw while rendering the level expands it afresh, so their values
// would disagree. Names are compared in sorted order, so which pair is reported
// does not depend on where the tokens sit.
func checkNoOverlap(format string, bound map[string]string, refs map[string]string) error {
func checkNoOverlap(format string, bound map[string]string, refs map[string]refBinding) error {
names := boundReaders(format, bound, refs)
// 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.
@@ -292,7 +296,7 @@ type reader struct{ name, label string }
// boundReaders lists every way a format reaches a bound field, 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 boundReaders(format string, bound map[string]string, refs map[string]string) []reader {
func boundReaders(format string, bound map[string]string, refs map[string]refBinding) []reader {
var names []reader
_ = eachToken(format, func(t ftoken) error {
if t.kind != 'b' {
@@ -336,7 +340,7 @@ func checkSegments(a arm) error {
}
// splitArms splits a token body's '|' alternatives.
func splitArms(body string, refs map[string]string) []arm {
func splitArms(body string, refs map[string]refBinding) []arm {
parts := strings.Split(body, "|")
arms := make([]arm, len(parts))
for i, p := range parts {
@@ -396,7 +400,7 @@ func (c *formatOps) hold(a arm, label string) {
}
}
func (c *formatOps) function(body string, refs map[string]string) {
func (c *formatOps) function(body string, refs map[string]refBinding) {
name, args, _ := funcCall(body)
var operands []arm
for _, operand := range tokenOperands(body) {
@@ -407,7 +411,7 @@ func (c *formatOps) function(body string, refs map[string]string) {
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) {
func (c *formatOps) field(body string, refs map[string]refBinding) {
arms := splitArms(body, refs)
for _, a := range arms {
if len(a.tail) > 0 {
@@ -419,7 +423,7 @@ func (c *formatOps) field(body string, refs map[string]string) {
// 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 {
func compileOps(format string, refs map[string]refBinding) formatOps {
var c formatOps
var lit strings.Builder
flush := func() {
@@ -450,7 +454,7 @@ func compileOps(format string, refs map[string]string) formatOps {
// 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 {
func checkNoRepeatedRead(format string, c formatOps, refs map[string]refBinding) error {
count := map[string]int{}
return eachToken(format, func(t ftoken) error {
if t.kind != 'b' {