Han-Wen Nienhuys <[email protected]> writes: > On Fri, Jun 5, 2026 at 12:42 AM David Kastrup <[email protected]> wrote: > >> >> "Pure" calls in the LilyPond backend produce grob properties dependent >> on line break decisions. Consequently the calls get passed "start" and >> "end" arguments denoting the current line's starting and ending musical >> columns (numbered sequentially from the start of the score). >> >> To make the calls more compatible, in a first step the arguments >> > > > What is the underlying problem you want to solve?
The problem that it is hard to confidently program callbacks that properly heed dependencies on line breaks because the programmer needs to know exactly which of other callbacks they may be using may depend on this data. That leads to an overabundance of "pure" functions requiring unnecessary reevaluations. > AFAIK, dynamic scoping has widely been considered a mistake, because > it breaks encapsulation boundaries, making it harder to reason about > programs. Why would we want to introduce that? Because it shifts the task of arguing about dependencies from having to be predetermined by the programmer at coding time to dynamically being determined by the typesetting engine at runtime. The line break algorithm actually is a "dynamic programming" optimisation and evaluating the dependencies statically means that the costs of caching are based on worst case exceptions that may not actually be possible in the valid combinations of conditions (or occur just with combinations that are pruned early because of not being able to improve on an already good score). "start" and "end" are passed through a whole lot here and may often end up not even being used, meaning that we keep reevaluating callbacks with different settings of "start" and "end" when a previous call already established that the callback is not even looking at them and the previous cached value would be fine to use. Static reasoning about a program's behavior is nice, but we are talking about that reasoning in the context of line breaking, a dynamic programming problem that is only manageable by pruning the combinatorial tree it is exploring. Evaluating the dependencies dynamically will avoid reevaluation of large parts of the trees. At the same time, it relieves the programmer from doing the dependency analysis on line breaks for callbacks they may not even have written themselves. Passing an opaque "line break control" around as an extra parameter that can be queried explicitly for "start" and "end" and will record such queries in the appropriate grob caching structures is certainly feasible but would constitute a more incompatible change in programming interface. With regard to the programming interface, we have grob properties that are static, plain callbacks, and unpure-pure-containers. When you don't know what kind a property you are querying might be, you always need to "assume the worst". An example of such "assuming the worst" is the generic function "grob-transformer" which has to end up being an unpure-pure container "just in case". Handling this kind of "just-in-case" complexity for callbacks out of the user's control is not helpful. At the access level, ly:pure-call and ly:unpure-call mitigate some of the complexity of having to figure out the difference between accessing static properties, callbacks, and unpure-pure-containers. But they are still two different calls for the different invocations of an unpure-pure container, and the resulting complexity of the derived accessor is still statically that of an unpure-pure-container that will get reevaluated for each start/end combination even if it turns out that it doesn't even access them. Static reasoning about functions may be fine, but what happens to the accessors of a property if the user replaces a plain callback with an unpure-pure container for achieving certain behavior? The static reasoning will no longer be valid and lead to wrong behavior. Passing a "break control structure" through explicitly would be feasible, but this would need to be done also for straight callbacks in order to get to a sort-of unified interface, meaning more of a disruption to existing user-provided code. The effect for automated reasoning would be similar. I am not beholden to a particular implementation. It may even be feasible to use the dynamic variable scheme as a compatibility fallback and not provide "magic" accessors like (*start*) and (*end*) but only revert to dynamic variable access for the break-control block when a callback needing it is reached via a (user-controlled) path where it got lost. But the more complexity in particular in dynamic context is managed by LilyPond and the less remains for the application-level programmer, the more likely are we to see nice and reliably working extensions or amendments in the typesetting stage. I want to get closer to the stage where what is _conceptually_ simple actually does not trip the programmer up completely in execution because he needs to be in control of everything happening behind the curtain. -- David Kastrup
