Why the heading number can be zero in v0.14.0-rc.2?

#set heading(numbering: (n, ..nums) => {
  assert(n > 0, message: "n = " + repr(n))
  [#n is not zero.]
})

= Heading

图片

The above code compiles in typst v0.13.1, but panics with Assertion failed: n = 0 in typst v0.14.0-rc.2. Why?

error: assertion failed: n = 0
  ┌─ <stdin>:2:2
  │
2 │   assert(n > 0, message: "n = " + repr(n))
  │   ^^^^^^^^^^^^^^^^^^

Moreover, if I remove the assert line, then v0.14.0-rc.2 also generates 1 is not zero. How can it be?

Use case

Sometimes we want a Chapter zero, but the numbering function only accepts nonnegative numbers.

  #set heading(
-   numbering: n => numbering("第一章", n - 1),  // v0.13.1
+   numbering: n => numbering("第一章", calc.max(n - 1, 0)),  // v0.14.0; calc.abs also works
  )

(I know that the chapter zero is for sections before the first chapter, but such sections do not always exist.)

Edits

Edit: This post also applies to the final v0.14.

Edit 2: There’s a related issue. Start a counter with zero? · Issue #6662 · typst/typst · GitHub

Edit 3: Laurenz Mädje commented on it:

the problem is that the heading numbering is resolved during synthesis now because it is needed for the PDF outline and synthesis doesn’t have the error delaying properties of show rules. probably it should have it too. I’m not sure I would consider it a bug per se as numbering functions are supposed to deal with the full set of possible inputs, but it is something that will probably work again in the future so I guess we can track it as a regression.
(Discord, 2025-11-20)

Edit 4: Heading numbering resolution change in 0.14 · Issue #7428 · typst/typst · GitHub

2 Likes

That behaviour is familiar from the layout iterations that typst is using. NOTE: my understanding (below) is still based on Typst usage, not internal knowledge.

  • First layout iteration:
    Context dependent numbering is resolved using initial/stale data
    Propagate state updates
  • Second layout iteration
    Context dependent numbering is updated using state information from previous iteration - heading gets the correct number

As usual in typst, if a function panics inside context that whole context block fails (produces empty output) but compilation continues. Typst will try again in the next iteration.

However if the context block contains something that the second iteration needs, then the second iteration will fail the same way as the first.

Errors from inside context are only presented to the user if they remain in the last layout iteration - so you never see the errors that resolve themselves.

Conclusion: in typst 0.14.0-rc.2, the counter(heading).step() probably never happens if the numbering function call fails. For some reason they are linked, as if they are inside the same context block.

1 Like

In Typst 0.13, this document compiles (in 2 iterations)

#import "@preview/layout-ltd:0.1.0": layout-limiter
#show: layout-limiter.with(max-iterations: 2)

#set heading(numbering: "1")
= Heading

In Typst 0.14.0, it needs 3 iterations to complete without layout convergence warning.

1 Like

Just saw this forum post now. As a small side note: In the next release cycle, it would be much appreciated to report directly to GitHub if you find an issue like this. :) I don’t monitor the Forum quite as actively as GitHub and that way we maybe could have fixed this for 0.14. Personally, I will remember to monitor the Forum more actively around RC phases.

1 Like

Fixed in Do not propagate errors in numbering resolution during heading synthesis by laurmaedje · Pull Request #7459 · typst/typst · GitHub, just released in 0.14.1 – Typst Documentation.

2 Likes