Repository: fonttools/fonttools

Issue:
feaLib should build simple contextual lookups when no backtrack/lookahead
The Adobe FEA spec does not distinguish between contextual positioning/substitution lookups (GPOS 7, GSUB 5), on the one hand, and _chaining_ contextual pos/subst lookups (GPOS 8, GSUB 6), on the other. The two types of lookups are essentially the same except that the latter also can define optional backtrack/lookahead sequences.

FeaLib builder currently always encodes contextual pos/subst rules with Format 3 (Coverage-based) ChainContex{Pos,Subst} lookup subtables.

https://github.com/fonttools/fonttools/blob/42dcfddfb9687de106e5deb59cea015280dd507f/Lib/fontTools/feaLib/builder.py#L1246

https://github.com/fonttools/fonttools/blob/42dcfddfb9687de106e5deb59cea015280dd507f/Lib/fontTools/feaLib/builder.py#L1293

Ideally it could use the more efficient types of non-chaining contextual lookups when a contextual rule contains no backtrack nor lookahend sequences.

http://adobe-type-tools.github.io/afdko/OpenTypeFeatureFileSpecification.html#5e-gsub-lookuptype-5-contextual-substitution
http://adobe-type-tools.github.io/afdko/OpenTypeFeatureFileSpecification.html#6g-gpos-lookuptype-7-contextual-positioning

I haven't had the chance to check, does makeotf do anything like that?




Discussion:
I'll work on this, but just for info, makeotf does not do this - it treats all contextuals as chaining contextuals.

Resolving patch:
diff --git a/Lib/fontTools/otlLib/builder.py b/Lib/fontTools/otlLib/builder.py
--- a/Lib/fontTools/otlLib/builder.py
+++ b/Lib/fontTools/otlLib/builder.py
@@ -172,6 +172,13 @@ def setInputCoverage_(self, glyphs, subtable):
             coverage = buildCoverage(g, self.glyphMap)
             subtable.InputCoverage.append(coverage)
 
+    def setCoverage_(self, glyphs, subtable):
+        subtable.GlyphCount = len(glyphs)
+        subtable.Coverage = []
+        for g in glyphs:
+            coverage = buildCoverage(g, self.glyphMap)
+            subtable.Coverage.append(coverage)
+
     def build_subst_subtables(self, mapping, klass):
         substitutions = [{}]
         for key in mapping:
@@ -244,11 +251,71 @@ def add_subtable_break(self, location):
         self.alternates[(self.SUBTABLE_BREAK_, location)] = self.SUBTABLE_BREAK_
 
 
+class ChainContextualRuleset():
+    def __init__(self):
+        self.rules = []
+
+    def addRule(self, prefix, glyphs, suffix, lookups):
+        self.rules.append((prefix, glyphs, suffix, lookups))
+
+    @property
+    def hasPrefixOrSuffix(self):
+        # Do we have any prefixes/suffixes? If this is False for all
+        # rulesets, we can express the whole lookup as GPOS5/GSUB7.
+        for (prefix, glyphs, suffix, lookups) in self.rules:
+            if len(prefix) > 0 or len(suffix) > 0:
+                return True
+        return False
+
+    @property
+    def hasAnyGlyphClasses(self):
+        # Do we use glyph classes anywhere in the rules? If this is False
+        # we can express this subtable as a Format 1.
+        for (prefix, glyphs, suffix, lookups) in self.rules:
+            for coverage in (prefix, glyphs, suffix):
+                if any(len(x) > 1 for x in coverage):
+                    return True
+        return False
+
+    def format2ClassDefs(self):
+        PREFIX, GLYPHS, SUFFIX = 0,1,2
+        classDefBuilders = []
+        for ix in [PREFIX, GLYPHS, SUFFIX]:
+            context = []
+            for r in self.rules:
+                context.append(r[ix])
+            classes = self._classBuilderForContext(context)
+            if not classes:
+                return None
+            classDefBuilders.append(classes)
+        return classDefBuilders
+
+    def _classBuilderForContext(self, context):
+        classdefbuilder = ClassDefBuilder(useClass0=False)
+        for position in context:
+            for glyphset in position:
+                if not classdefbuilder.canAdd(glyphset):
+                    return None
+                classdefbuilder.add(glyphset)
+        return classdefbuilder
+
 class ChainContextualBuilder(LookupBuilder):
     def equals(self, other):
         return (LookupBuilder.equals(self, other) and
                 self.rules == other.rules)
 
+    def rulesets(self):
+        # Return a list of ChainContextRuleset objects, taking explicit
+        # subtable breaks into account
+        ruleset = [ChainContextualRuleset()]
+        for (prefix, glyphs, suffix, lookups) in self.rules:
+            if prefix == self.SUBTABLE_BREAK_:
+                ruleset.append(ChainContextualRuleset())
+                continue
+            ruleset[-1].addRule(prefix, glyphs, suffix, lookups)
+        # Squish any empty subtables
+        return [x for x in ruleset if len(x.rules) > 0]
+
     def build(self):
         """Build the lookup.
 
@@ -257,40 +324,91 @@ def build(self):
             contextual positioning lookup.
         """
         subtables = []
