feat(parser): scry N draw that many instead replacement#3403
feat(parser): scry N draw that many instead replacement#3403glorysr1209-png wants to merge 1 commit into
Conversation
There was a problem hiding this comment.
Code Review
This pull request adds support for parsing Sphinx-class replacement effects ("If you would scry N, draw that many cards instead") in the oracle parser, along with a corresponding unit test. The feedback highlights two architectural violations of the repository's style guide: first, using scan_contains for parsing dispatch violates rule R1, which requires using nom combinators on the first pass; second, the parser implementation relies on fragile take_until and verbatim string parsing instead of decomposing the phrase into modular, reusable 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.
| if nom_primitives::scan_contains(&lower, "if you would scry ") | ||
| && nom_primitives::scan_contains(&lower, "draw that many cards instead") | ||
| { | ||
| if let Some(def) = parse_scry_that_many_draw_replacement(&lower, &text) { | ||
| return Some(def); | ||
| } | ||
| } |
There was a problem hiding this comment.
R1. Nom combinators on the first pass — no exceptions
Using scan_contains for parsing dispatch violates the hard architectural rule R1. Parser dispatch should not use string-search helpers like scan_contains or .contains(). Instead, call the parser function directly, allowing it to fail fast via nom combinators.
if let Some(def) = parse_scry_that_many_draw_replacement(&lower, &text) {
return Some(def);
}References
- R1. Nom combinators on the first pass — no exceptions. Every new parser dispatch under crates/engine/src/parser/ must use nom 8.0 combinators or delegate to existing helpers. Findings: any new .contains("..."), .starts_with("..."), .ends_with("..."), .find("..."), or .split_once("...") used for parsing dispatch in non-test parser code. (link)
| let (rest, _) = tag::<_, _, OracleError<'_>>("if you would scry ").parse(lower).ok()?; | ||
| let (rest, _) = take_until::<_, _, OracleError<'_>>(", draw that many cards instead").parse(rest).ok()?; | ||
| let (_, _) = tag(", draw that many cards instead").parse(rest).ok()?; |
There was a problem hiding this comment.
Avoid fragile take_until and verbatim string parsing
Using take_until to skip the scry count is fragile and bypasses structured parsing. Additionally, avoid verbatim string equality for parsing Oracle phrases as it creates fragile matches. Instead, decompose compound phrases into modular, reusable parsers for constituent parts (e.g., subjects, conjunctions) and compose them using idiomatic combinator aggregates (like nested alt and tag sequences) to prevent combinatorial explosion and improve maintainability.
let (rest, _) = parse_if_you_would(lower).ok()?;
let (rest, _) = parse_scry(rest).ok()?;
let (rest, _) = nom_primitives::parse_number_or_x.parse(rest).ok()?;
let (rest, _) = parse_draw_instead(rest).ok()?;
let (_, _) = opt(char('.')).parse(rest).ok()??;References
- R1. Nom combinators on the first pass — no exceptions. Every new parser dispatch under crates/engine/src/parser/ must use nom 8.0 combinators or delegate to existing helpers. (link)
- 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 (e.g., subjects, conjunctions) and compose them using idiomatic combinator aggregates (like nested
altandtagsequences) to prevent combinatorial explosion and improve maintainability.
221f9f5 to
3ce089b
Compare
3ce089b to
6927b1a
Compare
matthewevans
left a comment
There was a problem hiding this comment.
Blocking this as currently modeled because the new fixed-count antecedent is not represented in the runtime replacement.
Evidence from the actual head 6927b1aa: parse_scry_that_many_draw_replacement consumes "if you would scry ", then uses take_until(", draw that many cards instead") and discards whatever quantity was between those two phrases. The emitted ReplacementDefinition::new(ReplacementEvent::Scry) has no condition for that antecedent count. Runtime evidence: scry_matcher in game/replacement.rs matches every ProposedEvent::Scry { count, .. } where count > 0, and the applier only uses the event count as the replacement amount. So parsing "If you would scry 2, draw that many cards instead" would replace scry 1, scry 3, etc. as well.
The existing parse_scry_count_replacement already covers the supported class ("If you would scry a number of cards, draw that many cards instead") with EventContextAmount. For a fixed-N antecedent, either the engine needs a typed scry-count applicability gate and runtime coverage proving only that N is replaced, or this parser should not claim the pattern yet.
Summary
Closes 3402. Replacement parser gap: scry N draw that many cards instead.
Changes
crates/engine/src/parser/oracle_replacement.rswith focused parser arm + unit testTest plan
#[test]inoracle_replacement.rscovers representative Oracle textcargo test -p engine(authoritative gate — not run locally)