| Status | v4 — parse + check + IR + AOT binary caches; LSP document model |
| Owners | echo_fingerprint, echo_cache, echo_build, thin xo cache |
| Related | pipeline.md §2, architecture.md, ADR 0001 |
- Orthogonal to language meaning. Cache never redefines syntax, scopes, or runtime behavior. A hit must be bit-identical to a cold recompute of that phase.
- Phase-keyed. Artifacts are addressed by source inputs + compiler
component versions for that phase (see
phase_fingerprint). - Bump versions deliberately. When a crate changes the shape or meaning
of a cacheable output, bump the matching
*_VERSIONinecho_fingerprint. - No PHP / Composer. Project metadata is Echo-only (
ProjectMetadata); package roots followmodules.md/stdlib.md. - Hosts stay thin.
xoand LSP call these crates; they do not reimplement invalidation rules.
Project cache root: {project}/.xo/
.xo/
cache/
lex/<compiler-stamp>/ …
parse/<compiler-stamp>/ …
index/<compiler-stamp>/ …
resolve/<compiler-stamp>/ …
check/<compiler-stamp>/ …
lower/<compiler-stamp>/ …
codegen/<compiler-stamp>/ …
diagnostics/<compiler-stamp>/ …
index/ # reserved for project fact files
tmp/ # atomic renames
<compiler-stamp> is phase_fingerprint(phase, &[]) (format + phase +
component versions). Source extras stay in the blob file name. xo cache gc
deletes other stamps, leftover files in a phase root (pre-v2 flat layout), and
.xo/tmp leftovers. xo cache clean still removes the whole .xo tree.
CLI:
xo cache status [--path DIR]
xo cache doctor [--path DIR]
xo cache clean [--path DIR]
xo cache gc [--path DIR] # DIR is the project root; a file walks to Cargo.toml / .gitAligned with the standalone pipeline (not the old PHP toolchain):
| Phase | Rough producer | Typical consumer |
|---|---|---|
lex |
echo_lexer |
parse |
parse |
echo_parser / echo_ast |
index, check |
index |
echo_index |
resolve |
resolve |
echo_resolver |
check, multi-file lower |
check |
echo_semantics |
hosts, lower |
lower |
echo_hir + echo_mir |
codegen |
codegen |
echo_codegen |
run / build |
diagnostics |
shared codes | LSP / CLI |
BuildMode::Check stops after check; Execute includes lower + codegen.
Each phase fingerprint hashes CACHE_FORMAT_VERSION, the phase name, and the
versions of its CompilerComponents (lexer, parser, schemas, …).
Important: phase_components(phase) lists the full upstream stack that
can change that phase’s output without changing source bytes — not only the
crate that “owns” the phase. Example: codegen fingerprints include
mir_lowerer / hir_lowerer / semantics / parser, so a MIR/SSA fix
invalidates IR cache keys when MIR_LOWERER_VERSION (etc.) is bumped, even if
a caller forgets extra lower_fp fields.
| Mode | Behavior |
|---|---|
CacheMode::Safe |
Any component change → all phases dirty |
CacheMode::Phase |
Only phases that list the component, plus downstream cascade |
Downstream cascade: e.g. dirty parse invalidates index → resolve → check →
lower → codegen → diagnostics.
Bump the matching *_VERSION in echo_fingerprint whenever you change
cacheable compiler meaning (see crate header comments).
ArtifactPhase,CompilerComponent,CacheModeFingerprint,phase_fingerprint,phase_componentsinvalidated_phases,phase_and_downstream
CacheLayout::for_project→.xoPhaseCacheKey::for_source(phase, bytes, extra)ArtifactStore::put/get/contains/phase_counts/gc
BuildMode,BuildJob,BuildPlanplan_all_phases,filter_plan,cascade_from,phases_to_invalidateproject_root_for(Cargo.toml /.gitwalk)
| Step | Goal |
|---|---|
| v0 | Types, versions, store, plan, xo cache status/clean/doctor |
| v1 | xo check caches semantic diagnostics; --no-cache / --cache-status |
| v2 | Per-file parse AST cache (bincode) during resolve; index facts re-extracted |
| v3 | LLVM IR cache for xo run / ir / build; skips HIR/MIR/codegen on hit |
| v4 (this) | AOT binary cache; LSP document store + diagnostics over shared check; xo cache gc |
parse_with_cache(echo_parser): storesOption<File>+ diagnostics under.xo/cache/parse/.- Key: file bytes + path + Index phase fingerprint (extract/schema changes invalidate) + Parse component versions.
- On hit: deserialize AST,
remap_source_idsto the currentSourceMapid; lexer tokens are not stored (emptyLexed— use plainparsefor lex/ast tooling that needs tokens). resolve_entry_with_cacheuses it for every on-disk module; stats: hits/misses/bypasses.- Index (
extract) still runs after parse (cheap; no separate blob yet).
- Resolve always runs (closed graph); parse may hit per file.
- Caches semantic diagnostics under
.xo/cache/check/(graph content key). - Flags:
--no-cache,--cache-status(prints check + parse stats).
- After a successful check,
compile_to_llvm_withlooks up.xo/cache/codegen/with graph content + check_fp + lower_fp + codegen_fp + runtime_abi +opt(O0…OzviaOptLevel::as_str).PhaseCacheKey::for_source(Codegen, …)also hashes the full Codegen phase component stack (frontend → HIR/MIR → codegen → runtime ABI), so version bumps alone invalidate hits. - Hit: reuse LLVM IR text; skip HIR lower, MIR lower, and
emit_llvm. - Miss: lower + emit at the selected opt level, then store via
encode_ir_artifact(ECHOIR01header). - Used by
xo run,xo ir,xo build(same-O/--opt-levelsemantics; flags:--no-cache,--cache-statusprints check/parse/codegen). - Distinct opt levels never share an IR cache entry (including
O2vsOz). - Project cache root is
{project_root}/.xowhereproject_rootis found by walking up from the entry forCargo.toml/.git(else cwd). Files under/tmpmay use a different.xothan the workspace —xo cache cleanonly cleans the root you pass / the entry’s project.
- After IR is available,
xo run(AOT path) keys a native binary withaot_binary_cache_key_with_opt(ir, opt): post-opt IR bytes + opt token + runtime ABI + lower/codegen fingerprints under.xo/cache/codegen/(distinct blob name from IR text). Default run/build is dynamic link mode. - Opt participates via IR content and the explicit token (if two levels emit identical text they still do not collide).
xo build --static/--imagekeys withaot_binary_cache_key_with(ir, opt, LinkMode::Static)so a static ELF never reuses a dynamic blob (ADR 0018).- Hit: write cached bytes to a temp path and exec (skip clang).
- Miss:
link_aot(clang at-O0after in-process mid-end) then store. --cache-statusprintsaot cache: hit|miss|bypass.xo build -ouses the same AOT binary cache key and store asxo run(hit → write cached bytes to-o; miss → link, store, write-o), plus the link-mode token when--static/--imageis set.
- Crate
echo_lsp: open-document store, UTF-16 positions,analyze_pathviacheck_entry_with_overlays+ the same.xocache. - Dirty buffers are overlays (do not invent a second typechecker).
xo lsp: minimal stdio JSON-RPC —initialize, document sync,publishDiagnostics.
cargo test -p echo_fingerprint -p echo_cache -p echo_build -p echo_codegen -p echo_lsp
cargo build -p xo
./target/debug/xo cache gc
./target/debug/xo cache clean
./target/debug/xo run examples/misc/hello.echo --cache-status
./target/debug/xo run examples/misc/hello.echo --cache-status # ir + aot hitsThe prior echo-php-old tree had large fingerprint/cache/build crates including
Composer and PHP surface. This design reuses the phase + version + store
pattern only, rewritten for keyword-free Echo and the current crate graph.
Do not copy PHP-era code into this repository.