-        for (prefix, glyphs, suffix, lookups) in self.rules:
-            if prefix == self.SUBTABLE_BREAK_:
-                continue
-            st = self.newSubtable_()
-            subtables.append(st)
-            st.Format = 3
+        chaining = False
+        rulesets = self.rulesets()
+        chaining = any(ruleset.hasPrefixOrSuffix for ruleset in rulesets)
+        for ruleset in rulesets:
+            for rule in ruleset.rules:
+                subtables.append(self.buildFormat3Subtable(rule, chaining))
+        # If we are not chaining, lookup type will be automatically fixed by
+        # buildLookup_
+        return self.buildLookup_(subtables)
+
+    def buildFormat3Subtable(self, rule, chaining=True):
+        (prefix, glyphs, suffix, lookups) = rule
+        st = self.newSubtable_(chaining=chaining)
+        st.Format = 3
+        if chaining:
             self.setBacktrackCoverage_(prefix, st)
             self.setLookAheadCoverage_(suffix, st)
             self.setInputCoverage_(glyphs, st)
-
-            for sequenceIndex, lookupList in enumerate(lookups):
-                if lookupList is not None:
-                    if not isinstance(lookupList, list):
-                        # Can happen with synthesised lookups
-                        lookupList = [ lookupList ]
-                    for l in lookupList:
-                        if l.lookup_index is None:
-                            if isinstance(self, ChainContextPosBuilder):
-                                other = "substitution"
-                            else:
-                                other = "positioning"
-                            raise OpenTypeLibError('Missing index of the specified '
-                                f'lookup, might be a {other} lookup',
-                                self.location)
-                        rec = self.newLookupRecord_()
-                        rec.SequenceIndex = sequenceIndex
-                        rec.LookupListIndex = l.lookup_index
-                        self.addLookupRecordToSubtable_(st, rec)
-        return self.buildLookup_(subtables)
+        else:
+            self.setCoverage_(glyphs, st)
+
+        for sequenceIndex, lookupList in enumerate(lookups):
+            if lookupList is not None:
+                if not isinstance(lookupList, list):
+                    # Can happen with synthesised lookups
+                    lookupList = [lookupList]
+                for l in lookupList:
+                    if l.lookup_index is None:
+                        if isinstance(self, ChainContextPosBuilder):
+                            other = "substitution"
+                        else:
+                            other = "positioning"
+                        raise OpenTypeLibError('Missing index of the specified '
+                            f'lookup, might be a {other} lookup',
+                            self.location)
+                    rec = self.newLookupRecord_(st)
+                    rec.SequenceIndex = sequenceIndex
+                    rec.LookupListIndex = l.lookup_index
+        return st
 
     def add_subtable_break(self, location):
         self.rules.append((self.SUBTABLE_BREAK_, self.SUBTABLE_BREAK_,
                            self.SUBTABLE_BREAK_, [self.SUBTABLE_BREAK_]))
 
