Expand description
An on-disk cache of what checking one module reported (issues #3, #19).
Building a checker costs about 13.5 ms and type-checking a few thousand lines of Teal
costs about a second (crates/htl-core/benches/check.rs). A run that changed nothing
should pay neither, and a run that changed one module should pay for that module and
what depends on it rather than for the project.
§One entry per module
#3 shipped this as one entry per invocation, because at the time it looked as though skipping individual modules would change what the rest of them saw. Two things settled that: #21 made a file’s result independent of its position in the walk, and measurement showed the shared module store to be an optimisation rather than a precondition — a dependency being checked first does not change what its requirer reports. So an entry is now per module, and a run is the sum of them.
§What makes an entry
The key is the module as this invocation names it — the path spelling the checker
will put into the diagnostics, plus the lint selection and the working directory.
--strict and --format are deliberately absent: they change how a run is summarized
and what it exits with, not what any module reports, so runs that differ only in those
share their modules’ entries.
The inputs are the module and everything reading it required, by content hash. The
probes are the directories a require could resolve in, by the set of module names
in each — a new .tl appearing earlier on the search path changes what a name resolves
to while every recorded hash still matches, and nothing else would catch it. This is the
hole ccache documents in its direct mode.
§No mtimes anywhere
Content hashes only. Timestamp-based invalidation is where this class of tool historically breaks — second-granular filesystems on macOS, mtimes zeroed by Docker layers, clock skew, fresh CI checkouts invalidating everything — and hashing a module costs microseconds against the milliseconds it saves.
§Failure is a miss
Every error path here returns “no entry” rather than propagating. A corrupt file, an
unreadable directory, a store on a read-only filesystem: the module gets checked, as it
would have been anyway. The one invariant worth stating is mypy’s: an entry is written
whole or not at all, via a temporary file and a rename, so a reader never sees half of
one. Set HTL_CACHE_DEBUG=1 to print why a lookup missed.
§Why this lives in htl-core
The store began in the CLI, which was the only reader. The linker is a reader too
(#100): htl build and include_bundle! walk the require closure through
crate::link, and a module whose gen entry still holds — the same entry htl test
writes and replays — has no reason to be generated again. htl-cli depends on htl,
which depends on htl-macros, so a store the macros can open has to sit below both.
The CLI keeps its flags, its cache status command and its report types; what moved is
the store and the JSON shapes it writes, and the conversions between those shapes and
crate::CheckInfo, which every reader was carrying a copy of.
Only htl check sweeps (Cache::sweep). It is the one reader that sees the whole
project, so its keep-list is the whole project’s; a build or a macro expansion sees one
closure, and sweeping from that view would evict every other entry the project has.
Structs§
- Cache
- A store rooted at a project.
- Check
Info Json - A
CheckInfoas an entry stores it. - Contents
- What a project’s store holds.
- Dependency
Error Json - One
htl::DependencyErroras an entry stores it. - Dependency
Json - What a dependency’s diagnostic carries besides its text: the file that required it
and where the file lives. Stored with the diagnostic (
Recorded) so a replay says exactly what the run said, and decides the same way whether to say it. - Edit
Json - One replacement in a
FixJson: a half-open span and what goes there. - Entry
Summary - One entry, as
htl cache statusdescribes it. - FixJson
- A
Fixas an entry (and the CLI’s--format json) stores it. - Key
- The identity of one module under one invocation: same key, same question.
- Module
- What checking one module reported.
- Options
- Everything the store needs from outside itself.
- Recorded
- One diagnostic exactly as it was handed to the sink, so a replay goes through the same printing code the original run did rather than through a reconstruction of it. Reconstructing text from parsed fields is how a cache starts printing subtly different output from the run it claims to reproduce.
- Require
Json - One literal
requireand where the checker resolved it. Kept because the project-level cycle lint runs over every file’s requires, replayed ones included — a cycle that closes through a module nobody edited is still a cycle. - Stats
- What one run did with the store.
Enums§
- Mode
- How much of a run one entry covers.
Constants§
- CHECK
- An entry holding what checking a module reported.
- GEN
- An entry holding that, plus the Lua the module generates.
- MODULE
- An entry holding a required module’s generated Lua, keyed by the file it is.
- RUN
- One entry covering a whole walk (
Mode::WholeRun).
Functions§
- describe
- Read the store and say what is in it.
- gen_key
- The key for one test file’s checked-and-generated form.
- module_
gen_ key - The key for a module’s checked-and-generated form, by the file it is.
- module_
key - The key for one module in this invocation.
- normal
- A path in the form it is stored and compared in: absolute and symlink-resolved where possible.
- requires_
json CheckInfo’s requires in the form an entry stores them.- root_
for - Where a project’s store lives: beside the
htl.tomlfound frompath, or nowhere. - run_key
- The key for the walk as a whole, under
Mode::WholeRun. - scratch_
root - Why nothing may be written under
root, if nothing may — build scratch, named. - search_
dirs - Directories a
requirecould resolve in, listed whether or not they exist yet. - source_
mentions_ require - Whether a module’s source could
requireanything: the token appears in it.