When does counter step apply when used in a show rule?

Why does this show rule work as intended:

#set enum(numbering:"(1)", start:0)
#let enum_child_counter = counter("enum_child_counter")
#show enum.where(start: 0): it => {
  let args = it.fields()
  for i in args.remove("children") {
    context {
      let current_child_count = enum_child_counter.get().first()
      enum(..args, start: current_child_count+1, i)
    }
    enum_child_counter.step()
  }
}

= Assignment

first paragraph here

+ first point
+ second point

second paragraph here

+ third point, continuing from second
+ fourth point

… but these two adjustments (see comments) cause a maximum show rule depth exceeded error?

#set enum(numbering:"(1)", start:0)
#let enum_child_counter = counter("enum_child_counter")
#show enum.where(start: 0): it => {
  let args = it.fields()
  for i in args.remove("children") {
    enum_child_counter.step() // moved this up
    context {
      let current_child_count = enum_child_counter.get().first()
      enum(..args, start: current_child_count, i) // no adding 1
    }
  }
}

// same markup

I expected both to output this:

I am attempting to use the second approach given this direction from the docs:

When you define and use a custom counter, in general, you should first step the counter and then display it. This way, the stepping behaviour of a counter can depend on the element it is stepped for.

I thought this second approach, using step first, would work just fine, and be in line with the explanation given here:

The update will be in effect at the position where the returned content is inserted into the document. If you don’t put the output into the document, nothing happens! This would be the case, for example, if you write let _ = counter(page).step() . Counter updates are always applied in layout order and in that case, Typst wouldn’t know when to step the counter.

The only explanation I can think of why the second approach does not work is that somehow the context being used within the for loop does not get the stepped value of the counter for some reason, but that is contrary to my understanding of how this all works.

In your second approach, it looks like that current_child_count is always zero in Typst’s first layout run. As a result, we are creating another enum(..., start: 0) in show enum.where(start: 0), causing the maximum show rule depth exceeded error.

You can circumvent it by adding a if guard:

#show enum.where(start: 0): it => {
  let args = it.fields()
  for i in args.remove("children") {
    enum_child_counter.step()
    context {
      let current_child_count = enum_child_counter.get().first()
      if current_child_count > 0 { // šŸ‘ˆ Exclude the recursive case
        enum(..args, start: current_child_count, i)
      }
    }
  }
}

Related links:

When you define and use a custom counter, in general, you should first step the counter and then display it. This way, the stepping behaviour of a counter can depend on the element it is stepped for.

(Personally speaking, I think that’s only relevant to multi-level counts.)

1 Like

But why does the context block (presumably) execute on the first pass? If we make a custom ā€œenumā€ to emulate the original solution without the if-safeguard:

#let my-counter = counter("my-counter")

#let my-enum(it) = {
  my-counter.step()
  context {
    let current-count = my-counter.get()
    numbering("(1)", ..current-count)
    "  "
    it
    linebreak()
  }
}

#my-enum("test")

We see that this works fine, as one would expect.

Hovering over current_child_count also shows that it never takes the value of 1, and similarly removing the if-guard and replacing the enum with repr(enum(...)) the start is always 1 or more in the document.

I think there is some weird behavior going on with recursion in the show rule (the enum of start 0 gets shown as an enum with a contextual start and the compiler gets confused and assumes the ā€œnestedā€ enum also starts at 0?), but I can’t tell if this is a bug or just expected behavior that isn’t documented anywhere.

2 Likes

I’m not sure… I guess it depends on how show rules are evaluated?

Why the heading number can be zero in v0.14.0-rc.2? is a similar bug (now fixed).

2 Likes

Thanks for the reference. The linked PR #7459 indeed seems related. I’m not that familiar with the compiler internals, so I might ask about this on the discord, unless somebody here knows.

1 Like

Thanks to you both. @aarnent’s custom enum example is exactly my confusion. If anyone answers on the discord, post it here for me if you would.