Repository: fonttools/fonttools

Issue:
feaLib: handling of duplicate lookup references
Given the following FEA:

```fea
languagesystem DFLT dflt;
languagesystem latn dflt;

feature test {
    lookup alt1 {
	sub a by A;
    } alt1;
    lookup alt2 {
        sub b by B;
    } alt2;


    script latn;
    lookup alt1;

} test;
```

because `latn dflt` is one of the declared language systems, it inherits the two lookups that are declared at the top of the feature. The reference to `lookup alt1` under `latn` is redundant.

Currently, feaLib adds an extra lookup for this feature, even though it occurs earlier in the feature list. `makeotf` ignores the duplicate lookup, and the `latn/dflt` feature includes the two lookups inherited from `DFLT/dflt`.

I think that feaLib is incorrect here, although I'm not sure if this results in just slightly larger files, slightly less efficient shaping, or possibly errors in shaping?

(this test case is a reduction of an issue I found in about a hundred sources used by google fonts, so it may be worth some attention)


Discussion:
The extra lookup is probably harmless since it is the same feature so each lookup will be applied only once, at least HarfBuzz seems to apply each lookup only once:
```
$ hb-shape -V c.otf ab --features=+test
trace: start table GSUB	buffer: [a=0|b=1]
trace: start lookup 0 feature 'test'	buffer: [a=0|b=1]
trace: replacing glyph at 0 (single substitution)	buffer: [a=0|b=1]
trace: replaced glyph at 0 (single substitution)	buffer: [A=0|b=1]
trace: end lookup 0 feature 'test'	buffer: [A=0|b=1]
trace: start lookup 1 feature 'test'	buffer: [A=0|b=1]
trace: replacing glyph at 1 (single substitution)	buffer: [A=0|b=1]
trace: replaced glyph at 1 (single substitution)	buffer: [A=0|B=1]
trace: end lookup 1 feature 'test'	buffer: [A=0|B=1]
trace: end table GSUB	buffer: [A=0|B=1]
trace: start fallback mark	buffer: [A=0+525|B=1+525]
trace: end fallback mark	buffer: [A=0+525|B=1+525]
[A=0+525|B=1+525]
```
Sorry, I should've updated this issue: on closer look, the opentype layout algorithm (as [summarized in the FEA spec](https://github.com/adobe-type-tools/afdko/blob/develop/docs/OpenTypeFeatureFileSpecification.md#7a-an-opentype-layout-engines-layout-algorithm)) says,

> 3. Assemble all lookups in these features, in LookupList order, removing any duplicates. (All features and thus all lookups needn't be applied to every glyph in the run.)

So I agree that this isn't a correctness issue, just a bit of extra work for the shaper.




Patch:
diff --git a/Lib/fontTools/feaLib/builder.py b/Lib/fontTools/feaLib/builder.py
index 7921a3f179..a91381ddc1 100644
--- a/Lib/fontTools/feaLib/builder.py
+++ b/Lib/fontTools/feaLib/builder.py
@@ -880,8 +880,13 @@ def makeTable(self, tag):
             # l.lookup_index will be None when a lookup is not needed
             # for the table under construction. For example, substitution
             # rules will have no lookup_index while building GPOS tables.
+            # We also deduplicate lookup indices, as they only get applied once
+            # within a given feature:
+            # https://github.com/fonttools/fonttools/issues/2946
             lookup_indices = tuple(
-                [l.lookup_index for l in lookups if l.lookup_index is not None]
+                dict.fromkeys(
+                    l.lookup_index for l in lookups if l.lookup_index is not None
+                )
             )
 
             size_feature = tag == "GPOS" and feature_tag == "size"
diff --git a/Tests/feaLib/data/duplicate_lookup_reference.fea b/Tests/feaLib/data/duplicate_lookup_reference.fea
new file mode 100644
index 0000000000..f2746520e0
--- /dev/null
+++ b/Tests/feaLib/data/duplicate_lookup_reference.fea
@@ -0,0 +1,18 @@
+# https://github.com/fonttools/fonttools/issues/2946
+
+languagesystem DFLT dflt;
+languagesystem latn dflt;
+
+feature test {
+    lookup alt1 {
+	sub a by A;
+    } alt1;
+    lookup alt2 {
+        sub b by B;
+    } alt2;
+
+
+    script latn;
+    lookup alt1;
+
+} test;
diff --git a/Tests/feaLib/data/duplicate_lookup_reference.ttx b/Tests/feaLib/data/duplicate_lookup_reference.ttx
new file mode 100644
index 0000000000..e8be775c53
--- /dev/null
+++ b/Tests/feaLib/data/duplicate_lookup_reference.ttx
@@ -0,0 +1,63 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ttFont>
+
+  <GSUB>
+    <Version value="0x00010000"/>
+    <ScriptList>
+      <!-- ScriptCount=2 -->
+      <ScriptRecord index="0">
+        <ScriptTag value="DFLT"/>
+        <Script>
+          <DefaultLangSys>
+            <ReqFeatureIndex value="65535"/>
+            <!-- FeatureCount=1 -->
+            <FeatureIndex index="0" value="0"/>
+          </DefaultLangSys>
+          <!-- LangSysCount=0 -->
+        </Script>
+      </ScriptRecord>
+      <ScriptRecord index="1">
+        <ScriptTag value="latn"/>
+        <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=2 -->
+          <LookupListIndex index="0" value="0"/>
+          <LookupListIndex index="1" value="1"/>
+        </Feature>
+      </FeatureRecord>
+    </FeatureList>
+    <LookupList>
+      <!-- LookupCount=2 -->
+      <Lookup index="0">
+        <LookupType value="1"/>
+        <LookupFlag value="0"/>
+        <!-- SubTableCount=1 -->
+        <SingleSubst index="0">
+          <Substitution in="a" out="A"/>
+        </SingleSubst>
+      </Lookup>
+      <Lookup index="1">
+        <LookupType value="1"/>
+        <LookupFlag value="0"/>
+        <!-- SubTableCount=1 -->
+        <SingleSubst index="0">
+          <Substitution in="b" out="B"/>
+        </SingleSubst>
+      </Lookup>
+    </LookupList>
+  </GSUB>
+
+</ttFont>


Test patch:
diff --git a/Tests/feaLib/builder_test.py b/Tests/feaLib/builder_test.py
index 875223af74..4cb9b4a1d2 100644
--- a/Tests/feaLib/builder_test.py
+++ b/Tests/feaLib/builder_test.py
@@ -81,7 +81,7 @@ class BuilderTest(unittest.TestCase):
         MultipleLookupsPerGlyph MultipleLookupsPerGlyph2 GSUB_6_formats
         GSUB_5_formats delete_glyph STAT_test STAT_test_elidedFallbackNameID
         variable_scalar_valuerecord variable_scalar_anchor variable_conditionset
-        variable_mark_anchor
+        variable_mark_anchor duplicate_lookup_reference
     """.split()
 
     VARFONT_AXES = [