Is an assert supposed to be able to change behavior when it "doesn't" fail?

I was trying to minimize an issue in fletcher’s manual and reduced it down to the following (this still depends on CeTZ and fletcher so it’s not properly minimal, but hopefully small enough to discuss the issue):

#import "@preview/cetz:0.3.1": draw, vector
#import "@preview/fletcher:0.5.2" as fletcher: diagram, node

// this is copy-pasted from fletcher
#let pill(node, extrude) = {
  let size = node.size.map(i => i + 2*extrude)
  // assert(size.any(x => x > 0pt))
  draw.rect(
    vector.scale(size, -0.5),
    vector.scale(size, +0.5),
    radius: calc.min(..size)/2,
  )
}

#diagram(node((0,0), link(label("pill()"), raw("pill")), shape: pill,
  fill: green.lighten(90%), stroke: green))

= pill#label("pill()")

This does not compile, with the following error:

error: array is empty
   ┌─ @preview/cetz:0.3.1/src/drawable.typ:45:10
help: error occurred in this call of function `path`
     ┌─ @preview/cetz:0.3.1/src/draw/shapes.typ:1145:8
help: error occurred in this function call
   ┌─ @preview/cetz:0.3.1/src/process.typ:17:22
...

After some bisecting (because the stack trace is incomplete due to context being involved) I figured out that a rectangle with zero size causes this error inside CeTZ, and that the function that drew the rectangle was the one reproduced above.

I thought an assert() check could at least bring the mistake to light earlier, so I inserted assert(size.any(x => x > 0pt)) – and that made the document compile!

I have a rough idea what’s happening here: the node’s label from which the node size is computed is link(label("pill()"), raw("pill")). The link element probably gets its measurements late (probably because links in general also need to handle things such as inferring the text from the referenced label), thus an “early” measure() returns (0pt, 0pt), making CeTZ fail. By the time the size would be known, the compilation has already failed.
assert() seems to be handled differently, just trying again and eventually resulting in the node size to be positive, and thus a successful compilation.

My question now is: is this supposed to happen/expected or should Typst’s behavior not be influenced by a successful assert?

I assume there’s something wrong in how fletcher measures and then stores the result for later use, but I’m also pretty sure that this assert is not the ideal fix for that. I’d also be interested in uncovering the specific anti-pattern that is at fault here – but that’s probably my responsibility, further minimizing the example I’ve come up with here.

1 Like

This has come up before here: Execution error in context can have an effect on layout · Issue #5015 · typst/typst · GitHub

In general, Typst will catch any error in a show rule (or context, which is resolved by a show rule) and only print it if it remains by the last iteration. This frees the users from caring about things like query(heading).first() throwing in the first iteration (because nothing is known yet). But, on the downside, it can cause such strangeness. It’s a tradeoff.

1 Like

Thanks! One thing that is a bit different is that here, compilation fails with the assert not present. In other words, the intended behavior is to suppress compilation errors, but here one gets through.

I now minified it completely:

= foo <x>

#let body = link(label("x"), "foo")

// #context [
//   #let size = measure(body)
//   #assert(size.values().any(x => x > 0pt))

  #layout(_ => {
    let size = measure(body)
    assert(size.values().any(x => x > 0pt))
    body
  })
// ]

I was able to narrow it down to only happening inside layout, not with context or any of the deprecated functions (style, locate) which I assume just work like context under the hood now.

Is this a (separate) bug then, or is there some reason that makes it harder for errors in layout to get the suppression treatment that context does?

It’s not intentional that layout does not have the suppression treatment. Right now, the supression is not in context, but more generally in show rule application. And layout doesn’t go through that code path.

1 Like