Zulip Chat Archive

Stream: mathlib4

Topic: Strange behavior of compute_degree


Michael Stoll (Jun 15 2025 at 11:58):

While tinkering with Gelfand-Mazur, I came across the following.

import Mathlib.Tactic.ComputeDegree

variable {R : Type*} [CommRing R]

open Polynomial

variable (R) in
noncomputable
def p : R[X] := X ^ 2 + 2  X + C 1

variable [Nontrivial R]

theorem xxx : (p R).natDegree = 2 := by
  compute_degree
  -- 2 goals: `¬(p R).coeff 2 ≠ 0` *and* `(p R).natDegree = 2`
  all_goals sorry

I would expect compute_degree to clear the original goal and only leave the side goals necessary for proving it. @Damiano Testa ?

Michael Stoll (Jun 15 2025 at 12:01):

There is another strange error when I replace p by its definition.

import Mathlib.Tactic.ComputeDegree

variable {R : Type*} [CommRing R]

open Polynomial

theorem yyy : (X ^ 2 + 2  X + C 1 : R[X]).natDegree = 2 := by
/-  tactic 'apply' failed, could not unify the conclusion of
      'Mathlib.Tactic.ComputeDegree.natDegree_smul_le_of_le'
        (HSMul.hSMul.{?u.3352, ?u.3352, ?u.3352} ?a ?f).natDegree ≤ ?n
    with the goal
      (HSMul.hSMul.{0, u_1, u_1} 2 X).natDegree ≤ ?h_natDeg_le.hp.n
  -/
  compute_degree

This could be related to #lean4 > "@instHAdd ℕ instAddNat : HAdd ℕ ℕ ℕ" out of thin air @ 💬: some strange things seem to be going on when elaborating statements containing polynomials, but I am not sure how closely related it might be.

Damiano Testa (Jun 15 2025 at 15:40):

I'm not at a computer, but here is what I suspect.

First, compute_degree should not see through defs, so the first behaviour is "almost" what I would expect. I thought it would have left a p.natDegree ≤ 2 goal, though.

Second, I don't think thatcompute_degree is aware of . It would be really easy to add this feature, but I don't think that I was thinking about that possibility when writing the tactic.

Michael Stoll (Jun 15 2025 at 15:44):

Damiano Testa said:

I thought it would have left a p.natDegree ≤ 2 goal, though.

Exactly.

Michael Stoll (Jun 15 2025 at 15:49):

Damiano Testa said:

Second, I don't think thatcompute_degree is aware of . It would be really easy to add this feature, but I don't think that I was thinking about that possibility when writing the tactic.

I've been trying to manipulate polynomials a fair bit in the last few hours, and I got the impression that simp does not do a good job on them (meaning, so that after simp I can use ring to close equality goals, for example). Probably some thought should go into what a good simp normal form for polynomials may look like. For example, it currently uses map_pow etc., which changes C (a ^2) to (C a) ^ 2, and on this the lemma that says that the nth coefficient of C a is zero (if n is nonzero) doesn't fire.
I assume that you @Damiano Testa have thought about this while writing the tactics like compute_degree. Maybe one can extract a normalize_poly tactic from this?

Damiano Testa (Jun 15 2025 at 15:55):

I wrote the tactic in Lean3, then ported it to Lean4. The reason this is relevant is that coercions changed quite a bit and in Lean3, basically the only way to multiply constants and polynomials was via C. I think that very little thought has gone into simp-confluence for polynomials in Lean4 and your experiments are probably showing that such a cleanup is overdue.

Damiano Testa (Jun 15 2025 at 15:57):

Maybe an easy solution would be to have a custom simp set that converts polynomials to something where compute_degree has a chance of working, although going from2 * X to C 2 * X may be tricky with simp.

Yaël Dillies (Jun 15 2025 at 15:57):

I have a set of simprocs in the works

Damiano Testa (Jun 15 2025 at 15:58):

Ok, great! Then at least agreeing with an expected simp-normal form for polynomials would help making compute_degree more reliable.

Michael Stoll (Jun 15 2025 at 15:59):

It's not just compute_degree, one of my problems is that what simp produces is not good for ring. So hopefully Yaël's simprocs will produce a form that is ring-friendly.

Michael Stoll (Jun 15 2025 at 15:59):

(I need to cook dinner now...)

Damiano Testa (Jun 15 2025 at 16:00):

How reasonable/feasible would it be to try to ban completely C and only allowing * and ?

Yaël Dillies (Jun 15 2025 at 16:01):

Why not just make C a plain function?

Damiano Testa (Jun 15 2025 at 16:01):

(I mean as simp-normal form.)

Damiano Testa (Jun 15 2025 at 16:01):

A plain function as in refactoring polynomial?

Damiano Testa (Jun 15 2025 at 16:02):

I personally don't have it in me to embark in such a refactor.

Damiano Testa (Jun 15 2025 at 16:03):

Also, "fixing" compute_degree to allow would not be especially hard: likely it is just a matter of adding a lemma about the degree of a smul (which may even already be there) and then making compute_degree use it.

Michael Stoll (Jun 15 2025 at 17:18):

One problem with is that ring does not work well with it. To get my goals closed by ring, it seems best to write everything in terms of C.

And regarding compute_degree, the following works in my code:

have : (c ^ 2  (X ^ 2 - s  (2 * X)) + C (c ^ 2) * C t).natDegree  2 := by compute_degree

Yaël Dillies (Jun 15 2025 at 17:23):

Michael Stoll said:

One problem with is that ring does not work well with it.

Sounds like you want to hear about the module tactic :wink:

Michael Stoll (Jun 15 2025 at 17:27):

import Mathlib.Tactic

variable {R : Type*} [CommRing R]

open Polynomial

