Skip to content

Add the reading index and share the vocabulary compiler (#753, 3a) - #941

Merged
derek73 merged 6 commits into
masterfrom
feat/3a-reading-index
Oct 29, 2026
Merged

derek73 merged 6 commits into
masterfrom
feat/3a-reading-index

Conversation

@derek73

@derek73 derek73 commented Oct 29, 2026 •

Copy link
Copy Markdown
Owner

Sub-phase 3a of #753's work order: the reading index, the shared compiler, and default = false on name readings. Neutral: _compiled.py is byte-identical and tests/v2/test_snapshot.py passes unchanged.

What lands

  • The reading index, nameparser/config/_index.py (generated by tools/vocab/build.py).
    • One line per word, holding a row per reading in every file, on by default or not: (tag, kind, properties).
    • Only the properties the compiler reads are kept: each category's behavioral ones, plus same_as on a given reading. Counts, notes, exclusions and the stated may_be_given stay out.
    • 2,157 readings over 1,798 words; about 95 KB of source.
  • The compiler moves into the package, as nameparser/_reading_index.py.
    • It holds compile_readings, plus CATEGORY, BEHAVIORAL and GATED_WORDS, and the accessors for unwritten values, shared with the build's Reading.
    • The build loads it by path, as it loads _lexicon.py, so the build still never imports the package. The build keeps every check. It turns the readings into index rows in memory and writes both generated modules from those rows; it never reads _index.py back.
    • Only the compiler's own CompileError (the acronym-or-word backstop) becomes a vocabulary error (exit 2). Any other exception from it is internal (exit 3).
  • The no-choice invariant: tests/v2/test_reading_index.py holds that compile_readings(index()), rendered as the build renders it, is _compiled.py byte for byte. It also checks that the index is not stale against the TOML.
  • An anchor outside the index's own path: compiling the build's readings as written must equal compiling them after the index round trip (indexed(), to_row, from_row). This is checked over the shipped pool and over a probe that carries every behavioral property plus a given reading's same_as. Dropping any one of the nine properties from indexed() fails the test.
  • The run-time reader, for Reading.languages (added unfilled by 4a, Add Token.reading, the reading the parse took for each word (3.0, 4a) #942, and filled by a follow-up once both merge): readings_of(text) -> tuple[IndexReading, ...].
    • IndexReading(text, tag, kind, props) is frozen and slotted.
    • kind is the vocabulary's kind (credential, preposition), which is finer than ReadingKind. The mapping between them is that follow-up's.
    • The index loads on the first call.
  • default = false on given and family readings.
    • Such a reading derives no marker in the default, and counts for none of the checks that guard the default's compile.
    • An agnomen on by default needs a name reading on by default (check 11).
    • A particle whose word's only given readings are off is refused, and that question is left open (below).

Measured

All figures with bytecode cached, as a wheel install has it; Python 3.11.17, medians of fresh processes.

What Cost
Importing the index 0.56–0.68 ms, about 3% of import nameparser (18–24 ms)
First readings_of call 0.07 ms
Each later lookup about 2 µs
Compiling the whole index 2.1 ms

The import is a little above the 0.45 ms the RFC measured on a flat draft. Writing the index grouped by word is what keeps the first lookup to a single dict() call: grouping at run time cost about as much as the import itself.

For review: decided here, not settled elsewhere

Each is recorded in decisions/vocabulary-model's 2026-10-29 entry.

  1. Decided 2026-10-29: the index loads on the first readings_of call, not inside Lexicon.default(). This replaces the 2026-10-28 wording. The RFC's runtime section and the decisions entry describe the lazy load.
    • Today no parse reads the index, so no caller pays the import.
    • Once the follow-up fills Reading.languages, the first parse will load it. Every parsing caller will then pay once, and only import-only callers are spared.
  2. A particle whose word's only given readings are off is refused.
    • The default compiles such a particle never-given; a pack turning the given reading on compiles it possibly given.
    • Which of the two check 11's stated may_be_given describes is left to the first pack that needs it. None in 3.0 does: nl's prepositions wait with strict.
  3. Not touched:
    • Pickle layouts: no field is added to Lexicon or any other pickled type, so the open question of ADDED_IN on Lexicon's guard does not arise.
    • Public names: 3a forces none, so the configure-by-reading method names stay open.

Docs

  • The RFC's status line.
  • A decisions entry and a Standing line.
  • AGENTS.md's vocabulary, layering and sub-set notes.
  • The vocab README: the default row, name readings, derived markers and where CATEGORY lives.
  • mechanisms.md's COMPILED-DATA-COMMITTED entry.

The design-docs review and a code review both ran; their findings are fixed in the last commit.

Tracing the moved compiler

Plain git blame gives all of nameparser/_reading_index.py to this PR. With copy detection, most of it traces back to #737, #744, #890 and #894, where the code was written in tools/vocab/build.py:

git blame -C -C nameparser/_reading_index.py

Checks

All pass:

uv run pytest
uv run pytest -n0 -m serial
uv run mypy
uv run ruff check
uv run python tools/vocab/build.py --check
uv run --python 3.15 pytest -q

Refs #753.

🤖 Generated with Claude Code

derek73 and others added 3 commits October 29, 2026 12:38
…ge (#753, 3a)

tools/vocab/build.py now writes nameparser/config/_index.py, one row per
reading with its file's tag, beside _compiled.py, and compiles the
latter from the index through nameparser/_reading_index.py, the
compiler a choice of languages will share. A test holds that the index
compiled with no choice is the committed module. Name readings may be
written default = false: they derive no marker in the default.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The RFC's status, a decisions entry recording what 3a decided and
measured, AGENTS.md's vocabulary and layering notes, the vocab README
and mechanisms.md's COMPILED-DATA-COMMITTED entry.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Check 9 keeps reading every given reading: check 11's particle half
refuses the off-only shape first, so the docs now credit it. The vocab
README's name-reading and derived-marker sections mention default =
false; mechanisms.md counts three files; the uncached timing is a
range; a test holds that a repointed OUTPUT moves the index too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@derek73 derek73 added this to the 3.0 milestone Oct 29, 2026
@derek73 derek73 added enhancement vocabulary Per-word vocabulary model and language packs labels Oct 29, 2026
@derek73 derek73 self-assigned this Oct 29, 2026
derek73 and others added 3 commits October 29, 2026 13:09
compile_pool catches only the compiler's CompileError, so any other
exception from compile_readings is an internal error, exit 3, as the
build's docstring promises. A test compiles the build's readings as
written and as the index keeps them, so a property indexed() drops
fails it (each of nine mutations does). A test holds that
_reading_index imports nothing internal at module level. The docs say
the build writes both modules from the same rows rather than by way of
the index, and the lazy load is marked as proposed, awaiting a
decision.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
No parse reads the index yet: Reading.languages is filled by a
follow-up once 3a and 4a both merge. The RFC, the decisions entry and
the module's docstrings now say so in the future tense, and the entry
says what the lazy load buys before and after that follow-up.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@derek73
derek73 merged commit d8eef31 into master Oct 29, 2026
10 checks passed
derek73 added a commit that referenced this pull request Oct 29, 2026
Resolves docs/design/decisions/vocabulary-model.md by keeping 3a's
2026-10-29 #753 entry before 4a's, and says a follow-up adds
Reading.languages with its values (3a's RFC said #942 adds it unfilled).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@derek73
derek73 deleted the feat/3a-reading-index branch October 30, 2026 04:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement vocabulary Per-word vocabulary model and language packs

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant