fix(parser): parse supertype-qualified enchant targets#5109
Conversation
There was a problem hiding this comment.
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.
| 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); |
There was a problem hiding this comment.
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.
| 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); |
| let filter = if filters.len() == 1 { | ||
| filters.remove(0) | ||
| } else { | ||
| TargetFilter::Or { filters } | ||
| }; |
There was a problem hiding this comment.
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.
| 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 } | |
| }; |
Parse changes introduced by this PR · 5 card(s), 2 signature(s) (baseline: main
|
|
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
left a comment
There was a problem hiding this comment.
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.
Summary
Enchanttargets such assnow land you control,basic land you control, andlegendary creature.FilterProp::HasSupertypeinstead of dropping the Enchant keyword payload.legendary creature or planeswalkerdoes not leakLegendaryonto the planeswalker leg.On Thin Icefrom root cause chore: update coverage stats and badges #31 indocs/parser-misparse-backlog.md.Root Cause
parse_type_phrasealready understood supertype prefixes, but the Enchant keyword path used the narrowerparse_enchant_type_legdirectly. As a result,Enchant:snow land you controland sibling forms failed to produce a typedKeyword::Enchant(...)target filter.Parse Audit
Focused before/after export set:
Cabal Stronghold,Flaccify,On Thin Ice,Open the Omenpaths,Rainbow Vale,The Great MoundDimensional Exile,Ossification,Leyline ImmersionColossification; 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-diffreports no parse-tree changes, which is expected because this PR changes Enchant keyword payloads rather thanparse_detailsability trees.Review cleanup
Addressed current Gemini review comments in
8a0cddb39:EnchantTypeLegproperties directly instead of cloning them.filters.pop().unwrap()for the single-filter Enchant case.Validation
cargo fmt --all./scripts/check-parser-combinators.shenv -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.jsontarget/debug/card-data-validate /tmp/phase-enchant-supertype-after.jsontarget/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.jsoncargo fmt --all,./scripts/check-parser-combinators.sh,cargo test -p engine --features cli --lib enchant -- --nocapture(217 passed),git diff --checkCR references verified locally with
grepagainstdocs/MagicCompRules.txtfor205.4a,303.4a, and702.5a.