Backend programming

It has to be said that translation is complex, but the backend is truly complicated – especially when it comes to purity. You should take a cup of tea before reading this section.

The backend is the process of positioning and drawing graphical objects. This includes determining line and page breaks.

The backend relies heavily on the links between grobs created during translation.

Grob interfaces

Interfaces group several grob types based on common characteristics. This is similar to how music types have music classes. All interfaces, with the grob types supporting them, are listed in the Internals Reference under Graphical Object Interfaces.

Recognition on interfaces should be preferred over recognition on name.

(grob::has-interface grob interface)

Check whether grob has interface (given as symbol).

Properties and callbacks

Understanding callbacks

Grobs are structured around properties. The primary functions for dealing with them are similar to those for Probs: ly:grob-property and ly:grob-set-property!. Grob types are listed in the Internals Reference with their associated properties and default values, under All layout objects.

However, grobs are not Probs. They are more feature-rich than those.

A grob property may be simply set to a sensible default. The width of a stem, for example, does usually not depend on the spacing on the page or other parameters.

By constrast, many properties are not set in advance but calculated in the backend. This holds for extents. Bar lines made with \bar ":|.|:" are substantially wider than regular bar lines. Thus, the X-extent of a BarLine grob does not have a bare number pair as a default.

There are numerous examples of properties that depend on other properties. LilyPond has no pre-conceived order for computing them – that would not be flexible enough. Instead, the approach taken is to enable any grob property to be computed on-the-fly at the moment it is read.

Any grob property may be set to callback: a function taking the grob as parameter and returning the value for the property.

Here is a simple callback example. It changes the color of a note head depending on the position of this note on the staff lines.

\layout {
  \context {
    \Voice
    \override NoteHead.color =
      #(lambda (grob)
         (case (ly:grob-property grob 'staff-position)
           ((-8) "red")
           ((-7) "blue")
           ((-6) "green")
           ((-5) "orange")
           ((-4) "purple")
           ((-3) "grey")
           (else "black")))
  }
}

\relative {
  \time 3/4
  a8 b cis b a4
  b fis' b,
  c8 d e d c4
  d2.
}

When the NoteHead object is finally printed, or in case another callback requests its color property with ly:grob-property, the callback the property was set to is executed.

All grob properties are cached. Once the callback has run, it is never run again. Instead, the property is considered to have been resolved to the return value of the callback.

The function ly:grob-property-data returns a property of the grob without processing callbacks. It may thus return the actual value, or a callback function.

Because of this flow, it is untypical to set grob properties directly with ly:grob-set-property!. Do this with care: it can introduce dependencies in the resolution order of callbacks. If callback A reads property B, and callback C sets property B, then if A comes before C, it is too early to see the new value of B, and the property A is cached, calculated from the wrong value of B. This is why writing callbacks for every property that should vary is preferable over using a single callback to set various properties.

Many callbacks are associated to interfaces. This is reflected in their name, for example: ly:side-position-interface::y-aligned-side. Callbacks specific to one grob type are rather prefixed with that type, e.g., ly:note-head::calc-stem-attachment.

Important properties

The stencil property holds the object’s inking – see Stencils for details.

The X-offset and Y-offset properties control the placement of an object relative to its parents.

X-extent and Y-extent represent the span of the object on each axis, relative to itself.

The style property is supported by many stencil callbacks. It is a symbol, controlling how a grob is displayed.

The grob-transformer

Using the grob-transformer function, a callback can be added “on top” of the preexisting value or callback of a property. Its signature is (grob-transformer property transformer), where property is the property to take the original value from, and transformer is a procedure taking two arguments, the grob and the original value.

sharpenBeams =
\override Beam.positions =
  #(grob-transformer 'positions
     (lambda (grob original)
       (let ((left-Y (car original))
             (right-Y (cdr original)))
         (cons
           (+ left-Y (* 2.0 (- left-Y right-Y)))
           (+ right-Y (* 2.0 (- right-Y left-Y)))))))

\relative {
  c'8[ d] e[ c] c[ g'] c'[ c,]
  \sharpenBeams
  c,8[ d] e[ c] c[ g'] c'[ c,]
}

Grob pointers

There is a second kind of properties for grobs, using the accessor ly:grob-object instead of ly:grob-property. These “object properties” (not to be confused with Guile’s own concept of “object properties”) serve to store all links between grobs. For example, NoteHead grobs have a stem internal property, which contains a Stem grob. Conversely, Stem grobs contain a note-heads array of NoteHead grobs belonging to them, and uses the grobs from this array in order to compute its length. These links are put in place by engravers, using ly:grob-set-object!.