+    def newSubtable_(self, chaining=True):
+        subtablename = f"Context{self.subtable_type}"
+        if chaining:
+            subtablename = "Chain"+subtablename
+        st = getattr(ot,subtablename)() # ot.ChainContextPos()/ot.ChainSubst()/etc.
+        setattr(st, f"{self.subtable_type}Count", 0)
+        setattr(st, f"{self.subtable_type}LookupRecord", [])
+        return st
+
+    def attachSubtableWithCount_(self, st,
+        subtable_name, count_name,
+        existing=None,
+        index=None, chaining=False):
+        if chaining:
+            subtable_name = "Chain"+subtable_name
+            count_name    = "Chain"+count_name
+
+        if not hasattr(st, count_name):
+            setattr(st, count_name, 0)
+            setattr(st, subtable_name, [])
+
+        if existing:
+            new_subtable = existing
+        else:
+            # Create a new, empty subtable from otTables
+            new_subtable = getattr(ot, subtable_name)()
+
+        setattr(st, count_name, getattr(st, count_name) + 1)
+
+        if index:
+            getattr(st, subtable_name).insert(index, new_subtable)
+        else:
+            getattr(st, subtable_name).append(new_subtable)
+
+        return new_subtable
+
+    def newLookupRecord_(self, st):
+        return self.attachSubtableWithCount_(st,
+            f"{self.subtable_type}LookupRecord",
+            f"{self.subtable_type}Count",
+            chaining=False) # Oddly, it isn't ChainSubstLookupRecord
 
 class ChainContextPosBuilder(ChainContextualBuilder):
     """Builds a Chained Contextual Positioning (GPOS8) lookup.
@@ -321,19 +439,7 @@ class ChainContextPosBuilder(ChainContextualBuilder):
     def __init__(self, font, location):
         LookupBuilder.__init__(self, font, location, 'GPOS', 8)
         self.rules = []  # (prefix, input, suffix, lookups)
-
-    def newSubtable_(self):
-        st = ot.ChainContextPos()
-        st.PosCount = 0
-        st.PosLookupRecord = []
-        return st
-
-    def newLookupRecord_(self):
-        return ot.PosLookupRecord()
-
-    def addLookupRecordToSubtable_(self, st, rec):
-        st.PosCount += 1
-        st.PosLookupRecord.append(rec)
+        self.subtable_type = "Pos"
 
     def find_chainable_single_pos(self, lookups, glyphs, value):
         """Helper for add_single_pos_chained_()"""
@@ -376,19 +482,7 @@ class ChainContextSubstBuilder(ChainContextualBuilder):
     def __init__(self, font, location):
         LookupBuilder.__init__(self, font, location, 'GSUB', 6)
         self.rules = []  # (prefix, input, suffix, lookups)
-
-    def newSubtable_(self):
-        st = ot.ChainContextSubst()
-        st.SubstCount = 0
-        st.SubstLookupRecord = []
-        return st
-
-    def newLookupRecord_(self):
-        return ot.SubstLookupRecord()
-
-    def addLookupRecordToSubtable_(self, st, rec):
-        st.SubstCount += 1
-        st.SubstLookupRecord.append(rec)
+        self.subtable_type = "Subst"
 
     def getAlternateGlyphs(self):
         result = {}


Test patch:
diff --git a/Tests/feaLib/builder_test.py b/Tests/feaLib/builder_test.py
--- a/Tests/feaLib/builder_test.py
+++ b/Tests/feaLib/builder_test.py
@@ -69,10 +69,10 @@ class BuilderTest(unittest.TestCase):
         ZeroValue_SinglePos_horizontal ZeroValue_SinglePos_vertical
         ZeroValue_PairPos_horizontal ZeroValue_PairPos_vertical
         ZeroValue_ChainSinglePos_horizontal ZeroValue_ChainSinglePos_vertical
-        PairPosSubtable ChainSubstSubtable ChainPosSubtable LigatureSubtable
-        AlternateSubtable MultipleSubstSubtable SingleSubstSubtable
-        aalt_chain_contextual_subst AlternateChained MultipleLookupsPerGlyph
-        MultipleLookupsPerGlyph2
+        PairPosSubtable ChainSubstSubtable SubstSubtable ChainPosSubtable 
+        LigatureSubtable AlternateSubtable MultipleSubstSubtable 
+        SingleSubstSubtable aalt_chain_contextual_subst AlternateChained 
+        MultipleLookupsPerGlyph MultipleLookupsPerGlyph2
     """.split()
 
     def __init__(self, methodName):
diff --git a/Tests/feaLib/data/ChainSubstSubtable.fea b/Tests/feaLib/data/ChainSubstSubtable.fea
--- a/Tests/feaLib/data/ChainSubstSubtable.fea
+++ b/Tests/feaLib/data/ChainSubstSubtable.fea
@@ -1,9 +1,9 @@
 feature test {
-    sub G' by G.swash;
+    sub A G' by G.swash;
     subtable;
-    sub H' by H.swash;
+    sub A H' by H.swash;
     subtable;
-    sub G' by g;
+    sub A G' by g;
     subtable;
-    sub H' by H.swash;
+    sub A H' by H.swash;
 } test;
diff --git a/Tests/feaLib/data/ChainSubstSubtable.ttx b/Tests/feaLib/data/ChainSubstSubtable.ttx
--- a/Tests/feaLib/data/ChainSubstSubtable.ttx
+++ b/Tests/feaLib/data/ChainSubstSubtable.ttx
@@ -34,7 +34,10 @@
         <LookupFlag value="0"/>
         <!-- SubTableCount=4 -->
         <ChainContextSubst index="0" Format="3">
-          <!-- BacktrackGlyphCount=0 -->
+          <!-- BacktrackGlyphCount=1 -->
+          <BacktrackCoverage index="0">
+            <Glyph value="A"/>
+          </BacktrackCoverage>
           <!-- InputGlyphCount=1 -->
           <InputCoverage index="0">
             <Glyph value="G"/>
@@ -47,7 +50,10 @@
           </SubstLookupRecord>
         </ChainContextSubst>
         <ChainContextSubst index="1" Format="3">
-          <!-- BacktrackGlyphCount=0 -->
+          <!-- BacktrackGlyphCount=1 -->
+          <BacktrackCoverage index="0">
+            <Glyph value="A"/>
+          </BacktrackCoverage>
           <!-- InputGlyphCount=1 -->
           <InputCoverage index="0">
             <Glyph value="H"/>
@@ -60,7 +66,10 @@
           </SubstLookupRecord>
         </ChainContextSubst>
         <ChainContextSubst index="2" Format="3">
-          <!-- BacktrackGlyphCount=0 -->
+          <!-- BacktrackGlyphCount=1 -->
+          <BacktrackCoverage index="0">
+            <Glyph value="A"/>
+          </BacktrackCoverage>
           <!-- InputGlyphCount=1 -->
           <InputCoverage index="0">
             <Glyph value="G"/>
@@ -73,7 +82,10 @@
           </SubstLookupRecord>
         </ChainContextSubst>
         <ChainContextSubst index="3" Format="3">
-          <!-- BacktrackGlyphCount=0 -->
+          <!-- BacktrackGlyphCount=1 -->
+          <BacktrackCoverage index="0">
+            <Glyph value="A"/>
+          </BacktrackCoverage>
           <!-- InputGlyphCount=1 -->
           <InputCoverage index="0">
             <Glyph value="H"/>
diff --git a/Tests/feaLib/data/SubstSubtable.fea b/Tests/feaLib/data/SubstSubtable.fea
new file mode 100644
--- /dev/null
+++ b/Tests/feaLib/data/SubstSubtable.fea
@@ -0,0 +1,9 @@
+feature test {
+    sub G' by G.swash;
+    subtable;
+    sub H' by H.swash;
+    subtable;
+    sub G' by g;
+    subtable;
+    sub H' by H.swash;
+} test;
diff --git a/Tests/feaLib/data/SubstSubtable.ttx b/Tests/feaLib/data/SubstSubtable.ttx
new file mode 100644
--- /dev/null
+++ b/Tests/feaLib/data/SubstSubtable.ttx
@@ -0,0 +1,116 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ttFont>
+
+  <GSUB>
+    <Version value="0x00010000"/>
+    <ScriptList>
+      <!-- ScriptCount=1 -->
+      <ScriptRecord index="0">
+        <ScriptTag value="DFLT"/>
+        <Script>
+          <DefaultLangSys>
+            <ReqFeatureIndex value="65535"/>
+            <!-- FeatureCount=1 -->
+            <FeatureIndex index="0" value="0"/>
+          </DefaultLangSys>
+          <!-- LangSysCount=0 -->
+        </Script>
+      </ScriptRecord>
+    </ScriptList>
+    <FeatureList>
+      <!-- FeatureCount=1 -->
+      <FeatureRecord index="0">
+        <FeatureTag value="test"/>
+        <Feature>
+          <!-- LookupCount=1 -->
+          <LookupListIndex index="0" value="0"/>
+        </Feature>
+      </FeatureRecord>
+    </FeatureList>
+    <LookupList>
+      <!-- LookupCount=5 -->
+      <Lookup index="0">
+        <LookupType value="5"/>
+        <LookupFlag value="0"/>
+        <!-- SubTableCount=4 -->
+        <ContextSubst index="0" Format="3">
+          <!-- GlyphCount=1 -->
+          <!-- SubstCount=1 -->
+          <Coverage index="0">
+            <Glyph value="G"/>
+          </Coverage>
+          <SubstLookupRecord index="0">
+            <SequenceIndex value="0"/>
+            <LookupListIndex value="1"/>
+          </SubstLookupRecord>
+        </ContextSubst>
+        <ContextSubst index="1" Format="3">
+          <!-- GlyphCount=1 -->
+          <!-- SubstCount=1 -->
+          <Coverage index="0">
+            <Glyph value="H"/>
+          </Coverage>
+          <SubstLookupRecord index="0">
+            <SequenceIndex value="0"/>
+            <LookupListIndex value="2"/>
+          </SubstLookupRecord>
+        </ContextSubst>
+        <ContextSubst index="2" Format="3">
+          <!-- GlyphCount=1 -->
+          <!-- SubstCount=1 -->
+          <Coverage index="0">
+            <Glyph value="G"/>
+          </Coverage>
+          <SubstLookupRecord index="0">
+            <SequenceIndex value="0"/>
+            <LookupListIndex value="3"/>
+          </SubstLookupRecord>
+        </ContextSubst>
+        <ContextSubst index="3" Format="3">
+          <!-- GlyphCount=1 -->
+          <!-- SubstCount=1 -->
+          <Coverage index="0">
+            <Glyph value="H"/>
+          </Coverage>
+          <SubstLookupRecord index="0">
+            <SequenceIndex value="0"/>
+            <LookupListIndex value="4"/>
+          </SubstLookupRecord>
+        </ContextSubst>
+      </Lookup>
+      <Lookup index="1">
+        <LookupType value="1"/>
+        <LookupFlag value="0"/>
+        <!-- SubTableCount=1 -->
+        <SingleSubst index="0">
+          <Substitution in="G" out="G.swash"/>
+        </SingleSubst>
+      </Lookup>
+      <Lookup index="2">
+        <LookupType value="1"/>
+        <LookupFlag value="0"/>
+        <!-- SubTableCount=1 -->
+        <SingleSubst index="0">
+          <Substitution in="H" out="H.swash"/>
+        </SingleSubst>
+      </Lookup>
+      <Lookup index="3">
+        <LookupType value="1"/>
+        <LookupFlag value="0"/>
+        <!-- SubTableCount=1 -->
+        <SingleSubst index="0">
+          <Substitution in="G" out="g"/>
+        </SingleSubst>
+      </Lookup>
+      <Lookup index="4">
+        <LookupType value="1"/>
+        <LookupFlag value="0"/>
+        <!-- SubTableCount=1 -->
+        <SingleSubst index="0">
+          <Substitution in="H" out="H.swash"/>
+        </SingleSubst>
+      </Lookup>
+    </LookupList>
+  </GSUB>
+
+</ttFont>
diff --git a/Tests/feaLib/data/bug506.ttx b/Tests/feaLib/data/bug506.ttx
--- a/Tests/feaLib/data/bug506.ttx
+++ b/Tests/feaLib/data/bug506.ttx
@@ -30,25 +30,23 @@
     <LookupList>
       <!-- LookupCount=2 -->
       <Lookup index="0">
