Skip to content

fix(parser): parse supertype-qualified enchant targets#5109

Open
ntindle wants to merge 3 commits into
phase-rs:mainfrom
ntindle:codex/root31-enchant-supertype-targets
Open

fix(parser): parse supertype-qualified enchant targets#5109
ntindle wants to merge 3 commits into
phase-rs:mainfrom
ntindle:codex/root31-enchant-supertype-targets

Conversation

@ntindle

@ntindle ntindle commented Jul 4, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Parse supertype-qualified Enchant targets such as snow land you control, basic land you control, and legendary creature.
  • Reuse the shared target supertype parser so Aura legality emits FilterProp::HasSupertype instead of dropping the Enchant keyword payload.
  • Keep multi-leg Enchant lists per-leg, so legendary creature or planeswalker does not leak Legendary onto the planeswalker leg.
  • Remove On Thin Ice from root cause chore: update coverage stats and badges #31 in docs/parser-misparse-backlog.md.

Root Cause

parse_type_phrase already understood supertype prefixes, but the Enchant keyword path used the narrower parse_enchant_type_leg directly. As a result, Enchant:snow land you control and sibling forms failed to produce a typed Keyword::Enchant(...) target filter.

Parse Audit

Focused before/after export set:

  • Root chore: update coverage stats and badges #31: Cabal Stronghold, Flaccify, On Thin Ice, Open the Omenpaths, Rainbow Vale, The Great Mound
  • Expected sibling Aura cards: Dimensional Exile, Ossification, Leyline Immersion
  • The name filter also matched Colossification; it was included in the audit and did not change.

Raw JSON changed cards:

  • On Thin Ice: keywords: [] -> Enchant(Typed Land, controller You, HasSupertype Snow)
  • Dimensional Exile: keywords: [] -> Enchant(Typed Land, controller You, HasSupertype Basic)
  • Ossification: keywords: [] -> Enchant(Typed Land, controller You, HasSupertype Basic)
  • Leyline Immersion: keywords: [] -> Enchant(Typed Creature, HasSupertype Legendary)

After deleting keywords, all four changed card objects were byte-equivalent to baseline. The other focused cards did not change.

coverage-parse-diff reports no parse-tree changes, which is expected because this PR changes Enchant keyword payloads rather than parse_details ability trees.

Review cleanup

Addressed current Gemini review comments in 8a0cddb39:

  • Move EnchantTypeLeg properties directly instead of cloning them.
  • Use filters.pop().unwrap() for the single-filter Enchant case.

Validation

  • cargo fmt --all
  • ./scripts/check-parser-combinators.sh
  • env -u RUSTC_WRAPPER ZIG_GLOBAL_CACHE_DIR=/tmp/phase-zig-cache cargo --config 'build.rustc-wrapper=""' test -p engine --features cli --lib enchant (217 passed)
  • target/debug/card-data-validate /tmp/phase-enchant-supertype-baseline.json
  • target/debug/card-data-validate /tmp/phase-enchant-supertype-after.json
  • target/debug/coverage-parse-diff /tmp/phase-enchant-supertype-baseline.json /tmp/phase-enchant-supertype-after.json --markdown /tmp/phase-enchant-supertype-diff.md --json /tmp/phase-enchant-supertype-diff.json
  • review cleanup rerun: cargo fmt --all, ./scripts/check-parser-combinators.sh, cargo test -p engine --features cli --lib enchant -- --nocapture (217 passed), git diff --check

CR references verified locally with grep against docs/MagicCompRules.txt for 205.4a, 303.4a, and 702.5a.

@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 implements support for supertype-qualified Aura targets (such as "snow land", "basic land", or "legendary creature") in the MTG rules engine parser. It introduces the EnchantTypeLeg struct to preserve supertype properties per leg, ensuring they are scoped correctly and do not leak to sibling legs in multi-leg inline Enchant phrases (e.g., "Enchant legendary creature or planeswalker"), resolving a misparse backlog item for "On Thin Ice". The review feedback suggests two performance and idiomatic improvements: destructuring type_leg in parse_enchant_target to avoid cloning FilterProp elements, and using pop().unwrap() instead of remove(0) when extracting a single element from a vector of length 1.

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 1729 to +1743
let mut props = Vec::new();
if let Some(leg) = &type_leg {
props.extend(leg.properties.iter().cloned());
}
if let Some(z) = zone {
props.push(FilterProp::InZone { zone: z });
}
if let Some(prop) = attachment {
props.push(prop);
}
props.extend(without_keyword);
let mut filter = TypedFilter::new(type_filter.unwrap_or(TypeFilter::Card));
let type_filter = type_leg
.map(|leg| leg.type_filter)
.unwrap_or(TypeFilter::Card);
let mut filter = TypedFilter::new(type_filter);

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