As explained in Broken grobs, the line breaking process includes breaking numerous grobs into pieces for the individual systems. After this is done, the grobs from each system need to be made independent from the other systems. This is the reason for having a separate area of properties for storing grobs and grob arrays. The process of making the systems independent is called break substitution. Concretely, if you access an object’s staff-symbol after line breaking, you will get a staff symbol spanning just the system, whereas if you do it before line breaking, you get a long staff symbol spanning the whole piece, which is to be broken into pieces after line breaking.

In the Internals Reference, such “object properties” are in the Internal backend properties section. This does also contain properties of the regular kind, however.

(ly:grob-object grob property [default])
(ly:grob-set-object! grob property value)

Return or set the value of pointer named property (a symbol) in grob.

ly:grob-object falls back to default if the pointer does not exist, or the empty list if *default is not specified. The default parameter was added in version 2.23.4.

Grob pointers never have defaults. This is why, in the Internals manual, they can only appear in interfaces, not on a grob’s page.

Many of these object properties hold arrays of grobs. For technical reasons on the C++ side, these are not implemented with Guile arrays of grobs, but using a dedicated grob array type. Even though Scheme interfaces to some operations on grob arrays exist, first converting the array to a list and eventually converting back to an array is preferable in a majority of cases, to suit the nature of the Scheme language. (The difference between an array and a list in computer science can be read about in numerous web articles.)

(ly:grob-array-length grob-array)

Return the length of a grob array.

(ly:grob-array-ref grob-array index)

Retrieve the element located at index (starting from 0) in grob-array.

(ly:grob-array->list grob-array)

Turn grob-array into a Scheme list.

(ly:grob-list->grob-array lst)

Convert the Scheme list of grobs lst into a grob array. Available since version 2.23.4.

The causation chain

The cause property of a grob holds an event, or another grob, or the empty list, as set by the engraver with ly:engraver-make-grob.

(event-cause grob)

Find the cause of grob in stream events.

If the cause of grob is an event, it is returned. If the cause is a grob, its own cause is retrieved, or the cause of its cause, etc., until an event is found.

Grob suicide

As seen in Killing grobs, engravers occasionally need to remove an already created grob. This need can also arise in callbacks. Here are a few examples:

  • When the last note of a hairpin is on the beginning of a system, removing the part of the hairpin on that system (see also Dummy properties),

  • Removing system start delimiters when they are smaller than their collapse-height threshold,

  • In general, removing a grob if you detect an abnormal condition (after having issued a programming error) because you can’t do anything useful.

In all of these cases, you would use ly:grob-suicide! on the grob in some callback.

Grob flavors and line breaks

Items and spanners

Grobs come in several fashions. They are distinguished by the role they play in spacing, and how they are broken. In C++, this is reflected by two main subclasses of Grob.

Items are horizontally located on one column, like note heads, stems or bar lines. Some items are breakable, for example, clefs. When the system is broken on their column, they are duplicated.

Spanners extend horizontally between two locations in the score. They have no single parent on the X axis, but two bounds. They include beams, hairpins, slurs. When they are broken, they are split in several versions, one for every system where they have a part.

The item-interface and the spanner-interface characterize the type of a grob.

Paper columns

Columns form a subtype of items. They are abstract, invisible grobs. They group several objects on the same horizontal position. Columns are subdivided into musical and non-musical columns (PaperColumn and NonMusicalPaperColumn). The musical ones hold musical items, like note columns, dot columns, etc. Non-musical columns are used for non-musical items such as clefs and time signatures. Only non-musical columns can be broken.

Spanner bounds

The bounds of a spanner are two items.

(ly:spanner-bound spanner direction)
(ly:spanner-set-bound! spanner direction item)

Retrieve or set the bound of spanner in direction (LEFT or RIGHT).

Broken grobs

The functions dealing with broken pieces depend on the type of grob (item or spanner). A breakable item is broken into three pieces before line breaking. They are characterized by a break status direction, which is LEFT at end of line, CENTER in the middle of a line and RIGHT at the beginning of a line. One can imagine the original item on the center of the breakable column and two siblings, one at the left and one at the right, which replace the original at the end of a system and the beginning of the next system in case the column is the place of a line break. Spanners are broken into as many pieces as needed to have one piece per system crossed by the range of columns. Each piece has its bounds adjusted: a line break in the middle of a spanner causes the bounds of the two broken pieces to become the NonMusicalPaperColumn grob the break was made onto. Because there are many possible configurations, breaking spanners is only done after line breaking.