-        <LookupType value="6"/>
+        <LookupType value="5"/>
         <LookupFlag value="0"/>
         <!-- SubTableCount=1 -->
-        <ChainContextSubst index="0" Format="3">
-          <!-- BacktrackGlyphCount=0 -->
-          <!-- InputGlyphCount=2 -->
-          <InputCoverage index="0">
+        <ContextSubst index="0" Format="3">
+          <!-- GlyphCount=2 -->
+          <!-- SubstCount=1 -->
+          <Coverage index="0">
             <Glyph value="f"/>
-          </InputCoverage>
-          <InputCoverage index="1">
+          </Coverage>
+          <Coverage index="1">
             <Glyph value="i"/>
-          </InputCoverage>
-          <!-- LookAheadGlyphCount=0 -->
-          <!-- SubstCount=1 -->
+          </Coverage>
           <SubstLookupRecord index="0">
             <SequenceIndex value="0"/>
             <LookupListIndex value="1"/>
           </SubstLookupRecord>
-        </ChainContextSubst>
+        </ContextSubst>
       </Lookup>
       <Lookup index="1">
         <LookupType value="4"/>
diff --git a/Tests/feaLib/data/bug509.ttx b/Tests/feaLib/data/bug509.ttx
--- a/Tests/feaLib/data/bug509.ttx
+++ b/Tests/feaLib/data/bug509.ttx
@@ -30,33 +30,29 @@
     <LookupList>
       <!-- LookupCount=2 -->
       <Lookup index="0">
-        <LookupType value="6"/>
+        <LookupType value="5"/>
         <LookupFlag value="0"/>
         <!-- SubTableCount=2 -->
-        <ChainContextSubst index="0" Format="3">
-          <!-- BacktrackGlyphCount=0 -->
-          <!-- InputGlyphCount=1 -->
-          <InputCoverage index="0">
-            <Glyph value="A"/>
-          </InputCoverage>
-          <!-- LookAheadGlyphCount=0 -->
+        <ContextSubst index="0" Format="3">
+          <!-- GlyphCount=1 -->
           <!-- SubstCount=0 -->
-        </ChainContextSubst>
-        <ChainContextSubst index="1" Format="3">
-          <!-- BacktrackGlyphCount=0 -->
-          <!-- InputGlyphCount=1 -->
-          <InputCoverage index="0">
+          <Coverage index="0">
+            <Glyph value="A"/>
+          </Coverage>
+        </ContextSubst>
+        <ContextSubst index="1" Format="3">
+          <!-- GlyphCount=1 -->
+          <!-- SubstCount=1 -->
+          <Coverage index="0">
             <Glyph value="A"/>
             <Glyph value="A.sc"/>
             <Glyph value="A.alt1"/>
-          </InputCoverage>
-          <!-- LookAheadGlyphCount=0 -->
-          <!-- SubstCount=1 -->
+          </Coverage>
           <SubstLookupRecord index="0">
             <SequenceIndex value="0"/>
             <LookupListIndex value="1"/>
           </SubstLookupRecord>
-        </ChainContextSubst>
+        </ContextSubst>
       </Lookup>
       <Lookup index="1">
         <LookupType value="1"/>
diff --git a/Tests/feaLib/data/bug512.ttx b/Tests/feaLib/data/bug512.ttx
--- a/Tests/feaLib/data/bug512.ttx
+++ b/Tests/feaLib/data/bug512.ttx
@@ -30,61 +30,53 @@
     <LookupList>
       <!-- LookupCount=3 -->
       <Lookup index="0">
-        <LookupType value="6"/>
+        <LookupType value="5"/>
         <LookupFlag value="0"/>
         <!-- SubTableCount=4 -->
-        <ChainContextSubst index="0" Format="3">
-          <!-- BacktrackGlyphCount=0 -->
-          <!-- InputGlyphCount=1 -->
-          <InputCoverage index="0">
-            <Glyph value="G"/>
-          </InputCoverage>
-          <!-- LookAheadGlyphCount=0 -->
+        <ContextSubst index="0" Format="3">
+          <!-- GlyphCount=1 -->
           <!-- SubstCount=1 -->
+          <Coverage index="0">
+            <Glyph value="G"/>
+          </Coverage>
           <SubstLookupRecord index="0">
             <SequenceIndex value="0"/>
             <LookupListIndex value="1"/>
           </SubstLookupRecord>
-        </ChainContextSubst>
-        <ChainContextSubst index="1" Format="3">
-          <!-- BacktrackGlyphCount=0 -->
-          <!-- InputGlyphCount=1 -->
-          <InputCoverage index="0">
-            <Glyph value="H"/>
-          </InputCoverage>
-          <!-- LookAheadGlyphCount=0 -->
+        </ContextSubst>
+        <ContextSubst index="1" Format="3">
+          <!-- GlyphCount=1 -->
           <!-- SubstCount=1 -->
+          <Coverage index="0">
+            <Glyph value="H"/>
+          </Coverage>
           <SubstLookupRecord index="0">
             <SequenceIndex value="0"/>
             <LookupListIndex value="1"/>
           </SubstLookupRecord>
-        </ChainContextSubst>
-        <ChainContextSubst index="2" Format="3">
-          <!-- BacktrackGlyphCount=0 -->
-          <!-- InputGlyphCount=1 -->
-          <InputCoverage index="0">
-            <Glyph value="G"/>
-          </InputCoverage>
-          <!-- LookAheadGlyphCount=0 -->
+        </ContextSubst>
+        <ContextSubst index="2" Format="3">
+          <!-- GlyphCount=1 -->
           <!-- SubstCount=1 -->
+          <Coverage index="0">
+            <Glyph value="G"/>
+          </Coverage>
           <SubstLookupRecord index="0">
             <SequenceIndex value="0"/>
             <LookupListIndex value="2"/>
           </SubstLookupRecord>
-        </ChainContextSubst>
-        <ChainContextSubst index="3" Format="3">
-          <!-- BacktrackGlyphCount=0 -->
-          <!-- InputGlyphCount=1 -->
-          <InputCoverage index="0">
-            <Glyph value="H"/>
-          </InputCoverage>
-          <!-- LookAheadGlyphCount=0 -->
+        </ContextSubst>
+        <ContextSubst index="3" Format="3">
+          <!-- GlyphCount=1 -->
           <!-- SubstCount=1 -->
+          <Coverage index="0">
+            <Glyph value="H"/>
+          </Coverage>
           <SubstLookupRecord index="0">
             <SequenceIndex value="0"/>
             <LookupListIndex value="2"/>
           </SubstLookupRecord>
-        </ChainContextSubst>
+        </ContextSubst>
       </Lookup>
       <Lookup index="1">
         <LookupType value="1"/>
diff --git a/Tests/feaLib/data/spec6h_ii.ttx b/Tests/feaLib/data/spec6h_ii.ttx
--- a/Tests/feaLib/data/spec6h_ii.ttx
+++ b/Tests/feaLib/data/spec6h_ii.ttx
@@ -112,25 +112,23 @@
         </MarkBasePos>
       </Lookup>
       <Lookup index="2">
-        <LookupType value="8"/>
+        <LookupType value="7"/>
         <LookupFlag value="0"/>
         <!-- SubTableCount=1 -->
-        <ChainContextPos index="0" Format="3">
-          <!-- BacktrackGlyphCount=0 -->
-          <!-- InputGlyphCount=3 -->
-          <InputCoverage index="0">
+        <ContextPos index="0" Format="3">
+          <!-- GlyphCount=3 -->
+          <!-- PosCount=2 -->
+          <Coverage index="0">
             <Glyph value="T"/>
-          </InputCoverage>
-          <InputCoverage index="1">
+          </Coverage>
+          <Coverage index="1">
             <Glyph value="c"/>
             <Glyph value="o"/>
-          </InputCoverage>
-          <InputCoverage index="2">
+          </Coverage>
+          <Coverage index="2">
             <Glyph value="grave"/>
             <Glyph value="acute"/>
-          </InputCoverage>
-          <!-- LookAheadGlyphCount=0 -->
-          <!-- PosCount=2 -->
+          </Coverage>
           <PosLookupRecord index="0">
             <SequenceIndex value="0"/>
             <LookupListIndex value="0"/>
@@ -139,7 +137,7 @@
             <SequenceIndex value="2"/>
             <LookupListIndex value="1"/>
           </PosLookupRecord>
-        </ChainContextPos>
+        </ContextPos>
       </Lookup>
     </LookupList>
   </GPOS>
diff --git a/Tests/feaLib/data/spec6h_iii_3d.ttx b/Tests/feaLib/data/spec6h_iii_3d.ttx
--- a/Tests/feaLib/data/spec6h_iii_3d.ttx
+++ b/Tests/feaLib/data/spec6h_iii_3d.ttx
@@ -30,25 +30,23 @@
     <LookupList>
       <!-- LookupCount=2 -->
       <Lookup index="0">
-        <LookupType value="8"/>
+        <LookupType value="7"/>
         <LookupFlag value="0"/>
         <!-- SubTableCount=1 -->
-        <ChainContextPos index="0" Format="3">
-          <!-- BacktrackGlyphCount=0 -->
-          <!-- InputGlyphCount=2 -->
-          <InputCoverage index="0">
+        <ContextPos index="0" Format="3">
+          <!-- GlyphCount=2 -->
+          <!-- PosCount=1 -->
+          <Coverage index="0">
             <Glyph value="L"/>
-          </InputCoverage>
-          <InputCoverage index="1">
+          </Coverage>
+          <Coverage index="1">
             <Glyph value="quoteright"/>
-          </InputCoverage>
-          <!-- LookAheadGlyphCount=0 -->
-          <!-- PosCount=1 -->
+          </Coverage>
           <PosLookupRecord index="0">
             <SequenceIndex value="1"/>
             <LookupListIndex value="1"/>
           </PosLookupRecord>
