@@ -21,7 +21,7 @@ use cairo_lang_syntax::node::ids::{GreenId, SyntaxStablePtrId};
2121use cairo_lang_syntax:: node:: kind:: SyntaxKind ;
2222use cairo_lang_syntax:: node:: { SyntaxNode , SyntaxNodeId , TypedSyntaxNode , ast} ;
2323use cairo_lang_utils:: Intern ;
24- use cairo_lang_utils:: ordered_hash_map:: OrderedHashMap ;
24+ use cairo_lang_utils:: ordered_hash_map:: { Entry , OrderedHashMap } ;
2525use salsa:: Database ;
2626use serde:: { Deserialize , Serialize } ;
2727use thiserror:: Error ;
@@ -41,8 +41,11 @@ use crate::ids::{
4141} ;
4242use 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 ) ]
150154pub 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 ) ]
19452040pub struct FileIdCached ( usize ) ;
19462041impl 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) ;
0 commit comments