By destructuring type_leg directly, we can avoid cloning the FilterProp elements inside leg.properties and also eliminate the redundant map operation on type_leg. Since FilterProp can be a relatively large enum with nested allocations, avoiding these clones improves performance and memory efficiency.

Suggested change
let mut props = Vec::new();
if let Some(leg) = &type_leg {
props.extend(leg.properties.iter().cloned());
}
if let Some(z) = zone {
props.push(FilterProp::InZone { zone: z });
}
if let Some(prop) = attachment {
props.push(prop);
}
props.extend(without_keyword);
let mut filter = TypedFilter::new(type_filter.unwrap_or(TypeFilter::Card));
let type_filter = type_leg
.map(|leg| leg.type_filter)
.unwrap_or(TypeFilter::Card);
let mut filter = TypedFilter::new(type_filter);
let mut props = Vec::new();
let type_filter = if let Some(leg) = type_leg {
props.extend(leg.properties);
leg.type_filter
} else {
TypeFilter::Card
};
if let Some(z) = zone {
props.push(FilterProp::InZone { zone: z });
}
if let Some(prop) = attachment {
props.push(prop);
}
props.extend(without_keyword);
let mut filter = TypedFilter::new(type_filter);

Comment on lines +246 to +250
let filter = if filters.len() == 1 {
filters.remove(0)
} else {
TargetFilter::Or { filters }
};

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

Using filters.pop().unwrap() is more idiomatic and efficient than filters.remove(0) when extracting the single element from a vector of length 1, as it avoids shifting elements.

Suggested change
let filter = if filters.len() == 1 {
filters.remove(0)
} else {
TargetFilter::Or { filters }
};
let filter = if filters.len() == 1 {
filters.pop().unwrap()
} else {
TargetFilter::Or { filters }
};

@github-actions

github-actions Bot commented Jul 5, 2026

Copy link
Copy Markdown

Parse changes introduced by this PR · 5 card(s), 2 signature(s) (baseline: main 07dd0b8357d5)

4 card(s) · keyword/Enchant · added: Enchant

Examples: Dimensional Exile, Leyline Immersion, On Thin Ice (+1 more)

1 card(s) · cost/two other cards named Say Its Name from your graveyard · added: two other cards named ~ from your graveyard

Examples: Say Its Name

1 card(s) had Oracle-text changes (errata/reprint) — excluded as non-parser.

@ntindle ntindle marked this pull request as ready for review July 5, 2026 00:57
@ntindle ntindle requested a review from matthewevans as a code owner July 5, 2026 00:57
@gemini-code-assist

Copy link
Copy Markdown
Contributor

Warning

You have reached your daily quota limit. Please wait up to 24 hours and I will start processing your requests again!

…supertype-targets

# Conflicts:
#	docs/parser-misparse-backlog.md
@matthewevans matthewevans self-assigned this Jul 5, 2026
@matthewevans matthewevans added the bug Bug fix label Jul 5, 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.

Thanks for the update. The parser shape needs one more parse-diff/scope pass before I can approve:

[MED] Parse-diff includes an unexplained out-of-scope card change. Evidence: the coverage-parse-diff sticky reports Say Its Name gaining cost/two other cards named ~ from your graveyard, while this PR's diff is for supertype-qualified enchant targets. Why it matters: an enchant-target parser PR should not silently change an unrelated graveyard-cost parse signature. Suggested fix: regenerate/explain the parse-diff; if that change is real, split it or cover the Say Its Name parser change with its own discriminating evidence.

@matthewevans matthewevans removed their assignment Jul 5, 2026
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.

2 participants