Skip to content

Shrink compressed translations: dense alphabet, per-class Huffman, optimal parsing - #11369

Open
dhalbert wants to merge 3 commits into
adafruit:translation-shrinkfrom
dhalbert:smaller-translations
Open

Shrink compressed translations: dense alphabet, per-class Huffman, optimal parsing#11369
dhalbert wants to merge 3 commits into
adafruit:translation-shrinkfrom
dhalbert:smaller-translations

Conversation

@dhalbert

Copy link
Copy Markdown
Collaborator

Claude wrote the code and did the measurements in this PR, with @dhalbert directing and reviewing each step.

Motivation

The tightest SAMD builds are limited by whichever language compresses worst, so a new feature has to fit in every language. This PR shrinks the per-language translation block so there's more room for new firmware.

What changes

  • py/maketranslationdata.py: a language's non-ASCII characters are renumbered into 0x80.. by frequency and mapped back through a new alphabet[] table, so every symbol fits in 8 bits and the dictionary gets the remaining code points. Languages with more than 127 distinct non-ASCII characters (ja, ko) keep the existing 16-bit fallback.
  • The Huffman coding scheme itself is unchanged: still one canonical code over characters, dictionary words and the qstr escape, decoded by the same bit-by-bit walk. However, previously one table served every position in a string. Now there are several, and the table for the next symbol is chosen by the class of the byte just decoded. After a space the next symbol is almost always a word start; after a lowercase letter it is a letter, space or punctuation. Each table's code lengths fit that narrower distribution, so common symbols get shorter codes than one global table could give them.
  • The decoder sorts the previous byte into one of seven fixed classes: start of string or space, a-z, A-Z, 0-9, %, other ASCII, and >= 0x80. Seven tables are not always worth their size, so the generator emits a small class_map[] that assigns each of the seven classes to a table; classes that share a table share a code. The generator tries a few such assignments (one table, three, six, seven), and keeps the one with the smallest total of string data plus tables. On the boards measured here the three-table split (start or space, letter, other) won.
  • Each string is tokenized by dynamic programming for the fewest bits instead of greedy longest match, which also decides per occurrence whether a qstr reference pays off. Dictionary words left unused are dropped.
  • supervisor/shared/translate/translate.c: the decoder indexes lengths[] and values[] per class and maps alphabet symbols back to code points. decompress() and decompress_length() keep their signatures, so there are no caller changes.
  • supervisor/shared/translate/compressed_string.h: format comment rewritten for the new layout. Symbol values 2 and 3 are reserved for a follow-up PR for CJK languages (now ja and ko).

Results

Bytes free in flash, same commit, before and after:

Board / language Before After Gain
metro_m4_express fr 1,608 2,816 +1,208
metro_m4_express pl 1,980 3,140 +1,160
metro_m4_express de_DE 2,028 2,696 +668
metro_m4_express en_US 4,964 5,240 +276
metro_m0_express pl 500 1,172 +672
metro_m0_express fr 676 916 +240
metro_m0_express de_DE 616 804 +188
metro_m0_express en_US 2,340 2,344 +4

The tightest language on metro_m4_express goes from 1,608 to 2,696 bytes free, and on metro_m0_express from 500 to 804. The decoder grows 108 bytes on Cortex-M4 and 76 on Cortex-M0+. The generator takes about 1 to 2 seconds longer per build.

Testing

  • Unix coverage port: 979 tests pass.
  • Metro M4 Express in fr and en_US: locale.getlocale(), ValueError, %q and %d formatted messages, the errno text path, SyntaxError and accented French text all decode correctly at the REPL.
  • metro_m4_express ja builds and uses the 16-bit fallback; raspberry_pi_pico builds at compression level 1 (no dictionary, no qstrs); mpy-cross builds.
  • ports/zephyr-cp invokes the generator with the same flags but was not built.

Follow-ups

  • ja and ko: replace the 16-bit fallback with escapes for rare characters, measured at roughly 13% for both.

…timal parsing

Rework the translation compressor in `py/maketranslationdata.py` and the
decoder in `translate.c`:

- Renumber a language's non-ASCII characters into 0x80.. by frequency and
  map them back through `alphabet[]`, so every symbol fits in 8 bits and
  the dictionary gets the remaining code points. Languages with more than
  127 distinct non-ASCII characters (ja, ko) keep the 16-bit fallback.
- Choose Huffman codes per class of the previously decoded byte (start or
  space, letter, other, ...) through `class_map[]`; the generator tries
  several class presets and keeps the smallest result.
- Tokenize each string by dynamic programming for the fewest bits instead
  of greedy longest match, deciding per occurrence whether a qstr
  reference pays off. Dictionary words left unused are dropped.

The format is documented in `compressed_string.h`. Same-commit gains on
the tightest languages: metro_m4_express fr +1,208 bytes free, pl +1,160;
metro_m0_express pl +672. The decoder grows 76-108 bytes. Verified on the
unix coverage port (979 tests) and on a Metro M4 Express in fr and en_US.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@dhalbert
dhalbert changed the base branch from main to translation-shrink September 13, 2026 21:48
@dhalbert
dhalbert requested a review from tannewt September 14, 2026 00:29
Add `translation_class_t` and `translation_symbol_t` enums to
`compressed_string.h` and use the names in `base_class()`, the decode
loop, and the matching Python constants in `maketranslationdata.py`,
instead of bare 0..6 and 1. No change to generated data or firmware.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Write each preset as the list of Huffman tables to build, each naming the
base classes that share it, and derive `class_map[]` with `class_map_for()`
instead of spelling out index tuples. Generated output is unchanged.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
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