Why does layout diverge with automatic theorem numbering?

I am working on a library for automatic theorem numbering when I encountered some “layout did not converge within 5 attempts” issue. The minimal reproducing example is this:

#let section(loc) = (counter(heading).at(loc).at(0, default: 0), )

#let create-theorem(thm-name, heading-provider, thm-counter, last-heading-state) = body => block[
  #context {
    let curr-heading = heading-provider(here())
    if (last-heading-state.get() != curr-heading) {
      thm-counter.update(0)
      last-heading-state.update(curr-heading)
    }
  }
  #thm-counter.step()
  #context strong[#thm-name #numbering("1.", ..(heading-provider(here()) + thm-counter.get()))]
  #body
]

#let theorem = create-theorem("Theorem", section, counter("theorem"), state("last-heading-thm"))
#let corollary = create-theorem("Corollary", loc => section(loc) + counter("theorem").at(loc), counter("corollary"), state("last-heading-cor"))

#set heading(numbering: "1.")
= 
#theorem[]
#theorem[]
#corollary[]
#corollary[]

Here, each theorem-like environment uses a heading function, a counter to number the theorems, and a state to track whether we should reset this counter.
For example, we would expect theorems to be numbered absolutely and reset at each section as ., and corollaries to be attached to theorems, numbered as ...
However, once I add 2 theorems and 2 corollaries, as in this example, the layout does not converge.
I have completely no idea why this example would diverge, but here’s a few observations:

  • If we change the definition of #theorem and #corollary to:
#let theorem = create-theorem("Theorem", loc => (), counter("theorem"), state("last-heading-thm"))
#let corollary = create-theorem("Corollary", loc => counter("theorem").at(loc), counter("corollary"), state("last-heading-cor"))

The example will then converge, so the heading counter somehow plays a role here. (But it is constant throughout the layout. How?)

  • If we change the definition of #corollary to:
#let corollary = create-theorem("Corollary", section, counter("corollary"), state("last-heading-cor"))

to not depend on counter(“theorem”), then it would also converge. So counter(“theorem”) somehow also plays a role here.

  • If you only have 1 theorem and 2 corollaries, or 2 theorems and 1 corollary, everything converges. Only when you increase to 2 theorems and 2 corollaries does the layout diverge.

I’d like to know why this behavior happens and how to fix it.

Hello @Void,

There is a hint provided

= hint: check if any states or queries are updating themselves

In your code, you write a conditional that checks the value of a state, and updates it.

if (last-heading-state.get() != curr-heading) {
  thm-counter.update(0)
  last-heading-state.update(curr-heading)
}

that should be the source of your issues.

You can look up a great example of how to manage counter at rich-counters. It is used jointly with great-theorems to provide theorems environments.

Hey @Void, welcome to the forum! I’ve changed your question post’s title to better fit our guidelines: How to post in the Questions category

For future posts, make sure your title is a question you’d ask to a friend about Typst. :wink: