From b09bc56e8768f72b6402b85a8fdf906554bc3c78 Mon Sep 17 00:00:00 2001 From: M Date: Sun, 30 Aug 2026 23:05:00 +0200 Subject: [PATCH] Reject a repeated alternation arm MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit An alternation picks its arms evenly, so {a|a|b} was a covert 3:1 skew — a third spelling of what a choice's weight is for, reachable only by experiment. The check runs over the arms as written, before a reference arm is let through, so {..a|..a} is caught too. Co-Authored-By: Claude Opus 5 --- template.go | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/template.go b/template.go index 06c2036..0fa2962 100644 --- a/template.go +++ b/template.go @@ -133,7 +133,11 @@ func checkTokens(format string, fields map[string]node) error { if strings.IndexByte(t.body, '(') >= 0 { // a function token, not a field return checkFunc(t.body, fields) } - for _, name := range strings.Split(t.body, "|") { + names := strings.Split(t.body, "|") + if err := checkNoRepeatedArm(t.body, names); err != nil { + return err + } + for _, name := range names { if isRef(name) { if name == refPrefix { return fmt.Errorf("token {%s}: reference has no path", t.body) @@ -159,6 +163,24 @@ func checkTokens(format string, fields map[string]node) error { }) } +// checkNoRepeatedArm rejects {a|a|b}. An alternation picks its arms evenly, so a +// repeated one skews the odds — a third spelling of what weight is for, and one an +// author is far likelier to have typed by accident than meant. The error names the +// spelling that does skew a pick. +func checkNoRepeatedArm(body string, names []string) error { + if len(names) < 2 { + return nil + } + seen := make(map[string]bool, len(names)) + for _, name := range names { + if seen[name] { + return fmt.Errorf("token {%s}: arm %q is repeated; an alternation picks its arms evenly, so skew the odds with a choice's weights instead", body, name) + } + seen[name] = true + } + return nil +} + // fieldTokens returns the field and reference names a format renders via {name} // or {a|..b} tokens (function tokens, which carry no field edges, are excluded). // These are exactly the child nodes expand's resolve recurses into.