Skip to content

fix: support Hunspell flag aliases and lazy affix expansion - #1160

Open
fmagnucz wants to merge 1 commit into
vale-cli:v3from
fmagnucz:fix/hunspell-flag-aliases
Open

fix: support Hunspell flag aliases and lazy affix expansion#1160
fmagnucz wants to merge 1 commit into
vale-cli:v3from
fmagnucz:fix/hunspell-flag-aliases

Conversation

@fmagnucz

@fmagnucz fmagnucz commented Aug 30, 2026

Copy link
Copy Markdown

Problem

Vale previously expanded every Hunspell dictionary entry eagerly while loading
the dictionary. It generated all reachable prefixed, suffixed, cross-product,
and continuation forms, then retained every generated surface form in memory.

This approach becomes impractical for highly inflected languages. With the
Hungarian hu_HU dictionary:

  • a controlled sample of 1,000 entries generated 3,832,856 forms;
  • the estimated full expansion was approximately 370 million generated forms;
  • an earlier full load did not finish within 45 seconds and reached about
    1.7 GiB of memory.

Correctly parsing Hunspell flag aliases made this problem more visible because
the aliases enabled many affix rules that Vale had previously ignored.

The affected dictionaries also exposed several compatibility issues:

  • AF alias indices from .dic entries were interpreted as literal flags;
  • AF alias indices in affix-rule continuation fields were also interpreted as
    literal flags, so later suffixes in a chain were never reached;
  • the default 8-bit flag format was handled inconsistently between .aff and
    .dic files;
  • UTF-8, long, and numeric flag encodings needed explicit handling;
  • a hyphen inside a Hunspell character class is literal, but Go regular
    expressions interpreted it as a range;
  • single-byte special flags could be represented differently from dictionary
    entry flags, causing valid Italian words such as di to be rejected;
  • the hand-written default spelling filters treated UTF-8 bytes as non-letters,
    so Unicode words could be skipped before dictionary lookup.

Solution

  • parse Hunspell AF flag alias tables and resolve aliases in both .dic
    entries and affix continuation fields;
  • support default 8-bit, UTF-8, long, and numeric flag encodings;
  • normalize single-flag directives consistently between .aff and .dic
    files;
  • translate literal hyphens in Hunspell affix conditions correctly;
  • retain dictionary roots and their flag vectors instead of materializing every
    derived surface form;
  • resolve affixed words lazily using indexed reverse candidates followed by
    exact forward validation;
  • restrict recursive continuation and cross-product traversal to
    reverse-reachable candidates, avoiding the combinatorial expansion exposed by
    real Hungarian continuation aliases;
  • preserve continuation flags, prefix-suffix cross-products, compounds,
    homographs, and suggestions;
  • use a bounded, thread-safe cache for repeated spell checks;
  • implement the built-in spelling-filter semantics with Unicode-aware letter
    and uppercase checks while retaining the ASCII fast path;
  • add regression coverage for aliases, encodings, recursive affixes,
    cross-products, compounds, suggestions, and the Italian di case.

Follow-up regressions

A Hungarian AsciiDoc reproducer exposed that affix continuation values also use
the AF alias table. Native Hunspell accepts the inflected forms
szoftvertervezőt, képeslapot, lekérdezéseket, kigondolása, készítése,
and dokumentálása. Vale now accepts the same forms, while the intentional typo
szoftvvvvertervezőt is still reported.

A minimal AF 2 fixture verifies that a rule such as SFX A 0 ed/2 . resolves
continuation alias 2 instead of treating it as a literal flag.

The default spelling filters now pass Unicode words to the dictionary checker
and still skip words ending in Unicode uppercase letters. The spelling
end-to-end expectation was updated to confirm that the Japanese token
オプション is no longer silently skipped.

Performance

Measurements used the system Hungarian hu_HU dictionary.

