gruel_cache/wire_air.rs
1//! Wire format for cached AIR (ADR-0074 Phase 4).
2//!
3//! See `wire.rs` for the parse/RIR cache wire types. This module covers
4//! the typed-IR side: a [`CachedAirOutput`] envelope holding everything
5//! a downstream consumer needs to skip running sema on a file:
6//!
7//! - The per-file `InternerSnapshot` (same as parse/RIR) so cached
8//! `Spur`s can be remapped into the build's shared interner.
9//! - Each file's [`AnalyzedFunction`]s with their typed `Air`.
10//! - The serializable parts of `TypeInternPool` (the canonical
11//! `Vec<TypeData>`; structural-dedup maps reconstruct on load).
12//! - The string and byte literal tables (`strings`, `bytes`) that
13//! AIR's `*Const` instructions index into.
14//! - The interface definitions and vtable witnesses produced by sema.
15//! - The comptime `@dbg` output buffer collected during sema, so cache
16//! hits can replay it to stderr and remain observably identical to
17//! cache misses (ADR-0074 "Comptime side-effects replay" subsection).
18//!
19//! ## What this does NOT cache
20//!
21//! Compile *warnings* (`Vec<CompileWarning>`) are not yet serialized
22//! — `DiagnosticWrapper<WarningKind>` carries `Box<Diagnostic>` with
23//! complex label/note/help machinery whose serialization is its own
24//! focused implementation pass. On AIR cache hit, the build won't
25//! surface the cached warnings until that's done. This is a known
26//! regression that the integration code documents and the next
27//! follow-up will address.
28//!
29//! ## TypeId remapping
30//!
31//! The cached `TypeInternPool` is loaded into a fresh pool (replace,
32//! not merge). Cross-file caching needs a `TypeId` remap walker that
33//! visits every `Type`/`InternedType`/`StructId`/`EnumId`/`InterfaceId`
34//! field in the AIR and remaps it from cached numbering to the build's.
35//! That walker is the one piece of Phase 4 still missing; until it
36//! lands, AIR caching is single-file-only (sufficient for an end-to-end
37//! demo but not for a multi-file build).
38
39use serde::{Deserialize, Serialize};
40
41use gruel_air::{AnalyzedFunction, InterfaceDef, InterfaceVtables, TypeInternPool};
42
43use crate::wire::InternerSnapshot;
44
45/// Envelope around a per-file AIR + interner snapshot, ready for
46/// bincode serialization to the AIR cache.
47#[derive(Debug, Serialize, Deserialize)]
48pub struct CachedAirOutput {
49 /// Per-file interner snapshot (same role as in `CachedParseOutput`).
50 pub interner: InternerSnapshot,
51 /// All analyzed functions in this file with their typed AIR.
52 pub functions: Vec<AnalyzedFunction>,
53 /// Type intern pool snapshot. See `TypeInternPool`'s custom serde
54 /// impl: only the canonical `types: Vec<TypeData>` is captured;
55 /// the structural-dedup HashMaps reconstruct on load.
56 pub type_pool: TypeInternPool,
57 /// String literals, indexed by `AirInstData::StringConst` index.
58 pub strings: Vec<String>,
59 /// Byte-blob literals (`@embed_file`), indexed by `BytesConst` index.
60 pub bytes: Vec<Vec<u8>>,
61 /// Interface definitions, indexed by `InterfaceId.0`.
62 pub interface_defs: Vec<InterfaceDef>,
63 /// Vtable witnesses keyed by `(StructId, InterfaceId)`.
64 pub interface_vtables: InterfaceVtables,
65 /// Lines of `@dbg` output collected during comptime evaluation.
66 /// Replayed verbatim to stderr on cache hit so the build is
67 /// observably identical to a cold build.
68 pub comptime_dbg_output: Vec<String>,
69}
70
71impl CachedAirOutput {
72 /// Serialize to the bincode wire format used by `CacheStore::put`.
73 pub fn encode(&self) -> Result<Vec<u8>, bincode::error::EncodeError> {
74 bincode::serde::encode_to_vec(self, bincode::config::standard())
75 }
76
77 /// Deserialize from the bincode wire format.
78 pub fn decode(bytes: &[u8]) -> Result<Self, bincode::error::DecodeError> {
79 let (out, _read) = bincode::serde::decode_from_slice(bytes, bincode::config::standard())?;
80 Ok(out)
81 }
82}
83
84#[cfg(test)]
85mod tests {
86 use super::*;
87 use rustc_hash::FxHashMap;
88
89 #[test]
90 fn empty_air_output_round_trips() {
91 let cached = CachedAirOutput {
92 interner: InternerSnapshot::default(),
93 functions: Vec::new(),
94 type_pool: TypeInternPool::new(),
95 strings: Vec::new(),
96 bytes: Vec::new(),
97 interface_defs: Vec::new(),
98 interface_vtables: FxHashMap::default(),
99 comptime_dbg_output: Vec::new(),
100 };
101 let bytes = cached.encode().expect("encode");
102 let decoded = CachedAirOutput::decode(&bytes).expect("decode");
103 assert!(decoded.functions.is_empty());
104 assert!(decoded.strings.is_empty());
105 assert!(decoded.comptime_dbg_output.is_empty());
106 }
107}