Skip to content

fix(parser): accept literal thresholds in infix power/toughness comparisons (Wasp, Shrinking Savior #5874)#5885

Closed
Yurii214 wants to merge 1 commit into
phase-rs:mainfrom
Yurii214:fix/5874-pt-infix-literal-threshold
Closed

fix(parser): accept literal thresholds in infix power/toughness comparisons (Wasp, Shrinking Savior #5874)#5885
Yurii214 wants to merge 1 commit into
phase-rs:mainfrom
Yurii214:fix/5874-pt-infix-literal-threshold

Conversation

@Yurii214

Copy link
Copy Markdown
Contributor

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_tail read the threshold with parse_quantity_ref (dynamic game-quantity refs only), while the postfix and exact tails read it with parse_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_thresholddynamic 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 to LE/GE with 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 lowered Comparator + 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 through parse_oracle_text, extracts the Draw's ObjectCount, and resolves it through resolve_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 — ≤ 1 power → 3, strict > 4 toughness → 2, ≥ 4 toughness → 3. Remove the fallback and every count collapses to Fixed(1) (no ObjectCount to 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]

…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>
@Yurii214
Yurii214 requested a review from matthewevans as a code owner July 15, 2026 21:28

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

[MEDIUM] Incorrect CR annotation for drawing cards. Evidence: crates/engine/src/game/filter.rs:5826.
Why it matters: The comment cites CR 122.1 (which is about counters), but the test exercises drawing cards, which is governed by CR 121.1.
Suggested fix: Change CR 122.1 to CR 121.1.

Suggested change
/// 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
  1. Every rules-touching line of engine code must carry a comment of the form CR <number>: <description>. A CR <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)

@Yurii214

Copy link
Copy Markdown
Contributor Author

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.)

@Yurii214 Yurii214 closed this Jul 15, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Parser: infix "power/toughness less than N" with a literal threshold is dropped (Wasp, Shrinking Savior)

2 participants