If you look closely, you’ll see that Typst is actually warning you about a problem with your document:
Layout did not converge within 5 attempts
This problem is hard to debug in general, but here the source of the problem is fairly easy to recognize and fix: your state updates are structured like
context {
x = foo.get()
..
foo.update(x)
}
instead of the proper form:
foo.update(x => {
..
x
})
This uses the form of update()
where you give it a function that computes the new value from the old one.
The docs have the following example that matches your use case:
#let compute(expr) = [
#s.update(x =>
eval(expr.replace("x", str(x)))
)
New value is #context s.get().
]
here there is first an update
that uses a function, followed by a context get()
that displays the new value.
Applied to your use case, the code would look like this:
#let change_state(ref, num) = {
state("test_state").update(ts => {
if ts.at(ref).at(2) != "CHANGED" {
ts.at(ref).at(num) = "CHANGED"
}
ts
})
context [#state("test_state").get()]
}
Why is the form you chose not working? Each time you get()
, Typst has to access the state value, which depends on the current context
. If you then use that value to update()
, you’re actually invalidating get()
calls that come later in the document. This leads to a cascade:
- Non-contextual parts of your document are evaluated: the state still has its initial values since all updates are contextual.
- All
get()
s in your document run and fetch the state value. Since theupdate()
s are not applied yet allget()
calls see the initial value. - All
update()
s are inserted into the document, setting a value that is based on theget()
result (the initial value). - Typst sees that all
get()
s but the first used the wrong value (there’s a new update there) so the second and all followingcontext
blocks are reevaluated, using the value after the first update - Typst sees that all
get()
s but the first and second used the wrong value so the third and all followingcontext
blocks are reevaluated, using the value after the second update. - …
- After five updates, Typst gives up: the document did not “converge” in few enough attempts. The result of the
final()
call that you see is only based on the updates that were embedded into the document before then.
update(x => ..)
circumvents the cascade and makes update values available immediately, which is why it makes the document converge!