/- ───────────────────────────────────────────────────────────────────────────
   Proofs/EdDouble.lean — coordinate-level specs for POINT DOUBLING:
   `ProjectivePoint::double` (the ℙ² → ℙ¹×ℙ¹ doubling kernel) and
   `EdwardsPoint::double` (the extended-coordinates wrapper around it).

   WHAT THIS FILE PROVES
     * `as_projective_spec` — `EdwardsPoint::as_projective` is a plain
       struct rebuild: it copies X, Y, Z (dropping T), is total, and its
       output is `ProjValid` whenever the input is `ExtValid`.
     * `proj_double_spec` — for ProjValid p, `ProjectivePoint::double p`
       is total (no panic/overflow anywhere in the 9 field ops) and returns
       the completed point r with the per-field limb bounds
           Bnd r.X 2⁵², Bnd r.Y 2⁵³, Bnd r.Z 2⁵², Bnd r.T 2⁵²
       and the four COORDINATE EQUATIONS over 𝔽_p (p = 2²⁵⁵ − 19)
           ⟪r.X⟫ = 2·⟪p.X⟫·⟪p.Y⟫
           ⟪r.Y⟫ = ⟪p.Y⟫² + ⟪p.X⟫²
           ⟪r.Z⟫ = ⟪p.Y⟫² − ⟪p.X⟫²
           ⟪r.T⟫ = 2·⟪p.Z⟫² − (⟪p.Y⟫² − ⟪p.X⟫²)
       (the X equation is stated in the ring-normal form 2XY; the code
       literally computes (X+Y)² − (Y²+X²), which `ring` identifies with it).
     * `edwards_double_spec` — for ExtValid P, `EdwardsPoint::double P`
       (= as_projective ∘ ProjectivePoint::double ∘ CompletedPoint::
       as_extended) is total, all four output coordinates are reduced
       (Bnd · 2⁵²), and, writing X' Y' Z' T' for the four completed-point
       polynomials above evaluated at ⟪P.X⟫ ⟪P.Y⟫ ⟪P.Z⟫, the output denotes
           ⟪r.X⟫ = X'·T',   ⟪r.Y⟫ = Y'·Z',   ⟪r.Z⟫ = Z'·T',   ⟪r.T⟫ = X'·Y'
       — the ℙ¹×ℙ¹ → ℙ³ Segre re-embedding of the doubled point.

   STATEMENT POLICY (deliberate). Everything here is COORDINATE-LEVEL: the
   hypotheses are the validity predicates of Proofs/EdDenote.lean (limb
   bounds + ⟪Z⟫ ≠ 0), and the postconditions are limb bounds plus polynomial
   identities over 𝔽_p in the INPUT struct-field denotations. No curve
   constant, no division, no `OnCurve` appears: that r really doubles the
   denoted affine point (and that ⟪r.Z⟫ ≠ 0, which needs the curve equation)
   is the job of the algebra layer (Proofs/EdCurve.lean and the op-law
   files), which will consume these specs and `fp_div_*`/`two_ne_zero'`.

   RUST ANALOG
   `ProjectivePoint::double`, curve25519/solana-ed25519/src/backend/serial/
   curve_models.rs:381-397 (transpiled at gen/CurveField/Funs.lean:1464-1487):
       let XX          = self.X.square();
       let YY          = self.Y.square();
       let ZZ2         = self.Z.square2();
       let X_plus_Y    = &self.X + &self.Y;
       let X_plus_Y_sq = X_plus_Y.square();
       let YY_plus_XX  = &YY + &XX;
       let YY_minus_XX = &YY - &XX;
       CompletedPoint { X: &X_plus_Y_sq - &YY_plus_XX,   // = 2XY
                        Y: YY_plus_XX,
                        Z: YY_minus_XX,
                        T: &ZZ2 - &YY_minus_XX }
   — the "dbl-2008-hwcd" doubling of Hisil–Wong–Carter–Dawson 2008 §3.3
   (a = −1 twisted Edwards), with 2·Z² computed by the dedicated `square2`
   (verified in Proofs/Square2Spec.lean).
   `EdwardsPoint::double`, src/edwards.rs:774-776 (Funs.lean:3261-3265):
       self.as_projective().double().as_extended()
   with `as_projective` (edwards.rs:541-547, Funs.lean:3029-3033) the X,Y,Z
   copy and `CompletedPoint::as_extended` (curve_models.rs:365-372,
   Funs.lean:1397-1413) the four cross-multiplications
   (X·T, Y·Z, Z·T, X·Y) landing back in extended coordinates.

   PANIC-FREEDOM / BOUND BOOKKEEPING (the entire totality argument)
     square/square2 inputs need Bnd · 2⁵⁴ — satisfied by ProjValid's 2⁵²;
     fe_add on two reduced (2⁵²) inputs: limbwise sums < 2⁵³ < 2⁶⁴, output
       Bnd 2⁵³ (the one unreduced value in the body, also fine as a square /
       sub input since 2⁵³ < 2⁵⁴);
     fe_sub/fe_mul inputs need 2⁵⁴ — all arguments are ≤ 2⁵³ here;
     outputs: square/mul 2⁵¹+2¹³ (≤ 2⁵²), square2 2⁵³, sub 2⁵², add 2⁵³.
   The `edis` discharge macro below closes every such side condition.

   PROOF ARCHITECTURE — the InvertSpec playbook
   Each body is walked with the `let*` symbolic-execution steps, consuming
   one field op per line via the registered specs (`square_spec'`,
   `square2_spec'`, `mul_spec'` and the two LOCAL wrappers `add_spec''`/
   `sub_spec''` below, derived from AddSpec/SubNegSpec); the final `ok`
   struct is collapsed by `spec_ok` + the `mk_*` projection lemmas of
   EdDenote, the bound conjuncts are the step posts (weakened by Bnd.mono),
   and each coordinate equation closes by rewriting the chain of value
   posts and `ring`.

   Imports: Proofs/EdDenote (point predicates, mk_* lemmas),
            Proofs/Square2Spec (square2_spec'; transitively SquareSpec /
            MulSpec / SubNegSpec / AddSpec and the WP layer).
   Imported by: the forthcoming doubling-law file (EdCurve algebra phase).
   ─────────────────────────────────────────────────────────────────────── -/
