Skip to content

fix(parser): accept "of your choice ... in addition to its other types" (Navigator's Compass)#5860

Merged
matthewevans merged 2 commits into
phase-rs:mainfrom
claytonlin1110:fix/become-choice-in-addition-marker
Jul 16, 2026
Merged

fix(parser): accept "of your choice ... in addition to its other types" (Navigator's Compass)#5860
matthewevans merged 2 commits into
phase-rs:mainfrom
claytonlin1110:fix/become-choice-in-addition-marker

Conversation

@claytonlin1110

Copy link
Copy Markdown
Contributor

Summary

Fixes #5859. try_parse_become_choice (the "becomes the [type] of your choice" effect-clause parser) anchored the choice phrase on literally ending in "of your choice" — any trailing text after that, other than the one " and <keyword grant>" shape it already special-cased, made the whole predicate fall through unparsed.

Navigator's Compass (Oracle text verified against Scryfall, dom/225): {T}: Until end of turn, target land you control becomes the basic land type of your choice in addition to its other types. The "in addition to its other types" retention marker made this strict-fail — the activated ability was a complete no-op.

The fixed-value sibling path (build_become_clause's parse_animation_spec fallback, for "becomes a <type>" predicates) already recognizes this exact marker via has_in_addition_to_other_types/has_in_addition_to_other_colors (Possessed Goat: "becomes a black Demon in addition to its other colors and types"). try_parse_become_choice — the sibling for a chosen rather than fixed type — never picked up the same recognition.

Fix

Reuse the marker splitter the fixed-value path already relies on (split_in_addition_tail, exposed as pub(crate) from oracle_effect/animation.rs) inside try_parse_become_choice: peel the trailing marker off the choice phrase before the ends_with_of_your_choice anchor check, the same way the existing " and <grant>" split already runs first. AddChosenSubtype (the modification for both the creature-type and basic-land-type choice) is additive by construction (CR 205.1b) — it never clears prior subtypes — so accepting the marker changes only whether the line parses, not what modification is emitted. The color choice keeps its CR 105.3 Set default unconditionally; no printed card pairs it with the marker, so no behavior was invented there.

No new runtime: every modification (AddChosenSubtype, AddChosenColor) and combinator (split_in_addition_tail, has_in_addition_to_other_types) already existed and was already wired for the fixed-value sibling — this only makes an existing sibling function recognize a marker its neighbor already understands.

Files changed

  • crates/engine/src/parser/oracle_effect/animation.rs — expose split_in_addition_tail as pub(crate) for reuse (previously private, single in-file caller).
  • crates/engine/src/parser/oracle_effect/subject.rstry_parse_become_choice peels the marker before the "of your choice" anchor check.
  • crates/engine/src/parser/oracle_effect/tests.rs — new tests.

CR references

  • CR 205.1b — additive type/subtype grants ("in addition to its other types").
  • CR 305.7 — land-subtype-changing effects (the basic-land-type choice).
  • CR 205.3 — choosing a creature type or basic land type.

Verification

  • cargo fmt --package engine -- --check on the touched files — clean.
  • ./scripts/check-parser-combinators.sh (Gate A) — none of the touched files appear in its forbidden-pattern report; exit 0.
  • Traced the fix by hand against the exact combinator chain (split_in_addition_tailparse_in_addition_other_types_marker) for both the Navigator's Compass predicate and the pre-existing Mondo Gecko ("...of your choice and gains hexproof...") regression case, confirming the marker is consumed correctly and the "and " path is unaffected.
  • Could not run cargo test/clippy locally: Tilt is unreachable in this sandbox and there is no local MSVC linker or WSL distribution available as a fallback, so this repo's native toolchain cannot link on this machine. Relying on CI to run the full suite; happy to address any failures it surfaces.

Tests added

  • become_basic_land_type_of_choice_in_addition_to_other_types — Navigator's Compass's exact predicate now parses to Choose { BasicLandType } chained to AddChosenSubtype { BasicLandType } (no spurious RemoveAllSubtypes).
  • become_creature_type_of_choice_in_addition_to_other_types — same marker on the creature-type axis, proving the fix generalizes rather than special-casing the land branch.
  • become_color_of_choice_still_chains_trailing_keyword_grant — no-regression guard for the pre-existing Mondo Gecko " and <grant>" shape.

🤖 Generated with Claude Code

…s" (Navigator's Compass)

try_parse_become_choice anchored the choice phrase on literally ending in
"of your choice", so a trailing "in addition to its other types" retention
marker (Navigator's Compass: "becomes the basic land type of your choice in
addition to its other types") made the whole predicate fall through unparsed
— the fixed-value sibling path already handles this exact marker via
has_in_addition_to_other_types/has_in_addition_to_other_colors, but the
choice form never did. Reuse the existing split_in_addition_tail splitter to
peel the marker off before the anchor check; AddChosenSubtype is additive by
construction (CR 205.1b) so accepting it changes only whether the line
parses, not the emitted modification.

Fixes phase-rs#5859.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

@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 updates the Oracle parser to support the "in addition to its other types" retention marker within choice phrases by exposing and utilizing the split_in_addition_tail helper. The reviewer feedback correctly points out that this manual string-splitting approach violates the repository's hard architectural rule (R1) requiring nom combinators for parsing, and suggests refactoring the logic into modular, reusable nom-based parsers.

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.

Comment on lines +4505 to +4520
// CR 205.1b: the choice phrase may itself carry the "in addition to its
// other types" retention marker (Navigator's Compass: "becomes the basic
// land type of your choice in addition to its other types") — the same
// marker the fixed-value sibling path already recognizes via
// `has_in_addition_to_other_types` (see `build_become_clause`'s
// `parse_animation_spec` fallback). Peel it off with the shared
// `split_in_addition_tail` splitter before the "of your choice" anchor
// check below, so the choice phrase underneath is still recognized instead
// of the whole predicate falling through unparsed. `AddChosenSubtype` (the
// land/creature-type modification below) is additive by construction
// regardless of the marker, so no branching on the match is needed — it
// only needs to be accepted, not interpreted.
let choice_text = match split_in_addition_tail(choice_text) {
Some((prefix, _matched)) => prefix.trim(),
None => choice_text,
};

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.

high

[HIGH] Fragile manual string splitting and parsing of Oracle phrases.

The current implementation relies on manual string manipulation (split_around, split_in_addition_tail) to parse complex choice phrases and keyword grants. This approach is highly fragile and prone to parsing failures, such as losing trailing keyword grants when combined with the 'in addition to its other types' marker.

Instead of patching this with additional manual string splitting (which bypasses the robust parser and creates fragile matches), we should decompose these compound phrases into modular, reusable nom-based parsers. Use idiomatic combinator aggregates (like nested alt and tag sequences) to parse the constituent parts (subjects, conjunctions, and keyword grants) to prevent combinatorial explosion and improve maintainability.

Suggested change
// CR 205.1b: the choice phrase may itself carry the "in addition to its
// other types" retention marker (Navigator's Compass: "becomes the basic
// land type of your choice in addition to its other types") — the same
// marker the fixed-value sibling path already recognizes via
// `has_in_addition_to_other_types` (see `build_become_clause`'s
// `parse_animation_spec` fallback). Peel it off with the shared
// `split_in_addition_tail` splitter before the "of your choice" anchor
// check below, so the choice phrase underneath is still recognized instead
// of the whole predicate falling through unparsed. `AddChosenSubtype` (the
// land/creature-type modification below) is additive by construction
// regardless of the marker, so no branching on the match is needed — it
// only needs to be accepted, not interpreted.
let choice_text = match split_in_addition_tail(choice_text) {
Some((prefix, _matched)) => prefix.trim(),
None => choice_text,
};
use nom::{
branch::alt,
bytes::complete::tag,
combinator::opt,
sequence::tuple,
IResult,
};
fn parse_choice_and_grant(input: &str) -> IResult<&str, (Choice, Option<Grant>)> {
// Decompose compound phrases into modular, reusable parsers
let (input, (choice, _, _, grant)) = tuple((
parse_choice_type,
tag(" in addition to its other types"),
opt(tag(" and ")),
opt(parse_keyword_grant),
))(input)?;
Ok((input, (choice, grant)))
}
References
  1. R6. CR annotations are mandatory and verified. Every rules-touching line of engine code must carry a comment of the form CR : . (link)
  2. Avoid verbatim string equality or manual string splitting for parsing Oracle phrases as it bypasses the robust nom-based parser and creates fragile matches. Instead, decompose compound phrases into modular, reusable parsers for constituent parts and compose them using idiomatic combinator aggregates.

@github-actions

github-actions Bot commented Jul 15, 2026

Copy link
Copy Markdown

Parse changes introduced by this PR · 2 card(s), 3 signature(s) (baseline: main 553b97bd5c9f)

2 card(s) · ability/become · removed: become (duration=until end of turn, kind=activated)

Examples: Mistform Sliver, Navigator's Compass

1 card(s) · ability/Choose · added: Choose (choice=basic land type, duration=until end of turn, kind=activated)

Examples: Navigator's Compass

1 card(s) · ability/Choose · added: Choose (choice=creature type, duration=until end of turn, kind=activated)

Examples: Mistform Sliver

@matthewevans matthewevans self-assigned this Jul 16, 2026

@matthewevans matthewevans left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Approved after current-head implementation review.

@matthewevans matthewevans added the bug Bug fix label Jul 16, 2026
@matthewevans matthewevans added this pull request to the merge queue Jul 16, 2026
@matthewevans matthewevans removed their assignment Jul 16, 2026
Merged via the queue into phase-rs:main with commit 2b8d5ac Jul 16, 2026
13 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Bug fix

Projects

None yet

Development

Successfully merging this pull request may close these issues.

parser: "becomes the [type] of your choice in addition to its other types" strict-fails (Navigator's Compass)

2 participants