Skip to content

fix(parser): capture the count in "Devour [quality] N" keywords#5362

Merged
matthewevans merged 1 commit into
phase-rs:mainfrom
real-venus:fix/devour-count-with-type-qualifier
Jul 8, 2026
Merged

fix(parser): capture the count in "Devour [quality] N" keywords#5362
matthewevans merged 1 commit into
phase-rs:mainfrom
real-venus:fix/devour-count-with-type-qualifier

Conversation

@real-venus

Copy link
Copy Markdown
Contributor

Summary

Closes #5260

Famished Worldsire prints "Devour land 3" (CR 702.82c — "Devour [quality] N": as it enters you may sacrifice any number of lands, and it enters with 3 +1/+1 counters per sacrifice). The engine parsed this as Devour 1, losing the count.

Root cause

parse_keyword_from_oracle (crates/engine/src/parser/oracle_keyword.rs) routes devour through the numeric-count-keyword branch, which runs parse_number on the remainder "land 3". That fails on the leading land qualifier, so the param falls back to the whole "land 3" string, and Devour's FromStr ("devour" => Keyword::Devour(p.parse().unwrap_or(1))) hits its unwrap_or(1) fallback → Devour(1).

Fix

When parse_number fails at the head, skip a single leading non-numeric qualifier word (via take_until(" ") + tag(" ") — nom combinators) and retry, so the count token is captured. This generalizes the numeric-count extractor to the CR 702.82c Devour [quality] N templating.

  • Plain Devour N and every other numeric-count keyword (vanishing 3, crew 3, fading 2, …) are unaffected — the qualifier-skip only fires on the parse_number-fails branch, so there's no regression.

Tests

parse_keyword_from_oracle_devour_quality_qualifier_count: devour land 3Devour(3); plus devour 3 and vanishing 3 regression assertions.

CR

CR 702.82c ("Devour [quality] N" variant) — verified in docs/MagicCompRules.txt.

Scope note (honest follow-up)

Keyword::Devour(N) stores only the counter multiplier. The "lands only" sacrifice restriction (the CR 702.82c quality) is a separate synthesis-layer concern in database/synthesis.rs::synthesize_devour and is left as follow-up — the reported bug (count 3 vs 1) is fully resolved here.

Famished Worldsire prints "Devour land 3" (CR 702.82c — sacrifice any
number of LANDS, enters with 3 +1/+1 counters per sacrifice). The
numeric-count keyword extractor ran parse_number on "land 3", which
fails on the leading qualifier, so the param fell back to "land 3" and
the Devour FromStr's unwrap_or(1) produced Devour(1) — the reported bug.

Skip a single leading non-numeric qualifier word (via take_until) before
extracting the count when parse_number fails at the head. Plain
"Devour N" and other numeric-count keywords are unaffected (the skip
only fires on the parse_number-fails branch).

Note: Keyword::Devour(N) stores only the counter multiplier; the
"lands only" sacrifice restriction (CR 702.82c quality) is a separate
synthesis-layer gap and is left as follow-up.

Closes phase-rs#5260
@real-venus real-venus requested a review from matthewevans as a code owner July 8, 2026 12:04
@gemini-code-assist

Copy link
Copy Markdown
Contributor

Warning

Gemini encountered an error creating the review. You can try again by commenting /gemini review.

@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 numeric count keyword parser in crates/engine/src/parser/oracle_keyword.rs to support the 'Devour [quality] N' variant (e.g., 'Devour land 3') by skipping a leading non-numeric qualifier word when the initial number parsing fails, and adds corresponding unit tests. The review feedback suggests optimizing the parser to avoid double parsing the input string and improving robustness by using space1 instead of tag(" ") to handle multiple spaces between the qualifier and the count.

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 +1593 to 1606
let count_src = if nom_primitives::parse_number.parse(rest).is_ok() {
rest
} else {
(
take_until::<_, _, OracleError<'_>>(" "),
tag::<_, _, OracleError<'_>>(" "),
)
.parse(rest)
.map_or(rest, |(after_qualifier, _)| after_qualifier)
};
match nom_primitives::parse_number.parse(count_src) {
Ok((remainder, _)) => Cow::Borrowed(&count_src[..count_src.len() - remainder.len()]),
Err(_) => Cow::Borrowed(rest),
}

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

[MEDIUM] Avoid double parsing and handle multiple spaces between qualifier and count. Evidence: crates/engine/src/parser/oracle_keyword.rs:1593-1606.

Why it matters: Parsing rest twice when it already starts with a number is inefficient, and using tag(" ") fails if there are multiple spaces between the qualifier and the count.

Suggested fix: Use space1 instead of tag(" ") to handle multiple spaces, and restructure the logic to avoid parsing rest twice.

        match nom_primitives::parse_number.parse(rest) {
            Ok((remainder, _)) => Cow::Borrowed(&rest[..rest.len() - remainder.len()]),
            Err(_) => {
                let skipped = (
                    take_until::<_, _, OracleError<'_>>(" "),
                    space1::<_, OracleError<'_>>,
                )
                    .parse(rest)
                    .map(|(after_qualifier, _)| after_qualifier);

                match skipped.and_then(|count_src| {
                    nom_primitives::parse_number
                        .parse(count_src)
                        .map(|(remainder, _)| (count_src, remainder))
                }) {
                    Ok((count_src, remainder)) => {
                        Cow::Borrowed(&count_src[..count_src.len() - remainder.len()])
                    }
                    Err(_) => Cow::Borrowed(rest),
                }
            }
        }
References
  1. Avoid verbatim string equality 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 to prevent combinatorial explosion and improve maintainability.

@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 adds support for parsing the "Devour [quality] N" keyword variant (e.g., "Devour land 3") by skipping a leading non-numeric qualifier word if the count does not immediately follow the keyword, and includes corresponding unit tests. The review feedback points out incorrect Comprehensive Rules (CR) citations in both the implementation comments and the tests (citing CR 702.82c instead of CR 702.82b), and suggests using the space1 parser instead of tag(" ") to robustly handle potential multi-space or tab separators.

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 param: Cow<'_, str> = if is_numeric_count_keyword(name) {
match nom_primitives::parse_number.parse(rest) {
Ok((remainder, _)) => Cow::Borrowed(&rest[..rest.len() - remainder.len()]),
// CR 702.82c: "Devour [quality] N" is a variant where the count follows a

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

[MEDIUM] Incorrect CR citation for Devour [quality] N. Evidence: crates/engine/src/parser/oracle_keyword.rs:1586.
Why it matters: CR 702.82b is the rule that defines the "Devour [quality] N" variant, whereas CR 702.82c governs multiple instances of devour.
Suggested fix: Change the citation to CR 702.82b.

Suggested change
// CR 702.82c: "Devour [quality] N" is a variant where the count follows a
// CR 702.82b: "Devour [quality] N" is a variant where the count follows a
References
  1. CR annotations must be verified and accurate according to Rule R6 of the repository style guide. (link)

Comment on lines +1596 to +1599
(
take_until::<_, _, OracleError<'_>>(" "),
tag::<_, _, OracleError<'_>>(" "),
)

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

[MEDIUM] Use space1 instead of tag(" ") for robustness. Evidence: crates/engine/src/parser/oracle_keyword.rs:1596-1599.
Why it matters: Using tag(" ") only matches a single space, which can fail if there are multiple spaces or tabs between the qualifier and the count.
Suggested fix: Use space1 to consume any amount of whitespace.

Suggested change
(
take_until::<_, _, OracleError<'_>>(" "),
tag::<_, _, OracleError<'_>>(" "),
)
(
take_until::<_, _, OracleError<'_>>(" "),
space1::<_, OracleError<'_>>,
)
References
  1. Avoid verbatim string equality 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.

assert_eq!(kw, Keyword::Cascade);
}

/// CR 702.82c: "Devour [quality] N" (Famished Worldsire — "Devour land 3")

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

[MEDIUM] Incorrect CR citation for Devour [quality] N in test. Evidence: crates/engine/src/parser/oracle_keyword.rs:2221.
Why it matters: CR 702.82b is the rule that defines the "Devour [quality] N" variant, whereas CR 702.82c governs multiple instances of devour.
Suggested fix: Change the citation to CR 702.82b.

Suggested change
/// CR 702.82c: "Devour [quality] N" (Famished Worldsire — "Devour land 3")
/// CR 702.82b: "Devour [quality] N" (Famished Worldsire — "Devour land 3")
References
  1. CR annotations must be verified and accurate according to Rule R6 of the repository style guide. (link)

@github-actions

github-actions Bot commented Jul 8, 2026

Copy link
Copy Markdown

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

2 card(s) · replacement/Moved · added: Moved

Examples: Famished Worldsire, Feasting Hobbit

2 card(s) · replacement/Moved · removed: Moved

Examples: Famished Worldsire, Feasting Hobbit

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

@matthewevans matthewevans self-assigned this Jul 8, 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.

Maintainer review: parser change is at the right seam and scoped to the numeric-count keyword normalizer. Verified CR 702.82c locally, checked the parse-diff sticky comment for Famished Worldsire/Feasting Hobbit, and reviewed the Gemini comments as non-blocking/refuted.

@matthewevans matthewevans added the bug Bug fix label Jul 8, 2026
@matthewevans matthewevans added this pull request to the merge queue Jul 8, 2026
@matthewevans matthewevans removed their assignment Jul 8, 2026
Merged via the queue into phase-rs:main with commit 1182e77 Jul 8, 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.

Famished Worldsire - Devour Parsing Wrong — It appears the engine is parsing devour land 3 as just devour 1.

2 participants