example : (X (R := R) + 1) ^ 2 = 2  X + 1 + X ^ 2 := by
  --ring -- works
  /-
  ring failed, ring expressions not equal
  R : Type u_1
  inst✝ : CommRing R
  ⊢ 1 = 0
  -/
  module

Yaël Dillies (Jun 15 2025 at 17:50):

The idiom is ring_nf; module (but here ring_nf already closes the goal)

Michael Stoll (Jun 15 2025 at 17:54):

import Mathlib

open Polynomial

theorem foo {R : Type*} [CommRing R] (s t c : R) :
  (X ^ 2 - 2 * s  X + C t) ^ 1 - c ^ 1  2  (X - C s) + C (c ^ (2 * 1)) =
    ((X - C c) ^ 2 - 2 * s  (X - C c) + C t) * 1 := by
  ring_nf
  module -- ring failed, ring expressions not equal
  sorry

This is one of my use cases.

Damiano Testa (Jun 16 2025 at 01:59):

#25944

Damiano Testa (Jun 16 2025 at 02:00):

It turns out that compute_degree already supported smul, but only by the coefficients of the polynomial ring. In your case, the smul was coming from a natural number, so technically not from the coefficients. The PR above fixes this issue.

Damiano Testa (Jun 16 2025 at 02:00):

The PR also contains this test

variable [CommRing R] [Nontrivial R] in
example : (X ^ 2 + 2  X + C 1 : R[X]).natDegree = 2 := by
  compute_degree!
  guard_target = ((1 + 2 * X.coeff 2 : R)  0)
  simp [coeff_X]

where ideally compute_degree! should close the goal, not leave a side-goal. However, I have not worked out why X.coeff 2 does not get replaced by 0.

Michael Stoll (Jun 16 2025 at 13:55):

It seems that docs#Polynomial.coeff_X_of_ne_one and docs#Polynomial.coeff_X are not simp lemmas (and when I just give the first lemma to simp, then simpdoes not discharge the side goal 2 ≠ 1 by itself). I don't know if this is relevant to how compute_degree! works, though.

Damiano Testa (Jun 16 2025 at 16:09):

Yes, I think that this is indeed relevant: compute_degree! tries to discharge the left-over goals using norm_num <;> assumption. Thus, simp bringing a goal to 2 ≠ 1 should make norm_num close the goal.

Damiano Testa (Jun 16 2025 at 17:12):

I just checked: adding the simp attribute to coeff_X does indeed simplify the proof:

variable [CommRing R] [Nontrivial R] in
example : (X ^ 2 + 2  X + C 1 : R[X]).natDegree = 2 := by
  compute_degree!  -- works

Michael Stoll (Jun 16 2025 at 17:16):

Damiano Testa said:

Thus, simp bringing a goal to 2 ≠ 1 should make norm_num close the goal.

As far as I understand, simp does not fire when it cannot discharge the side goals.

Damiano Testa (Jun 16 2025 at 17:16):

It seems that inside norm_num, that is ok.

Damiano Testa (Jun 16 2025 at 17:23):

By the way, this also works:

import Mathlib

open Polynomial

attribute [simp] coeff_X

variable [CommRing R] [Nontrivial R] in
example : (1 + 2 * X.coeff 2 : R)  0 := by
  simp

So, it seems that simp is already able to close the goal.

Michael Stoll (Jun 16 2025 at 17:24):

It uses ↓reduceIte and one_ne_zero, so I would assume it's due to simprocs.

Michael Stoll (Jun 19 2025 at 07:40):

@Damiano Testa Currently this fails:

import Mathlib

open Polynomial

/-
'compute_degree' inapplicable. The goal
  (C 1).natDegree < 1
is expected to be '≤' or '='.
-/
example : (C 1 : Polynomial ).natDegree < 1 := by compute_degree

Would it be possible to extend compute_degree/compute_degree! to also deal with strict inequalities?

Damiano Testa (Jun 19 2025 at 07:41):

Fixed a bug, found a bug.

Damiano Testa (Jun 19 2025 at 07:44):

Given that

  apply lt_of_le_of_lt
  · compute_degree
  · norm_num

works, it is probably easy to get something working, at least leaving out of scope the inequality <actual_degree> < given bound, so 0 < 1 in your case.

Damiano Testa (Jun 19 2025 at 07:45):

I remember thinking about strict inequalities and deciding against doing them in the first place, since the algorithm for is somewhat easier.

Damiano Testa (Jun 19 2025 at 10:30):

#26145, where your example is solved by compute_degree!.

Damiano Testa (Jun 19 2025 at 10:34):

Note that the +37-16 change that github reports consists of

  • 2 added lines of code;
  • documentation and examples!

Michael Stoll (Jun 19 2025 at 10:35):

I noticed that (just had a look), but I don't feel competent enough with meta code to maintainer merge.

Damiano Testa (Jun 19 2025 at 10:37):

Sure, no worries! If it helps anyone, the strategy is to detect if the goal looks like f.natDegree < d and, if so, start the proof with apply lt_of_le_of_lt and then fall back on the earlier behaviour.

Damiano Testa (Jun 19 2025 at 10:38):

In fact, compute_degree does not "know" what the degree is at the start: it replaces the degree with a metavariable, imposes conditions on the metavariable by traversing the polynomial and then checks whether the constraints are compatible.

Damiano Testa (Jun 19 2025 at 10:38):

So, the first step of replacing the degree with a metavariable is what it would have done in the weak inequality as well.

Damiano Testa (Jun 19 2025 at 18:19):

Michael, strict inequalities are now handled by compute_degree also in the online server. Your example above is closed by compute_degree! (while compute_degree leaves a 0 < 1 goal).


Last updated: Dec 20 2025 at 21:32 UTC