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

Reply via email to