Skip to content

compute_document overwrites a suffixed target written by two different bases (last-in-loop wins) #7

Description

@typedev

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:

  1. a !suffixes entry (or = all) that maps some base X onto target
    X.sfx, and
  2. 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

  1. 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).
  2. Define precedence — pick a winner (direct match vs suffix-expanded) and
    document it. Simpler, still discards one side.
  3. 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).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions