R-576: go-parity gate also judges the CALL — arity vs printf verbs, and no key glued with +
Checks 4 (ARITY) and 5 (NO-CONCAT) in i18n_go_parity.py: at every message-helper call (MsgError/MsgErrorf/Text/Msgf/msg/msgLang/msgHU/note) whose key is a literal hu.json knows, the argument count must equal the Hungarian value's printf verbs; a known key literal may never be an operand of +. 485 calls judged, 0 defects today. Decoys: a dropped argument and a key+suffix concatenation on a real producer convict; a nested-paren argument is accepted. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
@@ -33,6 +33,12 @@ Checks:
|
||||
3. BYTE-EQUAL. A single-literal key: hu.json[key] == from, exactly. A joined key: hu.json[key]
|
||||
with its printf verbs removed == the `from` fragments concatenated in order, with their verbs
|
||||
removed too. Plural keys compare their `.one`/`.other`-less base form.
|
||||
4. ARITY (R-576). At a message-helper call whose key is a literal hu.json knows, the arguments
|
||||
after the key are as many as the Hungarian value's printf verbs.
|
||||
5. NO-CONCAT (R-576). A literal naming a bundle key is never an operand of `+`.
|
||||
Checks 1-3 ask whether the TEXT is real; 4-5 ask whether the CALL kept all of it. A structural
|
||||
gate over the text cannot see a defect in the call -- that is how 7 producers lost half their
|
||||
sentence on 2026-09-18 with this gate green.
|
||||
|
||||
The Go literal walker is the inventory's (felhom.eu/scripts/i18n_inventory.py `go_literals`),
|
||||
copied rather than imported: this gate runs from a controller clone that may not sit beside
|
||||
@@ -233,6 +239,150 @@ def base_form(hu: dict, key: str):
|
||||
return None
|
||||
|
||||
|
||||
# ---- R-576: the CALL, not only the TEXT ---------------------------------------------------------
|
||||
#
|
||||
# Checks 1-3 ask "is the text a key carries real?". They cannot see a call site that LOST text: on
|
||||
# 2026-09-18 the bulk converter turned `fmt.Errorf("a: "+ "b: %s", x)` into a call that kept only
|
||||
# the key and dropped the continuation and its argument -- 7 producers, and this gate stayed GREEN,
|
||||
# because every surviving fragment WAS a byte-equal base literal. Two more questions convict it:
|
||||
#
|
||||
# 4. ARITY. At a message-helper call whose key argument is a literal hu.json knows, the number of
|
||||
# arguments after the key equals the number of printf verbs in the Hungarian value. A call that
|
||||
# spreads a slice (`args...`) is not counted -- its arity is not visible in the source.
|
||||
# 5. NO-CONCAT. A literal that names a bundle key is never an operand of `+`. A key is a whole
|
||||
# name; a key glued to more text is either a half-converted concatenation or a key nobody can
|
||||
# find.
|
||||
#
|
||||
# helper name -> index of its KEY argument. Matched by NAME (no type checker), so only a call whose
|
||||
# key argument is a literal hu.json actually knows is ever judged.
|
||||
MSG_HELPERS = {
|
||||
"MsgError": 0, # util.MsgError(key, args...)
|
||||
"MsgErrorf": 1, # util.MsgErrorf(kind, key, args...)
|
||||
"Text": 1, # util.Text(lang, key, args...)
|
||||
"Msgf": 1, # (*i18n.Bundle).Msgf(lang, key, args...)
|
||||
"msg": 1, # (*Router|*Server).msg(req, key, args...)
|
||||
"msgLang": 1, # (*Router|*Server).msgLang(lang, key, args...)
|
||||
"msgHU": 0, # stacks.msgHU(key, args...)
|
||||
"note": 0, # (*backup.Manager).note(key, args...)
|
||||
}
|
||||
CALL_RE = re.compile(r"\b(" + "|".join(sorted(MSG_HELPERS, key=len, reverse=True)) + r")\(")
|
||||
|
||||
|
||||
def code_mask(src: str):
|
||||
"""(mask, lits): mask is 1 where src[i] is CODE (outside comments, strings and runes); lits is
|
||||
the (start, end) span of every interpreted string literal, quotes included."""
|
||||
mask = bytearray(len(src))
|
||||
lits = []
|
||||
i, n = 0, len(src)
|
||||
while i < n:
|
||||
c = src[i]
|
||||
if src.startswith("//", i):
|
||||
j = src.find("\n", i)
|
||||
i = n if j < 0 else j
|
||||
elif src.startswith("/*", i):
|
||||
j = src.find("*/", i + 2)
|
||||
i = n if j < 0 else j + 2
|
||||
elif c in "'\"":
|
||||
j = i + 1
|
||||
while j < n and src[j] != c and src[j] != "\n":
|
||||
j += 2 if src[j] == "\\" else 1
|
||||
if c == '"':
|
||||
lits.append((i, j + 1))
|
||||
i = j + 1
|
||||
elif c == "`":
|
||||
j = src.find("`", i + 1)
|
||||
i = n if j < 0 else j + 1
|
||||
else:
|
||||
mask[i] = 1
|
||||
i += 1
|
||||
return mask, lits
|
||||
|
||||
|
||||
def split_args(src: str, mask, open_paren: int):
|
||||
"""The top-level argument source strings of the call whose '(' is at open_paren, or None."""
|
||||
depth, start, args, i, n = 0, open_paren + 1, [], open_paren, len(src)
|
||||
while i < n:
|
||||
if mask[i]:
|
||||
c = src[i]
|
||||
if c in "([{":
|
||||
depth += 1
|
||||
elif c in ")]}":
|
||||
depth -= 1
|
||||
if depth == 0:
|
||||
tail = src[start:i].strip()
|
||||
if tail:
|
||||
args.append(tail)
|
||||
return args
|
||||
elif c == "," and depth == 1:
|
||||
args.append(src[start:i].strip())
|
||||
start = i + 1
|
||||
i += 1
|
||||
return None
|
||||
|
||||
|
||||
def verb_arity(value: str) -> int:
|
||||
"""How many arguments the printf verbs of value consume (explicit indexes respected)."""
|
||||
s = value.replace("%%", "")
|
||||
need, nxt = 0, 0
|
||||
for m in VERB_RE.finditer(s):
|
||||
idx = re.match(r"%\[(\d+)\]", m.group(0))
|
||||
if idx:
|
||||
nxt = int(idx.group(1))
|
||||
else:
|
||||
nxt += 1
|
||||
need = max(need, nxt)
|
||||
return need
|
||||
|
||||
|
||||
STR_LIT_RE = re.compile(r'^"((?:[^"\\\n]|\\.)*)"$')
|
||||
|
||||
|
||||
def call_site_defects(root: str, hu) -> tuple:
|
||||
"""Checks 4 (ARITY) and 5 (NO-CONCAT) over every in-scope Go file -> (defects, calls judged)."""
|
||||
bad, checked = [], 0
|
||||
if hu is None:
|
||||
return bad, checked
|
||||
for p in go_files(root):
|
||||
rel = os.path.relpath(p, root)
|
||||
src = read(p)
|
||||
mask, lits = code_mask(src)
|
||||
# 5. NO-CONCAT: a known key literal with a `+` on either side.
|
||||
for a, b in lits:
|
||||
text = unescape(src[a + 1:b - 1])
|
||||
if text not in hu or not KEY_SHAPE_RE.match(text):
|
||||
continue
|
||||
left = src[:a].rstrip(" \t")
|
||||
right = src[b:].lstrip(" \t")
|
||||
if left.endswith("+") or right.startswith("+"):
|
||||
line = src.count("\n", 0, a) + 1
|
||||
bad.append("CONCAT %s is glued to more text with + at %s:%d -- a key is a whole "
|
||||
"name; this is a half-converted concatenation" % (text, rel, line))
|
||||
# 4. ARITY.
|
||||
for m in CALL_RE.finditer(src):
|
||||
if not mask[m.start()]:
|
||||
continue
|
||||
args = split_args(src, mask, m.end() - 1)
|
||||
k = MSG_HELPERS[m.group(1)]
|
||||
if args is None or len(args) <= k:
|
||||
continue
|
||||
lit = STR_LIT_RE.match(args[k])
|
||||
if not lit:
|
||||
continue
|
||||
key = unescape(lit.group(1))
|
||||
if key not in hu or not KEY_SHAPE_RE.match(key):
|
||||
continue # a plural key (.one/.other only) or not a key at all
|
||||
rest = args[k + 1:]
|
||||
if rest and rest[-1].endswith("..."):
|
||||
continue # a spread slice: arity not visible
|
||||
checked += 1
|
||||
want = verb_arity(hu[key])
|
||||
if len(rest) != want:
|
||||
line = src.count("\n", 0, m.start()) + 1
|
||||
bad.append("ARITY %s called with %d argument(s) at %s:%d, its Hungarian value "
|
||||
"has %d printf verb(s): %r" % (key, len(rest), rel, line, want, hu[key]))
|
||||
return bad, checked
|
||||
|
||||
|
||||
def main(argv) -> int:
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--capture", action="store_true",
|
||||
@@ -334,16 +484,24 @@ def main(argv) -> int:
|
||||
" base (verbs removed): %r"
|
||||
% (key, len(froms), got, want))
|
||||
|
||||
# 4 + 5. ARITY and NO-CONCAT (R-576) -- the call, not the text.
|
||||
call_bad, judged = call_site_defects(CTRL, hu)
|
||||
bad.extend(call_bad)
|
||||
if judged == 0:
|
||||
bad.append("NO-CALLS no message-helper call with a literal key was found -- the call scanner "
|
||||
"no longer matches the code, so checks 4-5 would be green by seeing nothing")
|
||||
|
||||
print("go-parity: base capture %s (%d literals), %d slice-2 keys listed, %d pre-existing, "
|
||||
"%d named in Go"
|
||||
"%d named in Go, %d helper calls judged for arity"
|
||||
% (base.get("commit", "?")[:12] or "?", len(base_lits), len(keys), len(preexisting),
|
||||
len(named)))
|
||||
len(named), judged))
|
||||
if bad:
|
||||
print("\ngo-parity gate CONVICTS (%d):" % len(bad))
|
||||
for b in bad:
|
||||
print(" " + b)
|
||||
return 1
|
||||
print("go-parity gate OK: every Go-side key carries base-commit text, byte for byte.")
|
||||
print("go-parity gate OK: every Go-side key carries base-commit text, byte for byte, and every "
|
||||
"judged call passes as many arguments as its message has verbs.")
|
||||
return 0
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user