Skip to content

fix(parser): Lignify-class subtype-only enchanted type-change with base P/T (#5300)#5305

Merged
matthewevans merged 2 commits into
phase-rs:mainfrom
nickmopen:fix/issue-5300-lignify-subtype-basept
Jul 7, 2026
Merged

fix(parser): Lignify-class subtype-only enchanted type-change with base P/T (#5300)#5305
matthewevans merged 2 commits into
phase-rs:mainfrom
nickmopen:fix/issue-5300-lignify-subtype-basept

Conversation

@nickmopen

Copy link
Copy Markdown
Contributor

Summary

Closes #5300.

Lignify-class auras — "Enchanted creature is a <creature subtype> with base power and toughness N/N and loses all abilities." — silently dropped the subtype. On HEAD the line parses to a static with RemoveAllAbilities + SetPower + SetToughness but no SetCardTypes/AddSubtype, so the enchanted creature never becomes the named type (e.g. Lignify's enchanted creature never becomes a Treefolk).

Root cause (per @jsdevninja's analysis): parse_enchanted_is_type routes the is a <phrase> copula through classify_type, which pushes a bare creature subtype (Treefolk) to granted_subtypes but leaves granted_core_types empty (no core card-type word). The granted_core_types.is_empty() → return None guard then bails, and the line falls through to a partial handler that keeps only the P/T + ability loss.

Fix

A creature subtype makes the object a creature (CR 205.1a), and a base P/T is creature-only, so when the granted phrase is subtype-only and carries a base P/T, imply the Creature core type. The existing, tested emission then produces the full replacement in written order:

SetCardTypes[Creature]RemoveAllSubtypes{Creature}AddSubtype(Treefolk)SetPower(0)SetToughness(4)RemoveAllAbilities

I took the minimal in-place route rather than a separate parse_enchanted_is_creature_subtype_with_base_pt handler (the issue's proposed structure): it reuses the already-tested Frogify/Darksteel emission (SetCardTypes / RemoveAllSubtypes-before-AddSubtype / trailing-clause) with zero duplication, and only adds a branch that fires when granted_core_types is empty — so the Frogify/Darksteel family (which name a core type) is never touched. The base-P/T gate keeps the basic-land subtype change ("Enchanted land is a Mountain", no P/T) returning None here and deferring to the SetBasicLandType handler. No new engine variant, no new runtime.

Test plan (from the issue)

  • ✅ Lignify parses to SetCardTypes(Creature) + RemoveAllSubtypes(Creature) + AddSubtype(Treefolk) + SetPower(0) + SetToughness(4) + RemoveAllAbilities (with RemoveAllSubtypes before AddSubtype) — new test lignify_subtype_only_with_base_pt_type_change
  • ✅ Prefix-order sibling ("... loses all abilities and is a Treefolk with base power and toughness 0/4.") parses (verified)
  • ✅ Darksteel Mutation / Frogify still route through the core-type path (regression: darksteel_mutation_full_modification_set + full suite unchanged)
  • cargo fmt, scripts/check-parser-combinators.sh, scripts/check-engine-authorities.sh pass; clippy clean; full engine lib suite 15,612 passed / 0 failed

…se P/T (phase-rs#5300)

"Enchanted creature is a <creature subtype> with base power and toughness
N/N and loses all abilities." (Lignify: Treefolk 0/4) dropped the subtype:
parse_enchanted_is_type parsed the base P/T and loses-abilities but the
"is a Treefolk" copula names only a creature subtype, so granted_core_types
stayed empty and the handler bailed — the enchanted creature never became
the named type.

A creature subtype makes the object a creature (CR 205.1a), and a base P/T
is creature-only, so when the granted phrase is subtype-only AND carries a
base P/T, imply the Creature core type and let the existing emission produce
the full replacement (SetCardTypes[Creature] + RemoveAllSubtypes{Creature}
+ AddSubtype + SetPower/SetToughness + RemoveAllAbilities). The base-P/T
gate keeps the basic-land subtype change ("Enchanted land is a Mountain",
no P/T) on the SetBasicLandType handler. Reuses the tested Frogify/Darksteel
emission path — no new engine variant, no new runtime.
@nickmopen nickmopen requested a review from matthewevans as a code owner July 7, 2026 14:24

@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 addresses issue #5300 by implementing parsing support for Lignify-class subtype-only type-change auras that specify a base power and toughness. However, the current implementation incorrectly pushes CoreType::Creature to granted_core_types, which would emit SetCardTypes and strip other existing card types (such as Artifact or Land) from the enchanted creature, violating CR 205.1a. The feedback advises bypassing the empty check without adding CoreType::Creature to preserve other card types, and updating the corresponding test assertions to ensure SetCardTypes is not emitted.

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 1048 to 1064
if granted_core_types.is_empty() {
return None;
// CR 205.1a + CR 613.4b (issue #5300): the Lignify class names only a
// creature SUBTYPE in the copula — "Enchanted creature is a Treefolk
// with base power and toughness 0/4 and loses all abilities." No core
// card-type word appears, but a creature subtype makes the object a
// creature, so imply the Creature core type and let the emission below
// produce the full replacement (SetCardTypes + RemoveAllSubtypes +
// AddSubtype) instead of silently dropping the subtype. Gated on a base
// P/T (only creatures have one) so the basic-land subtype change
// ("Enchanted land is a Mountain", no P/T) still returns None here and
// defers to the SetBasicLandType handler.
if base_pt.is_some() && !granted_subtypes.is_empty() {
granted_core_types.push(CoreType::Creature);
} else {
return None;
}
}

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.

critical

[CRITICAL] Lignify-class auras (e.g., "Enchanted creature is a Treefolk...") only set the subtype of the enchanted creature; they do not set or overwrite its card types (CR 205.1a). Therefore, if the enchanted creature has other card types (such as Artifact or Land), it must retain them (e.g., Lignify on Gingerbrute results in an Artifact Creature — Treefolk Food, and on Dryad Arbor results in a Land Creature — Forest Treefolk).

By pushing CoreType::Creature to granted_core_types, the engine will emit SetCardTypes { core_types: vec![CoreType::Creature] }, which incorrectly strips all other card types. Instead, we should bypass the granted_core_types.is_empty() check when base_pt.is_some() && !granted_subtypes.is_empty() but leave granted_core_types empty so that SetCardTypes is not emitted.

        if granted_core_types.is_empty() {
            if base_pt.is_some() && !granted_subtypes.is_empty() {
                // CR 205.1a: Lignify-class subtype-only type-change auras set the subtype but do not set or overwrite the card type.
                // By leaving granted_core_types empty, we avoid emitting SetCardTypes, which would incorrectly strip other card types (like Artifact or Land).
            } else {
                return None;
            }
        }
References
  1. Strict fidelity to the MTG Comprehensive Rules (CR) — every game rule, validation, and computed value matches the CR exactly. (link)

Comment on lines +14149 to +14154
assert!(
mods.contains(&ContinuousModification::SetCardTypes {
core_types: vec![CoreType::Creature],
}),
"creature subtype must imply SetCardTypes[Creature]: {mods:?}"
);

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] Since Lignify-class auras only set the subtype and do not set or overwrite the card type, the test should assert that SetCardTypes is not emitted.

    assert!(
        !mods.iter().any(|m| matches!(m, ContinuousModification::SetCardTypes { .. })),
        "Lignify must not emit SetCardTypes as it only sets the subtype and preserves other card types: {mods:?}"
    );

@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown

Parse changes introduced by this PR · 7 card(s), 9 signature(s) (baseline: main ec764a208255)

1 card(s) · static/Continuous · field mods: base power 0, base toughness 1base power 0, base toughness 1, remove all Creature subtypes, add subtype Turtle

Examples: Retro-Mutation

1 card(s) · static/Continuous · field mods: base power 1, base toughness 1base power 1, base toughness 1, remove all Creature subtypes, add subtype Citizen

Examples: Spider-Man No More

1 card(s) · static/Continuous · field mods: base power 4, base toughness 2, grant Flying, grant Deathtouchset color Black, base power 4, base toughness 2, remove all Creature subtypes, grant Flying, grant Deathtouch, add subt…

Examples: Eye of Nidhogg

1 card(s) · static/Continuous · field mods: remove all abilities, base power 0, base toughness 1remove all abilities, set color Blue, base power 0, base toughness 1, remove all Creature subtypes, add subtype Fish

Examples: Ichthyomorphosis

1 card(s) · static/Continuous · field mods: remove all abilities, base power 0, base toughness 4base power 0, base toughness 4, remove all Creature subtypes, remove all abilities, add subtype Treefolk

Examples: Lignify

1 card(s) · static/Continuous · field mods: remove all abilities, base power 1, base toughness 1remove all abilities, base power 1, base toughness 1, remove all Creature subtypes, add subtype Citizen

Examples: Honest Work

1 card(s) · static/Continuous · field mods: remove all abilities, base power 1, base toughness 1remove all abilities, set color , base power 1, base toughness 1, remove all Creature subtypes, add subtype Noggle

Examples: Noggle the Mind

1 card(s) · ability/Mana · added: Mana (kind=activated, mana={C} x1)

Examples: Honest Work

1 card(s) · ability/Mana · removed: Mana (kind=activated, mana={C} x1)

Examples: Honest Work

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

[HIGH] Subtype-only Lignify effects replace all card types with Creature. Evidence: crates/engine/src/parser/oracle_static/type_change.rs:1059 pushes CoreType::Creature, which flows into SetCardTypes at crates/engine/src/parser/oracle_static/type_change.rs:1097; SetCardTypes replaces obj.card_types.core_types in crates/engine/src/game/layers.rs:4885. Why it matters: "is a Treefolk" sets/replaces the creature subtype for an already enchanted creature; CR 205.1a says subtype changes replace subtypes from the appropriate set and that removing a subtype does not affect card types, so an enchanted artifact creature or land creature would incorrectly lose Artifact/Land and become only Creature. Suggested fix: do not synthesize CoreType::Creature for the subtype-only branch; preserve existing card types while emitting the creature subtype replacement (RemoveAllSubtypes { Creature } before AddSubtype(Treefolk)), and update crates/engine/src/parser/oracle_static/tests.rs:14150 to assert no SetCardTypes is emitted for Lignify.

…205.1a)

Address review (matthewevans, gemini): synthesizing CoreType::Creature
emitted SetCardTypes[Creature], which replaces the enchanted creature's
card types and strips Artifact/Land — wrong per CR 205.1a (a subtype
change does not affect card types). Lignify on Gingerbrute must stay an
Artifact Creature — Treefolk; on Dryad Arbor a Land Creature — Forest
Treefolk.

Leave granted_core_types empty for the subtype-only branch (a
`subtype_only_creature_change` flag gates it on base P/T + a granted
subtype), so no SetCardTypes is emitted; guard the SetCardTypes push on a
non-empty core-type set; and let the RemoveAllSubtypes{Creature} injection
fire for the flag so the subtype replacement (RemoveAllSubtypes before
AddSubtype) still runs. Test updated to assert no SetCardTypes is emitted.
@nickmopen

Copy link
Copy Markdown
Contributor Author

Thanks — fixed the CR 205.1a violation.

You're right: synthesizing CoreType::Creature emitted SetCardTypes[Creature], which replaces the card types and strips Artifact/Land. Setting a creature subtype must not touch card types.

Revised so the subtype-only branch leaves granted_core_types empty (no SetCardTypes), while still emitting the subtype replacement:

  • A subtype_only_creature_change flag (gated on base_pt.is_some() && !granted_subtypes.is_empty()) bypasses the granted_core_types.is_empty() bail without pushing a core type.
  • The SetCardTypes push is guarded on a non-empty core-type set, so nothing is emitted for the subtype-only case.
  • The RemoveAllSubtypes{Creature} injection now also fires for that flag, so the subtype replacement (RemoveAllSubtypes before AddSubtype) still runs.

Lignify now emits exactly RemoveAllSubtypes{Creature} + AddSubtype(Treefolk) + SetPower(0) + SetToughness(4) + RemoveAllAbilities — no SetCardTypes — so Gingerbrute stays an Artifact Creature — Treefolk and Dryad Arbor a Land Creature — Forest Treefolk. Test updated to assert no SetCardTypes is emitted; Frogify/Darksteel (which name core types) are unchanged.

Local: full engine lib suite 15,612 / 0, fmt + parser-combinator + engine-authorities gates pass, clippy clean.

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

Reviewed current head after the subtype-only fix. The change stays in the existing type-change parser seam, preserves card types per CR 205.1a, has focused parser coverage, and CI is green.

@matthewevans matthewevans added the bug Bug fix label Jul 7, 2026
@matthewevans matthewevans added this pull request to the merge queue Jul 7, 2026
@matthewevans matthewevans removed their assignment Jul 7, 2026
Merged via the queue into phase-rs:main with commit 3a8cc66 Jul 7, 2026
13 checks passed
jsdevninja added a commit to jsdevninja/phase that referenced this pull request Jul 8, 2026
…type

Follow-up to phase-rs#5305: Lignify coverage landed on main via
parse_enchanted_is_type. Remove redundant dedicated dispatch arm; add
parse_pt_mod_with_remainder and wire it into parse_enchanted_is_type base-P/T
and inline-P/T paths, replacing find('/') byte-slicing.

Co-authored-by: Cursor <cursoragent@cursor.com>
matthewevans pushed a commit to claytonlin1110/phase that referenced this pull request Jul 8, 2026
…type (phase-rs#5300 follow-up) (phase-rs#5302)

* feat(parser): Lignify-class enchanted subtype-only type-change with base P/T (phase-rs#5300)

Auras that name only a creature subtype before the base-P/T seam (Lignify) strict-failed because parse_enchanted_is_type requires a core card type word. Add parse_enchanted_is_creature_subtype_with_base_pt composing SetCardTypes, RemoveAllSubtypes, AddSubtype, SetPower/Toughness, and RemoveAllAbilities from existing layer modifications with no new runtime.

Closes phase-rs#5300.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(parser): address Lignify PR review — P/T trim, CR annotations, test

Trim trailing dot on lowercased base-P/T fragment before remainder
extraction (Gemini). Add inline CR 205.1a / CR 613.4b annotations on
modification pushes. Add trailing-dot regression test via parse_static_line.

Co-authored-by: Cursor <cursoragent@cursor.com>

* refactor(parser): nom P/T remainder boundary for Lignify handler

Add parse_pt_mod_with_remainder in grammar.rs composing signed and unsigned
nom P/T parsers; replace hand-scanned find('/') tail extraction in the
Lignify-class handler. Remove redundant inline CoreType/SubtypeSet import
(already in prelude). Add combinator-level unit test.

Co-authored-by: Cursor <cursoragent@cursor.com>

* chore: rustfmt tests.rs

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(parser): Lignify subtype-only must not emit SetCardTypes (CR 205.1a)

Setting a creature subtype replaces creature subtypes only — card types
stay (Artifact/Land creatures keep Artifact/Land). Align dedicated
handler with parse_enchanted_is_type and lignify_subtype_only test.

Co-authored-by: Cursor <cursoragent@cursor.com>

* refactor(parser): nom P/T remainder combinator in parse_enchanted_is_type

Follow-up to phase-rs#5305: Lignify coverage landed on main via
parse_enchanted_is_type. Remove redundant dedicated dispatch arm; add
parse_pt_mod_with_remainder and wire it into parse_enchanted_is_type base-P/T
and inline-P/T paths, replacing find('/') byte-slicing.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
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.

feat(parser): Lignify-class enchanted subtype-only type-change with base P/T (CR 205.1a / CR 613.1d)

2 participants