Problem
compute_document iterates every font glyph as a base, matches rules
against that base's name, then expands the result onto base + sfx targets.
Two different base glyphs can therefore resolve to the same target, and
the write is a plain dict assignment (apply.py:217 placed[gname] = anchors),
so the last base in font order silently wins — the earlier result is dropped,
not merged.
To be clear about the mechanism (a common misreading): the base glyph is not
matched by the variant's selector. The collision is two independent routes to
the same target glyph.
Exact condition
It needs both:
- a
!suffixes entry (or = all) that maps some base X onto target
X.sfx, and
- the glyph
X.sfx independently matching a rule on its own name — in
practice a name-glob (*.sfx), its own literal name, or a category.
If you only write base rules (by name/unicode) and fan them out via
!suffixes/all, the variants usually carry no unicode and match nothing
directly, so accumulate(X.sfx) is empty and there is no collision. (Earlier
revision of this issue overstated all as making it the common case — it does
not; the direct-match in (2) is required.)
Trace — font {C, C.sc}
!suffixes = .sc
C = top (box.center @top), bottom (box.center @bottom)
*.sc += marker (box.center @xHeight)
- base
C: matches C (not *.sc) → specs [top, bottom]; suffix .sc
→ placed["C.sc"] = [top, bottom] (C.sc written as a variant of base C)
- base
C.sc: matches *.sc → specs [marker]; suffix ""
→ placed["C.sc"] = [marker] (overwrites the previous line)
Final placed["C.sc"] is [marker] or [top, bottom] depending solely on
font iteration order. The intuitive expectation is the union [top, bottom, marker]. Because the two contributions come from different bases, the
existing per-base same-name dedup never sees them together — they only meet at
the dict key.
Locations
apply.py:181 outer loop treats each font glyph as a base;
apply.py:185 suffix expansion maps base X → target X.sfx;
apply.py:217 placed[gname] = anchors overwrites when gname was already
produced by another base.
Options
- Accumulate at the target key — if
gname already exists in placed,
merge under the same replace same-name rule (last wins per name) instead of
overwriting. Most aligned with the accumulation model; needs a defined order
between the two contributors (font order is not a good contract — e.g. prefer
the direct match, or the suffix-expanded one, deterministically).
- Define precedence — pick a winner (direct match vs suffix-expanded) and
document it. Simpler, still discards one side.
- Detect and diagnose — emit a
ComputeDiagnostic (severity="warning")
when a target is written by more than one base, so the loss is surfaced
rather than silent. Pairs with 1 or 2.
Recommendation: (1) accumulate with (3) as a safety net, once the
contributor order is decided.
Acceptance
- The trace above yields a deterministic result independent of font glyph order
(union under the chosen precedence).
- A test exercises the suffix-variant × direct-glob collision on one target.
- Anchors are never silently dropped on overlapping targets (diagnostic if so).
Problem
compute_documentiterates every font glyph as a base, matches rulesagainst that base's name, then expands the result onto
base + sfxtargets.Two different base glyphs can therefore resolve to the same target, and
the write is a plain dict assignment (
apply.py:217placed[gname] = anchors),so the last base in font order silently wins — the earlier result is dropped,
not merged.
To be clear about the mechanism (a common misreading): the base glyph is not
matched by the variant's selector. The collision is two independent routes to
the same target glyph.
Exact condition
It needs both:
!suffixesentry (or= all) that maps some baseXonto targetX.sfx, andX.sfxindependently matching a rule on its own name — inpractice a name-glob (
*.sfx), its own literal name, or a category.If you only write base rules (by name/unicode) and fan them out via
!suffixes/all, the variants usually carry no unicode and match nothingdirectly, so
accumulate(X.sfx)is empty and there is no collision. (Earlierrevision of this issue overstated
allas making it the common case — it doesnot; the direct-match in (2) is required.)
Trace — font
{C, C.sc}C: matchesC(not*.sc) → specs[top, bottom]; suffix.sc→
placed["C.sc"] = [top, bottom](C.sc written as a variant of base C)C.sc: matches*.sc→ specs[marker]; suffix""→
placed["C.sc"] = [marker](overwrites the previous line)Final
placed["C.sc"]is[marker]or[top, bottom]depending solely onfont iteration order. The intuitive expectation is the union
[top, bottom, marker]. Because the two contributions come from different bases, theexisting per-base same-name dedup never sees them together — they only meet at
the dict key.
Locations
apply.py:181outer loop treats each font glyph as a base;apply.py:185suffix expansion maps baseX→ targetX.sfx;apply.py:217placed[gname] = anchorsoverwrites whengnamewas alreadyproduced by another base.
Options
gnamealready exists inplaced,merge under the same
replacesame-name rule (last wins per name) instead ofoverwriting. Most aligned with the accumulation model; needs a defined order
between the two contributors (font order is not a good contract — e.g. prefer
the direct match, or the suffix-expanded one, deterministically).
document it. Simpler, still discards one side.
ComputeDiagnostic(severity="warning")when a target is written by more than one base, so the loss is surfaced
rather than silent. Pairs with 1 or 2.
Recommendation: (1) accumulate with (3) as a safety net, once the
contributor order is decided.
Acceptance
(union under the chosen precedence).