feat(parser): parse "for each <type> you control with <keyword>" counts#5210
Conversation
Add `parse_for_each_controlled_type_with_keyword` to the "for each" quantity dispatch, the controller-scoped counterpart to the existing any-controller `parse_for_each_battlefield_type_with_keyword`. The bare `parse_for_each_controlled_type` arm matched "<type> you control" and stranded " with <keyword>" as an unconsumed remainder, failing the for-each full-consumption requirement so the whole quantity (and its dependent P/T pump / life-gain / mana amount) was silently dropped. The new arm composes the shared `parse_type_filter_word`, controller phrase, and `parse_keyword_name` + `FilterProp::WithKeyword` building blocks over the whole evergreen keyword table, with the optional other/another self-exclusion, binding the count to the source controller (CR 109.4). Covers the class: Skycat Sovereign, Aven Gagglemaster, Aerial Assault, Alert Heedbonder, Overgrown Battlement. Closes phase-rs#5018
There was a problem hiding this comment.
Code Review
This pull request introduces the parse_for_each_controlled_type_with_keyword parser to support parsing controller-scoped quantity counts gated on keyword predicates (e.g., 'for each other creature you control with flying'), along with corresponding unit tests. The feedback highlights a potential runtime panic where .unwrap() is used on keyword parsing, suggesting a safer approach using map_res to handle conversion errors gracefully.
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.
Parse changes introduced by this PR✓ No card-parse changes detected. |
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
|
@matthewevans |
matthewevans
left a comment
There was a problem hiding this comment.
[MED] The PR claims production card coverage, but the review evidence only reaches the quantity helper and the parse-diff shows no card-level change. Evidence: crates/engine/src/parser/oracle_nom/quantity.rs:4339 names Skycat Sovereign/Aven Gagglemaster/Aerial Assault/Alert Heedbonder/Overgrown Battlement as the backed class, while the only new test calls parse_for_each_clause_ref directly at crates/engine/src/parser/oracle_nom/quantity.rs:4946; the current parse-diff sticky says No card-parse changes detected. Why it matters: a helper-only test can pass even if the production static/effect parser path never feeds these Oracle lines through the new arm, so the claimed card fixes may still be unsupported or already handled elsewhere. Suggested fix: add a production-path parser test for at least one representative card line through parse_static_line/parse_effect as appropriate, and reconcile why the card-level parse diff is empty if the PR is intended to change supported card output.
… keyword" Address review: prove the new controller-scoped keyword `for each` arm is reached by the real production parser, not only the `parse_for_each_clause_ref` helper in isolation. Add two `parse_effect` end-to-end tests: - Aven Gagglemaster / Aerial Assault: "You gain 1 life for each creature you control with flying." lowers to `GainLife` whose amount is the controller-scoped (`ControllerRef::You`) count of creatures with flying. - Skycat Sovereign: "gets +1/+1 for each other creature you control with flying" lowers to a `Pump` whose dynamic count carries both the `Another` self-exclusion and the flying `WithKeyword` predicate. Both fail if reverted to before the new arm, when the stranded " with flying" made the for-each clause fail full consumption and the dynamic quantity was dropped.
@matthewevans |
matthewevans
left a comment
There was a problem hiding this comment.
Maintainer re-review: the latest head adds production-path parser coverage for the previously requested for each <type> you control with <keyword> path, parse-diff reports no card-level drift, and CI is green. Approving for merge queue.
Summary
Closes #5018
Adds
parse_for_each_controlled_type_with_keywordto the "for each" quantity dispatch — the controller-scoped counterpart to the existing any-controllerparse_for_each_battlefield_type_with_keyword.The
for eachgrammar could parse<type> on the battlefield with <keyword>(any controller) and<type> you control with a <kind> counter on it(controller-scoped, counter predicate), but had no arm for<type> you control with <keyword>. The bareparse_for_each_controlled_typearm matchedcreature you controland strandedwith flyingas an unconsumed remainder; because thefor eachclause requires full consumption, the whole quantity — and the effect depending on it — was silently dropped.Cards fixed (class, not one card)
Root cause
The
for eachdispatch (crates/engine/src/parser/oracle_nom/quantity.rs) already had a combinator for the any-controller form (parse_for_each_battlefield_type_with_keyword,<type> on the battlefield with <keyword>) and a controller-scoped counter form (parse_for_each_controlled_type_with_counter), but no controller-scoped keyword form. The bareparse_for_each_controlled_typearm consumed<type> you controland leftwith <keyword>unconsumed, so the clause failed full-consumption and the dependent effect was silently dropped — exactly the failure mode the battlefield-wide sibling already fixed, but for theyou controlscope.Implementation
The new combinator composes only existing building blocks:
parse_type_filter_wordfor the type head,parse_for_each_controlled_type_with_counter, tolerating thealreadyadverb in "you already control"),parse_keyword_name+FilterProp::WithKeyword, generalized over the whole evergreen keyword table.The optional
other/anotherprefix lowers toFilterProp::Another(Skycat's "other creature you control"). The count binds to the source controller viaControllerRef::You(CR 109.4 — only battlefield/stack objects have a controller), which is the discriminator from the any-controller battlefield form. No new enum variants and no engine/runtime changes — theFilterProp::WithKeywordruntime evaluation path already backs the battlefield sibling.It is registered immediately before the bare
parse_for_each_controlled_typearm so the keyword suffix is consumed before the shorteryou controltag can match.Tests
parse_for_each_controlled_type_with_keyword_scoped_countexercises the class across the keyword table (flying / vigilance / defender), theother/anotherself-exclusion, and thealreadyadverb, asserting full consumption andcontroller == Some(You)(the discriminator vs. the any-controller battlefield form).CR annotations
docs/MagicCompRules.txt).with <keyword>predicate; verified).