Skip to content

Commit 3fdb64b

Browse files
(feat): cache external (plugin-generated) file content in the defs cache (#10164)
1 parent 1102603 commit 3fdb64b

5 files changed

Lines changed: 259 additions & 13 deletions

File tree

crates/cairo-lang-defs/src/cache/mod.rs

Lines changed: 125 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ use cairo_lang_syntax::node::ids::{GreenId, SyntaxStablePtrId};
2121
use cairo_lang_syntax::node::kind::SyntaxKind;
2222
use cairo_lang_syntax::node::{SyntaxNode, SyntaxNodeId, TypedSyntaxNode, ast};
2323
use cairo_lang_utils::Intern;
24-
use cairo_lang_utils::ordered_hash_map::OrderedHashMap;
24+
use cairo_lang_utils::ordered_hash_map::{Entry, OrderedHashMap};
2525
use salsa::Database;
2626
use serde::{Deserialize, Serialize};
2727
use thiserror::Error;
@@ -41,8 +41,11 @@ use crate::ids::{
4141
};
4242
use crate::plugin::{DynGeneratedFileAuxData, PluginDiagnostic};
4343

44+
/// The index of the `external` section in a crate cache blob.
45+
pub const EXTERNAL_CACHE_SECTION: u8 = 0;
46+
4447
/// The index of the `def` section in a crate cache blob.
45-
pub const DEF_CACHE_SECTION: u8 = 0;
48+
pub const DEF_CACHE_SECTION: u8 = EXTERNAL_CACHE_SECTION + 1;
4649

4750
/// Metadata for a cached crate.
4851
#[derive(Serialize, Deserialize)]
@@ -99,7 +102,7 @@ pub fn load_cached_crate_modules<'db>(
99102
};
100103

101104
let (metadata, module_data, defs_lookups): DefCache<'_> =
102-
CacheBlobReader::read_section(db, content, DEF_CACHE_SECTION, &crate_id);
105+
CacheBlobReader::read_section(db, content, DEF_CACHE_SECTION, crate_id);
103106

104107
validate_metadata(crate_id, &metadata, db);
105108

