fix(parser): accept literal thresholds in infix power/toughness comparisons (Wasp, Shrinking Savior #5874)#5885
Conversation
…risons
An infix P/T comparison with a literal threshold -- "power less than 3",
"toughness greater than 4" -- failed to parse. parse_pt_infix_tail read the
threshold with parse_quantity_ref (dynamic game-quantity refs only), while
the postfix ("3 or less") and exact tails use parse_quantity_expr_number
(literals + X). So a bare-number infix threshold couldn't parse and the whole
comparison -- and any count built on it -- was silently dropped. Wasp,
Shrinking Savior ("draw a card for each creature with power less than 0")
collapsed to a flat Draw { count: Fixed(1) }, always drawing one.
Factor the threshold grammar into a shared parse_pt_threshold combinator
(dynamic ref, else literal/X) and use it in the infix tail; the dynamic ref
is tried first so prior behaviour is preserved. Strict "< N" / "> N" still
lower to LE/GE with a -+1 offset (CR 107.1: P/T are integers). The threshold
is now an axis independent of the comparison form -- general across
power/toughness, less-than/greater-than, strict/or-equal-to.
Closes phase-rs#5874
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Code Review
This pull request fixes an issue where infix power/toughness comparisons with literal thresholds (such as "power less than 0") failed to parse. It introduces a parse_pt_threshold helper to handle both dynamic references and literal numbers in the infix tail, and adds comprehensive unit and integration tests. The review feedback points out an incorrect Comprehensive Rules (CR) annotation in the new test comments, citing CR 122.1 (counters) instead of CR 121.1 (drawing cards).
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| id | ||
| } | ||
|
|
||
| /// CR 208.1 + CR 107.1 + CR 122.1 — production-path RESOLUTION proof for the |
There was a problem hiding this comment.
[MEDIUM] Incorrect CR annotation for drawing cards. Evidence:
crates/engine/src/game/filter.rs:5826.
Why it matters: The comment citesCR 122.1(which is about counters), but the test exercises drawing cards, which is governed byCR 121.1.
Suggested fix: ChangeCR 122.1toCR 121.1.
| /// CR 208.1 + CR 107.1 + CR 122.1 — production-path RESOLUTION proof for the | |
| /// CR 208.1 + CR 107.1 + CR 121.1 — production-path RESOLUTION proof for the |
References
- Every rules-touching line of engine code must carry a comment of the form
CR <number>: <description>. ACR <n>annotation where the cited rule's body does not describe what the code is doing is a violation. CR 119 (starting life) / CR 120 (damage) / CR 121 (drawing) are adjacent and easily confused. (link)
|
Closing — #5876 landed the same infix literal-threshold fix and resolves #5874 on main. Thanks @matthewevans; no need for a duplicate. (The full-grammar-axis production-resolution test here was the delta, but the merged fix already covers the case.) |
Closes #5874.
The bug
An infix P/T comparison with a literal threshold — "power less than 3", "toughness greater than 4" — fails to parse, so the filter (and any count built on it) is silently dropped. The postfix form ("3 or less") and the infix form with a dynamic threshold ("less than the number of cards in your hand") both already work.
Wasp, Shrinking Savior — "…draw a card for each creature with power less than 0 on the battlefield" — collapses to a flat
Draw { count: Fixed(1) }, always drawing 1 instead of one per creature with power < 0.Root cause
parse_pt_infix_tailread the threshold withparse_quantity_ref(dynamic game-quantity refs only), while the postfix and exact tails read it withparse_quantity_expr_number(literals + X). So an infix comparison against a bare number couldn't parse its threshold and the whole comparison failed.Fix
Factor the threshold grammar into a shared combinator
parse_pt_threshold— dynamic ref, else literal/X — and use it in the infix tail. The dynamic ref is tried first, so prior infix behaviour on a dynamic threshold is byte-for-byte preserved; a bare number / X now falls back to the same value grammar the postfix and exact tails already use. Strict</>still lower toLE/GEwith a ∓1 offset (CR 107.1: P/T are integers, so "less than N" ≡ "≤ N−1"). The threshold is now an axis independent of the comparison form — general across power/toughness, less-than/greater-than, strict/or-equal-to.Scoped to the infix tail only; the postfix/exact tails are unchanged (routing them through the union would silently widen them to accept dynamic thresholds — out of scope).
Tests
Parser (leaf grammar):
test_pt_infix_literal_threshold_full_grammar_axis— power/toughness × less/greater-than × strict/or-equal-to, all with literal thresholds, asserting the exact loweredComparator+Offset.test_pt_infix_dynamic_threshold_still_parses— the dynamic-ref path is preserved (no regression).Production-path resolution (full grammar axis):
pt_infix_literal_threshold_resolves_through_production_path_full_axis— parses real "…draw a card for each creature with<comparison>…" cards throughparse_oracle_text, extracts the Draw'sObjectCount, and resolves it throughresolve_quantity(the same shared authority the Draw effect uses in production) against one fixed battlefield of creatures A–E at power/toughness (-1,3)(0,5)(1,4)(2,5)(3,2). Case 1 is Wasp, Shrinking Savior through its exact printed text (→ 1); the others span the axis —≤ 1power → 3, strict> 4toughness → 2,≥ 4toughness → 3. Remove the fallback and every count collapses toFixed(1)(noObjectCountto extract), so the proof is tied to the fix.cargo clippy -p engine --lib -- -D warnings— clean. CR 107.1 annotated.Model: claude-opus-4-8[1m]