Repository: googlefonts/ufo2ft
Pull request #666 (2022-10-11 09:43:53)

Revert "Split kerning by script, not by direction (#636)"
This reverts commit bdac61e1e0c347a64ad2cee7d5de725ae9817c00.

We need to test it more thoroughly first.

anthrotype: for reference, linking below the related issues
https://github.com/googlefonts/ufo2ft/issues/642
https://github.com/googlefonts/ufo2ft/issues/658

Patch:
diff --git a/Lib/ufo2ft/constants.py b/Lib/ufo2ft/constants.py
index 0c8327f..de033e5 100644
--- a/Lib/ufo2ft/constants.py
+++ b/Lib/ufo2ft/constants.py
@@ -38,8 +38,6 @@ OPENTYPE_META_KEY = "public.openTypeMeta"
 
 UNICODE_VARIATION_SEQUENCES_KEY = "public.unicodeVariationSequences"
 
-COMMON_SCRIPT = "Zyyy"
-
 INDIC_SCRIPTS = [
     "Beng",  # Bengali
     "Cham",  # Cham
diff --git a/Lib/ufo2ft/featureWriters/kernFeatureWriter.py b/Lib/ufo2ft/featureWriters/kernFeatureWriter.py
index d0b1a6f..a071eff 100644
--- a/Lib/ufo2ft/featureWriters/kernFeatureWriter.py
+++ b/Lib/ufo2ft/featureWriters/kernFeatureWriter.py
@@ -1,25 +1,10 @@
-from __future__ import annotations
-
-import itertools
-import logging
 from types import SimpleNamespace
-from typing import TYPE_CHECKING
 
 from fontTools import unicodedata
-from fontTools.feaLib import ast
-
-from ufo2ft.constants import COMMON_SCRIPT, INDIC_SCRIPTS, USE_SCRIPTS
-from ufo2ft.featureWriters import BaseFeatureWriter
-from ufo2ft.featureWriters.ast import (
-    addLookupReferences,
-    getScriptLanguageSystems,
-    makeGlyphClassDefinitions,
-    makeLookupFlag,
-)
-from ufo2ft.util import DFLT_SCRIPTS, classifyGlyphs, quantize, unicodeScriptDirection
 
-if TYPE_CHECKING:
-    from typing import Iterator, Literal
+from ufo2ft.constants import INDIC_SCRIPTS, USE_SCRIPTS
+from ufo2ft.featureWriters import BaseFeatureWriter, ast
+from ufo2ft.util import classifyGlyphs, quantize, unicodeScriptDirection
 
 SIDE1_PREFIX = "public.kern1."
 SIDE2_PREFIX = "public.kern2."
@@ -31,13 +16,13 @@ SIDE2_PREFIX = "public.kern2."
 #   src/hb-ot-shape-complex-khmer.cc
 # We derived the list of scripts associated to each dist-enabled shaper from
 # `hb_ot_shape_complex_categorize` in src/hb-ot-shape-complex-private.hh
-DIST_ENABLED_SCRIPTS = set(INDIC_SCRIPTS) | {"Khmr", "Mymr"} | set(USE_SCRIPTS)
+DIST_ENABLED_SCRIPTS = set(INDIC_SCRIPTS) | set(["Khmr", "Mymr"]) | set(USE_SCRIPTS)
 
 RTL_BIDI_TYPES = {"R", "AL"}
 LTR_BIDI_TYPES = {"L", "AN", "EN"}
 
 
-def unicodeBidiType(uv: int) -> Literal["R"] | Literal["L"] | None:
+def unicodeBidiType(uv):
     """Return "R" for characters with RTL direction, or "L" for LTR (whether
     'strong' or 'weak'), or None for neutral direction.
     """
@@ -47,188 +32,62 @@ def unicodeBidiType(uv: int) -> Literal["R"] | Literal["L"] | None:
         return "R"
     elif bidiType in LTR_BIDI_TYPES:
         return "L"
-    return None
+    else:
+        return None
 
 
 class KerningPair:
 
-    __slots__ = ("side1", "side2", "value", "scripts", "directions", "bidiTypes")
+    __slots__ = ("side1", "side2", "value", "directions", "bidiTypes")
 
-    def __init__(
-        self,
-        side1: str | ast.GlyphClassDefinition | list[str] | set[str],
-        side2: str | ast.GlyphClassDefinition | list[str] | set[str],
-        value: float,
-        scripts: set[str] | None = None,
-        directions: set[str] | None = None,
-        bidiTypes: set[str] | None = None,
-    ):
-        self.side1: ast.GlyphName | ast.GlyphClassName | ast.GlyphClass
+    def __init__(self, side1, side2, value, directions=None, bidiTypes=None):
         if isinstance(side1, str):
             self.side1 = ast.GlyphName(side1)
         elif isinstance(side1, ast.GlyphClassDefinition):
             self.side1 = ast.GlyphClassName(side1)
-        elif isinstance(side1, (list, set)):
-            if len(side1) == 1:
-                self.side1 = ast.GlyphName(list(side1)[0])
-            else:
-                self.side1 = ast.GlyphClass([ast.GlyphName(g) for g in sorted(side1)])
         else:
             raise AssertionError(side1)
 
-        self.side2: ast.GlyphName | ast.GlyphClassName | ast.GlyphClass
         if isinstance(side2, str):
             self.side2 = ast.GlyphName(side2)
         elif isinstance(side2, ast.GlyphClassDefinition):
             self.side2 = ast.GlyphClassName(side2)
-        elif isinstance(side2, (list, set)):
-            if len(side2) == 1:
-                self.side2 = ast.GlyphName(list(side2)[0])
-            else:
-                self.side2 = ast.GlyphClass([ast.GlyphName(g) for g in sorted(side2)])
         else:
             raise AssertionError(side2)
 
-        self.value: float = value
-        self.scripts: set[str] = scripts or set()
-        self.directions: set[str] = directions or set()
-        self.bidiTypes: set[str] = bidiTypes or set()
-
-    # pyright: basic
-    def partitionByScript(
-        self, glyphScripts: dict[str, set[str]]
-    ) -> Iterator[tuple[str, KerningPair]]:
-        """Split a potentially mixed-script pair into pairs that make sense based
-        on the dominant script, and yield each combination with its dominant script."""
-
-        # First, partition the pair by their assigned scripts
-        allFirstScripts: dict[tuple[str, ...], list[str]] = {}
-        allSecondScripts: dict[tuple[str, ...], list[str]] = {}
-        for glyph in self.firstGlyphs:
-            if glyph not in glyphScripts:
-                glyphScripts[glyph] = {COMMON_SCRIPT}
-            allFirstScripts.setdefault(tuple(glyphScripts[glyph]), []).append(glyph)
-        for glyph in self.secondGlyphs:
-            if glyph not in glyphScripts:
-                glyphScripts[glyph] = {COMMON_SCRIPT}
-            allSecondScripts.setdefault(tuple(glyphScripts[glyph]), []).append(glyph)
-
-        # Super common case: both sides are of the same, one script. Nothing to do, emit
-        # self as is.
-        if (
-            len(allFirstScripts.keys()) == 1
-            and allFirstScripts.keys() == allSecondScripts.keys()
-        ):
-            for script in list(allFirstScripts.keys())[0]:
-                yield script, self
-            return
-
-        # Now let's go through the script combinations
-        for firstScripts, secondScripts in itertools.product(
-            allFirstScripts.keys(), allSecondScripts.keys()
-        ):
-            localPair = KerningPair(
-                sorted(allFirstScripts[firstScripts]),
-                sorted(allSecondScripts[secondScripts]),
-                self.value,
-                scripts=self.scripts,
-                directions=self.directions,
-                bidiTypes=self.bidiTypes,
-            )
-            # Handle very obvious common cases: one script, same on both sides
-            if (
-                len(firstScripts) == 1
-                and len(secondScripts) == 1
-                and firstScripts == secondScripts
-            ):
-                localPair.scripts = {firstScripts[0]}
-                yield firstScripts[0], localPair
-            # First is single script, second is common
-            elif len(firstScripts) == 1 and set(secondScripts).issubset(DFLT_SCRIPTS):
-                localPair.scripts = {firstScripts[0]}
-                yield firstScripts[0], localPair
-            # First is common, second is single script
-            elif set(firstScripts).issubset(DFLT_SCRIPTS) and len(secondScripts) == 1:
-                localPair.scripts = {secondScripts[0]}
-                yield secondScripts[0], localPair
-            # One script and it's different on both sides and it's not common
-            elif len(firstScripts) == 1 and len(secondScripts) == 1:
-                logger = ".".join([self.__class__.__module__, self.__class__.__name__])
-                logging.getLogger(logger).info(
-                    "Mixed script kerning pair %s ignored" % localPair
-                )
-                pass
-            else:
-                # At this point, we have a pair which has different sets of
-                # scripts on each side, and we have to find commonalities.
-                # For example, the pair
-                #   [A A-cy] {Latn, Cyrl}  --  [T Te-cy Tau] {Latn, Cyrl, Grek}
-                # must be split into
-                #   A -- T
-                #   A-cy -- Te-cy
-                # and the Tau ignored.
-                commonScripts = set(firstScripts) & set(secondScripts)
-                commonFirstGlyphs = set()
-                commonSecondGlyphs = set()
-                for scripts, glyphs in allFirstScripts.items():
-                    if commonScripts.issubset(set(scripts)):
-                        commonFirstGlyphs |= set(glyphs)
-                for scripts, glyphs in allSecondScripts.items():
-                    if commonScripts.issubset(set(scripts)):
-                        commonSecondGlyphs |= set(glyphs)
-                for common in commonScripts:
-                    localPair = KerningPair(
-                        commonFirstGlyphs,
-                        commonSecondGlyphs,
-                        self.value,
-                        directions=self.directions,
-                        bidiTypes=self.bidiTypes,
-                        scripts={common},
-                    )
-                    yield common, localPair
+        self.value = value
+        self.directions = directions or set()
+        self.bidiTypes = bidiTypes or set()
 
     @property
-    def firstIsClass(self) -> bool:
-        return isinstance(self.side1, (ast.GlyphClassName, ast.GlyphClass))
+    def firstIsClass(self):
+        return isinstance(self.side1, ast.GlyphClassName)
 
     @property
-    def secondIsClass(self) -> bool:
-        return isinstance(self.side2, (ast.GlyphClassName, ast.GlyphClass))
+    def secondIsClass(self):
+        return isinstance(self.side2, ast.GlyphClassName)
 
     @property
-    def firstGlyphs(self) -> set[str]:
+    def glyphs(self):
         if self.firstIsClass:
-            if isinstance(self.side1, ast.GlyphClassName):
-                classDef1 = self.side1.glyphclass
-            else:
-                classDef1 = self.side1
-            return {g.asFea() for g in classDef1.glyphSet()}
+            classDef1 = self.side1.glyphclass
+            glyphs1 = {g.asFea() for g in classDef1.glyphSet()}
         else:
-            return {self.side1.asFea()}
-
-    @property
-    def secondGlyphs(self) -> set[str]:
+            glyphs1 = {self.side1.asFea()}
         if self.secondIsClass:
-            if isinstance(self.side2, ast.GlyphClassName):
-                classDef2 = self.side2.glyphclass
-            else:
-                classDef2 = self.side2
-            return {g.asFea() for g in classDef2.glyphSet()}
+            classDef2 = self.side2.glyphclass
+            glyphs2 = {g.asFea() for g in classDef2.glyphSet()}
         else:
-            return {self.side2.asFea()}
+            glyphs2 = {self.side2.asFea()}
+        return glyphs1 | glyphs2
 
-    @property
-    def glyphs(self) -> set[str]:
-        return self.firstGlyphs | self.secondGlyphs
-
-    def __repr__(self) -> str:
-        return "<{} {} {} {}{}{}{}>".format(
+    def __repr__(self):
+        return "<{} {} {} {}{}{}>".format(
             self.__class__.__name__,
             self.side1,
             self.side2,
             self.value,
             " %r" % self.directions if self.directions else "",
-            " %r" % self.scripts if self.scripts else "",
             " %r" % self.bidiTypes if self.bidiTypes else "",
         )
 
@@ -255,9 +114,9 @@ class KernFeatureWriter(BaseFeatureWriter):
         ctx.gdefClasses = self.getGDEFGlyphClasses()
         ctx.kerning = self.getKerningData(font, feaFile, self.getOrderedGlyphSet())
 
-        feaScripts = getScriptLanguageSystems(feaFile)
+        feaScripts = ast.getScriptLanguageSystems(feaFile)
         ctx.scriptGroups = self._groupScriptsByTagAndDirection(feaScripts)
-        ctx.knownScripts = feaScripts.keys()
+
         return ctx
 
     def shouldContinue(self):
@@ -297,7 +156,7 @@ class KernFeatureWriter(BaseFeatureWriter):
 
         lookupGroups = []
         for _, lookupGroup in sorted(lookups.items()):
-            lookupGroups.extend(lookupGroup.values())
+            lookupGroups.extend(lookupGroup)
 
         self._insert(
             feaFile=feaFile,
@@ -339,10 +198,10 @@ class KernFeatureWriter(BaseFeatureWriter):
     @classmethod
     def getKerningClasses(cls, font, feaFile=None, glyphSet=None):
         side1Groups, side2Groups = cls.getKerningGroups(font, glyphSet)
-        side1Classes = makeGlyphClassDefinitions(
+        side1Classes = ast.makeGlyphClassDefinitions(
             side1Groups, feaFile, stripPrefix="public."
         )
-        side2Classes = makeGlyphClassDefinitions(
+        side2Classes = ast.makeGlyphClassDefinitions(
             side2Groups, feaFile, stripPrefix="public."
         )
         return side1Classes, side2Classes
@@ -431,59 +290,99 @@ class KernFeatureWriter(BaseFeatureWriter):
             enumerated=enumerated,
         )
 
-    def _makeKerningLookup(self, name, ignoreMarks=True):
-        lookup = ast.LookupBlock(name)
-        if ignoreMarks and self.options.ignoreMarks:
-            lookup.statements.append(makeLookupFlag("IgnoreMarks"))
-        return lookup
-
-    def _addPairToLookup(self, lookup, pair, rtl=False):
-        lookup.statements.append(
-            self._makePairPosRule(pair, rtl=rtl, quantization=self.options.quantization)
-        )
+    def _makeKerningLookup(
+        self, name, pairs, exclude=None, rtl=False, ignoreMarks=True
+    ):
+        assert pairs
+        rules = []
+        for pair in pairs:
+            if exclude is not None and exclude(pair):
+                self.log.debug("pair excluded from '%s' lookup: %r", name, pair)
+                continue
+            rules.append(
+                self._makePairPosRule(
+                    pair, rtl=rtl, quantization=self.options.quantization
+                )
+            )
 
-    def knownScriptsPerCodepoint(self, uv):
-        if not self.context.knownScripts:
-            # If there are no languagesystems, consider everything common;
-            # it'll all end in DFLT/dflt anyway
-            return COMMON_SCRIPT
-        return [
-            x
-            for x in unicodedata.script_extension(chr(uv))
-            if x in self.context.knownScripts or x in DFLT_SCRIPTS
-        ]
+        if rules:
+            lookup = ast.LookupBlock(name)
+            if ignoreMarks and self.options.ignoreMarks:
+                lookup.statements.append(ast.makeLookupFlag("IgnoreMarks"))
+            lookup.statements.extend(rules)
+            return lookup
 
     def _makeKerningLookups(self):
+        cmap = self.makeUnicodeToGlyphNameMapping()
+        if any(unicodeScriptDirection(uv) == "RTL" for uv in cmap):
+            # If there are any characters from globally RTL scripts in the
+            # cmap, we compile a temporary GSUB table to resolve substitutions
+            # and group glyphs by script horizontal direction and bidirectional
+            # type. We then mark each kerning pair with these properties when
+            # any of the glyphs involved in a pair intersects these groups.
+            gsub = self.compileGSUB()
+            dirGlyphs = classifyGlyphs(unicodeScriptDirection, cmap, gsub)
+            directions = self._intersectPairs("directions", dirGlyphs)
+            shouldSplit = "RTL" in directions
+            if shouldSplit:
+                bidiGlyphs = classifyGlyphs(unicodeBidiType, cmap, gsub)
+                self._intersectPairs("bidiTypes", bidiGlyphs)
+        else:
+            shouldSplit = False
+
         marks = self.context.gdefClasses.mark
         lookups = {}
-        cmap = self.makeUnicodeToGlyphNameMapping()
-        gsub = self.compileGSUB()
-        dirGlyphs = classifyGlyphs(unicodeScriptDirection, cmap, gsub)
-        self._intersectPairs("directions", dirGlyphs)
-
-        scriptGlyphs = classifyGlyphs(self.knownScriptsPerCodepoint, cmap, gsub)
-        self._intersectPairs("scripts", scriptGlyphs)
-        bidiGlyphs = classifyGlyphs(unicodeBidiType, cmap, gsub)
-        self._intersectPairs("bidiTypes", bidiGlyphs)
-        pairs = self.context.kerning.pairs
-
-        glyphScripts = {}
-        for script, glyphs in scriptGlyphs.items():
-            for g in glyphs:
-                glyphScripts.setdefault(g, set()).add(script)
-
-        if self.options.ignoreMarks:
-            basePairs, markPairs = self._splitBaseAndMarkPairs(
-                self.context.kerning.pairs, marks
-            )
-            if basePairs:
-                self._makeSplitScriptKernLookups(lookups, basePairs, glyphScripts)
-            if markPairs:
-                self._makeSplitScriptKernLookups(
-                    lookups, markPairs, glyphScripts, ignoreMarks=False, suffix="_marks"
-                )
+        if shouldSplit:
+            # make one DFLT lookup with script-agnostic characters, and two
+            # LTR/RTL lookups excluding pairs from the opposite group.
+            # We drop kerning pairs with ambiguous direction: i.e. those containing
+            # glyphs from scripts with different overall horizontal direction, or
+            # glyphs with incompatible bidirectional type (e.g. arabic letters vs
+            # arabic numerals).
+            pairs = []
+            for pair in self.context.kerning.pairs:
+                if ("RTL" in pair.directions and "LTR" in pair.directions) or (
+                    "R" in pair.bidiTypes and "L" in pair.bidiTypes
+                ):
+                    self.log.warning(
+                        "skipped kern pair with ambiguous direction: %r", pair
+                    )
+                    continue
+                pairs.append(pair)
+            if not pairs:
+                return lookups
+
+            if self.options.ignoreMarks:
+                # If there are pairs with a mix of mark/base then the IgnoreMarks
+                # flag is unnecessary and should not be set
+                basePairs, markPairs = self._splitBaseAndMarkPairs(pairs, marks)
+                if basePairs:
+                    self._makeSplitDirectionKernLookups(lookups, basePairs)
+                if markPairs:
+                    self._makeSplitDirectionKernLookups(
+                        lookups, markPairs, ignoreMarks=False, suffix="_marks"
+                    )
+            else:
+                self._makeSplitDirectionKernLookups(lookups, pairs)
         else:
-            self._makeSplitScriptKernLookups(lookups, pairs, glyphScripts)
+            # only make a single (implicitly LTR) lookup including all base/base pairs
+            # and a single lookup including all base/mark pairs (if any)
+            pairs = self.context.kerning.pairs
+            if self.options.ignoreMarks:
+                basePairs, markPairs = self._splitBaseAndMarkPairs(pairs, marks)
+                lookups["LTR"] = []
+                if basePairs:
+                    lookups["LTR"].append(
+                        self._makeKerningLookup("kern_ltr", basePairs)
+                    )
+                if markPairs:
+                    lookups["LTR"].append(
+                        self._makeKerningLookup(
+                            "kern_ltr_marks", markPairs, ignoreMarks=False
+                        )
+                    )
+            else:
+                lookups["LTR"] = [self._makeKerningLookup("kern_ltr", pairs)]
         return lookups
 
     def _splitBaseAndMarkPairs(self, pairs, marks):
@@ -498,68 +397,111 @@ class KernFeatureWriter(BaseFeatureWriter):
             basePairs[:] = pairs
         return basePairs, markPairs
 
-    def _makeSplitScriptKernLookups(
-        self, lookups, pairs, glyphScripts, ignoreMarks=True, suffix=""
+    def _makeSplitDirectionKernLookups(
+        self, lookups, pairs, ignoreMarks=True, suffix=""
     ):
-        for pair in pairs:
-            for script, splitpair in pair.partitionByScript(glyphScripts):
-                key = "kern_" + script + suffix
-                script_lookups = lookups.setdefault(script, {})
-                lookup = script_lookups.get(key)
-                if not lookup:
-                    lookup = self._makeKerningLookup(
-                        key.replace(COMMON_SCRIPT, "Common"),  # For neatness
-                        ignoreMarks=ignoreMarks,
-                    )
-                    script_lookups[key] = lookup
-                self._addPairToLookup(lookup, splitpair, rtl="RTL" in pair.directions)
+        dfltKern = self._makeKerningLookup(
+            "kern_dflt" + suffix,
+            pairs,
+            exclude=(lambda pair: {"LTR", "RTL"}.intersection(pair.directions)),
+            rtl=False,
+            ignoreMarks=ignoreMarks,
+        )
+        if dfltKern:
+            lookups.setdefault("DFLT", []).append(dfltKern)
+
+        ltrKern = self._makeKerningLookup(
+            "kern_ltr" + suffix,
+            pairs,
+            exclude=(lambda pair: not pair.directions or "RTL" in pair.directions),
+            rtl=False,
+            ignoreMarks=ignoreMarks,
+        )
+        if ltrKern:
+            lookups.setdefault("LTR", []).append(ltrKern)
+
+        rtlKern = self._makeKerningLookup(
+            "kern_rtl" + suffix,
+            pairs,
+            exclude=(lambda pair: not pair.directions or "LTR" in pair.directions),
+            rtl=True,
+            ignoreMarks=ignoreMarks,
+        )
+        if rtlKern:
+            lookups.setdefault("RTL", []).append(rtlKern)
 
     def _makeFeatureBlocks(self, lookups):
         features = {}
         if "kern" in self.context.todo:
             kern = ast.FeatureBlock("kern")
-            self._registerLookups(kern, lookups)
+            self._registerKernLookups(kern, lookups)
             if kern.statements:
                 features["kern"] = kern
         if "dist" in self.context.todo:
             dist = ast.FeatureBlock("dist")
-            self._registerLookups(dist, lookups)
+            self._registerDistLookups(dist, lookups)
             if dist.statements:
                 features["dist"] = dist
         return features
 
-    def _registerLookups(self, feature, lookups):
-        scriptGroups = self.context.scriptGroups
-        scripts = scriptGroups.get(feature.name, {})
+    def _registerKernLookups(self, feature, lookups):
+        if "DFLT" in lookups:
+            ast.addLookupReferences(feature, lookups["DFLT"])
 
-        # Ensure we have kerning for pure common script runs (e.g. ">1")
-        if feature.name == "kern" and COMMON_SCRIPT in lookups:
-            addLookupReferences(
-                feature, lookups[COMMON_SCRIPT].values(), "DFLT", ["dflt"]
-            )
-        if not scripts:
-            return
-
-        # Collapse scripts
-        scripts = scripts.get("LTR", []) + scripts.get("RTL", [])
-        otscript2uniscript = {}
-        for uniscript in lookups.keys():
-            for ot2script in unicodedata.ot_tags_from_script(uniscript):
-                otscript2uniscript[ot2script] = uniscript
-
-        for script, langs in sorted(scripts):
-            if script not in otscript2uniscript:
-                continue
-            uniscript = otscript2uniscript[script]
-            lookups_for_this_script = []
-            if uniscript not in lookups:
-                continue
-            if feature.statements:
-                feature.statements.append(ast.Comment(""))
-            # We have something for this script. First add the default
-            # lookups, then the script-specific ones
-            for dflt_script in DFLT_SCRIPTS:
-                if dflt_script in lookups:
-                    lookups_for_this_script.extend(lookups[dflt_script].values())
-            lookups_for_this_script.extend(lookups[uniscript].values())
-            addLookupReferences(feature, lookups_for_this_script, script, langs)
+        scriptGroups = self.context.scriptGroups
+        if "dist" in self.context.todo:
+            distScripts = scriptGroups["dist"]
+        else:
+            distScripts = {}
+        kernScripts = scriptGroups.get("kern", {})
+        ltrScripts = kernScripts.get("LTR", [])
+        rtlScripts = kernScripts.get("RTL", [])
+
+        ltrLookups = lookups.get("LTR")
+        rtlLookups = lookups.get("RTL")
+        if ltrLookups and rtlLookups:
+            if ltrScripts and rtlScripts:
+                for script, langs in ltrScripts:
+                    ast.addLookupReferences(feature, ltrLookups, script, langs)
+                for script, langs in rtlScripts:
+                    ast.addLookupReferences(feature, rtlLookups, script, langs)
+            elif ltrScripts:
+                ast.addLookupReferences(feature, rtlLookups, script="DFLT")
+                for script, langs in ltrScripts:
+                    ast.addLookupReferences(feature, ltrLookups, script, langs)
+            elif rtlScripts:
+                ast.addLookupReferences(feature, ltrLookups, script="DFLT")
+                for script, langs in rtlScripts:
+                    ast.addLookupReferences(feature, rtlLookups, script, langs)
+            else:
+                if not (distScripts.get("LTR") and distScripts.get("RTL")):
+                    raise ValueError(
+                        "cannot use DFLT script for both LTR and RTL kern "
+                        "lookups; add 'languagesystems' to features for at "
+                        "least one LTR or RTL script using the kern feature"
+                    )
+        elif ltrLookups:
+            if not (rtlScripts or distScripts):
+                ast.addLookupReferences(feature, ltrLookups)
+            else:
+                ast.addLookupReferences(feature, ltrLookups, script="DFLT")
+                for script, langs in ltrScripts:
+                    ast.addLookupReferences(feature, ltrLookups, script, langs)
+        elif rtlLookups:
+            if not (ltrScripts or distScripts):
+                ast.addLookupReferences(feature, rtlLookups)
+            else:
+                ast.addLookupReferences(feature, rtlLookups, script="DFLT")
+                for script, langs in rtlScripts:
+                    ast.addLookupReferences(feature, rtlLookups, script, langs)
+
+    def _registerDistLookups(self, feature, lookups):
+        scripts = self.context.scriptGroups["dist"]
+        ltrLookups = lookups.get("LTR")
+        if ltrLookups:
+            for script, langs in scripts.get("LTR", []):
+                ast.addLookupReferences(feature, ltrLookups, script, langs)
+        rtlLookups = lookups.get("RTL")
+        if rtlLookups:
+            for script, langs in scripts.get("RTL", []):
+                ast.addLookupReferences(feature, rtlLookups, script, langs)
diff --git a/Lib/ufo2ft/util.py b/Lib/ufo2ft/util.py
index 8bc8e25..4fec5aa 100644
--- a/Lib/ufo2ft/util.py
+++ b/Lib/ufo2ft/util.py
@@ -281,12 +281,12 @@ def closeGlyphsOverGSUB(gsub, glyphs):
 
 def classifyGlyphs(unicodeFunc, cmap, gsub=None):
     """'unicodeFunc' is a callable that takes a Unicode codepoint and
-    returns a string, or collection of strings, denoting some Unicode
-    property associated with the given character (or None if a character
-    is considered 'neutral'). 'cmap' is a dictionary mapping Unicode
-    codepoints to glyph names. 'gsub' is an (optional) fonttools GSUB
-    table object, used to find all the glyphs that are "reachable" via
-    substitutions from the initial sets of glyphs defined in the cmap.
+    returns a string denoting some Unicode property associated with the
+    given character (or None if a character is considered 'neutral').
+    'cmap' is a dictionary mapping Unicode codepoints to glyph names.
+    'gsub' is an (optional) fonttools GSUB table object, used to find all
+    the glyphs that are "reachable" via substitutions from the initial
+    sets of glyphs defined in the cmap.
 
     Returns a dictionary of glyph sets associated with the given Unicode
     properties.
@@ -294,14 +294,11 @@ def classifyGlyphs(unicodeFunc, cmap, gsub=None):
     glyphSets = {}
     neutralGlyphs = set()
     for uv, glyphName in cmap.items():
-        key_or_keys = unicodeFunc(uv)
-        if key_or_keys is None:
+        key = unicodeFunc(uv)
+        if key is None:
             neutralGlyphs.add(glyphName)
-        elif isinstance(key_or_keys, (list, set)):
-            for key in key_or_keys:
-                glyphSets.setdefault(key, set()).add(glyphName)
         else:
-            glyphSets.setdefault(key_or_keys, set()).add(glyphName)
+            glyphSets.setdefault(key, set()).add(glyphName)
 
     if gsub is not None:
         if neutralGlyphs:


Test patch:
diff --git a/tests/featureCompiler_test.py b/tests/featureCompiler_test.py
index 5f05059..8613d83 100644
--- a/tests/featureCompiler_test.py
+++ b/tests/featureCompiler_test.py
@@ -231,11 +231,7 @@ class FeatureCompilerTest:
                 foo = ast.FeatureBlock("FOO ")
                 foo.statements.append(
                     ast.SingleSubstStatement(
-                        [ast.GlyphName("a")],
-                        [ast.GlyphName("v")],
-                        prefix="",
-                        suffix="",
-                        forceChain=None,
+                        "a", "v", prefix="", suffix="", forceChain=None
                     )
                 )
                 feaFile.statements.append(foo)
@@ -331,15 +327,13 @@ class FeatureCompilerTest:
             } liga;
 
 
-            lookup kern_Common {
+            lookup kern_ltr {
                 lookupflag IgnoreMarks;
                 pos a v -40;
-            } kern_Common;
+            } kern_ltr;
 
             feature kern {
-                script DFLT;
-                language dflt;
-                lookup kern_Common;
+                lookup kern_ltr;
             } kern;
             """
         )
diff --git a/tests/featureWriters/kernFeatureWriter_test.py b/tests/featureWriters/kernFeatureWriter_test.py
index 0dc0d66..a7fe869 100644
--- a/tests/featureWriters/kernFeatureWriter_test.py
+++ b/tests/featureWriters/kernFeatureWriter_test.py
@@ -6,7 +6,6 @@ import pytest
 from ufo2ft.errors import InvalidFeaturesData
 from ufo2ft.featureCompiler import parseLayoutFeatures
 from ufo2ft.featureWriters import KernFeatureWriter, ast
-from ufo2ft.featureWriters.kernFeatureWriter import KerningPair
 
 from . import FeatureWriterTest
 
@@ -46,50 +45,6 @@ class KernFeatureWriterTest(FeatureWriterTest):
 
     FeatureWriter = KernFeatureWriter
 
-    def test_split_pair(self, caplog):
-        caplog.set_level(logging.INFO)
-        glyphScripts = {
-            "V": {"Latn"},
-            "W": {"Latn"},
-            "gba-nko": {"Nkoo"},
-            "period": {"Zyyy"},
-            "comma-ar": {"Arab", "Nkoo", "Syrc", "Thaa", "Rohg", "Yezi"},
-            "lam-ar": {"Arab"},
-        }
-
-        # Mixed script pairs don't go anywhere
-        pair = KerningPair(["V", "W"], ["gba-nko", "W"], -20)
-        split = dict(pair.partitionByScript(glyphScripts))
-        assert len(split) == 1
-        assert str(split["Latn"]) == "<KerningPair [V W] W -20 {'Latn'}>"
-        assert (
-            "Mixed script kerning pair <KerningPair [V W] gba-nko -20> ignored"
-            in caplog.text
-        )
-
-        # Everyone gets common-script glyphs, but they get it per-script
-        pair = KerningPair(["V", "gba-nko", "W"], "period", -20)
-        split = dict(pair.partitionByScript(glyphScripts))
-        assert len(split) == 2
-        assert "Latn" in split and "Nkoo" in split
-        assert str(split["Latn"]) == "<KerningPair [V W] period -20 {'Latn'}>"
-        assert str(split["Nkoo"]) == "<KerningPair gba-nko period -20 {'Nkoo'}>"
-
-        # But if both sides are common then the output is common
-        pair = KerningPair("period", "period", -20)
-        split = dict(pair.partitionByScript(glyphScripts))
-        assert len(split) == 1
-        assert "Zyyy" in split
-
-        # Glyphs with more than one script get associated with all of the
-        # relevant scripts in the pair
-        pair = KerningPair(["lam-ar", "gba-nko"], ["comma-ar"], -20)
-        split = dict(pair.partitionByScript(glyphScripts))
-        assert len(split) == 2
-        assert "Arab" in split and "Nkoo" in split
-        assert str(split["Arab"]) == "<KerningPair lam-ar comma-ar -20 {'Arab'}>"
-        assert str(split["Nkoo"]) == "<KerningPair gba-nko comma-ar -20 {'Nkoo'}>"
-
     def test_cleanup_missing_glyphs(self, FontClass):
         groups = {
             "public.kern1.A": ["A", "Aacute", "Abreve", "Acircumflex"],
@@ -125,10 +80,9 @@ class KernFeatureWriterTest(FeatureWriterTest):
 
         lookups = getLookups(feaFile)
         assert len(lookups) == 1
-        kern_lookup = lookups[0]
-        # We have no codepoints defined for these, so they're considered common
-        assert kern_lookup.name == "kern_Common"
-        rules = getPairPosRules(kern_lookup)
+        kern_ltr = lookups[0]
+        assert kern_ltr.name == "kern_ltr"
+        rules = getPairPosRules(kern_ltr)
         assert len(rules) == 1
         assert str(rules[0]) == "pos @kern1.A @kern2.B 10;"
 
@@ -144,16 +98,14 @@ class KernFeatureWriterTest(FeatureWriterTest):
 
         assert str(feaFile) == dedent(
             """\
-            lookup kern_Common {
+            lookup kern_ltr {
                 lookupflag IgnoreMarks;
                 pos four six -55;
                 pos one six -30;
-            } kern_Common;
+            } kern_ltr;
 
             feature kern {
-                script DFLT;
-                language dflt;
-                lookup kern_Common;
+                lookup kern_ltr;
             } kern;
             """
         )
@@ -164,15 +116,13 @@ class KernFeatureWriterTest(FeatureWriterTest):
 
         assert str(feaFile) == dedent(
             """\
-            lookup kern_Common {
+            lookup kern_ltr {
                 pos four six -55;
                 pos one six -30;
-            } kern_Common;
+            } kern_ltr;
 
             feature kern {
-                script DFLT;
-                language dflt;
-                lookup kern_Common;
+                lookup kern_ltr;
             } kern;
             """
         )
@@ -180,7 +130,7 @@ class KernFeatureWriterTest(FeatureWriterTest):
     def test_mark_to_base_kern(self, FontClass):
         font = FontClass()
         for name in ("A", "B", "C"):
-            font.newGlyph(name).unicode = ord(name)
+            font.newGlyph(name)
         font.newGlyph("acutecomb").unicode = 0x0301
         font.kerning.update({("A", "acutecomb"): -55.0, ("B", "C"): -30.0})
 
@@ -196,23 +146,20 @@ class KernFeatureWriterTest(FeatureWriterTest):
 
         # default is ignoreMarks=True
         feaFile = self.writeFeatures(font)
-        # Zyyy because no languagesystems defined
         assert str(feaFile) == dedent(
             """
-            lookup kern_Common {
+            lookup kern_ltr {
                 lookupflag IgnoreMarks;
                 pos B C -30;
-            } kern_Common;
+            } kern_ltr;
 
-            lookup kern_Common_marks {
+            lookup kern_ltr_marks {
                 pos A acutecomb -55;
-            } kern_Common_marks;
+            } kern_ltr_marks;
 
             feature kern {
-                script DFLT;
-                language dflt;
-                lookup kern_Common;
-                lookup kern_Common_marks;
+                lookup kern_ltr;
+                lookup kern_ltr_marks;
             } kern;
             """
         )
@@ -220,15 +167,13 @@ class KernFeatureWriterTest(FeatureWriterTest):
         feaFile = self.writeFeatures(font, ignoreMarks=False)
         assert str(feaFile) == dedent(
             """
-            lookup kern_Common {
+            lookup kern_ltr {
                 pos A acutecomb -55;
                 pos B C -30;
-            } kern_Common;
+            } kern_ltr;
 
             feature kern {
-                script DFLT;
-                language dflt;
-                lookup kern_Common;
+                lookup kern_ltr;
             } kern;
             """
         )
@@ -254,14 +199,12 @@ class KernFeatureWriterTest(FeatureWriterTest):
         feaFile = self.writeFeatures(font)
         assert str(feaFile) == dedent(
             """
-            lookup kern_Common_marks {
+            lookup kern_ltr_marks {
                 pos A acutecomb -55;
-            } kern_Common_marks;
+            } kern_ltr_marks;
 
             feature kern {
-                script DFLT;
-                language dflt;
-                lookup kern_Common_marks;
+                lookup kern_ltr_marks;
             } kern;
             """
         )
@@ -294,15 +237,13 @@ class KernFeatureWriterTest(FeatureWriterTest):
         expected = existing + dedent(
             """
 
-            lookup kern_Common {
+            lookup kern_ltr {
                 lookupflag IgnoreMarks;
                 pos seven six 25;
-            } kern_Common;
+            } kern_ltr;
 
             feature kern {
-                script DFLT;
-                language dflt;
-                lookup kern_Common;
+                lookup kern_ltr;
             } kern;
             """
         )
@@ -338,15 +279,13 @@ class KernFeatureWriterTest(FeatureWriterTest):
 
         expected = dedent(
             """\
-            lookup kern_Common {
+            lookup kern_ltr {
                 lookupflag IgnoreMarks;
                 pos seven six 25;
-            } kern_Common;
+            } kern_ltr;
 
             feature kern {
-                script DFLT;
-                language dflt;
-                lookup kern_Common;
+                lookup kern_ltr;
             } kern;
 
             feature kern {
@@ -363,15 +302,13 @@ class KernFeatureWriterTest(FeatureWriterTest):
         generated = self.writeFeatures(ufo, mode="append")
         assert str(generated) == dedent(
             """
-            lookup kern_Common {
+            lookup kern_ltr {
                 lookupflag IgnoreMarks;
                 pos seven six 25;
-            } kern_Common;
+            } kern_ltr;
 
             feature kern {
-                script DFLT;
-                language dflt;
-                lookup kern_Common;
+                lookup kern_ltr;
             } kern;
             """
         )
@@ -399,15 +336,13 @@ class KernFeatureWriterTest(FeatureWriterTest):
 
         expected = dedent(
             """\
-            lookup kern_Common {
+            lookup kern_ltr {
                 lookupflag IgnoreMarks;
                 pos seven six 25;
-            } kern_Common;
+            } kern_ltr;
 
             feature kern {
-                script DFLT;
-                language dflt;
-                lookup kern_Common;
+                lookup kern_ltr;
             } kern;
 
             feature kern {
@@ -450,15 +385,13 @@ class KernFeatureWriterTest(FeatureWriterTest):
             } kern;
 
 
-            lookup kern_Common {
+            lookup kern_ltr {
                 lookupflag IgnoreMarks;
                 pos seven six 25;
-            } kern_Common;
+            } kern_ltr;
 
             feature kern {
-                script DFLT;
-                language dflt;
-                lookup kern_Common;
+                lookup kern_ltr;
             } kern;
             """
         )
@@ -469,15 +402,13 @@ class KernFeatureWriterTest(FeatureWriterTest):
         generated = self.writeFeatures(ufo, mode="append")
         assert str(generated) == dedent(
             """
-            lookup kern_Common {
+            lookup kern_ltr {
                 lookupflag IgnoreMarks;
                 pos seven six 25;
-            } kern_Common;
+            } kern_ltr;
 
             feature kern {
-                script DFLT;
-                language dflt;
-                lookup kern_Common;
+                lookup kern_ltr;
             } kern;
             """
         )
@@ -514,15 +445,13 @@ class KernFeatureWriterTest(FeatureWriterTest):
         generated = self.writeFeatures(ufo, mode="append")
         assert str(generated) == dedent(
             """
-            lookup kern_Common {
+            lookup kern_ltr {
                 lookupflag IgnoreMarks;
                 pos seven six 25;
-            } kern_Common;
+            } kern_ltr;
 
             feature kern {
-                script DFLT;
-                language dflt;
-                lookup kern_Common;
+                lookup kern_ltr;
             } kern;
             """
         )
@@ -546,17 +475,15 @@ class KernFeatureWriterTest(FeatureWriterTest):
 
         generated = self.writeFeatures(ufo)
 
-        assert dedent(str(generated)) == dedent(
+        assert str(generated) == dedent(
             """
-            lookup kern_Arab {
+            lookup kern_rtl {
                 lookupflag IgnoreMarks;
                 pos four-ar seven-ar -30;
-            } kern_Arab;
+            } kern_rtl;
 
             feature kern {
-                script arab;
-                language dflt;
-                lookup kern_Arab;
+                lookup kern_rtl;
             } kern;
             """
         )
@@ -740,11 +667,6 @@ class KernFeatureWriterTest(FeatureWriterTest):
                 sub reh-ar by reh-ar.fina;
                 language URD;
             } fina;
-
-            feature isol {
-                script arab;
-                sub alef-ar by alef-ar.isol;
-            } isol;
             """
         )
 
@@ -752,42 +674,36 @@ class KernFeatureWriterTest(FeatureWriterTest):
 
         newFeatures = self.writeFeatures(ufo, ignoreMarks=False)
 
-        assert dedent(str(newFeatures)) == dedent(
+        assert str(newFeatures) == dedent(
             """\
             @kern1.A = [A Aacute];
             @kern1.reh = [reh-ar zain-ar reh-ar.fina];
             @kern2.alef = [alef-ar alef-ar.isol];
 
-            lookup kern_Arab {
-                pos four-ar seven-ar -30;
-                pos reh-ar.fina lam-ar.init <-80 0 -80 0>;
-                pos @kern1.reh @kern2.alef <-100 0 -100 0>;
-            } kern_Arab;
+            lookup kern_dflt {
+                pos seven four -25;
+            } kern_dflt;
 
-            lookup kern_Latn {
+            lookup kern_ltr {
                 enum pos @kern1.A V -40;
-            } kern_Latn;
+            } kern_ltr;
 
-            lookup kern_Common {
-                pos seven four -25;
-            } kern_Common;
+            lookup kern_rtl {
+                pos four-ar seven-ar -30;
+                pos reh-ar.fina lam-ar.init <-80 0 -80 0>;
+                pos @kern1.reh @kern2.alef <-100 0 -100 0>;
+            } kern_rtl;
 
             feature kern {
-                script DFLT;
+                lookup kern_dflt;
+                script latn;
                 language dflt;
-                lookup kern_Common;
-
+                lookup kern_ltr;
+                language TRK;
                 script arab;
                 language dflt;
-                lookup kern_Common;
-                lookup kern_Arab;
+                lookup kern_rtl;
                 language URD;
-
-                script latn;
-                language dflt;
-                lookup kern_Common;
-                lookup kern_Latn;
-                language TRK;
             } kern;
             """
         )
@@ -848,11 +764,6 @@ class KernFeatureWriterTest(FeatureWriterTest):
                 language URD;
             } fina;
 
-            feature isol {
-                script arab;
-                sub alef-ar by alef-ar.isol;
-            } isol;
-
             @Bases = [A V Aacute alef-ar reh-ar zain-ar lam-ar
                       alef-ar.isol lam-ar.init reh-ar.fina];
             @Marks = [acutecomb fatha-ar];
@@ -866,55 +777,49 @@ class KernFeatureWriterTest(FeatureWriterTest):
 
         newFeatures = self.writeFeatures(ufo)
 
-        assert dedent(str(newFeatures)) == dedent(
+        assert str(newFeatures) == dedent(
             """\
             @kern1.A = [A Aacute];
             @kern1.reh = [reh-ar zain-ar reh-ar.fina];
             @kern2.alef = [alef-ar alef-ar.isol];
 
-            lookup kern_Arab {
+            lookup kern_dflt {
                 lookupflag IgnoreMarks;
-                pos four-ar seven-ar -30;
-                pos reh-ar.fina lam-ar.init <-80 0 -80 0>;
-                pos @kern1.reh @kern2.alef <-100 0 -100 0>;
-            } kern_Arab;
-
-            lookup kern_Arab_marks {
-                pos reh-ar fatha-ar <80 0 80 0>;
-            } kern_Arab_marks;
+                pos seven four -25;
+            } kern_dflt;
 
-            lookup kern_Latn {
+            lookup kern_ltr {
                 lookupflag IgnoreMarks;
                 enum pos @kern1.A V -40;
-            } kern_Latn;
+            } kern_ltr;
 
-            lookup kern_Latn_marks {
+            lookup kern_ltr_marks {
                 pos V acutecomb 70;
-            } kern_Latn_marks;
+            } kern_ltr_marks;
 
-            lookup kern_Common {
+            lookup kern_rtl {
                 lookupflag IgnoreMarks;
-                pos seven four -25;
-            } kern_Common;
+                pos four-ar seven-ar -30;
+                pos reh-ar.fina lam-ar.init <-80 0 -80 0>;
+                pos @kern1.reh @kern2.alef <-100 0 -100 0>;
+            } kern_rtl;
+
+            lookup kern_rtl_marks {
+                pos reh-ar fatha-ar <80 0 80 0>;
+            } kern_rtl_marks;
 
             feature kern {
-                script DFLT;
+                lookup kern_dflt;
+                script latn;
                 language dflt;
-                lookup kern_Common;
-
+                lookup kern_ltr;
+                lookup kern_ltr_marks;
+                language TRK;
                 script arab;
                 language dflt;
-                lookup kern_Common;
-                lookup kern_Arab;
-                lookup kern_Arab_marks;
+                lookup kern_rtl;
+                lookup kern_rtl_marks;
                 language URD;
-
-                script latn;
-                language dflt;
-                lookup kern_Common;
-                lookup kern_Latn;
-                lookup kern_Latn_marks;
-                language TRK;
             } kern;
             """
         )
@@ -959,11 +864,6 @@ class KernFeatureWriterTest(FeatureWriterTest):
                 sub reh-ar by reh-ar.fina;
             } fina;
 
-            feature isol {
-                script arab;
-                sub alef-ar by alef-ar.isol;
-            } isol;
-
             @Bases = [alef-ar reh-ar zain-ar lam-ar alef-ar.isol lam-ar.init reh-ar.fina];
             @Marks = [fatha-ar];
             table GDEF {
@@ -976,27 +876,24 @@ class KernFeatureWriterTest(FeatureWriterTest):
 
         newFeatures = self.writeFeatures(ufo)
 
-        assert dedent(str(newFeatures)) == dedent(
+        assert str(newFeatures) == dedent(
             """\
             @kern1.reh = [reh-ar zain-ar reh-ar.fina];
             @kern2.alef = [alef-ar alef-ar.isol];
 
-            lookup kern_Arab {
+            lookup kern_rtl {
                 lookupflag IgnoreMarks;
                 pos reh-ar.fina lam-ar.init <-80 0 -80 0>;
                 pos @kern1.reh @kern2.alef <-100 0 -100 0>;
-            } kern_Arab;
+            } kern_rtl;
 
-            lookup kern_Arab_marks {
+            lookup kern_rtl_marks {
                 pos reh-ar fatha-ar <80 0 80 0>;
-            } kern_Arab_marks;
+            } kern_rtl_marks;
 
             feature kern {
-                script arab;
-                language dflt;
-                lookup kern_Arab;
-                lookup kern_Arab_marks;
-                language ARA;
+                lookup kern_rtl;
+                lookup kern_rtl_marks;
             } kern;
             """
         )
@@ -1008,27 +905,25 @@ class KernFeatureWriterTest(FeatureWriterTest):
         ufo = makeUFO(FontClass, glyphs, kerning=kerning, features=features)
         generated = self.writeFeatures(ufo)
 
-        assert dedent(str(generated)) == dedent(
+        assert str(generated) == dedent(
             """
-            lookup kern_Latn {
+            lookup kern_ltr {
                 lookupflag IgnoreMarks;
                 pos A V -40;
-            } kern_Latn;
+            } kern_ltr;
 
-            lookup kern_Common {
+            lookup kern_rtl {
                 lookupflag IgnoreMarks;
                 pos reh-ar alef-ar <-100 0 -100 0>;
-            } kern_Common;
+            } kern_rtl;
 
             feature kern {
                 script DFLT;
                 language dflt;
-                lookup kern_Common;
-
+                lookup kern_rtl;
                 script latn;
                 language dflt;
-                lookup kern_Common;
-                lookup kern_Latn;
+                lookup kern_ltr;
             } kern;
             """
         )
@@ -1037,51 +932,35 @@ class KernFeatureWriterTest(FeatureWriterTest):
         ufo = makeUFO(FontClass, glyphs, kerning=kerning, features=features)
         generated = self.writeFeatures(ufo)
 
-        assert dedent(str(generated)) == dedent(
+        assert str(generated) == dedent(
             """
-            lookup kern_Arab {
+            lookup kern_ltr {
                 lookupflag IgnoreMarks;
-                pos reh-ar alef-ar <-100 0 -100 0>;
-            } kern_Arab;
+                pos A V -40;
+            } kern_ltr;
 
-            lookup kern_Common {
+            lookup kern_rtl {
                 lookupflag IgnoreMarks;
-                pos A V -40;
-            } kern_Common;
+                pos reh-ar alef-ar <-100 0 -100 0>;
+            } kern_rtl;
 
             feature kern {
                 script DFLT;
                 language dflt;
-                lookup kern_Common;
-
+                lookup kern_ltr;
                 script arab;
                 language dflt;
-                lookup kern_Common;
-                lookup kern_Arab;
+                lookup kern_rtl;
             } kern;
             """
         )
 
-    def test_kern_LTR_and_RTL_no_DFLT(self, FontClass):
+    def test_kern_LTR_and_RTL_cannot_use_DFLT(self, FontClass):
         glyphs = {"A": 0x41, "V": 0x56, "reh-ar": 0x631, "alef-ar": 0x627}
         kerning = {("A", "V"): -40, ("reh-ar", "alef-ar"): -100}
         ufo = makeUFO(FontClass, glyphs, kerning=kerning)
-        generated = self.writeFeatures(ufo)
-        assert "\n" + str(generated) == dedent(
-            """
-            lookup kern_Common {
-                lookupflag IgnoreMarks;
-                pos A V -40;
-                pos reh-ar alef-ar <-100 0 -100 0>;
-            } kern_Common;
-
-            feature kern {
-                script DFLT;
-                language dflt;
-                lookup kern_Common;
-            } kern;
-            """
-        )
+        with pytest.raises(ValueError, match="cannot use DFLT script"):
+            self.writeFeatures(ufo)
 
     def test_dist_LTR(self, FontClass):
         glyphs = {"aaMatra_kannada": 0x0CBE, "ailength_kannada": 0xCD6}
@@ -1102,24 +981,32 @@ class KernFeatureWriterTest(FeatureWriterTest):
         ufo = makeUFO(FontClass, glyphs, groups, kerning, features)
         generated = self.writeFeatures(ufo)
 
-        assert dedent(str(generated)) == dedent(
+        assert str(generated) == dedent(
             """\
             @kern1.KND_aaMatra_R = [aaMatra_kannada];
             @kern2.KND_ailength_L = [aaMatra_kannada];
 
-            lookup kern_Knda {
+            lookup kern_ltr {
                 lookupflag IgnoreMarks;
                 pos @kern1.KND_aaMatra_R @kern2.KND_ailength_L 34;
-            } kern_Knda;
+            } kern_ltr;
 
-            feature dist {
-                script knd2;
+            feature kern {
+                script DFLT;
+                language dflt;
+                lookup kern_ltr;
+                script latn;
                 language dflt;
-                lookup kern_Knda;
+                lookup kern_ltr;
+            } kern;
 
+            feature dist {
                 script knda;
                 language dflt;
-                lookup kern_Knda;
+                lookup kern_ltr;
+                script knd2;
+                language dflt;
+                lookup kern_ltr;
             } dist;
             """
         )
@@ -1139,15 +1026,24 @@ class KernFeatureWriterTest(FeatureWriterTest):
 
         assert str(generated) == dedent(
             """
-            lookup kern_Khar {
+            lookup kern_rtl {
                 lookupflag IgnoreMarks;
                 pos u10A1E u10A06 <117 0 117 0>;
-            } kern_Khar;
+            } kern_rtl;
+
+            feature kern {
+                script DFLT;
+                language dflt;
+                lookup kern_rtl;
+                script arab;
+                language dflt;
+                lookup kern_rtl;
+            } kern;
 
             feature dist {
                 script khar;
                 language dflt;
-                lookup kern_Khar;
+                lookup kern_rtl;
             } dist;
             """
         )
@@ -1179,38 +1075,38 @@ class KernFeatureWriterTest(FeatureWriterTest):
         ufo = makeUFO(FontClass, glyphs, groups, kerning, features)
         generated = self.writeFeatures(ufo)
 
-        assert dedent(str(generated)) == dedent(
+        assert str(generated) == dedent(
             """\
             @kern1.KND_aaMatra_R = [aaMatra_kannada];
             @kern2.KND_ailength_L = [aaMatra_kannada];
 
-            lookup kern_Khar {
+            lookup kern_ltr {
                 lookupflag IgnoreMarks;
-                pos u10A1E u10A06 <117 0 117 0>;
-            } kern_Khar;
+                pos @kern1.KND_aaMatra_R @kern2.KND_ailength_L 34;
+            } kern_ltr;
 
-            lookup kern_Knda {
+            lookup kern_rtl {
                 lookupflag IgnoreMarks;
-                pos @kern1.KND_aaMatra_R @kern2.KND_ailength_L 34;
-            } kern_Knda;
+                pos u10A1E u10A06 <117 0 117 0>;
+            } kern_rtl;
 
             feature dist {
-                script khar;
+                script knda;
                 language dflt;
-                lookup kern_Khar;
-
+                lookup kern_ltr;
                 script knd2;
                 language dflt;
-                lookup kern_Knda;
-
-                script knda;
+                lookup kern_ltr;
+                script khar;
                 language dflt;
-                lookup kern_Knda;
+                lookup kern_rtl;
             } dist;
             """
         )
 
-    def test_ambiguous_direction_pair(self, FontClass, caplog):
+    def test_skip_ambiguous_direction_pair(self, FontClass, caplog):
+        caplog.set_level(logging.ERROR)
+
         ufo = FontClass()
         ufo.newGlyph("A").unicode = 0x41
         ufo.newGlyph("one").unicode = 0x31
@@ -1239,42 +1135,9 @@ class KernFeatureWriterTest(FeatureWriterTest):
         with caplog.at_level(logging.WARNING, logger=logger):
             generated = self.writeFeatures(ufo)
 
-        assert dedent(str(generated)) == dedent(
-            """
-            lookup kern_Arab {
-                lookupflag IgnoreMarks;
-                pos bar bar 1;
-                pos reh-ar one-ar 4;
-            } kern_Arab;
-
-            lookup kern_Latn {
-                lookupflag IgnoreMarks;
-                pos bar A 2;
-                pos bar bar 1;
-            } kern_Latn;
-
-            lookup kern_Common {
-                lookupflag IgnoreMarks;
-                pos yod-hb one 5;
-            } kern_Common;
-
-            feature kern {
-                script DFLT;
-                language dflt;
-                lookup kern_Common;
-
-                script arab;
-                language dflt;
-                lookup kern_Common;
-                lookup kern_Arab;
-
-                script latn;
-                language dflt;
-                lookup kern_Common;
-                lookup kern_Latn;
-            } kern;
-        """
-        )
+        assert not generated
+        assert len(caplog.records) == 5
+        assert "skipped kern pair with ambiguous direction" in caplog.text
 
     def test_kern_RTL_and_DFLT_numbers(self, FontClass):
         glyphs = {"four": 0x34, "seven": 0x37, "bet-hb": 0x5D1, "yod-hb": 0x5D9}
@@ -1289,27 +1152,21 @@ class KernFeatureWriterTest(FeatureWriterTest):
         ufo = makeUFO(FontClass, glyphs, kerning=kerning, features=features)
         generated = self.writeFeatures(ufo)
 
-        assert dedent(str(generated)) == dedent(
+        assert str(generated) == dedent(
             """
-            lookup kern_Hebr {
+            lookup kern_dflt {
                 lookupflag IgnoreMarks;
-                pos yod-hb bet-hb <-100 0 -100 0>;
-            } kern_Hebr;
+                pos seven four -25;
+            } kern_dflt;
 
-            lookup kern_Common {
+            lookup kern_rtl {
                 lookupflag IgnoreMarks;
-                pos seven four -25;
-            } kern_Common;
+                pos yod-hb bet-hb <-100 0 -100 0>;
+            } kern_rtl;
 
             feature kern {
-                script DFLT;
-                language dflt;
-                lookup kern_Common;
-
-                script hebr;
-                language dflt;
-                lookup kern_Common;
-                lookup kern_Hebr;
+                lookup kern_dflt;
+                lookup kern_rtl;
             } kern;
             """
         )
@@ -1325,16 +1182,14 @@ class KernFeatureWriterTest(FeatureWriterTest):
 
         assert str(feaFile) == dedent(
             """\
-            lookup kern_Common {
+            lookup kern_ltr {
                 lookupflag IgnoreMarks;
                 pos four six -55;
                 pos one six -25;
-            } kern_Common;
+            } kern_ltr;
 
             feature kern {
-                script DFLT;
-                language dflt;
-                lookup kern_Common;
+                lookup kern_ltr;
             } kern;
             """
         )