(ly:grob-original grob)

Return the unbroken original version of grob. For an item, this is the corresponding sibling whose break status direction is CENTER. For a spanner, it is the unbroken spanner common to all broken pieces.

(ly:item-break-dir item)

Return the break status direction of this item.

(ly:spanner-broken-into spanner)

Return the list of all children of this spanner, if it is broken. To get the siblings of a broken spanner, call this function on the original.

Note that for items, there is no direct way to get the siblings.

Spacing topics

Directions and axes

Some functions expect an axis. By convention, 0 denotes the X axis, and 1 the Y axis. For code understandability, the global constants X and Y are defined as 0 and 1, respectively.

Many properties can be up or down, left or right, sometimes center. Directions are to be understood as axis-agnostic directions. They have only three possible values: -1, 0, and 1.

  • -1: left, down, begin;

  • 0: center;

  • 1: right, up, end;

The type predicate ly:dir? checks that its argument is one of these three values.

The constants for directions are LEFT, RIGHT, DOWN, UP and CENTER.

Many properties that are primarily intended as directions are not restricted to these three values, but interpolate when they receive intermediary values. This is the case of self-alignment-X for example.

Intervals

An interval is any kind of numeric span. Grob extents, for example, are intervals. In Scheme, an interval is a pair of numbers.

The empty interval is defined as (+inf.0 . -inf.0), and available under the convenient name empty-interval. This is not equivalent to a single-point interval like (0 . 0)!

The following helpers analyze intervals:

(interval-start interval)
(interval-end interval)

Human-friendly aliases for car and cdr.

(interval-bound interval direction)

Either the left or right bound of interval, depending on direction.

(interval-empty? interval)

True if interval is empty. The empty interval and the interval (1 . -1) are both empty in the sense of this function.

(interval-length interval)

The length that this interval spans.

(interval-center interval)

The middle of this interval.

(interval-index interval position)

Interpolate between the left and right bound of interval, according to position: -1 for the left bound, 1 for the right bound, 0 for the center, and any number for intermediate values.

(interval-sane? interval)

Test if this interval contains no infinities or NaNs, and has bounds in the right order.

The following helpers construct new intervals:

(interval-scale interval factor)

Scale interval by factor on both sides.

(interval-widen interval amount)

This interval, with amount added as padding on every side.

(interval-union interval1 interval2)

Unite two intervals.

(interval-intersection interval1 interval2)

The part common to these intervals.

(reverse-interval interval)

Invert the bounds of interval.

(add-point interval point)

Enlarge interval as necessary on one side to include point.

(symmetric-interval x)

The interval from -x to x.

(ordered-cons a b)

An interval with bounds a and b. The smallest one is the left bound.

Coordinates, extents and reference points

The X-offset and Y-offset properties of any grob hold its two coordinates relative to its parent on each axis. X-extent and Y-extent are its extents relative to itself.

Because all coordinates are relative, the offset between any two grobs is not directly accessible through their basic coordinates. This is why functions exist to search common parents and compute coordinates relative to these parents.

(ly:grob-common-refpoint grob1 grob2 axis)

Search and return a common reference point for grob1 and grob2 on axis (empty list if not found).

A common reference point is a grob that is a direct or indirect parent of both grobs.

(ly:grob-relative-coordinate grob refpoint axis)
(ly:grob-extent grob refpoint axis)

Compute the coordinate or extent of grob along axis relative to refpoint, which must be a direct or indirect parent of grob along axis.

In general, care must be taken with coordinates on the Y axis. They are computed fairly lately, and retrieving them without caution can lead to cyclic dependencies. The delicate topic of Y positioning is tackled in the section about unpure-pure containers.

Conversely, things are simple on the X axis, where it is almost always safe to ask for the extent of an item, or the extent of a spanner after line breaking (but most spanners naturally can’t compute their X extent before line breaking).

Units

The two main parameters related to the font size are the staff space and the line thickness. Many distances are expressed in staff space so they can have unified numeric defaults for all staff sizes. All thicknesses are expressed as multiples of the line thickness. Both staff space and line thickness are properties of the StaffSymbol. The idea is that you can tweak them independently.

