Here's a weird corner of OpenType that I don't quite understand, and which cost me an hour and a lot of head scratching this morning.
Assume that sdb.yb is a mark, and that everything else is a base. Assume also that sdb.yb does
not have a mark attachment rule positioning it.
With the text "BEi17|sdb.yb|SINi1|BEm7|sdb.yb" and the following rules:
lookup FixYBPositions {
pos BEi17 <NULL> [sdb.yb] <330 0 0 0>;
pos BEm7 <NULL> [sdb.yb] <330 0 0 0>;
} FixYBPositions;
Both rules will fire, repositioning both sdb.yb glyphs. But add another rule:
lookup FixYBPositions {
pos BEi17 <NULL> [sdb.yb] <330 0 0 0>;
pos BEm7 <NULL> [sdb.yb] <330 0 0 0>;
pos SINi1 <NULL> [sdb.yb] <290 0 0 0>;
} FixYBPositions;
Now only the BEi17 rule will fire, repositioning the first sdb.yb, and the second glyph is not repositioned.
For more fun: Remove the classes and replace with bare "sdb.yb",
changing it from a format 2 pairpos subtable to a format 1 subtable, and both rules will fire. This happens with both Harfbuzz and CoreText.
I expect we're well into "undefined behaviour" territory here, but just in case anyone ever comes across something like this in the future, hopefully it will save you the same head-scratching I went through.
Comments
In RTL glyph runs, pairpos adjustments need to be implemented as corresponding negative dx and width adjustments on the right (first) glyph, otherise every other pair in the string gets skipped. Not sure what the syntax is in AFDKO code. This is what it looks like in VOLT:
What puzzled me is the <NULL> in the code, which appears to be an empty positioning record (unsupported by Adobe?). It is supported by Glyphs 3, when I tried out the code. At least it is accepted and has an effect, which may or may not be intentional. It adds extra class pairs to the lookup using class 0 for the second class. I am not trying to figure out what that means in the context of RTL shaping, but it should give confusing results.
Following is what the binary contains, with Latin based placeholder glyphs. The "a" in coverage is the implicit class 0 for the first of classDef. What is shifted with -1 should be all glyphs except the acute. These entries disappear when <Null> is removed from the code.
Now, makeotf does produce a null value record, but only for the format B positioning rule (pos a b <0 0 10 0> or pos [a] [b] <0 0 10 0>), which is equivalent to pos a <0 0 10 0> b <NULL> (though makeotf still won't accept the <NULL> here).
I have yet to investigate why makeotf produces those dummy value records, even if it normally skips zero adjustments as well. Typically in such cases, I would blame some layout engine for requiring it, despite the OpenType spec being clear that each value format for each subtable type can be null. But because makeotf does produce a null value record for the rules above, I now wonder if that’s just some makeotf legacy or even a bug. Note that the presence of those dummy value records seems to have no effect in Simon's case.
So those dummy zero-advance records are a means to let people decide to either include the second glyph while applying the next lookup (<NULL>, format zero) or skip it and jump straight to the next pair (<0 0 0 0>, dummy zero-advance). This is how it works in Glyphs 3 since, anyway.