fix(parser): thread optional flag for "you may choose N" triggered modals (#5640)#5754
Conversation
There was a problem hiding this comment.
Code Review
This pull request implements support for CR 608.2c by identifying triggered modal headers of the form 'you may choose N' as optional triggers, propagating this optionality during lowering, and adding corresponding tests. The review feedback highlights a violation of Rule R2 (No bool fields) across several files, recommending the replacement of the optional_trigger boolean field with a typed enum ModalOptionality to better express the design space.
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.
matthewevans
left a comment
There was a problem hiding this comment.
Changes requested — the optional-trigger design is plausible, but the current head does not compile and has no runtime proof.
🔴 Blocker
crates/engine/src/parser/oracle_modal.rs:722-724leaves the secondnom::tagerror type unconstrained; every Rust CI consumer reports E0283 there. Use the same explicitOracleErrortype as the preceding combinator (or an existing typed helper) so the parser is compilable.crates/engine/src/parser/oracle_trigger.rs:1387pattern-matchesTriggerBody::PreLowered(mut ability)through&ir.body, movingBox<AbilityDefinition>out of a shared reference (E0507). Borrow/clone the boxed body before stamping optionality, preservinglower_trigger_ir's non-consuming contract.- The PR body leaves the Shadrix runtime decline/accept behavior unchecked. Parser-shape assertions cannot prove that a real begin-combat trigger offers a decline and still enforces exactly two modes on acceptance; add a registered scenario regression for both branches.
🟡 Non-blocking
- None.
✅ Clean
- The proposed distinction between
you may choose Nandyou may choose up to Ncorrectly preserves the fixed-count modal shape in the parser tests.
Recommendation: repair the two compile errors and add the discriminating triggered-modal runtime regression, then request re-review.
Parse changes introduced by this PR · 1 card(s), 1 signature(s) (baseline: main
|
matthewevans
left a comment
There was a problem hiding this comment.
Changes requested
🔴 Blocker
- Current head
44115cdeb4a1bd49a7a501e51699c5946f674463fails CI.cargo fmt --checkreports formatting drift inoracle_modal.rsandissue_5640_shadrix_optional_triggered_modal.rs; the integration test also fails to compile because lines 100 and 204 pass privaterunner.stateinstead ofrunner.state()todrain_order_triggers_with_identity.
🟡 Non-blocking
- The prior request for a discriminating runtime proof remains relevant after the build is fixed: exercise the Shadrix decline and accept paths, including the exactly-two-mode constraint.
✅ Clean
- The previous inference/type and borrowed-body compiler errors are absent from this current CI result.
Recommendation: fix the current CI blockers, add the runtime proof, and request re-review.
matthewevans
left a comment
There was a problem hiding this comment.
Changes requested — the new runtime regression proves optionality is applied after modal selection has already been required.
🔴 Blocker
-
crates/engine/src/game/triggers.rs:4964-5057builds the trigger's modal choice and pauses forAbilityModeChoicebefore the triggeredexecuteability can reach itsoptionalresolution path. The current regression proves the consequence:crates/engine/tests/integration/issue_5640_shadrix_optional_triggered_modal.rs:111-123tries to decline, immediately receivesAbilityModeChoice, and CI fails withMust choose at least 2 modes, got 0. Shadrix's verified Oracle text is: “At the beginning of combat on your turn, you may choose two. Each mode must target a different player.” The controller must be offered the ability-level decline before the fixed two-mode chooser; stampingoptionalatoracle_modal.rs:1043-1079is too late. Route that choice through the pending-trigger/stack construction path first, then entermodal_choice_for_playeronly after acceptance. -
crates/engine/src/parser/oracle_ir/ast.rs:1803introducesoptional_trigger: boolfor a distinct modal semantic axis. Keep the triggered-ability optionality typed at the same boundary that owns the routing decision (with explicit mandatory vs may-decline variants); the suggestedModalOptionalityname does not exist in the current tree, so introducing a well-scoped typed representation is preferable to adding another bool and only comparing it differently.
🟡 Non-blocking
- CI also reports rustfmt drift in
crates/engine/src/parser/oracle_modal.rs:2158-2164; resolve it with the behavioral fix.
✅ Clean
issue_5640_shadrix_optional_triggered_modal.rsis registered and, unlike the earlier AST-only coverage, reaches the real begin-combat trigger and precisely exposes the ordering failure.- The current parse artifact is scoped to the intended Shadrix
optionalfield change.
Recommendation: redesign the optional-trigger handoff so decline precedes AbilityModeChoice, replace the new boolean with the typed routing state, then retain this regression plus an accepted exactly-two-modes path.
Add missing nom type annotation on the "you may choose up to" tag check, clone PreLowered trigger bodies before mutating optional, and apply rustfmt to the new modal header tests. Co-authored-by: Cursor <cursoragent@cursor.com>
…se-rs#5640) Exercise runtime decline and accept paths: declining skips all modes, accepting rejects a single-mode selection, and choosing two modes resolves both effects. Co-authored-by: Cursor <cursoragent@cursor.com>
Use runner.state_mut() for order-trigger draining and align oracle_modal and issue_5640 integration test formatting with cargo fmt. Co-authored-by: Cursor <cursoragent@cursor.com>
Shadrix's "you may choose two" mode prompt was offered before the optional gate, so the decline branch hit AbilityModeChoice with zero indices. Pause on OptionalEffectChoice first; decline drops the mid-construction stack entry, accept clears optionality and proceeds to exactly-N modes. Co-authored-by: Cursor <cursoragent@cursor.com>
Clone optional-gate fields and the modal before push/restore so the imm borrow of pending_trigger does not overlap &mut state. Also apply cargo fmt to the payment-choices gate and modal header test. Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Andriy Polanski <andriy.polanski@gmail.com>
cf016a1 to
db687dc
Compare
matthewevans
left a comment
There was a problem hiding this comment.
Verdict — the current head is correct at the pending-trigger construction seam and is ready for the queue.
🔴 Blocker
- None.
🟡 Non-blocking
- None.
✅ Clean
ModalOptionality::{Mandatory, MayDecline}replaces the boolean routing state; all constructors and lowering sites use the typed boundary.- Decline now precedes
AbilityModeChoice, while acceptance preserves the fixed two-mode requirement and the different-target-player constraint. - The registered Shadrix scenario drives the real begin-combat path for decline, invalid one-mode acceptance, and successful two-mode resolution; it would fail if the pre-modal decision route were removed.
- Current-head CI is green, the parse diff is limited to Shadrix's trigger optionality, and all seven resolved Gemini threads were verified against this head.
Recommendation: approve and enqueue.
Closes #5640
Summary
Shadrix Silverquill's begin-combat trigger (
you may choose two) was parsed as a mandatory modal because theTriggeredModalblock path split the header off the condition but never stamped optionality on the trigger or execute ability. The count spec correctly stays(2, 2)— declining means skipping the entire ability, not choosing zero modes.This fix adds an
optional_triggerflag toModalHeaderAstfor the"you may choose N"class (distinct from"you may choose up to N", which only lowersmin_choices).TriggeredModallowering now sets bothTriggerDefinition.optionalandexecute.optional. A companion fix propagatesmodifiers.optionalontoPreLoweredtrigger bodies (inline modals that already detected leadingyou may).Files changed
crates/engine/src/parser/oracle_ir/ast.rsModalHeaderAst.optional_triggerfieldcrates/engine/src/parser/oracle_modal.rsparse_modal_header_ast; stamp optional onTriggeredModallowering; parser testscrates/engine/src/parser/oracle_trigger.rsmodifiers.optionalonPreLoweredbodiescrates/engine/src/parser/oracle_tests.rsDifferentTargetPlayersunchangedTest plan
parse_modal_header_you_may_choose_fixed_count_sets_optional_triggerparse_modal_header_you_may_choose_up_to_does_not_set_optional_triggertriggered_modal_header_supports_you_may_choose_and_constraints(optional + constraints)DifferentTargetPlayersconstraint enforced at mode selectionIssue
Closes #5640