Repro (canonical, no face involved)
fn f() -> Int { let mut total = 0; total = total + 1; total }
parse error: Syntax error (at `total` in the `let` binding)
Rename total → sum/acc and it compiles + runs fine.
Cause
total is lexed as the keyword TOTAL (lib/lexer.ml:39), the totality modifier for total fn (lib/parser.mly:249). The grammar already recovers TOTAL as an ordinary identifier in expression position (lib/parser.mly:1359 — | TOTAL { mk_ident "total" … }, added for hand-ports like HealthIndex.total). But that recovery does not cover:
let/let mut binding-name position, and
- assignment-target (LHS) position.
total is the only soft-keyword in the table (lib/lexer.ml:13-40) that reads like a common variable name, so this bites real code — especially face-authored code (a Python-shaped mod naturally writes total). The other keywords (while, let, …) nobody names a variable.
Fix direction
Extend the TOTAL-as-ident recovery to the let-pattern and assignment-target nonterminals (mirroring parser.mly:1359). Per ADR-012 the change must be conflict-neutral — verify just build-loud / dune build show no new S/R or R/R conflicts (the comment at parser.mly:1350 argues TOTAL is safe because as a decl-modifier it always precedes FN; the same reasoning should extend to binding/assignment positions, but confirm against the conflict count).
Discovered
2026-07-07, while proving a Python-face program end-to-end to wasm (the faces path surfaces exactly this class of real-world naming friction).
Repro (canonical, no face involved)
Rename
total→sum/accand it compiles + runs fine.Cause
totalis lexed as the keywordTOTAL(lib/lexer.ml:39), the totality modifier fortotal fn(lib/parser.mly:249). The grammar already recoversTOTALas an ordinary identifier in expression position (lib/parser.mly:1359—| TOTAL { mk_ident "total" … }, added for hand-ports likeHealthIndex.total). But that recovery does not cover:let/let mutbinding-name position, andtotalis the only soft-keyword in the table (lib/lexer.ml:13-40) that reads like a common variable name, so this bites real code — especially face-authored code (a Python-shaped mod naturally writestotal). The other keywords (while,let, …) nobody names a variable.Fix direction
Extend the
TOTAL-as-ident recovery to the let-pattern and assignment-target nonterminals (mirroringparser.mly:1359). Per ADR-012 the change must be conflict-neutral — verifyjust build-loud/dune buildshow no new S/R or R/R conflicts (the comment atparser.mly:1350arguesTOTALis safe because as a decl-modifier it always precedesFN; the same reasoning should extend to binding/assignment positions, but confirm against the conflict count).Discovered
2026-07-07, while proving a Python-face program end-to-end to wasm (the faces path surfaces exactly this class of real-world naming friction).