The staff symbol a grob resides on (when this makes sense) is found in its staff-symbol pointer.

(ly:staff-symbol-staff-space staff-symbol)
(ly:staff-symbol-line-thickness staff-symbol)

These give the units you should scale by when manipulating properties of a grob that are expressed in staff-space or in line-thickness. These functions access the staff symbol, and retrieve its staff-space or line-thickness property, then multiply by the value found in the output definition.

Drawing objects

Stencils are inkings of objects. Markups are pieces of code ready to be turned into stencils.

Stencils

The stencil of every object is determined by its stencil property. Internally, the representation of stencils is close to what is output as vector commands in PostScript or SVG.

There are two special stencils: empty-stencil and point-stencil. The former is, as its name suggests, completely empty. Its extents are equal to empty-interval. It does typically not trigger additional spacing. By contrast, point-stencil is a stencil with extents reduced to the single-point interval (0.0 . 0.0).

empty-stencil and point-stencil have different use cases. The empty stencil is the only way to suppress any space taken by a stencil in alignment. This is often desirable, but not always. For instance, ties on omitted NoteHead objects do not work (and lead to a crash, issue 6040).

{
  \omit NoteHead
  e'1~ e'1
}

Instead of #f (which \omit sets the stencil to under the hood, equivalently to empty-stencil), the head should have point-stencil in order to provide an attachment point for the tie.

{
  \override NoteHead.stencil = #point-stencil
  e'1~ e'1
}

There are many functions related to stencils, mostly for use in stencil callbacks. The C++-defined ones are documented in the Internals Reference under Scheme functions (all functions named ly:stencil-...). The most important ones follow.

(ly:stencil-add stencil1 stencil2 ...)

Combine any number of stencils.

(ly:stencil-translate-axis stencil amount axis)

A copy of stencil, translated by amount on axis.

(ly:stencil-extent stencil axis)

The dimensions of stencil on axis.

(ly:stencil-aligned-to stencil axis side)

Translate stencil on axis to align it on side using its own extents.

For example, when axis is X and side is LEFT, for a stencil having X extent '(-2 . 3), this yields a stencil having X extent (0 . 5).

side is not necessarily -1 or 1. Interpolation is performed, so 0 centers the stencil for example.

(ly:stencil-outline stencil outline)

Return stencil with dimensions and outline from outline.

The dimensions are used in many grob extents. The outline is used for skylines in the process of collision avoidance. The main vertical skylines can be viewed by switching an option:

#(ly:set-option 'debug-skylines)

{ c'^"Hi!" }

Since 2.23.6, you can also \override the show-horizontal-skylines and show-vertical-skylines of an object to visualize specific skylines 1.

{
  \override Score.PaperColumn.show-horizontal-skylines = ##t
  c'^"Hi"
}
(make-line-stencil width start-X start-Y end-X end-Y)

Draw a line between the points (start-X, start-Y) and (end-X, end-Y) with given width.

(make-filled-box-stencil X-extent Y-extent)

Make a filled box with the specified extents.

(make-circle-stencil radius thickness filled)

Make a circle with radius and thickness, optionally filled.

There are many more functions to draw various kinds of stencils. They are coded in the source file scm/stencil.scm.

Markups

Markups are commonly constructed by hand with the dedicated \markup syntax:

\markup \bold \center-column { Arranged by }

They may also be built in Scheme using the markup macro.

\markup #(markup #:bold #:center-column ("Arranged" "by"))

Markup commands are implemented as procedures. Every something command corresponds to make-something-markup. Thus, a third way to write the example above is

\markup #(make-bold-markup (make-center-column-markup '("Arranged" "by")))

The type predicate for markups is markup?. It also returns true on strings, which are one of the simplest forms markups can take.

The \stencil markup function makes a constant stencil markup.

\markup \stencil #(make-oval-stencil 4 3 0.1 #t)

The empty-markup always evaluates to the empty-stencil. There is no point-markup: use "" (empty string) or (make-null-markup).

A markup is not a stencil. It is the code for a stencil. The operation of turning the markup into stencil is called interpreting the markup. This requires two ingredients:

  • an output definition (\layout block),

  • properties.

The interpret-markup function takes layout, properties, and markup,

New markup commands may be defined with the define-markup-command macro. This is similar to defining music functions, except that the name of the function is included just before the arguments (no LilyPond assignment), and two extra parameters are passed, layout and props. Markup functions must return stencils.

#(define-markup-command (strong-emphasis layout props arg) (markup?)
   (interpret-markup layout props
     (make-bold-markup
       (make-italic-markup
         (make-underline-markup
           arg)))))

