Skip to content

ship/t79 instead else#5782

Merged
matthewevans merged 4 commits into
mainfrom
ship/t79-instead-else
Jul 14, 2026
Merged

ship/t79 instead else#5782
matthewevans merged 4 commits into
mainfrom
ship/t79-instead-else

Conversation

@matthewevans

Copy link
Copy Markdown
Member
  • fix(parser): a cross-LINE "instead" override must BIND or FAIL — never publish as a sibling
  • fix(parser): bind the CR 614.15 PARTIAL cross-line override ("… instead of ")
  • fix(parser): the residual cross-line overrides fail honestly — no face double-executes

…r publish as a sibling

CR 614.15 says a self-replacement effect replaces "part or all of that spell or
ability's own effect(s)", and that its text "can be a separate ability,
particularly when preceded by an ability word". CR 614.6 says a replaced event
never happens. So an ability-word override line ("Corrupted — Counter that spell
instead if …") must be BOUND into the ability it replaces. Publishing it as a
second, independent top-level ability makes the engine perform the base effect
AND the replacement — the exact CR 614.6 violation #44 killed intra-chain.

The cross-line binder already existed and built the right shape. Two defects fed
it a condition-less override, and it silently fell through to a sibling emission:

1. VOCABULARY ASYMMETRY. The cross-line path lowered its condition through the
   nom `StaticCondition` grammar alone, which cannot express a target-relative
   predicate. The intra-chain path (`build_instead_def`) ran a three-parser
   ladder that can. A condition the chain lowers fine — "its controller has three
   or more poison counters" — was therefore dropped at the line level. Both paths
   now lower through ONE authority, `conditions::lower_instead_condition`, so the
   two vocabularies cannot drift apart again.

2. The poison predicate was not parameterized. CR 122.1f DEFINES "poisoned" as
   one or more poison counters, so "its controller is poisoned" (Corrupted
   Resolve) and "its controller has three or more poison counters" (the Corrupted
   ability word) are the same predicate at two thresholds — same subject, same
   `QuantityRef::TargetControllerCounter`, same `GE`. They are now one combinator
   parameterized on its threshold, not two.

And the floor: when a cross-line override cannot be conditioned at all, it is now
reported as `Effect::unimplemented` rather than published unconditioned. An
honest red, never a silent double-execution (mirrors the intra-chain
`InsteadLowering::ConditionUnlowerable` floor).

Runtime witness (Anoint with Affliction), watched RED before this commit:
"Exile target creature if it has mana value 3 or less. / Corrupted — Exile that
creature instead if its controller has three or more poison counters." Its
override published a second, condition-less `ChangeZone -> Exile { ParentTarget }`.
Targeting a mana-value-5 creature whose controller has ZERO poison counters —
both printed gates false — the engine EXILED it anyway (left: Exile, right:
Battlefield). It now stays on the battlefield, and the SHAPE test pins that the
override is bound as a `ConditionInstead` branch on a single top-level ability
rather than merely neutered into an inert sibling.

8122 parser tests pass; parser combinator gate A PASS.
…ad of <N>")

CR 614.15: a self-replacement effect replaces "part or all" of the ability's own
effect, and the printed grammar says WHICH. A bare trailing "instead" replaces the
whole clause. A trailing "instead of <N>" replaces only a PART — the antecedent's
quantity — and names the OLD value it displaces ("put up to two creature cards …
into your hand instead of one").

The cross-line gate recognized only the whole-clause printings, so every partial
override fell through it and was published as an INDEPENDENT top-level ability —
CR 614.6 double-execution. Two things were needed:

1. The trailing "of <N>" is a MARKER, not an operand. It is redundant with the
   antecedent, which still holds the old value, so it carries nothing the branch
   needs. `is_replacement_marker_tail` now accepts a bare "instead" and an
   "instead of <N>" as the same replacement marker, via one combinator. Anything
   else after "instead" ("instead of putting it into your hand") names a
   NON-quantity part and is deliberately NOT accepted: treating it as a
   whole-clause marker would silently drop the part it names.

2. The override's body cannot be lowered in isolation. Gather the Pack's
   "put up to two creature cards from among the revealed cards into your hand"
   parses alone to a bare `ChangeZone` — losing the reveal, the library source and
   the rest-to-graveyard rider the printed Dig carries. Binding THAT as the
   replacement would trade the double-execution for an effect LOSS.
   `try_parse_dig_instead_alternative` is the existing antecedent-parameterized
   handler for exactly this shape (it already serves Follow the Lumarets
   intra-chain): it rebuilds the alternative as a full `Dig` that reuses the
   preceding Dig's source and reveal-mode and swaps only what the override
   changes. It is now also reached across a line boundary, handed the previous
   LINE's def as its antecedent — the same relationship, one line further out.

The rebuilt alternative carries its own condition, so it flows into the existing
ability-word merge and the existing cross-line binder untouched: the binder wraps
it in `ConditionInstead` and parks the printed Dig as the `else_ability` fallback.
No new binding mechanism.

Runtime witness (Gather the Pack), watched RED before this commit — TWO top-level
abilities (left: 2, right: 1). The stray second ability was
`ChangeZone { destination: Hand, target: Creature, up_to, multi_target: 0..2 }`:
with spell mastery on, the card both dug AND offered to BOUNCE up to two creatures
off the battlefield, an effect it does not have. It is now one ability whose Dig
carries a ConditionInstead alternative Dig — keep_count 1 -> 2, with the top-five
source, the reveal and the rest-to-graveyard rider all preserved (asserted).

8122 parser tests pass; parser combinator gate A PASS.
…e double-executes

Closes the class. The two commits before this one BIND the cross-line
self-replacement printings that can be bound faithfully. This one makes the
remainder fail honestly, so that after this commit NO face in the class publishes
a self-replacement override as an independent ability (CR 614.6).

The residuals are the CR 614.15 PARTIAL printings whose antecedent is not a Dig:

  Nissa's Pilgrimage    "search your library for up to three basic Forest cards
                         instead of two"       -- replaces the FIRST clause's count
  Talent of the Telepath "... instead of one"  -- replaces a NON-FIRST clause
  See the Unwritten      "put two creature cards onto the battlefield instead of
                         one"                  -- Dig antecedent, but the body omits
                                                  "from among them", so the alternative
                                                  cannot be rebuilt from it
  Caravan Vigil          "put that card onto the battlefield instead of putting it
                         into your hand"       -- replaces a NON-FIRST clause's
                                                  destination

These all carry a perfectly good condition, so it is tempting to hand them to the
binder. That would be WRONG, and silently so. The binder binds the FIRST emitted
clause and parks the base's tail in `else_ability`, which the runtime walks ONLY
when the swap does NOT fire: Nissa's Pilgrimage would search for three basic
Forests and then never reveal them, never put one onto the battlefield, never
shuffle. That trades a double-execution for an effect LOSS — a different silent
wrong, and the one this unit was explicitly chartered not to introduce.

A faithful bind needs clause-level antecedent selection (the override replaces the
base clause it PARALLELS, not necessarily the first) plus a tail that survives in
BOTH branches. That is a distinct mechanism; it is filed, not guessed at. Until it
exists these fail honestly: the base ability stands exactly as printed, and the
override is reported `Effect::unimplemented`.

The "would" exclusion is CR 614.1 — a replacement effect watches for an event that
WOULD happen. A "would" clause names an EVENT (CR 614.1a) and is owned by the
replacement machinery, so claiming it here would replace a WORKING rider encoding
with an honest red (this is the boundary #44 established; 68 faces regressed when
it was missing).

Full-pool ledger (35,396 faces, base origin/main efe7409 vs after, ONE export
each, zero noise floor post-#5745) -- MEASURED:
  8 faces changed, 4 reds gained, 0 reds lost.
    3 BOUND  (2 -> 1 top-level abilities): Anoint with Affliction, Bring the Ending,
             Gather the Pack.
    4 silent double-execution -> HONEST RED: Caravan Vigil, Nissa's Pilgrimage,
             See the Unwritten, Talent of the Telepath.
    1 shape change, behaviour verified unchanged and pinned: Precognitive Perception.
  Nothing else in the pool moved: the residual predicate did not over-fire.
  Tomb of Horrors Adventurer (the #80 regression face) is NOT in the changed set —
  its "instead" is INTRA-line inside a trigger and never reaches this path.

Precognitive Perception is the face the ledger surfaced that this unit did not
predict. Widening the condition vocabulary made its Addendum line take the
strip-the-body path, moving the override's second clause ("then draw three cards")
out from under the condition the chain had been stamping on it. The engine runs an
unswapped `ConditionInstead` sub's tail when it has no `else_ability`, so an
unguarded tail would have drawn three MORE cards outside your main phase — six off
a card that draws three. Driven through the real cast pipeline in the upkeep it
draws exactly three; the assertion is pinned, and proven able to fail (asserting 6
reports left: 3, right: 6).

8122 parser tests pass; parser combinator gate A PASS.
@matthewevans
matthewevans enabled auto-merge July 14, 2026 04:04
@gemini-code-assist

Copy link
Copy Markdown
Contributor

Warning

You have reached your daily quota limit. Please wait up to 24 hours and I will start processing your requests again!

@github-actions

Copy link
Copy Markdown

Parse changes introduced by this PR · 8 card(s), 12 signature(s) (baseline: main 78dec82485f2)

4 card(s) · ability/instead_override · added: instead_override

Examples: Caravan Vigil, Nissa's Pilgrimage, See the Unwritten (+1 more)

1 card(s) · ability/CastFromZone · removed: CastFromZone (conditional=instant/sorcery cards in your graveyard ≥ 2, target=any target)

Examples: Talent of the Telepath

1 card(s) · ability/ChangeZone · added: ChangeZone (conditional=instead if (# of poison counters its controller has ≥ 3), target=parent target, to=exile)

Examples: Anoint with Affliction

1 card(s) · ability/ChangeZone · removed: ChangeZone (conditional=# of power ≥4 in battlefield you control creature ≥ 1, target=creature, to=battlefield)

Examples: See the Unwritten

1 card(s) · ability/ChangeZone · removed: ChangeZone (conditional=creature zone changes this turn (Some(Battlefield)->Some(Graveyard)) ≥ 1, target=parent target, to=battlefield)

Examples: Caravan Vigil

1 card(s) · ability/ChangeZone · removed: ChangeZone (conditional=instant/sorcery cards in your graveyard ≥ 2, target=creature, targets=0-2, to=hand, up_to=true)

Examples: Gather the Pack

1 card(s) · ability/ChangeZone · removed: ChangeZone (target=parent target, to=exile)

Examples: Anoint with Affliction

1 card(s) · ability/Counter · added: Counter (conditional=instead if (# of poison counters its controller has ≥ 3), target=triggering source)

Examples: Bring the Ending

1 card(s) · ability/Counter · removed: Counter (target=triggering source)

Examples: Bring the Ending

1 card(s) · ability/Dig · added: Dig (conditional=instead if (instant/sorcery cards in your graveyard ≥ 2), count=5, filter=creature, keep_count=2, rest_to=graveyard, reveal=true, to=hand, up_…

Examples: Gather the Pack

1 card(s) · ability/Draw · field conditional: cast during precombat main or postcombat main

Examples: Precognitive Perception

1 card(s) · ability/SearchLibrary · removed: SearchLibrary (conditional=instant/sorcery cards in your graveyard ≥ 2, count=up to 3, find=basic land Forest)

Examples: Nissa's Pilgrimage

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

@matthewevans
matthewevans added this pull request to the merge queue Jul 14, 2026
Merged via the queue into main with commit f2a7baf Jul 14, 2026
13 checks passed
@matthewevans
matthewevans deleted the ship/t79-instead-else branch July 14, 2026 04:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant