feat(parser): compound negated-type subject animation with granted abilities (Bello, Bard of the Brambles)#5444
Conversation
…ilities (Bello, Bard of the Brambles) "During your turn, each non-Equipment artifact and non-Aura enchantment you control with mana value 4 or greater is a 4/4 Elemental creature in addition to its other types and has indestructible, haste, and \"Whenever this creature deals combat damage to a player, draw a card.\"" was fully dropped (Unimplemented "static_structure") — no handler recognized the heterogeneous compound subject (a non-Equipment artifact union'd with a non-Aura enchantment), and the additive-type-clause helper's granted-ability tail only extracted the quoted trigger, silently dropping bare keywords listed alongside it. parse_each_compound_subject_type_change (crates/engine/src/parser/oracle_static/type_change.rs) delegates the subject wholesale to the general target-phrase grammar (parse_type_phrase), which already recurses per "and"-leg and backfills shared trailing qualifiers (controller, mana value) onto every leg, then composes the predicate from parse_animation_spec (base P/T + leading type/subtype) + parse_additive_type_clause_modifications (any extra type noun) + a granted-ability-tail extraction (kept local to this function, not folded into the shared helper) that applies the same split_keyword_list + push_grant_clause_modifications + parse_quoted_ability_modifications composition parse_continuous_modifications already uses elsewhere, so a mixed bare-keyword/quoted-trigger list is captured in full. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Code Review
This pull request introduces support for parsing compound-subject continuous animation effects with negated-type legs and mixed granted abilities, specifically targeting cards like Bello, Bard of the Brambles. It adds the parse_each_compound_subject_type_change parser in type_change.rs, integrates it into the static line parser dispatch, and includes comprehensive unit tests verifying correct parsing, additive semantics, and fall-through behavior. The implementation adheres to the repository's architectural guidelines, utilizing existing parser helpers and providing appropriate Magic Comprehensive Rules (CR) annotations. I have no feedback to provide as the changes are highly idiomatic and well-tested.
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.
…gger
QuantityExpr::Fixed is a struct variant ({ value: i32 }), not a tuple
variant — CI caught the tuple-call-syntax typo in the new Bello test
(E0533). Separately, a doc-comment line wrapped right at "P/T + type
change", making "+ type change ..." look like a line-leading markdown
list bullet to clippy::doc_lazy_continuation; rewrapped so no comment
line starts with +/-/*/N.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Parse changes introduced by this PR · 1 card(s), 2 signature(s) (baseline: main
|
matthewevans
left a comment
There was a problem hiding this comment.
[MED] The Bello subject filter cross-contaminates one conjunct's negated subtype into the other. Evidence: crates/engine/src/parser/oracle_static/type_change.rs:1866 delegates the whole subject to parse_type_phrase, and the parse-diff sticky for this head reports Bello as artifact non-Equipment or ... enchantment non-Aura non-Equipment; Oracle only says non-Aura enchantment. The new test at crates/engine/src/parser/oracle_static/tests.rs:4111 asserts the enchantment leg contains Non(Aura) but never asserts it does not also contain Non(Equipment), so this regression is not pinned. Why it matters: Bello would incorrectly exclude any qualifying enchantment that also has the Equipment subtype or otherwise carries that subtype in a type-changing edge case, and the parser output no longer matches the card text. Suggested fix: split/parse the two subject legs so each leg keeps only its own non-* qualifier while shared you control / mana-value constraints are still distributed, and add negative assertions for both legs.
matthewevans
left a comment
There was a problem hiding this comment.
Parser change reviewed against current head: prior negated-subtype finding is resolved, parse-diff is limited to Bello, and required checks are green.
Summary
Fixes #5442. Bello, Bard of the Brambles' static —
(Oracle text verified against the Scryfall API) — previously strict-failed (
Unimplemented("static_structure"),data/card-data.json); the whole line collapsed into a gap.Same structural shape as #5219 (Life and Limb's compound-subject animation static): a compound subject joined by "and" where each conjunct is independently resolvable and the predicate class is independently resolvable elsewhere, but the combination was unreached. Bello's variant differs from #5219's own fix (
parse_compound_all_subjects_type_change) in two ways that make it a distinct class, not a duplicate:<X>and all<Y>", plural "are"); Bello has a single shared quantifier and per-conjunct negated-type exclusions ("each non-Equipment artifact and non-Aura enchantment", singular "is") with a trailing qualifier (you control,with mana value 4 or greater) that applies to both conjuncts despite appearing once, after the second.Fix
parse_each_compound_subject_type_change(crates/engine/src/parser/oracle_static/type_change.rs), dispatched inparse_static_line_innerright after its closest sibling (parse_each_noncreature_subject_is_creature_with_pt_mv):parse_type_phrase) instead of hand-rolling a conjunct splitter. That grammar already recurses per"and"-leg — restarting its own leadingnon-scan on each recursive call (starts_with_type_word'snon-arm) — and backfills the shared trailing qualifiers (controller, mana value) from the last leg onto every earlier leg viadistribute_controller_to_or/distribute_properties_to_or. Declines (via a2+-legOr-of-Typedguard) unless the subject is a genuine compound, so single-subject lines keep falling through to their existing dispatcher unchanged.parse_animation_spec(base P/T + leading type/subtype grant) withparse_additive_type_clause_modifications(any extra type noun the animation spec stops short of), then extracts the granted-ability tail locally — reusing the samesplit_keyword_list+push_grant_clause_modifications+parse_quoted_ability_modificationscompositionparse_continuous_modificationsalready uses elsewhere — so a mixed bare-keyword/quoted-trigger list is captured in full. Kept local to the new function rather than folded into the shared additive-type-clause helper, which has several other call sites this change should not risk perturbing.I cross-checked this design against #5406 (Rukarumel, cited in #5442 as the structurally closest precedent): that PR's subject-delegation target (
parse_continuous_subject_filter) requires every conjunct to independently carry its own controller anchor ("Slivers you control and nontoken creatures you control", repeated per leg) — which does not fit Bello's shared, non-repeated"you control".parse_type_phrase's distribution machinery is the correct fit here.CR references (grep-verified against
docs/MagicCompRules.txt)Or-distributed affected set; theDuring your turn,window).GrantTrigger).Testing
Environment note: this sandbox has no Tilt, no MSVC Build Tools, and no WSL/Docker, so I could not run
cargo build/test/clippylocally. I verified as much as this environment allows:cargo fmt --all -- --check— clean../scripts/check-parser-combinators.sh upstream/main— clean (no forbidden string-dispatch patterns in the added lines).parse_type_phrase's recursive per-leg negation handling and itsdistribute_*_to_orbackfill functions line-by-line to confirm the compound-subject + shared-qualifier distribution actually produces the intendedOrshape.cargo test -p engine/Tilt'stest-engineas the first real verification pass before merge.New tests (
oracle_static/tests.rs):bello_compound_negated_type_subject_animation_with_granted_abilities— full Oracle text:Or-of-2 negated-type legs (non-Equipment artifact / non-Aura enchantment, bothyou control+Cmc >= 4),DuringYourTurncondition, base P/T, type/subtype grant, both bare keywords, and the granted trigger.compound_negated_type_subject_animation_declines_non_additive_predicate— the CR 205.1b gate declines a compound predicate missing the additive marker.compound_negated_type_subject_animation_single_subject_falls_through— a single-subject line is not claimed by the compound handler.Scope
Parser-only (
crates/engine/src/parser/oracle_static/{type_change,dispatch,tests}.rs) — no engine types/runtime changes;SetPower/SetToughness/AddType/AddSubtype/AddKeyword/GrantTriggerare all pre-existing, already-wired modifications.🤖 Generated with Claude Code