-        </ChainContextPos>
+        </ContextPos>
       </Lookup>
       <Lookup index="1">
         <LookupType value="1"/>
diff --git a/Tests/otlLib/builder_test.py b/Tests/otlLib/builder_test.py
--- a/Tests/otlLib/builder_test.py
+++ b/Tests/otlLib/builder_test.py
@@ -1392,6 +1392,57 @@ def test_stat_infinities():
     assert struct.pack(">l", posInf) == b"\x7f\xff\xff\xff"
 
 
+class ChainContextualRulesetTest(object):
+    def test_makeRulesets(self):
+        font = ttLib.TTFont()
+        font.setGlyphOrder(["a","b","c","d","A","B","C","D","E"])
+        sb = builder.ChainContextSubstBuilder(font, None)
+        prefix, input_, suffix, lookups = [["a"], ["b"]], [["c"]], [], [None]
+        sb.rules.append((prefix, input_, suffix, lookups))
+
+        prefix, input_, suffix, lookups = [["a"], ["d"]], [["c"]], [], [None]
+        sb.rules.append((prefix, input_, suffix, lookups))
+
+        sb.add_subtable_break(None)
+
+        # Second subtable has some glyph classes
+        prefix, input_, suffix, lookups = [["A"]], [["E"]], [], [None]
+        sb.rules.append((prefix, input_, suffix, lookups))
+        prefix, input_, suffix, lookups = [["A"]], [["C","D"]], [], [None]
+        sb.rules.append((prefix, input_, suffix, lookups))
+        prefix, input_, suffix, lookups = [["A", "B"]], [["E"]], [], [None]
+        sb.rules.append((prefix, input_, suffix, lookups))
+
+        sb.add_subtable_break(None)
+
+        # Third subtable has no pre/post context
+        prefix, input_, suffix, lookups = [], [["E"]], [], [None]
+        sb.rules.append((prefix, input_, suffix, lookups))
+        prefix, input_, suffix, lookups = [], [["C","D"]], [], [None]
+        sb.rules.append((prefix, input_, suffix, lookups))
+
+        rulesets = sb.rulesets()
+        assert len(rulesets) == 3
+        assert rulesets[0].hasPrefixOrSuffix
+        assert not rulesets[0].hasAnyGlyphClasses
+        cd = rulesets[0].format2ClassDefs()
+        assert set(cd[0].classes()[1:]) == set([("d",),("b",),("a",)])
+        assert set(cd[1].classes()[1:]) == set([("c",)])
+        assert set(cd[2].classes()[1:]) == set()
+
+        assert rulesets[1].hasPrefixOrSuffix
+        assert rulesets[1].hasAnyGlyphClasses
+        assert not rulesets[1].format2ClassDefs()
+
+        assert not rulesets[2].hasPrefixOrSuffix
+        assert rulesets[2].hasAnyGlyphClasses
+        assert rulesets[2].format2ClassDefs()
+        cd = rulesets[2].format2ClassDefs()
+        assert set(cd[0].classes()[1:]) == set()
+        assert set(cd[1].classes()[1:]) == set([("C","D"), ("E",)])
+        assert set(cd[2].classes()[1:]) == set()
+
+
 if __name__ == "__main__":
     import sys
 
diff --git a/Tests/varLib/data/test_results/InterpolateLayoutGPOS_8_diff.ttx b/Tests/varLib/data/test_results/InterpolateLayoutGPOS_7_diff.ttx
similarity index 87%
rename from Tests/varLib/data/test_results/InterpolateLayoutGPOS_8_diff.ttx
rename to Tests/varLib/data/test_results/InterpolateLayoutGPOS_7_diff.ttx
--- a/Tests/varLib/data/test_results/InterpolateLayoutGPOS_8_diff.ttx
+++ b/Tests/varLib/data/test_results/InterpolateLayoutGPOS_7_diff.ttx
@@ -83,23 +83,21 @@
         </MarkBasePos>
       </Lookup>
       <Lookup index="2">
-        <LookupType value="8"/>
+        <LookupType value="7"/>
         <LookupFlag value="0"/>
         <!-- SubTableCount=1 -->
-        <ChainContextPos index="0" Format="3">
-          <!-- BacktrackGlyphCount=0 -->
-          <!-- InputGlyphCount=3 -->
-          <InputCoverage index="0" Format="1">
+        <ContextPos index="0" Format="3">
+          <!-- GlyphCount=3 -->
+          <!-- PosCount=2 -->
+          <Coverage index="0" Format="1">
             <Glyph value="A"/>
-          </InputCoverage>
-          <InputCoverage index="1" Format="1">
+          </Coverage>
+          <Coverage index="1" Format="1">
             <Glyph value="a"/>
-          </InputCoverage>
-          <InputCoverage index="2" Format="1">
+          </Coverage>
+          <Coverage index="2" Format="1">
             <Glyph value="uni0303"/>
-          </InputCoverage>
-          <!-- LookAheadGlyphCount=0 -->
-          <!-- PosCount=2 -->
+          </Coverage>
           <PosLookupRecord index="0">
             <SequenceIndex value="0"/>
             <LookupListIndex value="0"/>