@@ -142,10 +145,11 @@ pub fn write_cache_section<T: Serialize>(
142145

143146
/// A cursor over the length-prefixed sections of a crate cache blob (see [`write_cache_section`]).
144147
///
145-
/// A blob is a sequence of sections in the order `[def, semantic, lowering]`; each consumer
146-
/// advances past the sections preceding the one it needs with [`Self::next_section`].
148+
/// A blob is a sequence of sections in the order `[external, def, semantic, lowering]`; each
149+
/// consumer advances past the sections preceding the one it needs with [`Self::next_section`].
147150
/// Use [`Self::read_section`] to read and parse a specific section only.
148-
/// Each section should have its index as a const in the cache mod, see [`DEF_CACHE_SECTION`] et al.
151+
/// Each section should have its index as a const in the cache mod, see [`EXTERNAL_CACHE_SECTION`]
152+
/// et al.
149153
#[derive(Debug, Clone, Copy)]
150154
pub struct CacheBlobReader<'a> {
151155
content: &'a [u8],
@@ -178,7 +182,7 @@ impl<'a> CacheBlobReader<'a> {
178182
db: &'db dyn Database,
179183
content: &'a [u8],
180184
section: u8,
181-
crate_id: &CrateId<'db>,
185+
crate_id: CrateId<'db>,
182186
) -> T {
183187
let mut reader = CacheBlobReader::new(content);
184188
for _ in 0..section {
@@ -400,6 +404,14 @@ impl<'db> DefCacheSavingContext<'db> {
400404
pub fn new(db: &'db dyn Database, self_crate_id: CrateId<'db>) -> Self {
401405
Self { db, data: DefCacheSavingData::default(), self_crate_id }
402406
}
407+
408+
/// The external-file content table, written as its own blob section (read back by the
409+
/// `external_file_contents` query).
410+
pub fn external_file_contents(
411+
&self,
412+
) -> &OrderedHashMap<ExternalFileKey, ExternalFileContentCached> {
413+
&self.data.external_file_contents
414+
}
403415
}
404416

405417
/// Data for saving cache from the database.
@@ -426,6 +438,9 @@ pub struct DefCacheSavingData<'db> {
426438
syntax_nodes: OrderedHashMap<SyntaxNode<'db>, SyntaxNodeCached>,
427439
file_ids: OrderedHashMap<FileId<'db>, FileIdCached>,
428440

441+
/// External (plugin-generated) file content, written as its own blob section in cache.
442+
external_file_contents: OrderedHashMap<ExternalFileKey, ExternalFileContentCached>,
443+
429444
pub lookups: DefCacheLookups,
430445
}
431446

@@ -1941,14 +1956,116 @@ impl FileCached {
19411956
}
19421957
}
19431958

1959+
/// Portable, parse-free identity for an external file — the key into the external-file content
1960+
/// table. Computed during cache load by reading only already-materialized fields, so it never
1961+
/// parses or re-runs plugins (which would cycle back into the cache).
1962+
#[derive(Serialize, Deserialize, Clone, Eq, Hash, PartialEq, Debug)]
1963+
pub enum ExternalFileKey {
1964+
/// An on-disk file, identified by its path.
1965+
OnDisk(PathBuf),
1966+
/// A virtual file: parent (key + span), name, and a content hash (hashed, not stored, so a
1967+
/// shared ancestor isn't duplicated across keys).
1968+
Virtual { parent: Option<(Box<ExternalFileKey>, TextSpan)>, name: String, content_hash: u64 },
1969+
/// A plugin-generated file: the parent file's key, the stable-ptr offset within it, and name.
1970+
Generated { parent: Box<ExternalFileKey>, offset: u32, name: String },
1971+
}
1972+
1973+
impl ExternalFileKey {
1974+
/// Computes the key for `file_id`; see the type docs for why this is parse/plugin/mint-free.
1975+
pub(crate) fn new<'db>(db: &'db dyn Database, file_id: FileId<'db>) -> Self {
1976+
match file_id.long(db) {
1977+
FileLongId::OnDisk(path) => ExternalFileKey::OnDisk(path.clone()),
1978+
FileLongId::Virtual(vf) => ExternalFileKey::Virtual {
1979+
parent: vf.parent.map(|parent| {
1980+
(Box::new(ExternalFileKey::new(db, parent.file_id)), parent.span)
1981+
}),
1982+
name: vf.name.long(db).to_string(),
1983+
content_hash: {
1984+
let mut hasher = xxhash_rust::xxh3::Xxh3::default();
1985+
vf.content.long(db).as_str().hash(&mut hasher);
1986+
hasher.finish()
1987+
},
1988+
},
1989+
FileLongId::External(external_id) => {
1990+
let long_id = PluginGeneratedFileId::from_intern_id(*external_id).long(db);
1991+
ExternalFileKey::Generated {
1992+
parent: Box::new(ExternalFileKey::new(db, long_id.stable_ptr.file_id(db))),
1993+
offset: long_id.stable_ptr.0.offset(db).as_u32(),
1994+
name: long_id.name.clone(),
1995+
}
1996+
}
1997+
}
1998+
}
1999+
}
2000+
2001+
/// An external file's content, enough to rebuild its `VirtualFile` on load. `parent` and `name` are
2002+
/// not stored — they come from the runtime `PluginGeneratedFileLongId` at lookup.
2003+
#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Debug)]
2004+
pub struct ExternalFileContentCached {
2005+
content: String,
2006+
code_mappings: Vec<CodeMapping>,
2007+
kind: FileKind,
2008+
original_item_removed: bool,
2009+
}
2010+
2011+
impl ExternalFileContentCached {
2012+
fn new<'db>(virtual_file: &VirtualFile<'db>, db: &'db dyn Database) -> Self {
2013+
Self {
2014+
content: virtual_file.content.to_string(db),
2015+
code_mappings: virtual_file.code_mappings.to_vec(),
2016+
kind: virtual_file.kind,
2017+
original_item_removed: virtual_file.original_item_removed,
2018+
}
2019+
}
2020+
2021+
/// Rebuilds the `VirtualFile`; `parent` and `name` come from the runtime stable ptr / long id.
2022+
pub(crate) fn to_virtual_file<'db>(
2023+
&self,
2024+
db: &'db dyn Database,
2025+
name: &str,
2026+
parent: Option<SpanInFile<'db>>,
2027+
) -> VirtualFile<'db> {
2028+
VirtualFile {
2029+
parent,
2030+
name: SmolStrId::from(db, name),
2031+
content: SmolStrId::from(db, self.content.clone()),
2032+
code_mappings: self.code_mappings.clone().into(),
2033+
kind: self.kind,
2034+
original_item_removed: self.original_item_removed,
2035+
}
2036+
}
2037+
}
2038+
19442039
#[derive(Serialize, Deserialize, Clone, Copy, Eq, Hash, PartialEq, salsa::Update, Debug)]
19452040
pub struct FileIdCached(usize);
19462041
impl FileIdCached {
19472042
fn new<'db>(id: FileId<'db>, ctx: &mut DefCacheSavingContext<'db>) -> Self {
19482043
if let Some(cached_id) = ctx.file_ids.get(&id) {
19492044
return *cached_id;
19502045
}
1951-
let cached = FileCached::new(id.long(ctx.db), ctx);
2046+
let long_id = id.long(ctx.db);
2047+
// For external (plugin-generated) files, also cache their content so `ext_as_virtual` can
2048+
// serve it from the blob instead of re-running plugins.
2049+
if let FileLongId::External(external_id) = long_id {
2050+
// Distinct `FileId`s can map to the same `ExternalFileKey` (the key is coarser), so
2051+
// dedup against the keys already stored in `external_file_contents`.
2052+
let key = ExternalFileKey::new(ctx.db, id);
2053+
let virtual_file = cairo_lang_filesystem::db::ext_as_virtual(ctx.db, *external_id);
2054+
let content = ExternalFileContentCached::new(virtual_file, ctx.db);
2055+
match ctx.external_file_contents.entry(key) {
2056+
Entry::Vacant(entry) => {
2057+
entry.insert(content);
2058+
}
2059+
// Two distinct external files collapsing to one key must carry identical content,
2060+
// else the load would serve the wrong bytes. Holds today (generated files have
2061+
// distinct parent+offset); guard it so a future key change fails loud, not silent.
2062+
Entry::Occupied(entry) => debug_assert!(
2063+
*entry.get() == content,
2064+
"external file content key collision with differing content"
2065+
),
2066+
}
2067+
}
2068+
let cached = FileCached::new(long_id, ctx);
19522069
let cached_id = FileIdCached(ctx.file_ids_lookup.len());
19532070
ctx.file_ids_lookup.push(cached);
19542071
ctx.file_ids.insert(id, cached_id);

crates/cairo-lang-defs/src/db.rs

Lines changed: 57 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ use cairo_lang_diagnostics::{
66
};
77
use cairo_lang_filesystem::db::{ExtAsVirtual, FilesGroup, files_group_input};
88
use cairo_lang_filesystem::ids::{
9-
CrateId, CrateInput, Directory, FileId, FileKind, FileLongId, SmolStrId, Tracked, VirtualFile,
9+
BlobId, CrateId, CrateInput, Directory, FileId, FileKind, FileLongId, SmolStrId, Tracked,
10+
VirtualFile,
1011
};
1112
use cairo_lang_parser::db::ParserGroup;
1213
use cairo_lang_syntax::attribute::consts::{
@@ -25,7 +26,10 @@ use cairo_lang_utils::ordered_hash_set::OrderedHashSet;
2526
use itertools::{Itertools, chain};
2627
use salsa::{Database, Setter};
2728

28-
use crate::cache::{DefCacheLoadingData, load_cached_crate_modules};
29+
use crate::cache::{
30+
CacheBlobReader, DefCacheLoadingData, EXTERNAL_CACHE_SECTION, ExternalFileContentCached,
31+
ExternalFileKey, load_cached_crate_modules,
32+
};
2933
use crate::ids::*;
3034
use crate::plugin::{DynGeneratedFileAuxData, MacroPlugin, MacroPluginMetadata, PluginDiagnostic};
3135
use crate::plugin_utils::try_extract_unnamed_arg;
@@ -1110,13 +1114,63 @@ pub fn init_external_files<T: DefsGroup>(db: &mut T) {
11101114
files_group_input(db).set_ext_as_virtual_obj(db).to(Some(ext_as_virtual_impl));
11111115
}
11121116

1117+
/// The external-file content table from a crate's cache blob, keyed only on the blob — so reading
1118+
/// it from `ext_as_virtual` adds no module-data edge, keeping cache loading cycle-free.
1119+
#[salsa::tracked(returns(ref))]
1120+
fn external_file_contents<'db>(
1121+
db: &'db dyn Database,
1122+
blob_id: BlobId<'db>,
1123+
crate_id: CrateId<'db>,
1124+
) -> OrderedHashMap<ExternalFileKey, ExternalFileContentCached> {
1125+
let Some(content) = db.blob_content(blob_id) else {
1126+
return Default::default();
1127+
};
1128+
1129+
CacheBlobReader::read_section(db, content, EXTERNAL_CACHE_SECTION, crate_id)
1130+
}
1131+
1132+
/// Rebuilds an external file's `VirtualFile` from the cache blob. `None` if the crate has no cache
1133+
/// blob or the blob lacks this file — `ext_as_virtual_impl` then falls back to `module_sub_files`.
1134+
#[salsa::tracked(returns(ref))]
1135+
fn cached_external_virtual_file<'db>(
1136+
db: &'db dyn Database,
1137+
external_file: FileId<'db>,
1138+
) -> Option<VirtualFile<'db>> {
1139+
let FileLongId::External(raw_external_id) = external_file.long(db) else {
1140+
return None;
1141+
};
1142+
let long_id = PluginGeneratedFileId::from_intern_id(*raw_external_id).long(db);
1143+
let crate_id = long_id.module_id.owning_crate(db);
1144+
let blob_id = db.crate_config(crate_id)?.cache_file?;
1145+
// Parse/plugin/mint-free, so it cannot re-enter the cache. See `ExternalFileKey::new`.
1146+
let key = ExternalFileKey::new(db, external_file);
1147+
let content = external_file_contents(db, blob_id, crate_id).get(&key)?;
1148+
Some(content.to_virtual_file(db, &long_id.name, Some(long_id.stable_ptr.span_in_file(db))))
1149+
}
1150+
11131151
/// Returns the `VirtualFile` matching the given external id.
11141152
pub fn ext_as_virtual_impl<'db>(
11151153
db: &'db dyn Database,
11161154
external_id: salsa::Id,
11171155
) -> &'db VirtualFile<'db> {
1118-
let long_id = PluginGeneratedFileId::from_intern_id(external_id).long(db);
11191156
let file_id = FileLongId::External(external_id).intern(db);
1157+
// Serve from the cache blob first (no module-data edge); see `external_file_contents`.
1158+
if let Some(virtual_file) = cached_external_virtual_file(db, file_id) {
1159+
return virtual_file;
1160+
}
1161+
let long_id = PluginGeneratedFileId::from_intern_id(external_id).long(db);
1162+
// A loaded blob collected every external file at save time, so a miss means a stale/corrupt
1163+
// cache; falling back to `module_sub_files` would re-enter the in-flight load (cycle), so fail
1164+
// loud. An absent/unreadable blob means the crate isn't cache-backed — the fallback is safe.
1165+
if let Some(blob_id) =
1166+
db.crate_config(long_id.module_id.owning_crate(db)).and_then(|config| config.cache_file)
1167+
{
1168+
assert!(
1169+
db.blob_content(blob_id).is_none(),
1170+
"external file missing from a loaded crate cache: {:?}",
1171+
file_id.long(db)
1172+
);
1173+
}
11201174
let data =
11211175
module_sub_files(db, long_id.module_id, long_id.stable_ptr.file_id(db)).as_ref().unwrap();
11221176
&data.files[&file_id]

crates/cairo-lang-lowering/src/cache/mod.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ pub fn load_cached_crate_functions<'db>(
7373
let semantic_loading_data = db.cached_crate_semantic_data(crate_id)?.loading_data;
7474

7575
let (lookups, lowerings): LookupCache =
76-
CacheBlobReader::read_section(db, content, LOWERING_CACHE_SECTION, &crate_id);
76+
CacheBlobReader::read_section(db, content, LOWERING_CACHE_SECTION, crate_id);
7777

7878
// TODO(tomer): Fail on version, cfg, and dependencies mismatch.
7979

@@ -152,6 +152,11 @@ pub fn generate_crate_cache<'db>(
152152

153153
let mut artifact = Vec::<u8>::new();
154154

155+
// External (plugin-generated) file content comes first: it's filesystem-layer content (served
156+
// by `ext_as_virtual`) that the phase sections below depend on, and putting it first lets the
157+
// `external_file_contents` query (in defs) read it without deserializing them.
158+
write_cache_section(&mut artifact, ctx.semantic_ctx.defs_ctx.external_file_contents())?;
159+
155160
write_cache_section(
156161
&mut artifact,
157162
&(CachedCrateMetadata::new(crate_id, db), def_cache, &ctx.semantic_ctx.defs_ctx.lookups),

crates/cairo-lang-lowering/src/cache/test_data/cache

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -265,3 +265,73 @@ blk0 (root):
265265
Statements:
266266
End:
267267
Return()
268+
269+
//! > ==========================================================================
270+
271+
//! > Cache Derive Drop
272+
273+
//! > test_runner_name
274+
test_cache_check
275+
276+
//! > function_code
277+
fn foo() -> felt252 {
278+
let s = MyStruct { x: 5 };
279+
s.x
280+
}
281+
282+
//! > function_name
283+
foo
284+
285+
//! > module_code
286+
#[derive(Drop)]
287+
struct MyStruct {
288+
x: felt252,
289+
}
290+
291+
//! > semantic_diagnostics
292+
293+
//! > lowering_diagnostics
294+
295+
//! > lowering_flat
296+
Parameters:
297+
blk0 (root):
298+
Statements:
299+
(v0: core::felt252) <- 5
300+
End:
301+
Return(v0)
302+
303+
//! > ==========================================================================
304+
305+
//! > Cache Derive in Submodule
306+
307+
//! > test_runner_name
308+
test_cache_check
309+
310+
//! > function_code
311+
fn foo() -> felt252 {
312+
let s = inner::InnerStruct { x: 5 };
313+
s.x
314+
}
315+
316+
//! > function_name
317+
foo
318+
319+
//! > module_code
320+
mod inner {
321+
#[derive(Drop)]
322+
pub struct InnerStruct {
323+
pub x: felt252,
324+
}
325+
}
326+
327+
//! > semantic_diagnostics
328+
329+
//! > lowering_diagnostics
330+
331+
//! > lowering_flat
332+
Parameters:
333+
blk0 (root):
334+
Statements:
335+
(v0: core::felt252) <- 5
336+
End:
337+
Return(v0)

crates/cairo-lang-semantic/src/cache/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ pub fn load_cached_crate_modules_semantic<'db>(
8080
};
8181

8282
let (module_data, semantic_lookups): SemanticCache<'_> =
83-
CacheBlobReader::read_section(db, content, SEMANTIC_CACHE_SECTION, &crate_id);
83+
CacheBlobReader::read_section(db, content, SEMANTIC_CACHE_SECTION, crate_id);
8484

8585
let mut ctx = SemanticCacheLoadingContext::new(db, semantic_lookups, def_loading_data);
8686
Some(ModuleSemanticDataCacheAndLoadingData {

0 commit comments

Comments
 (0)