import Proofs.EdDenote
import Proofs.Square2Spec
open Aeneas Aeneas.Std Result
open curve25519_dalek

set_option maxHeartbeats 4000000
set_option maxRecDepth 8000
-- The step machinery sometimes discharges a side condition before the
-- attached `by edis` block runs (e.g. when an exact hypothesis is already
-- in context); keeping the uniform `by edis` on every step is clearer than
-- special-casing those, so the two "unused tactic" lints are disabled.
set_option linter.unusedTactic false
set_option linter.unreachableTactic false

namespace CurveFieldProofs

-- the weakest-precondition layer: spec_mono / spec_ok used by the wrappers
open Aeneas.Std.WP

/-- Discharge tactic for the side conditions of the doubling bodies: either
    a hypothesis verbatim (validity predicates), linear arithmetic
    (`scalar_tac`), or a `Bnd _ c ≤ Bnd _ c'` weakening from any hypothesis
    via `Bnd.mono` (e.g. a 2⁵¹+2¹³ square output fed to a 2⁵⁴-input op).
    Same pattern as `bnd` in Proofs/InvertSpec.lean (macros are file-local,
    so it is re-declared here under a fresh name).
    (`name :=` disambiguates the generated syntax-kind declaration from the
    sibling files' `edis` macros so they can all be imported together.) -/
macro (name := edisDouble) "edis" : tactic =>
  `(tactic| (first
      | assumption
      | scalar_tac
      | exact Bnd.mono (by assumption) (by norm_num)))

/-! ## Local step-friendly wrappers for `+` and `-`

The base `add_spec` (Proofs/AddSpec.lean) needs explicit limb lists and
per-limb no-overflow hypotheses, and `sub_spec` (Proofs/SubNegSpec.lean)
needs explicit limb lists; the `let*` machinery cannot invent those. The two
wrappers below repackage them with the limbs hidden (via `Fe.exists_limbs`),
exactly like `mul_spec'`/`square_spec'` in InvertSpec. They are `private`
(file-local): other op-spec files declare their own copies, and privacy
prevents name clashes when several such files are imported together. -/

/-- Rust: `impl Add for FieldElement51` (limbwise `+`, NO reduction),
    curve25519/solana-ed25519/src/backend/serial/u64/field.rs:58-73.

    MATH:  Bnd(a,2⁵²) and Bnd(b,2⁵²)  ==>  fe_add a b = ok r  with
           Bnd(r, 2⁵³)  and  ⟪r⟫ = ⟪a⟫ + ⟪b⟫.
    The 2⁵² input bound is the reduced-operand discipline: limbwise sums are
    < 2⁵³ < 2⁶⁴ (no u64 overflow), and the output bound doubles — the
    generic law `∀ c, Bnd a c → Bnd b c → Bnd r (2c)` of `add_spec`
    instantiated at c = 2⁵². The value is exact over ℕ, so it adds in 𝔽_p.
    WHY NEEDED: `double` adds X+Y and YY+XX; both arguments are reduced
    (≤ 2⁵²), and the 2⁵³ output is exactly the bound the postcondition of
    `proj_double_spec` exposes for r.Y. -/
private theorem add_spec'' (a b : Fe) (ha : Bnd a (2^52)) (hb : Bnd b (2^52)) :
    fe_add a b ⦃ r => Bnd r (2^53) ∧ ⟪r⟫ = ⟪a⟫ + ⟪b⟫ ⦄ := by
  -- name the limbs and turn the two Bnd's into per-limb inequalities
  obtain ⟨a0, a1, a2, a3, a4, hla⟩ := Fe.exists_limbs a
  obtain ⟨b0, b1, b2, b3, b4, hlb⟩ := Fe.exists_limbs b
  have hba := (Bnd_eq a a0 a1 a2 a3 a4 _ hla).mp ha
  have hbb := (Bnd_eq b b0 b1 b2 b3 b4 _ hlb).mp hb
  -- run add_spec; each pairwise sum < 2⁵² + 2⁵² = 2⁵³ < 2⁶⁴ is closed by omega
  apply spec_mono (add_spec a b a0 a1 a2 a3 a4 b0 b1 b2 b3 b4 hla hlb
    ⟨by omega, by omega, by omega, by omega, by omega⟩)
  rintro r ⟨-, hval, hbnd⟩
  -- bound: instantiate the "doubles any common bound" law at 2⁵² (2·2⁵² = 2⁵³)
  refine ⟨by simpa using hbnd (2^52) ha hb, ?_⟩
  -- value: feVal r = feVal a + feVal b over ℕ, then push through the cast
  simp [denote, hval]

/-- Rust: `impl Sub for FieldElement51` (add 16p limbwise, subtract, reduce),
    curve25519/solana-ed25519/src/backend/serial/u64/field.rs.

    MATH:  Bnd(a,2⁵⁴) and Bnd(b,2⁵⁴)  ==>  fe_sub a b = ok r  with
           Bnd(r, 2⁵²)  and  ⟪r⟫ = ⟪a⟫ − ⟪b⟫
    — `sub_spec` (Proofs/SubNegSpec.lean) verbatim, with the limb lists
    destructured internally so `let*` can apply it.
    WHY NEEDED: `double` subtracts three times (YY−XX, (X+Y)²−(YY+XX),
    2Z²−(YY−XX)); all six arguments are ≤ 2⁵³ < 2⁵⁴ here. -/
private theorem sub_spec'' (a b : Fe) (ha : Bnd a (2^54)) (hb : Bnd b (2^54)) :
    fe_sub a b ⦃ r => Bnd r (2^52) ∧ ⟪r⟫ = ⟪a⟫ - ⟪b⟫ ⦄ := by
  obtain ⟨a0, a1, a2, a3, a4, hla⟩ := Fe.exists_limbs a
  obtain ⟨b0, b1, b2, b3, b4, hlb⟩ := Fe.exists_limbs b
  exact sub_spec a b a0 a1 a2 a3 a4 b0 b1 b2 b3 b4 hla hlb ha hb

/-! ## The doubling kernel on ℙ² -/

/-- Coordinate-level spec for `ProjectivePoint::double`.

    Rust: curve25519/solana-ed25519/src/backend/serial/curve_models.rs:
    381-397, transpiled at gen/CurveField/Funs.lean:1464-1487 — the
    Hisil–Wong–Carter–Dawson doubling producing a COMPLETED (ℙ¹×ℙ¹) point.

    MATH:  ProjValid p  ==>  double p = ok r  with
      Bnd r.X 2⁵², Bnd r.Y 2⁵³, Bnd r.Z 2⁵², Bnd r.T 2⁵²   and, in 𝔽_p,
      ⟪r.X⟫ = 2·⟪p.X⟫·⟪p.Y⟫                (computed as (X+Y)² − (Y²+X²))
      ⟪r.Y⟫ = ⟪p.Y⟫² + ⟪p.X⟫²
      ⟪r.Z⟫ = ⟪p.Y⟫² − ⟪p.X⟫²
      ⟪r.T⟫ = 2·⟪p.Z⟫² − (⟪p.Y⟫² − ⟪p.X⟫²).
    LaTeX: $(X':Z') = (2XY : Y^2 - X^2)$, $(Y':T') = (Y^2+X^2 : 2Z^2-(Y^2-X^2))$,
    i.e. the doubled point in ℙ¹×ℙ¹ — writing x = X/Z, y = Y/Z, this encodes
    x' = 2xy/(2(Z/Z)²−(y²−x²))-style fractions whose algebra (and the
    nonvanishing of the denominators) is established in the law layer, NOT
    here.

    Bounds: r.Y is the single unreduced addition of the body (two ≤ 2⁵²
    summands ⇒ 2⁵³); the other three fields are `sub` outputs (2⁵²). All
    four are < 2⁵⁴, so the completed point can feed any field op directly
    (`as_extended`/`as_projective` consume it below / in the add file).

    WHY NEEDED: the computational core of point doubling; consumed by
    `edwards_double_spec` below and by the scalar-multiplication ladder
    specs later. -/
theorem proj_double_spec (p : ProjPoint) (hp : ProjValid p) :
    backend.serial.curve_models.ProjectivePoint.double p ⦃ r =>
      Bnd r.X (2^52) ∧ Bnd r.Y (2^53) ∧ Bnd r.Z (2^52) ∧ Bnd r.T (2^52) ∧
      ⟪r.X⟫ = 2 * ⟪p.X⟫ * ⟪p.Y⟫ ∧
      ⟪r.Y⟫ = ⟪p.Y⟫^2 + ⟪p.X⟫^2 ∧
      ⟪r.Z⟫ = ⟪p.Y⟫^2 - ⟪p.X⟫^2 ∧
      ⟪r.T⟫ = 2 * ⟪p.Z⟫^2 - (⟪p.Y⟫^2 - ⟪p.X⟫^2) ⦄ := by
  -- the limb bounds of the validity predicate (⟪Z⟫ ≠ 0 is not needed here:
  -- doubling never divides — it is carried for the law layer only)
  obtain ⟨hpX, hpY, hpZ, -⟩ := hp
  -- expose the transpiled 9-op monadic body
  unfold backend.serial.curve_models.ProjectivePoint.double
  -- XX ← square p.X            ⟪XX⟫ = ⟪p.X⟫·⟪p.X⟫,  Bnd 2⁵¹+2¹³
  let* ⟨ XX, XX_post1, XX_post2 ⟩ ← square_spec' by edis
  -- YY ← square p.Y            ⟪YY⟫ = ⟪p.Y⟫·⟪p.Y⟫
  let* ⟨ YY, YY_post1, YY_post2 ⟩ ← square_spec' by edis
  -- ZZ2 ← square2 p.Z          ⟪ZZ2⟫ = 2·(⟪p.Z⟫·⟪p.Z⟫),  Bnd 2⁵³
  let* ⟨ ZZ2, ZZ2_post1, ZZ2_post2 ⟩ ← square2_spec' by edis
  -- v4.1.2 ORDER (differs from the v4.1.3/risc0 body — same ops, reordered):
  -- X_plus_Y ← p.X + p.Y       (unreduced add of two reduced values, Bnd 2⁵³)
  let* ⟨ XpY, XpY_post1, XpY_post2 ⟩ ← add_spec'' by edis
  -- X_plus_Y_sq ← square X_plus_Y
  let* ⟨ XpYsq, XpYsq_post1, XpYsq_post2 ⟩ ← square_spec' by edis
  -- YY_plus_XX ← YY + XX       (both ≤ 2⁵¹+2¹³ ≤ 2⁵², output Bnd 2⁵³ = r.Y)
  let* ⟨ YpX, YpX_post1, YpX_post2 ⟩ ← add_spec'' by edis
  -- YY_minus_XX ← YY − XX      (sub reduces: Bnd 2⁵² = r.Z)
  let* ⟨ YmX, YmX_post1, YmX_post2 ⟩ ← sub_spec'' by edis
  -- fe ← X_plus_Y_sq − YY_plus_XX        (= 2XY, the r.X numerator)
  let* ⟨ feX, feX_post1, feX_post2 ⟩ ← sub_spec'' by edis
  -- fe1 ← ZZ2 − YY_minus_XX              (= 2Z² − (Y²−X²), the r.T field)
  let* ⟨ feT, feT_post1, feT_post2 ⟩ ← sub_spec'' by edis
  -- the body returns `ok { X := feX, Y := YpX, Z := YmX, T := feT }`; the
  -- `let*` machinery already collapsed the triple on the literal and reduced
  -- the struct projections, so the goal is the bare conjunction.
  -- Bounds are the step posts verbatim; each equation is the chain of value
  -- posts followed by polynomial normalization.
  refine ⟨feX_post1, YpX_post1, YmX_post1, feT_post1, ?_, ?_, ?_, ?_⟩
  · -- ⟪r.X⟫ = (⟪p.X⟫+⟪p.Y⟫)² − (⟪p.Y⟫²+⟪p.X⟫²) = 2·⟪p.X⟫·⟪p.Y⟫
    rw [feX_post2, XpYsq_post2, XpY_post2, YpX_post2, YY_post2, XX_post2]; ring
  · -- ⟪r.Y⟫ = ⟪p.Y⟫² + ⟪p.X⟫²
    rw [YpX_post2, YY_post2, XX_post2]; ring
  · -- ⟪r.Z⟫ = ⟪p.Y⟫² − ⟪p.X⟫²
    rw [YmX_post2, YY_post2, XX_post2]; ring
  · -- ⟪r.T⟫ = 2·⟪p.Z⟫² − (⟪p.Y⟫² − ⟪p.X⟫²)
    rw [feT_post2, ZZ2_post2, YmX_post2, YY_post2, XX_post2]; ring

/-! ## The extended-coordinates wrapper -/

/-- Spec for `EdwardsPoint::as_projective`: the ℙ³ → ℙ² forgetful map is a
    plain struct rebuild copying X, Y, Z (and dropping the cached T).

    Rust: curve25519/solana-ed25519/src/edwards.rs:541-547, transpiled at
    gen/CurveField/Funs.lean:3029-3033 (a single `ok { … }`, no field op).

    MATH:  ExtValid P  ==>  as_projective P = ok pp  with  ProjValid pp  and
           pp.X = P.X,  pp.Y = P.Y,  pp.Z = P.Z   (Fe-level equality of the
    limb vectors — strictly stronger than denotation equality, which is what
    downstream rewriting uses).  ProjValid is inherited: the three copied
    fields keep their 2⁵² bounds and ⟪Z⟫ ≠ 0 is ExtValid's own clause; the
    extended coherence X·Y = Z·T is simply forgotten.
    WHY NEEDED: first step of `EdwardsPoint::double` (and of `is_valid`);
    proving it once keeps `edwards_double_spec` a three-step composition. -/
theorem as_projective_spec (P : EdPoint) (hP : ExtValid P) :
    edwards.EdwardsPoint.as_projective P ⦃ pp =>
      ProjValid pp ∧ pp.X = P.X ∧ pp.Y = P.Y ∧ pp.Z = P.Z ⦄ := by
  obtain ⟨hX, hY, hZ, -, hZ0, -⟩ := hP
  -- the body is literally `ok { X := P.X, Y := P.Y, Z := P.Z }`
  unfold edwards.EdwardsPoint.as_projective
  -- collapse the triple on the `ok` literal; the three copy equations reduce
  -- to `True` (definitional projections of the literal)
  simp only [spec_ok]
  -- validity: bounds and ⟪Z⟫ ≠ 0 are ExtValid clauses
  exact ⟨⟨hX, hY, hZ, hZ0⟩, trivial, trivial, trivial⟩

/-- Coordinate-level spec for `EdwardsPoint::double`.

    Rust: curve25519/solana-ed25519/src/edwards.rs:774-776, transpiled at
    gen/CurveField/Funs.lean:3261-3265:
        self.as_projective().double().as_extended()
    — drop to ℙ², run the doubling kernel (`proj_double_spec`), then re-embed
    the completed ℙ¹×ℙ¹ result into extended ℙ³ coordinates via
    `CompletedPoint::as_extended` (curve_models.rs:365-372, Funs.lean:
    1397-1413), which cross-multiplies (X,Y,Z,T) ↦ (X·T, Y·Z, Z·T, X·Y).

    MATH:  ExtValid P  ==>  double P = ok r  with all four fields reduced
    (Bnd · 2⁵², they are mul outputs) and, abbreviating in 𝔽_p
        X' := 2·⟪P.X⟫·⟪P.Y⟫,             Y' := ⟪P.Y⟫² + ⟪P.X⟫²,
        Z' := ⟪P.Y⟫² − ⟪P.X⟫²,           T' := 2·⟪P.Z⟫² − (⟪P.Y⟫² − ⟪P.X⟫²),
    the four SEGRE-PRODUCT equations
        ⟪r.X⟫ = X'·T',   ⟪r.Y⟫ = Y'·Z',   ⟪r.Z⟫ = Z'·T',   ⟪r.T⟫ = X'·Y'
    (spelled out below with the primed names inlined — the postcondition is
    STRICTLY a polynomial identity in ⟪P.X⟫, ⟪P.Y⟫, ⟪P.Z⟫).

    ⚠ The output is stated coordinate-only, deliberately WITHOUT ⟪r.Z⟫ ≠ 0
    and without the extended coherence ⟪r.X⟫·⟪r.Y⟫ = ⟪r.Z⟫·⟪r.T⟫ (so not as
    `ExtValid r`): the coherence is a one-line `ring` consequence of the
    four equations (X'T'·Y'Z' = Z'T'·X'Y'), and Z'·T' ≠ 0 genuinely needs
    the curve equation on (edX P, edY P) — both belong to the algebra layer
    that packages doubling as a group law.  Note the cached T of the INPUT
    is not consumed by doubling at all (as_projective drops it), so the
    equations mention only ⟪P.X⟫, ⟪P.Y⟫, ⟪P.Z⟫.

    WHY NEEDED: the public doubling entry point of the crate; the scalar
    multiplication ladder and the group-law file build directly on it. -/
theorem edwards_double_spec (P : EdPoint) (hP : ExtValid P) :
    edwards.EdwardsPoint.double P ⦃ r =>
      Bnd r.X (2^52) ∧ Bnd r.Y (2^52) ∧ Bnd r.Z (2^52) ∧ Bnd r.T (2^52) ∧
      ⟪r.X⟫ = (2 * ⟪P.X⟫ * ⟪P.Y⟫) *
              (2 * ⟪P.Z⟫^2 - (⟪P.Y⟫^2 - ⟪P.X⟫^2)) ∧
      ⟪r.Y⟫ = (⟪P.Y⟫^2 + ⟪P.X⟫^2) * (⟪P.Y⟫^2 - ⟪P.X⟫^2) ∧
      ⟪r.Z⟫ = (⟪P.Y⟫^2 - ⟪P.X⟫^2) *
              (2 * ⟪P.Z⟫^2 - (⟪P.Y⟫^2 - ⟪P.X⟫^2)) ∧
      ⟪r.T⟫ = (2 * ⟪P.X⟫ * ⟪P.Y⟫) * (⟪P.Y⟫^2 + ⟪P.X⟫^2) ⦄ := by
  -- expose the 3-step transpiled body
  unfold edwards.EdwardsPoint.double
  -- pp ← as_projective P       (copies X, Y, Z; ProjValid from ExtValid)
  let* ⟨ pp, pp_valid, ppX, ppY, ppZ ⟩ ← as_projective_spec by edis
  -- cp ← ProjectivePoint.double pp      (the kernel, spec above)
  let* ⟨ cp, cpX_b, cpY_b, cpZ_b, cpT_b, cpX_v, cpY_v, cpZ_v, cpT_v ⟩ ←
    proj_double_spec by edis
  -- the tail call: as_extended cp — four cross-multiplications
  unfold backend.serial.curve_models.CompletedPoint.as_extended
  -- rX ← cp.X * cp.T,  rY ← cp.Y * cp.Z,  rZ ← cp.Z * cp.T,  rT ← cp.X * cp.Y
  -- (every factor is ≤ 2⁵³ < 2⁵⁴ by the kernel's bounds — edis weakens)
  let* ⟨ rX, rX_post1, rX_post2 ⟩ ← mul_spec' by edis
  let* ⟨ rY, rY_post1, rY_post2 ⟩ ← mul_spec' by edis
  let* ⟨ rZ, rZ_post1, rZ_post2 ⟩ ← mul_spec' by edis
  let* ⟨ rT, rT_post1, rT_post2 ⟩ ← mul_spec' by edis
  -- final struct literal: the `let*` machinery already collapsed the triple
  -- and reduced the projections, leaving the bare conjunction.
  -- bounds: mul outputs are 2⁵¹+2¹³ ≤ 2⁵²; equations: substitute the mul
  -- posts, the kernel's coordinate equations, and the as_projective copies,
  -- then normalize
  refine ⟨rX_post1.mono (by norm_num), rY_post1.mono (by norm_num),
          rZ_post1.mono (by norm_num), rT_post1.mono (by norm_num),
          ?_, ?_, ?_, ?_⟩
  -- (after the rewrites both sides are syntactically identical, so the `rfl`
  -- built into `rw` closes each goal — no `ring` needed)
  · -- ⟪r.X⟫ = ⟪cp.X⟫·⟪cp.T⟫ = X'·T'
    rw [rX_post2, cpX_v, cpT_v, ppX, ppY, ppZ]
  · -- ⟪r.Y⟫ = ⟪cp.Y⟫·⟪cp.Z⟫ = Y'·Z'
    rw [rY_post2, cpY_v, cpZ_v, ppX, ppY]
  · -- ⟪r.Z⟫ = ⟪cp.Z⟫·⟪cp.T⟫ = Z'·T'
    rw [rZ_post2, cpZ_v, cpT_v, ppX, ppY, ppZ]
  · -- ⟪r.T⟫ = ⟪cp.X⟫·⟪cp.Y⟫ = X'·Y'
    rw [rT_post2, cpX_v, cpY_v, ppX, ppY]

end CurveFieldProofs
