From dc41c99d3046df5f441dcc099f93776ff25b3381 Mon Sep 17 00:00:00 2001 From: hyperpolymath <6759885+hyperpolymath@users.noreply.github.com> Date: Mon, 18 May 2026 11:46:13 +0100 Subject: [PATCH] fix(parser): resolve state-41 effect-row MINUS/ARROW S/R conflict via precedence MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit First PR of the dedicated grammar-conflict-elimination workstream (epic #215), family A (effect-row MINUS overload), owner-prioritised first. In `type_expr_arrow`, after a `type_expr_primary`, lookahead `ARROW` (continue an arrow type) or `MINUS` (open the legacy `-{E}->` effect row, parser.mly:410) raced against reducing the base case `type_expr_arrow -> type_expr_primary` (state 41, tokens MINUS ARROW). Shifting is *always* the correct choice (a bare type can never both be followed by `->`/`-{` and want to stop there), and was already Menhir's arbitrary resolution — so making it explicit changes no parse. Fix: a lowest-precedence pseudo-token `LOWEST_TYPE_ARROW` (never lexed) tags the base reduction; `ARROW` is given a precedence just above it. `MINUS` already outranks both, so on either lookahead the parser shifts *by declared precedence* instead of by an arbitrary tie-break, and the conflict disappears. `ARROW` is a conflict lookahead ONLY in state 41 (verified via `menhir --explain`), so giving it precedence is side-effect-free. Rigorous verification (full `menhir --explain` diff, baseline = #214): - S/R arbitrarily resolved: 75 -> 72 - S/R conflict states: 25 -> 23 (state 41 eliminated) - R/R: 10 (untouched); no new conflict states introduced - The 16 pre-existing "precedence/`%prec` never useful" warnings are byte-for-byte identical before and after — NOT introduced here (they are dead precedence declarations; filed as family F on #215). - `dune test --force` green at 257/257 (parse-behaviour-preserving). Refs #215 Co-Authored-By: Claude Opus 4.7 (1M context) --- lib/parser.mly | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/lib/parser.mly b/lib/parser.mly index d1ded99..a3a5d58 100644 --- a/lib/parser.mly +++ b/lib/parser.mly @@ -83,6 +83,20 @@ let rec effect_union_of_list = function %token EOF /* Precedence (lowest to highest) */ +/* `LOWEST_TYPE_ARROW` is a pseudo-token (never lexed) carrying the + lowest precedence. It tags the base `type_expr_arrow -> type_expr_primary` + reduction so that in state 41 — after a `type_expr_primary`, lookahead + `ARROW` (continue an arrow type) or `MINUS` (open the legacy `-{E}->` + effect row) — the parser SHIFTS rather than reducing the base case. + Shift was already Menhir's (correct) arbitrary resolution there (a + bare type can never be followed by `->`/`-{` *and* want to stop), so + declaring this precedence is parse-behaviour-preserving and only + silences the spurious conflict. `ARROW` has no precedence elsewhere + and is a conflict lookahead ONLY in state 41 (verified via + `menhir --explain`), so giving it precedence here is side-effect-free. + See affinescript#215 (family A). */ +%nonassoc LOWEST_TYPE_ARROW +%nonassoc ARROW %right EQ PLUSEQ MINUSEQ STAREQ SLASHEQ %left PIPEPIPE %left AMPAMP @@ -416,7 +430,7 @@ type_expr_arrow: past the closing RPAREN. */ | LPAREN ty1 = type_expr COMMA tys = separated_nonempty_list(COMMA, type_expr) RPAREN ARROW ret = type_expr_arrow { List.fold_right (fun p acc -> TyArrow (p, None, acc, None)) (ty1 :: tys) ret } - | ty = type_expr_primary { ty } + | ty = type_expr_primary %prec LOWEST_TYPE_ARROW { ty } type_expr_primary: | LPAREN RPAREN { TyTuple [] }