Repository navigation
feat(lite): lazily load "*-full" dictionaries one word at a time - #649
Merged
Merged
Conversation
A dictionary file whose stem ends in "-full" (e.g. en-full.kbb, en-full.dict) is no longer read whole into the KB. It is kept open and binary-searched per word the first time that word is needed during analysis, so only the text's words are loaded instead of the entire (~370k-word) dictionary. - readKBBs/readDicts detect "*-full" files and open them lazily; if a file is not sorted (binary-search precondition) they warn and fall back to the original full load. - Refactor readKBB/readDict into shared parseKBBLine/parseDictLine so the lazy loader reuses the same parsing as the full load. - CG::findWordConcept: on a KB miss, binary-search the open file(s). The flat ".dict" (pos directly on the word) is consulted first so the tokenizer's findAttrs(word) sees pos; the ".kbb" also populates the top "dictionary" concept and links the word to its kbb entry via a "meaning" attribute. - Caching: found words via sym->con / dict_get_word; a run-wide negative cache skips re-searching absent words. - Words not found in any "*-full" file are recorded in missing-words.log (canonical lowercase, deduped) in each input file's output "*_log" folder, independently per file. Also fixes a latent Linux bug exposed by this change: the CG constructor only zeroed hkbdll_ under #ifndef LINUX, leaving the compiled-KB handle uninitialized on Linux. readKB() treats a nonzero hkbdll_ as "compiled KB loaded" and skips reading the dictionary files; adding members to CG shifted its layout so the uninitialized value became nonzero, producing an empty KB (every word "unknown") on the Linux runners. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ddehilster
force-pushed
the
feat/lite-lazy-full-dictionary
branch
from
June 11, 2026 03:13
6f633d1 to
d7218ba
Compare
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This was referenced Jun 11, 2026
ddehilster
added a commit
that referenced
this pull request
Jun 17, 2026
#669) (#670) * fix: infinite hang on input chars that sort past the last full-dict word (#669) nlp.exe hung forever (observed 11+ hours) when input contained an em-dash (U+2014) or en-dash (U+2013) -- and, generally, any character that sorts after every word in a lazily-loaded "*full" dictionary. Root cause: the lazy full-dict/full-kbb binary search (findFullWord, added in #649). nextDictKey/nextKBBKey set `afterKey = f.stream.tellg()`. Reading the file's last line (no trailing newline) leaves the stream at EOF, where tellg() returns -1. For a search key greater than every word, the lower-bound search walks to that last line; `afterKey = -1` then makes searchDictFile/searchKBBFile do `lo = -1`, so `lo < hi` stays true and the search restarts from the middle forever. Normal words sort within the file and converge before hitting this, so only past-the-end keys (em/en-dash, other high code points) trigger it. Fix: - nextDictKey / nextKBBKey: clamp `afterKey` to `f.size` when tellg() returns negative (EOF after the last line). That is the true "position past the last key", so the search terminates cleanly and reports the word absent. - searchDictFile / searchKBBFile: defensive guard -- if `afterKey <= lo` in the key<str branch, break instead of moving lo backwards/sideways (belt and suspenders against any future non-progress). Verified with the parse-pt-br analyzer (which ships pt-full.dict): em-dash, en-dash, and a full Portuguese travessao dialogue all previously hung and now complete; in-dict words still get their POS/meaning attributes; parse-en-us unaffected. Bumps engine to 3.5.7. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * test: CI regression for the full-dict binary-search hang (#669) Add a self-contained fixture analyzer that reproduces the infinite hang: a tiny sorted "zz-full.dict" (last line intentionally has no trailing newline, so the final getline hits EOF and tellg() returns -1) plus an em-dash input (which sorts after every dict word, driving the lower-bound search to that last line). Validated against the pre-fix engine: the fixture hangs (timeout) without the fix and completes with it. Wired into the Windows and Linux builds; a hang trips the step timeout and fails the job. A .gitattributes marks the dict binary so git never appends a newline or normalizes EOLs (which would mask the bug). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
A dictionary file whose stem ends in
-full(e.g.en-full.kbb,en-full.dict) is no longer read whole into the KB. It is kept open and binary-searched per word the first time that word is needed during analysis, so only the text's words are loaded instead of the entire (~370k-word) dictionary.On the
TestEnFullKbbanalyzer with a 1019-word text this took KB load from ~291 s → ~1–2 s.What it does
readKBBs/readDictsdetect*-fullfiles and open them lazily. The file must be sorted (binary-search precondition); if not, the engine warns and falls back to the original full load.readKBB/readDictintoparseKBBLine/parseDictLineso the lazy loader reuses the exact same parsing as the full load (no behavior drift for non-fullfiles).CG::findWordConcept— on a KB miss, binary-search the open file(s):.dict(which carriesposdirectly on the word) is consulted first, so the tokenizer'sfindAttrs(word)attachespos;.kbbis also consulted to populate the topdictionaryconcept (getConcept(findRoot(),"dictionary")) and to link the word to its kbb entry via ameaningattribute (mirrors the full-loadmatchDictKBpairing).sym->con/dict_get_word; a run-wide negative cache skips re-searching absent words.missing-words.log— words not found in any*-fullfile are recorded (canonical lowercase, deduped) into each input file's output*_logfolder, independently per file.Validation (built with MSVC/x86 + vcpkg ICU)
pos; e.g.long→ noun/verb/adj/adv/abbr.meaningattribute appears on every token:("meaning" concept:"<word>").kbdumptreeofdictionarycontained 530 words = 539 unique text words − 9 genuinely missing — i.e. only the text's words loaded.missing-words.logverified: all 9 logged words confirmed absent from both files; multi-file run confirmed each file's log is independent.Files
cs/libconsh/cg.cpp,include/Api/consh/cg.h— lazy loading, binary search, caching,meaninglink, missing-words log.lite/nlp_engine.cpp— points the missing-words log at each file's*_logoutput folder.Note (not in this PR)
While testing I found a pre-existing crash:
nlp.exebuilds astd::stringfrom a nullworkdirwhen-WORKis omitted (main.cpp:53). Unrelated to this change; happy to fix separately.🤖 Generated with Claude Code