\markup \strong-emphasis "Very important!"

The #:properties keyword is a convenient way to retrieve properties from the props argument. A good example is found in the official documentation:

#(define-markup-command (double-box layout props text) (markup?)
  #:properties ((inter-box-padding 0.4)
                (box-padding 0.6))
  "Draw a double box around text."
  (interpret-markup layout props
    (markup #:override `(box-padding . ,inter-box-padding) #:box
            #:override `(box-padding . ,box-padding) #:box text)))

\markup \override #'(inter-box-padding . 0.2) \double-box "Erik Satie"

The grob-interpret-markup function takes a grob and a markup. It interprets the markup using layout and properties from the grob. This is where the notion of markups as code makes sense. The same markup, interpreted by two different grobs, can produce stencils varying in appearance. This is the reason why this works:

{
  \override TextScript.thickness = 10
  c'^\markup \draw-line #'(10 . 10)
}

This might seem surprising, since TextScript has no interface for the thickness property. Yet, its properties are used during markup interpretation, and the \draw-line markup command does read thickness.

Here is an example using grob-interpret-markup from a stencil callback to print a time signature with a slash:

#(define (slashed-time-signature-stencil grob)
   (let* ((fraction (ly:grob-property grob 'fraction))
          (num (number->string (car fraction)))
          (den (number->string (cdr fraction))))
     (ly:stencil-aligned-to
       (grob-interpret-markup grob
         (markup num "/" den))
       Y
       CENTER)))

\layout {
  \context {
    \Staff
    \override TimeSignature.stencil = #slashed-time-signature-stencil
  }
}

{ c'1 }

Interpreting a certain markup is frequent for stencil callbacks. The predefined function ly:text-interface::print interprets the markup in the text property of an object. Many grobs use it as stencil callback. It is a practical way to change the drawing of a grob to any markup.

{
  \tweak stencil #ly:text-interface::print
  \tweak text \markup \circle G
  \tweak thickness 2
  g'1
}

In the above example, note in particular how the tweak for thickness affects the note head, even though NoteHead objects are not supposed a priori to respond to the thickness property as per the Internals Reference.

Dummy properties

A few properties have no interesting value. They are only used to trigger a callback at a specific point of the positioning. They are only meaningfully set to a callback with side effects. These are:

  • before-line-breaking and after-line-breaking, computed at the moment their respective names suggest. A frequent callback for after-line-breaking is ly:spanner::kill-zero-spanned-time, which removes, for example, a hairpin, when it is a broken part that does not extend farther than one note.

    {
      \override Hairpin.to-barline = ##f
      c'1\< \break c'1\! \break
      \once \override Hairpin.after-line-breaking = #'()
      c'1\< \break c'1\!
    }
    
  • positioning-done is called to space certain objects. For instance, ly:stem::calc-positioning-done shifts certain note heads when a chord has seconds in order to avoid collisions. Every note head calls its stem’s positioning-done at some point. Since grob properties are cached, this ensures that the positioning is only done once.

  • springs-and-rods is triggered just after before-line-breaking. Its purpose is to add spacing springs (flexible distances) and rods (minimum distances) to columns, setting up the horizontal spacing problem.

Unpure-pure containers

XXX to be rewritten when I have done my wholesale restructuring of purity.

Please write me if you find errors in this section – I am not a specialist of this topic and it is tricky. This reflects my current understanding of pure/unpure.

The problem

Unpure-pure containers are meant to address a thorny issue. Observe this input, with kneed beams between staves:

<<
  \new Staff = "up" \relative c'' {
    a8 g f e d c \change Staff = "down" b a
    \change Staff = "up" c' b a g c c \change Staff = "down" c,, c
  }
  \new Staff = "down" {
    \clef bass
    s1
    s1
  }
>>

To determine how many systems can be placed on the page, the page breaker would like to know how tall the system is. This depends on whether the beams are kneed or not – if they are, more space should be allocated for the beam.

On the other hand, the page breaker may go for a tightly packed configuration, perhaps because a small number of systems was requested by the user through systems-per-page. In this case, beams should rather not be kneed, because kneed beams take up more space.

Similar trouble is run into with text scripts.

filler = \relative c' { c8 e g c g c g e }

\relative c' {
  \repeat unfold 4 \filler
  c4^\markup "Crescendo poco a poco" d e f
  \mark "Tempo primo"
  g^"Rallentando ma non troppo" f e d
  \repeat unfold 20 \filler
}

In this example, observe how the musical indications avoid each other on the second system, which is tall.

The page breaker could have chosen this configuration instead:

filler = \relative c' { c8 e g c g c g e }

\relative c' {
  \repeat unfold 4 \filler
  c4^\markup "Crescendo poco a poco" d e f
  \break
  \mark "Tempo primo"
  g^"Rallentando ma non troppo" f e d
  \repeat unfold 20 \filler
}

Because the break separates the two measures with colliding text scripts, the second system is now smaller, while the first system got slightly taller. This may in turn influence page breaking decisions: a page break could be inserted between the first and the second system (assuming more music before) to space pages more evenly.

Both of these examples show how a cyclic dependency can occur between calculating the spacing inside a system and choosing breaks.

Page and line breaking rely on scoring different configurations, attributing them penalties on criteria such as cramped spacing. It is infeasible, however, to score all possible configurations – that would make the compilation gigantically slower. Other classic constraint optimization techniques are not workable, either. There simply are too many parameters to take into account.

This is why LilyPond has heuristic algorithms instead. The goal of pure properties is to provide estimates of certain spacing parameters before line breaking in order to help the breaking algorithms make informed decisions. Afterwards, when one configuration is finally settled on, the properties are computed for real.

The art of writing pure callbacks is to make the best estimates possible while ensuring no cyclic dependencies, including corner cases. This is very difficult.

It has to be noted that purity is concerned with all Y axis positioning. There is no concept of pure X-offset or X-extent because, by design, positioning constraints on the X axis are determined in advance (springs and rods). The operation of page breakers is to try a spacing that seems sensible on the X axis and see how it looks like on the Y axis using pure properties.

Writing an unpure-pure container

To begin with, an unpure-pure container is only needed if the callback needs different code paths for pure and unpure positioning. For instance, many stencil callbacks do not depend on line breaking, such as the one for NoteHead. Consequently, there is no unpure-pure container involved even though the callback is triggered before line breaking. When this differentiation is required, the callback is replaced with an unpure-pure container containing two callbacks.

(ly:make-unpure-pure-container unpure-function pure-function)

Construct an unpure-pure container.

unpure-function is a regular callback.

pure-function takes three arguments: the grob, a start column, and an end column. It is meant to estimate the property for a potential broken part of the grob between start and end. For items, these parameters are mandatory, but they are not meaningful: an item should only check that it is between these columns. A spanner should try to make use of the start and end parameters for better estimates.

In a more advanced form, unpure-function can take n + 1 arguments, the grob and other arguments. In this case, pure-function takes n + 3 arguments, the grob, start and end column, and the other arguments. The property can then be read using ly:unpure-call or ly:pure-call, passing these arguments. They may be used for estimates as well.

(ly:unpure-pure-container-unpure-part container)
(ly:unpure-pure-container-pure-part container)

Extract either part of container.

(ly:unpure-call data grob . rest)
(ly:pure-call data grob start end . rest)

Call a procedure with arguments. If the data is an unpure-pure container, extract its unpure or pure part to get the procedure. If data is already a procedure, use it directly.

The purity contract

Pure functions must not have side-effects. They must avoid calls to the following functions or their C++ equivalents:

  • ly:grob-set-property!,

  • ly:grob-set-object!,

  • ly:grob-suicide!.

This means that they should not only avoid using these functions, but also calling other functions that use them.

TODOs

  • Totally unclear in what situation the extra arguments to pure and unpure callbacks may be used.

    It is not obvious that they are actually used somewhere, either.

  • What the hell happens for Y-extents and skylines when no callback is given?

  • What is the point of the purity contract? Caching?

  • Why should pure callbacks for items return 0 when the item does not fall in the start-end range if pure properties for items are cached?

  • Investigate caching.

  • For pure-by-design functions like NoteHead.stencil, extra-spacing-height, X-extent et al, is there a purity contract?

  • Find meaningful example of pure functions written in Scheme.

Further reading on purity


The given example actually requires 2.23.8 because it uses \override. Previously, you needed \overrideProperty for an override to a paper column to have immediate effect.