@@ -108,7 +106,7 @@
             <SequenceIndex value="2"/>
             <LookupListIndex value="1"/>
           </PosLookupRecord>
-        </ChainContextPos>
+        </ContextPos>
       </Lookup>
     </LookupList>
   </GPOS>
diff --git a/Tests/varLib/data/test_results/InterpolateLayoutGPOS_8_same.ttx b/Tests/varLib/data/test_results/InterpolateLayoutGPOS_7_same.ttx
similarity index 87%
rename from Tests/varLib/data/test_results/InterpolateLayoutGPOS_8_same.ttx
rename to Tests/varLib/data/test_results/InterpolateLayoutGPOS_7_same.ttx
--- a/Tests/varLib/data/test_results/InterpolateLayoutGPOS_8_same.ttx
+++ b/Tests/varLib/data/test_results/InterpolateLayoutGPOS_7_same.ttx
@@ -83,23 +83,21 @@
         </MarkBasePos>
       </Lookup>
       <Lookup index="2">
-        <LookupType value="8"/>
+        <LookupType value="7"/>
         <LookupFlag value="0"/>
         <!-- SubTableCount=1 -->
-        <ChainContextPos index="0" Format="3">
-          <!-- BacktrackGlyphCount=0 -->
-          <!-- InputGlyphCount=3 -->
-          <InputCoverage index="0" Format="1">
+        <ContextPos index="0" Format="3">
+          <!-- GlyphCount=3 -->
+          <!-- PosCount=2 -->
+          <Coverage index="0" Format="1">
             <Glyph value="A"/>
-          </InputCoverage>
-          <InputCoverage index="1" Format="1">
+          </Coverage>
+          <Coverage index="1" Format="1">
             <Glyph value="a"/>
-          </InputCoverage>
-          <InputCoverage index="2" Format="1">
+          </Coverage>
+          <Coverage index="2" Format="1">
             <Glyph value="uni0303"/>
-          </InputCoverage>
-          <!-- LookAheadGlyphCount=0 -->
-          <!-- PosCount=2 -->
+          </Coverage>
           <PosLookupRecord index="0">
             <SequenceIndex value="0"/>
             <LookupListIndex value="0"/>
@@ -108,7 +106,7 @@
             <SequenceIndex value="2"/>
             <LookupListIndex value="1"/>
           </PosLookupRecord>
-        </ChainContextPos>
+        </ContextPos>
       </Lookup>
     </LookupList>
   </GPOS>
diff --git a/Tests/varLib/interpolate_layout_test.py b/Tests/varLib/interpolate_layout_test.py
--- a/Tests/varLib/interpolate_layout_test.py
+++ b/Tests/varLib/interpolate_layout_test.py
@@ -748,8 +748,8 @@ def test_varlib_interpolate_layout_GPOS_only_LookupType_6_diff_val_ttf(self):
         self.check_ttx_dump(instfont, expected_ttx_path, tables, suffix)
 
 
-    def test_varlib_interpolate_layout_GPOS_only_LookupType_8_same_val_ttf(self):
-        """Only GPOS; LookupType 8; same values in all masters.
+    def test_varlib_interpolate_layout_GPOS_only_LookupType_7_same_val_ttf(self):
+        """Only GPOS; LookupType 7; same values in all masters.
         """
         suffix = '.ttf'
         ds_path = self.get_test_input('InterpolateLayout.designspace')
@@ -781,13 +781,13 @@ def test_varlib_interpolate_layout_GPOS_only_LookupType_8_same_val_ttf(self):
         instfont = interpolate_layout(ds_path, {'weight': 500}, finder)
 
         tables = ['GPOS']
-        expected_ttx_path = self.get_test_output('InterpolateLayoutGPOS_8_same.ttx')
+        expected_ttx_path = self.get_test_output('InterpolateLayoutGPOS_7_same.ttx')
         self.expect_ttx(instfont, expected_ttx_path, tables)
         self.check_ttx_dump(instfont, expected_ttx_path, tables, suffix)
 
 
-    def test_varlib_interpolate_layout_GPOS_only_LookupType_8_diff_val_ttf(self):
-        """Only GPOS; LookupType 8; different values in each master.
+    def test_varlib_interpolate_layout_GPOS_only_LookupType_7_diff_val_ttf(self):
+        """Only GPOS; LookupType 7; different values in each master.
         """
         suffix = '.ttf'
         ds_path = self.get_test_input('InterpolateLayout.designspace')
@@ -833,7 +833,7 @@ def test_varlib_interpolate_layout_GPOS_only_LookupType_8_diff_val_ttf(self):
         instfont = interpolate_layout(ds_path, {'weight': 500}, finder)
 
         tables = ['GPOS']
-        expected_ttx_path = self.get_test_output('InterpolateLayoutGPOS_8_diff.ttx')
+        expected_ttx_path = self.get_test_output('InterpolateLayoutGPOS_7_diff.ttx')
         self.expect_ttx(instfont, expected_ttx_path, tables)
         self.check_ttx_dump(instfont, expected_ttx_path, tables, suffix)