fix(parser): split imperative sibling sentence after a token's quoted ability (Alien Invasion)#5879
Conversation
There was a problem hiding this comment.
Code Review
This pull request addresses issue #5844 (CR 111.3 + CR 608.2c) by allowing a token/permanent granted-ability quote that ends a sentence to be followed by a fresh imperative sibling sentence. It introduces starts_imperative_action_continuation to identify these imperative continuations and split them, while keeping anaphoric-subject continuations attached. Feedback points out a violation of Rule R1 (Nom combinators on the first pass) because starts_imperative_action_continuation uses .split_whitespace() for parsing dispatch instead of nom combinators.
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.
| /// `quote_closes_sentence_before_sequence`); only a bare imperative verb marks a | ||
| /// genuine sibling effect here. | ||
| fn starts_imperative_action_continuation(remainder_lower: &str) -> bool { | ||
| let first_word = remainder_lower.split_whitespace().next().unwrap_or(""); |
There was a problem hiding this comment.
[MEDIUM] Architectural rule violation: use of string manipulation for parsing dispatch.
Evidence: crates/engine/src/parser/oracle_effect/sequence.rs:1447.
Why it matters: The project style guide (R1) mandates using nom combinators for all new parser dispatch logic to ensure consistency and robustness. The use of .split_whitespace() deviates from this architectural rule. Furthermore, we must avoid verbatim string equality for parsing Oracle phrases, and instead decompose compound phrases into modular, reusable parsers. Finally, avoid swallowing parsing errors with .ok(); propagate unexpected errors to prevent masking critical bugs.
Suggested fix: Replace the string split with a nom combinator like take_till to extract the first word, and propagate the error using ?.
let (remainder, first_word) = nom::bytes::complete::take_till::<_, _, OracleError<'_>>(char::is_whitespace)(remainder_lower)?;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. The use of.split_whitespace()for parsing dispatch is a violation of this rule. (link) - Avoid swallowing all errors (e.g., using
.ok()) when only specific errors are expected to be handled or ignored. Propagate unexpected errors (such as invariant violations or invalid actions) to prevent masking critical bugs. - 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.
Parse changes introduced by this PR · 10 card(s), 14 signature(s) (baseline: main
|
matthewevans
left a comment
There was a problem hiding this comment.
Request changes — parser dispatch must remain composable nom grammar.
🟡 Finding
[MED] crates/engine/src/parser/oracle_effect/sequence.rs:1447 splits the Oracle text with string operations in parser dispatch. Why it matters: this bypasses the repository's nom parser authority and makes phrase variation brittle. Suggested fix: express the clause split with alt(tag(...)), following the existing starts_clause_text_lower combinator pattern.
Recommendation: request-changes.
… ability (Alien Invasion phase-rs#5844) The clause splitter only broke after a token/permanent's quoted granted ability (a span ending in `."`) when the following sentence began with a whitelisted continuation (`then`/`if`/`otherwise`/`may have`/emblem). A fresh IMPERATIVE sibling sentence acting on the created object was swallowed into the token-creation clause. For Alien Invasion — `create a 1/1 red Alien creature token with haste and "This token attacks each combat if able." Put a +1/+1 counter on it for each invasion counter on this enchantment, then put an invasion counter on this enchantment.` — this meant the token clause absorbed the trailing sentence, mis-reading its "for each invasion counter" as the token COUNT (creating one token per invasion counter) and dropping the "+1/+1 counter on it" pump entirely. Extend `quote_closes_sentence_before_sequence` to also close the sentence when the continuation begins with an imperative game-action verb, via a new `starts_imperative_action_continuation` helper. It reuses the shared clause-starter verb table (`starts_clause_text_lower`) but first excludes the pronoun/determiner subject starters it also recognizes (`it`/`that`/`this`/ `those`/`they`/`you`/`all`/`each`/`the`). Those, after a granted quote, are anaphors the token-creation path resolves inline ("It becomes a 2/2 …", "The token is goaded"), so they stay attached; only a bare imperative verb marks a genuine sibling effect. This is a class-level fix — every "create a token with '<ability.>' <imperative on the token>" card benefits, not just Alien Invasion. Tests: - sequence.rs: `quoted_grant_splits_before_imperative_sibling_sentence` (splits the Alien Invasion remainder), `quoted_grant_keeps_anaphoric_subject_continuation` (an "It gains …" anaphor stays attached). Existing guardrails `quoted_grant_keeps_nonrecognized_capitalized_continuation` ("The token is goaded") and `quoted_grant_splits_before_following_sentence` (Requiem Monolith "may have") still pass. - oracle_effect/tests.rs: `alien_invasion_creates_one_token_and_pumps_it_per_counter` parses the full card and asserts (1) the token count is a literal 1, (2) the "+1/+1 counter … for each invasion counter" pump survives as a PutCounter on the last-created token, (3) the enchantment still accrues one invasion counter. Closes phase-rs#5844
…sion split assertion The split-boundary test's `.starts_with` check on the pre-tokenized chunk vector is a structural assertion, not parser dispatch, but the parser-combinator gate scans inline test fixtures in non-`tests.rs` parser files. Route the check through a named bool with the sanctioned `// allow-noncombinator:` annotation so the gate exempts it. No behavior change.
Address review feedback on phase-rs#5879: the token-quote continuation dispatch used `.split_whitespace()` + a `matches!` over string literals to exclude anaphoric subject starters, which bypasses the nom-combinator parser authority. Replace it with a `starts_anaphoric_subject` combinator built from `alt((tag(...)))` (each starter carrying a trailing space so only a complete word matches), mirroring the `starts_clause_text_lower` verb-tag grammar. Behavior is unchanged; all splitter and guardrail tests pass.
|
Addressed in a7c0c6e. Replaced the fn starts_anaphoric_subject(remainder_lower: &str) -> bool {
alt((
tag::<_, _, OracleError<'_>>("it "),
tag("its "), tag("that "), tag("this "), tag("those "),
tag("they "), tag("you "), tag("all "), tag("each "), tag("the "),
))
.parse(remainder_lower)
.is_ok()
}Each starter carries a trailing space so only a complete word matches (e.g. Behavior is unchanged: the splitter tests (imperative sibling splits, anaphoric subject stays attached) and the guardrails ( |
f3cc84c to
a7c0c6e
Compare
matthewevans
left a comment
There was a problem hiding this comment.
Request changes — do not classify non-anaphoric clause starters as token anaphors.
[MED] crates/engine/src/parser/oracle_effect/sequence.rs:1456-1467 treats you, all, and each as anaphoric subjects. Evidence: the shared authority starts_clause_text_lower explicitly recognizes all , each / each opponent , and you as clause starters at sequence.rs:2113-2128 and 2164-2166, but starts_imperative_action_continuation rejects them before consulting that authority. Why it matters: a sentence after a granted-ability quote such as "…" You gain … or "…" Each opponent … is a fresh sibling instruction, not an anaphor to the created object; it remains glued to the token clause and can be swallowed or mis-lowered, recreating the class of bug this PR is intended to fix.
Please distinguish actual last-created anaphors from independent subject-led clauses, rather than filtering by the first word alone. Add direct splitter coverage that those You … and Each … continuations split, alongside the existing It … non-split guard. The nom conversion itself correctly resolves the earlier parser-dispatch finding.
Problem
Alien Invasion (#5844) reads:
The engine created one Alien token per invasion counter and the "+1/+1 counter � for each invasion counter" pump was missing entirely.
Root cause
The clause splitter (
split_clause_sequence) closes a sentence after a token/permanent's quoted granted ability (a span ending in.") only when the following sentence starts with a whitelisted continuation (then/if/otherwise/may have/ emblem head). A fresh imperative sibling sentence was not recognized, so the whole remainder was swallowed into the token-creation clause:for each invasion counterwas mis-read as the token count �Token { count: CountersOn(invasion) }(one token per counter), and+1/+1 counter on itpump was dropped.Isolation probe: with the quoted ability removed, the same card parses correctly (
Token { count: 1 }+PutCounter { P1P1, CountersOn(invasion), LastCreated }+PutCounter { invasion, 1, SelfRef }) � confirming the quoted-ability sentence boundary is the sole trigger.Fix
Extend
quote_closes_sentence_before_sequenceto also close the sentence when the continuation begins with an imperative game-action verb, via a newstarts_imperative_action_continuationhelper. It reuses the shared clause-starter verb table (starts_clause_text_lower) but first excludes the pronoun/determiner subject starters it also recognizes (it/that/this/those/they/you/all/each/the). Those, after a granted quote, are anaphors the token-creation path resolves inline ("It becomes a 2/2 �", "The token is goaded"), so they stay attached; only a bare imperative verb marks a genuine sibling effect.This is a class-level fix: every
create a token with "<ability.>" <imperative on the token>card benefits, not just Alien Invasion.Tests
sequence.rs:quoted_grant_splits_before_imperative_sibling_sentence� the Alien Invasion remainder splits off.quoted_grant_keeps_anaphoric_subject_continuation� an "It gains �" anaphor stays attached.quoted_grant_keeps_nonrecognized_capitalized_continuation("The token is goaded") andquoted_grant_splits_before_following_sentence(Requiem Monolith "may have").oracle_effect/tests.rs:alien_invasion_creates_one_token_and_pumps_it_per_counterparses the full card and asserts (1) the token count is a literal1, (2) the "+1/+1 counter � for each invasion counter" pump survives as aPutCounteron the last-created token, (3) the enchantment still accrues one invasion counter.Full
enginelib suite green (16,682 passed, 0 failed; snapshot tests included);cargo fmtandclippy -p engine --testsclean.Closes #5844