Skip to main content

aam_core/aot/
mod.rs

1use crate::aaml::parsing::{strip_comment, unwrap_quotes};
2use crate::error::AamlError;
3use crate::pipeline::Pipeline;
4use memmap2::{Mmap, MmapOptions};
5use rustc_hash::FxHasher;
6use std::fs;
7use std::fs::File;
8use std::hash::Hasher;
9use std::path::{Path, PathBuf};
10use tinyvec::TinyVec;
11
12// AOT mode-specific code paths are cfg-gated below and can coexist in all-features builds.
13
14const INVALID_INDEX: u32 = u32::MAX;
15const CURRENT_AOT_VERSION: u32 = 1;
16type FrontendErrors = TinyVec<[Option<AamlError>; 4]>;
17
18#[inline]
19fn push_frontend_error(errors: &mut FrontendErrors, err: AamlError) {
20    errors.push(Some(err));
21}
22
23#[inline]
24fn finalize_frontend_errors(errors: FrontendErrors) -> Option<Vec<AamlError>> {
25    if errors.is_empty() {
26        return None;
27    }
28
29    Some(errors.into_iter().flatten().collect())
30}
31
32#[repr(u32)]
33#[derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize, Debug, Clone, Copy)]
34pub enum NodeKind {
35    Root = 0,
36    Assignment = 1,
37}
38
39/// Byte-range into `string_blob`.
40#[repr(C)]
41#[derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize, Debug, Clone, Copy, Default)]
42pub struct Span {
43    pub start: u32,
44    pub len: u32,
45}
46
47/// Fixed-size (16-byte) node used by the cooked `SoA` layout.
48#[repr(C)]
49#[derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize, Debug, Clone, Copy)]
50pub struct FlatNode {
51    pub kind: u32,
52    pub parent: u32,
53    pub first_child: u32,
54    pub next_sibling: u32,
55}
56
57#[repr(C)]
58#[derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize, Debug, Clone, Copy)]
59pub struct SymbolEntry {
60    pub hash: u64,
61    pub node_index: u32,
62}
63
64/// Fully cooked DOD payload used by runtime mmap loading.
65#[repr(C)]
66#[derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize, Debug, Clone)]
67pub struct CookedAotBlob {
68    pub version: u32,
69    pub string_blob: Vec<u8>,
70    pub nodes: Vec<FlatNode>,
71    pub key_spans: Vec<Span>,
72    pub value_spans: Vec<Span>,
73    pub hash_table: Vec<SymbolEntry>,
74}
75
76impl CookedAotBlob {
77    fn empty() -> Self {
78        Self {
79            version: CURRENT_AOT_VERSION,
80            string_blob: Vec::new(),
81            nodes: vec![FlatNode {
82                kind: NodeKind::Root as u32,
83                parent: INVALID_INDEX,
84                first_child: INVALID_INDEX,
85                next_sibling: INVALID_INDEX,
86            }],
87            key_spans: vec![Span::default()],
88            value_spans: vec![Span::default()],
89            hash_table: Vec::new(),
90        }
91    }
92}
93
94struct SoaBuilder {
95    string_blob: Vec<u8>,
96    nodes: Vec<FlatNode>,
97    key_spans: Vec<Span>,
98    value_spans: Vec<Span>,
99    symbols: Vec<SymbolEntry>,
100    last_root_child: Option<u32>,
101    #[cfg(feature = "release")]
102    queue_type_checks: Vec<u32>,
103    #[cfg(feature = "release")]
104    queue_ref_resolve: Vec<u32>,
105}
106
107impl SoaBuilder {
108    fn with_capacity(n: usize) -> Self {
109        let base = CookedAotBlob::empty();
110        Self {
111            string_blob: Vec::with_capacity(n.saturating_mul(32)),
112            nodes: {
113                let mut v = base.nodes;
114                v.reserve(n);
115                v
116            },
117            key_spans: {
118                let mut v = base.key_spans;
119                v.reserve(n);
120                v
121            },
122            value_spans: {
123                let mut v = base.value_spans;
124                v.reserve(n);
125                v
126            },
127            symbols: Vec::with_capacity(n),
128            last_root_child: None,
129            #[cfg(feature = "release")]
130            queue_type_checks: Vec::with_capacity(n),
131            #[cfg(feature = "release")]
132            queue_ref_resolve: Vec::new(),
133        }
134    }
135
136    fn push_string(&mut self, value: &str) -> Result<Span, AamlError> {
137        let start = u32::try_from(self.string_blob.len()).map_err(|_| AamlError::InvalidValue {
138            details: "string blob exceeds u32 address space".to_string(),
139            expected: "input <= 4GB".to_string(),
140            diagnostics: None,
141        })?;
142        let len = u32::try_from(value.len()).map_err(|_| AamlError::InvalidValue {
143            details: "string length exceeds u32".to_string(),
144            expected: "single token <= 4GB".to_string(),
145            diagnostics: None,
146        })?;
147        self.string_blob.extend_from_slice(value.as_bytes());
148        Ok(Span { start, len })
149    }
150
151    fn push_assignment(&mut self, key: &str, value: &str) -> Result<u32, AamlError> {
152        let key_span = self.push_string(key)?;
153        let value_span = self.push_string(value)?;
154        let idx = u32::try_from(self.nodes.len()).map_err(|_| AamlError::InvalidValue {
155            details: "node count exceeds u32".to_string(),
156            expected: "<= 4,294,967,295 nodes".to_string(),
157            diagnostics: None,
158        })?;
159
160        self.nodes.push(FlatNode {
161            kind: NodeKind::Assignment as u32,
162            parent: 0,
163            first_child: INVALID_INDEX,
164            next_sibling: INVALID_INDEX,
165        });
166        self.key_spans.push(key_span);
167        self.value_spans.push(value_span);
168
169        match self.last_root_child {
170            Some(last) => {
171                self.nodes[last as usize].next_sibling = idx;
172            }
173            None => {
174                self.nodes[0].first_child = idx;
175            }
176        }
177        self.last_root_child = Some(idx);
178
179        #[cfg(feature = "release")]
180        {
181            self.queue_type_checks.push(idx);
182            if value.starts_with('$') {
183                self.queue_ref_resolve.push(idx);
184            }
185        }
186
187        Ok(idx)
188    }
189    #[cfg(feature = "release")]
190    fn validate_release_type_checks(&self) -> Result<(), AamlError> {
191        for &node_index in &self.queue_type_checks {
192            let i = node_index as usize;
193            if self.key_spans[i].len == 0 || self.value_spans[i].len == 0 {
194                return Err(AamlError::InvalidValue {
195                    details: format!("empty key/value at node {node_index}"),
196                    expected: "non-empty key and value".to_string(),
197                    diagnostics: None,
198                });
199            }
200        }
201
202        Ok(())
203    }
204
205    #[cfg(feature = "release")]
206    fn validate_release_references(&self) -> Result<(), AamlError> {
207        for &node_index in &self.queue_ref_resolve {
208            let value = self.span_bytes(self.value_spans[node_index as usize])?;
209            if !value.starts_with(b"$") {
210                continue;
211            }
212
213            let reference =
214                std::str::from_utf8(&value[1..]).map_err(|_| AamlError::InvalidValue {
215                    details: "reference value is not utf8".to_string(),
216                    expected: "references like $some.key".to_string(),
217                    diagnostics: None,
218                })?;
219
220            if self.find_symbol(reference).is_none() {
221                return Err(AamlError::NotFound {
222                    key: reference.to_string(),
223                    context: "release reference validation".to_string(),
224                    diagnostics: None,
225                });
226            }
227        }
228
229        Ok(())
230    }
231
232    #[cfg(feature = "release")]
233    fn run_release_validation(&self) -> Result<(), AamlError> {
234        self.validate_release_type_checks()?;
235        self.validate_release_references()
236    }
237
238    #[cfg(feature = "release")]
239    fn span_bytes(&self, span: Span) -> Result<&[u8], AamlError> {
240        let start = span.start as usize;
241        let end = start + span.len as usize;
242        self.string_blob
243            .get(start..end)
244            .ok_or_else(|| AamlError::InvalidValue {
245                details: "span points outside string blob".to_string(),
246                expected: "valid span range".to_string(),
247                diagnostics: None,
248            })
249    }
250    #[cfg(feature = "release")]
251    fn lower_bound_symbol_hash(&self, target_hash: u64) -> usize {
252        let mut left = 0usize;
253        let mut right = self.symbols.len();
254
255        while left < right {
256            let mid = left + ((right - left) / 2);
257            let entry = self.symbols[mid];
258            if entry.hash < target_hash {
259                left = mid + 1;
260            } else {
261                right = mid;
262            }
263        }
264
265        left
266    }
267
268    #[cfg(feature = "release")]
269    fn find_symbol(&self, key: &str) -> Option<u32> {
270        let target_hash = hash64(key.as_bytes());
271        let mut i = self.lower_bound_symbol_hash(target_hash);
272        while i < self.symbols.len() {
273            let entry = self.symbols[i];
274            if entry.hash != target_hash {
275                break;
276            }
277            let span = self.key_spans[entry.node_index as usize];
278            if self.span_bytes(span).ok()? == key.as_bytes() {
279                return Some(entry.node_index);
280            }
281            i += 1;
282        }
283
284        None
285    }
286
287    fn finish(self) -> CookedAotBlob {
288        let capacity = (self.symbols.len() * 2).next_power_of_two();
289        let capacity = std::cmp::max(capacity, 16);
290
291        let mut hash_table = vec![
292            SymbolEntry {
293                hash: 0,
294                node_index: INVALID_INDEX
295            };
296            capacity
297        ];
298
299        let mask = (capacity - 1) as u64;
300
301        for entry in self.symbols {
302            #[allow(clippy::cast_possible_truncation)]
303            let mut idx = (entry.hash & mask) as usize;
304            loop {
305                if hash_table[idx].node_index == INVALID_INDEX {
306                    hash_table[idx] = entry;
307                    break;
308                }
309                #[allow(clippy::cast_possible_truncation)]
310                let m_idx = mask as usize;
311                idx = (idx + 1) & m_idx;
312            }
313        }
314
315        CookedAotBlob {
316            version: CURRENT_AOT_VERSION,
317            string_blob: self.string_blob,
318            nodes: self.nodes,
319            key_spans: self.key_spans,
320            value_spans: self.value_spans,
321            hash_table,
322        }
323    }
324}
325
326#[inline]
327fn hash64(bytes: &[u8]) -> u64 {
328    let mut hasher = FxHasher::default();
329    hasher.write(bytes);
330    let h = hasher.finish();
331    h ^ h.rotate_right(32)
332}
333
334fn split_assignment(line: &str) -> Option<(&str, &str)> {
335    let pos = line.find('=')?;
336    let key = line[..pos].trim();
337    let raw_value = line[pos + 1..].trim();
338    if key.is_empty() || raw_value.is_empty() {
339        return None;
340    }
341
342    let value = if raw_value.starts_with('{') || raw_value.starts_with('[') {
343        raw_value
344    } else {
345        unwrap_quotes(raw_value)
346    };
347    Some((key, value))
348}
349
350fn should_use_pipeline_compile(text: &str) -> bool {
351    if text
352        .lines()
353        .any(|line| strip_comment(line).trim_start().starts_with('@'))
354    {
355        return true;
356    }
357    has_multiline_block(text)
358}
359
360fn process_bracket_depth(line: &str) -> i32 {
361    let mut depth: i32 = 0;
362    let mut in_string = false;
363    let mut string_char = '"';
364    for ch in line.chars() {
365        if in_string {
366            if ch == string_char {
367                in_string = false;
368            }
369            continue;
370        }
371        match ch {
372            '"' | '\'' => {
373                in_string = true;
374                string_char = ch;
375            }
376            '{' | '[' => depth += 1,
377            '}' | ']' if depth > 0 => {
378                depth -= 1;
379            }
380            _ => {}
381        }
382    }
383    depth
384}
385
386fn has_multiline_block(text: &str) -> bool {
387    for line in text.lines() {
388        let line = strip_comment(line);
389        if process_bracket_depth(line) > 0 {
390            return true;
391        }
392    }
393    false
394}
395
396fn parse_assignment_line(
397    builder: &mut SoaBuilder,
398    errors: &mut FrontendErrors,
399    line_idx: usize,
400    raw_line: &str,
401    line: &str,
402    #[cfg(feature = "dev")] dev_symbols: &mut rustc_hash::FxHashMap<u64, TinyVec<[u32; 4]>>,
403    #[cfg(feature = "dev")] dev_root_children: &mut tinyvec::ArrayVec<[u32; 16]>,
404) {
405    let Some((key, value)) = split_assignment(line) else {
406        push_frontend_error(
407            errors,
408            AamlError::ParseError {
409                line: line_idx + 1,
410                content: raw_line.to_string(),
411                details: "expected 'key = value' assignment".to_string(),
412                diagnostics: None,
413            },
414        );
415        return;
416    };
417
418    match builder.push_assignment(key, value) {
419        Ok(node_index) => {
420            let hash = hash64(key.as_bytes());
421            #[cfg(feature = "dev")]
422            {
423                dev_symbols.entry(hash).or_default().push(node_index);
424                if dev_root_children.len() < dev_root_children.capacity() {
425                    dev_root_children.push(node_index);
426                }
427            }
428            #[cfg(not(feature = "dev"))]
429            {
430                builder.symbols.push(SymbolEntry { hash, node_index });
431            }
432        }
433        Err(err) => push_frontend_error(errors, err),
434    }
435}
436
437fn compile_text(text: &str, source_dir: Option<&Path>) -> Result<CookedAotBlob, Vec<AamlError>> {
438    if should_use_pipeline_compile(text) {
439        return compile_via_pipeline(text, source_dir);
440    }
441
442    let estimated = text.lines().count();
443    let mut builder = SoaBuilder::with_capacity(estimated);
444    let mut errors: FrontendErrors = TinyVec::new();
445
446    #[cfg(feature = "dev")]
447    let mut dev_symbols: rustc_hash::FxHashMap<u64, TinyVec<[u32; 4]>> =
448        rustc_hash::FxHashMap::default();
449    #[cfg(feature = "dev")]
450    let mut dev_root_children: tinyvec::ArrayVec<[u32; 16]> = tinyvec::ArrayVec::new();
451
452    for (line_idx, raw_line) in text.lines().enumerate() {
453        let line = strip_comment(raw_line).trim();
454        if line.is_empty() {
455            continue;
456        }
457
458        if line.starts_with('@') {
459            continue;
460        }
461
462        parse_assignment_line(
463            &mut builder,
464            &mut errors,
465            line_idx,
466            raw_line,
467            line,
468            #[cfg(feature = "dev")]
469            &mut dev_symbols,
470            #[cfg(feature = "dev")]
471            &mut dev_root_children,
472        );
473    }
474
475    if let Some(errors) = finalize_frontend_errors(errors) {
476        return Err(errors);
477    }
478
479    #[cfg(feature = "dev")]
480    {
481        for (hash, indices) in dev_symbols {
482            for node_index in indices {
483                builder.symbols.push(SymbolEntry { hash, node_index });
484            }
485        }
486        let _ = dev_root_children;
487    }
488
489    builder.symbols.sort_unstable_by_key(|entry| entry.hash);
490
491    #[cfg(feature = "release")]
492    if let Err(err) = builder.run_release_validation() {
493        return Err(vec![err]);
494    }
495
496    Ok(builder.finish())
497}
498
499fn compile_via_pipeline(
500    text: &str,
501    source_dir: Option<&Path>,
502) -> Result<CookedAotBlob, Vec<AamlError>> {
503    let pipeline = Pipeline::new();
504    let output = pipeline.process_with_source_dir(text, source_dir)?;
505
506    let estimated = output.map.len();
507    let mut builder = SoaBuilder::with_capacity(estimated);
508    let mut errors: FrontendErrors = TinyVec::new();
509
510    for (key, value) in output.map {
511        let hash = hash64(key.as_bytes());
512        match builder.push_assignment(&key, &value) {
513            Ok(node_index) => {
514                builder.symbols.push(SymbolEntry { hash, node_index });
515            }
516            Err(err) => push_frontend_error(&mut errors, err),
517        }
518    }
519
520    if let Some(errors) = finalize_frontend_errors(errors) {
521        return Err(errors);
522    }
523
524    builder.symbols.sort_unstable_by_key(|entry| entry.hash);
525
526    #[cfg(feature = "release")]
527    if let Err(err) = builder.run_release_validation() {
528        return Err(vec![err]);
529    }
530
531    Ok(builder.finish())
532}
533
534fn io_err(details: impl Into<String>) -> AamlError {
535    AamlError::IoError {
536        details: details.into(),
537        diagnostics: None,
538    }
539}
540
541fn cache_path(source: &Path) -> PathBuf {
542    source.with_extension("aam.bin")
543}
544
545fn cache_needs_rebuild(source: &Path, cache: &Path) -> Result<bool, AamlError> {
546    let cache_meta = match fs::metadata(cache) {
547        Ok(meta) => meta,
548        Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(true),
549        Err(e) => {
550            return Err(io_err(format!(
551                "failed to stat cache '{}': {e}",
552                cache.display()
553            )));
554        }
555    };
556
557    let source_meta = fs::metadata(source)
558        .map_err(|e| io_err(format!("failed to stat source '{}': {e}", source.display())))?;
559
560    let cache_mtime = cache_meta
561        .modified()
562        .map_err(|e| io_err(format!("failed to read cache mtime: {e}")))?;
563    let source_mtime = source_meta
564        .modified()
565        .map_err(|e| io_err(format!("failed to read source mtime: {e}")))?;
566
567    Ok(cache_mtime < source_mtime)
568}
569
570pub struct AamCompiler;
571
572impl AamCompiler {
573    /// Compiles a text `.aam` asset into a cooked `SoA` `.aam.bin` blob.
574    ///
575    /// # Errors
576    ///
577    /// Returns errors if the source file cannot be read, fails to parse/validate,
578    /// fails to serialize or write the cooked binary cache, or encounters I/O issues.
579    pub fn cook(source_path: impl AsRef<Path>) -> Result<PathBuf, Vec<AamlError>> {
580        let source_path = source_path.as_ref();
581        let cache = cache_path(source_path);
582
583        let content = fs::read_to_string(source_path).map_err(|e| {
584            vec![io_err(format!(
585                "failed to read source asset '{}': {e}",
586                source_path.display()
587            ))]
588        })?;
589
590        let cooked = compile_text(&content, source_path.parent())?;
591        let bytes = rkyv::to_bytes::<rkyv::rancor::Error>(&cooked).map_err(|e| {
592            vec![AamlError::DirectiveError {
593                directive: "cook".to_string(),
594                message: format!("failed to archive cooked output: {e}"),
595                diagnostics: None,
596            }]
597        })?;
598
599        let tmp_cache = cache.with_extension("aam.bin.tmp");
600
601        let mut temp_file = File::create(&tmp_cache)
602            .map_err(|e| vec![io_err(format!("failed to create temp file: {e}"))])?;
603
604        std::io::Write::write_all(&mut temp_file, bytes.as_slice())
605            .map_err(|e| vec![io_err(format!("failed to write cooked cache: {e}"))])?;
606
607        temp_file
608            .sync_all()
609            .map_err(|e| vec![io_err(format!("failed to sync cooked cache: {e}"))])?;
610
611        drop(temp_file);
612
613        fs::rename(&tmp_cache, &cache)
614            .map_err(|e| vec![io_err(format!("failed to rename cooked cache: {e}"))])?;
615
616        Ok(cache)
617    }
618}
619
620/// Zero-copy runtime view over a cooked `.aam.bin` cache.
621#[derive(Debug)]
622pub struct MappedAam {
623    mmap: Mmap,
624}
625
626impl MappedAam {
627    #[must_use]
628    pub fn archived(&self) -> &rkyv::Archived<CookedAotBlob> {
629        // SAFETY: `AamLoader::load_fast` validates archive format in checked modes.
630        unsafe { rkyv::access_unchecked::<rkyv::Archived<CookedAotBlob>>(&self.mmap) }
631    }
632
633    const fn span_bounds(span: &rkyv::Archived<Span>) -> (usize, usize) {
634        let start = span.start.to_native() as usize;
635        let end = start + span.len.to_native() as usize;
636        (start, end)
637    }
638
639    #[must_use]
640    pub fn get(&self, key: &str) -> Option<&str> {
641        let archived = self.archived();
642        let table = archived.hash_table.as_slice();
643        if table.is_empty() {
644            return None;
645        }
646
647        let key_spans = archived.key_spans.as_slice();
648        let value_spans = archived.value_spans.as_slice();
649        let blob = archived.string_blob.as_slice();
650
651        let key_bytes = key.as_bytes();
652        let hash = hash64(key_bytes);
653
654        let mask = table.len() - 1;
655        #[allow(clippy::cast_possible_truncation)]
656        let mut idx = (hash as usize) & mask;
657
658        loop {
659            let entry = unsafe { table.get_unchecked(idx) };
660            let node_idx = entry.node_index.to_native() as usize;
661
662            if node_idx == INVALID_INDEX as usize {
663                return None;
664            }
665
666            if entry.hash.to_native() == hash {
667                let span = unsafe { key_spans.get_unchecked(node_idx) };
668                let start = span.start.to_native() as usize;
669                let end = start + span.len.to_native() as usize;
670
671                if blob.get(start..end) == Some(key_bytes) {
672                    let val_span = unsafe { value_spans.get_unchecked(node_idx) };
673                    let v_start = val_span.start.to_native() as usize;
674                    let v_end = v_start + val_span.len.to_native() as usize;
675
676                    return Some(unsafe {
677                        std::str::from_utf8_unchecked(blob.get_unchecked(v_start..v_end))
678                    });
679                }
680            }
681            idx = (idx + 1) & mask;
682        }
683    }
684    #[must_use]
685    pub fn len(&self) -> usize {
686        self.archived().hash_table.len()
687    }
688
689    #[must_use]
690    pub fn is_empty(&self) -> bool {
691        self.len() == 0
692    }
693
694    pub fn iter_pairs(&self) -> impl Iterator<Item = (&str, &str)> {
695        let archived = self.archived();
696        let blob = archived.string_blob.as_slice();
697        let key_spans = archived.key_spans.as_slice();
698        let value_spans = archived.value_spans.as_slice();
699
700        (1..archived.nodes.len()).filter_map(move |idx| {
701            let key = key_spans.get(idx)?;
702            let value = value_spans.get(idx)?;
703            let (k_start, k_end) = Self::span_bounds(key);
704            let (v_start, v_end) = Self::span_bounds(value);
705            let k = std::str::from_utf8(blob.get(k_start..k_end)?).ok()?;
706            let v = std::str::from_utf8(blob.get(v_start..v_end)?).ok()?;
707            Some((k, v))
708        })
709    }
710}
711
712pub struct AamLoader;
713
714impl AamLoader {
715    /// Loads a cooked `.aam.bin` cache into a zero-copy memory-mapped view.
716    ///
717    /// Rebuilds the cache if the source `.aam` file is newer than the `.aam.bin`.
718    ///
719    /// # Errors
720    ///
721    /// Returns errors if the file cannot be read, the cache cannot be memory-mapped,
722    /// or the source file is invalid and rebuilding the cache fails.
723    pub fn load_fast(source_path: impl AsRef<Path>) -> Result<MappedAam, Vec<AamlError>> {
724        let source_path = source_path.as_ref();
725        let cache = cache_path(source_path);
726
727        let needs_rebuild = cache_needs_rebuild(source_path, &cache).map_err(|e| vec![e])?;
728        if needs_rebuild {
729            AamCompiler::cook(source_path)?;
730        }
731
732        let file = File::open(&cache).map_err(|e| {
733            vec![io_err(format!(
734                "failed to open cooked cache '{}': {e}",
735                cache.display()
736            ))]
737        })?;
738
739        let mmap = unsafe {
740            MmapOptions::new().map(&file).map_err(|e| {
741                vec![io_err(format!(
742                    "failed to mmap cooked cache '{}': {e}",
743                    cache.display()
744                ))]
745            })?
746        };
747
748        #[cfg(not(feature = "unsafe_fast_path"))]
749        rkyv::access::<rkyv::Archived<CookedAotBlob>, rkyv::rancor::Error>(&mmap).map_err(|e| {
750            vec![AamlError::DirectiveError {
751                directive: "load_fast".to_string(),
752                message: format!("invalid cooked archive '{}': {e}", cache.display()),
753                diagnostics: None,
754            }]
755        })?;
756
757        let archived_version = {
758            // Safe minimal check: read only the archived version field.
759            let blob = unsafe { rkyv::access_unchecked::<rkyv::Archived<CookedAotBlob>>(&mmap) };
760            blob.version.to_native()
761        };
762
763        if archived_version != CURRENT_AOT_VERSION {
764            let _ = fs::remove_file(&cache);
765            AamCompiler::cook(source_path)?;
766            return Self::load_fast(source_path);
767        }
768
769        Ok(MappedAam { mmap })
770    }
771}