Scenario Result
Eager expansion of a representative 100-entry subset 766,715 unique surface forms
Time for the eager 100-entry subset 4.04 s
Maximum RSS for the eager 100-entry subset 492,640 KiB
Lazy loading of the complete dictionary approximately 0.19 s
Maximum RSS for the complete lazy dictionary 78,720 KiB
Roots loaded from the complete dictionary 91,242
Warm spell lookup 1.21–1.36 µs/op
Warm spell lookup allocations 37 B/op, 4 allocs/op

The eager full-dictionary figure is not presented as a completed benchmark:
the approximately 370-million-form value is an extrapolation from the
controlled 1,000-entry sample.

On the supplied Hungarian AsciiDoc reproducer, the pruned continuation lookup
completed in approximately 0.46–0.60 s. The earlier lookup took about 7.17 s,
while resolving continuation aliases without reverse-reachable pruning did not
complete within 90 s.

Automated testing

The following checks passed on the final implementation:

  • go test ./internal/spell
  • go test -race ./internal/spell
  • go vet ./internal/spell
  • go test ./internal/check
  • go test ./internal/e2e -run 'TestScenarios/checks/spelling'
  • gofmt on all changed Go files
  • git diff --check v3...HEAD

A full go test ./... run was attempted for the follow-up. It could not
complete in the restricted environment because package synchronization
required unavailable network access and a Snap test required
cap_dac_override. The generated Tree-sitter Lua NUL-character compiler
warning was non-fatal. golangci-lint was not available locally.

Cross-language smoke tests

Five dictionaries with different flag formats and morphological characteristics
were tested:

  • English: en_US
  • German: de_DE from hunspell-de-de 20161207-12
  • Italian: it_IT from hunspell-it 1:24.2.1-1
  • French: fr from hunspell-fr-classical 1:7.0-1
  • Russian: ru_RU from hunspell-ru 1:24.2.1-1

Each sentence was checked first with native Hunspell and then with the final
Vale CLI after the follow-up fixes. In every language, the correct sentence
produced no alerts and the misspelled sentence produced exactly the same three
alerts in native Hunspell and Vale.

English — en_US

Correct:

The careful engineer reviews the updated documentation before deploying the
reliable service to production.

Misspelled:

The carefull engineer revievs the updated documentaton before deploying the
reliable service to production.

Detected: carefull, revievs, documentaton.

German — de_DE

Correct:

Der freundliche Entwickler prüft heute die neue Dokumentation und installiert
anschließend das zuverlässige Programm auf dem Rechner.

Misspelled:

Der freundliche Entwikler prüft heute die neue Dokumentazion und installiert
anschließend das zuverlassige Programm auf dem Rechner.

Detected: Entwikler, Dokumentazion, zuverlassige.

Italian — it_IT

Correct:

La giovane sviluppatrice controlla attentamente la nuova documentazione prima
di installare il programma aggiornato sul computer moderno.

Misspelled:

La giovane svilupatrice controlla attentamente la nuova documentazzione prima
di installare il programma aggiornatto sul computer moderno.

Detected: svilupatrice, documentazzione, aggiornatto.

The correct sentence deliberately includes di, covering the Italian
single-byte flag regression.

French — fr

Correct:

Cette jeune développeuse relit attentivement la nouvelle documentation avant
de vérifier le programme installé sur chaque ordinateur du bureau.

Misspelled:

Cette jeune dévelopeuse relit attentivement la nouvelle documentattion avant
de vérifier le programne installé sur chaque ordinateur du bureau.

Detected: dévelopeuse, documentattion, programne.

Russian — ru_RU

Correct:

разработчик внимательно проверяет новую документацию перед установкой
обновлённой программы на рабочий компьютер в офисе.

Misspelled:

разработтчик внимателно проверяет новую докуминтацию перед установкой
обновлённой программы на рабочий компьютер в офисе.

Detected: разработтчик, внимателно, докуминтацию.

@fmagnucz
fmagnucz force-pushed the fix/hunspell-flag-aliases branch from 63b2143 to 4c0077c Compare August 30, 2026 15:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant