Skip to main content

wamex_cli/analysis/
split_point.rs

1use std::{
2    collections::{BTreeMap, BTreeSet},
3    fmt::{Debug, Display},
4};
5
6use anyhow::{anyhow, bail};
7use wamex_types::map_vec::MiniSet;
8
9use super::dep_graph::DepGraph;
10use crate::{
11    SplitPointExtractor,
12    analysis::{
13        self,
14        dep_graph::{DepMiniSet, DepSet, NamedGraph, find_reachable_deps},
15        symbols::SymbolKind,
16    },
17    index::{ExportId, IdMap, ImportId, InputFuncId, SymbolId},
18};
19
20// TODO: impl merge and use it in emit_modules as one of strategies to emit modules.
21// The other possible is to emit it as separate chunk and allow linkage.
22#[derive(Default, Clone)]
23pub struct OutputModuleInfo {
24    pub defined_symbols: DepSet,
25    // Shared imports that should be imported from other modules.
26    pub imports: DepMiniSet,
27    pub exports: DepMiniSet,
28    // TODO: Instead of split points we need list of what "split-points" we exports, and what we imports
29    pub split_points: Vec<SplitPoint>,
30}
31
32#[derive(Debug, PartialEq, Eq, Clone)]
33pub struct SplitPoint {
34    // Name of split function that will be moved to the submodule.
35    module_name: String,
36    // Unique id to identify split point functions.
37    unique_id: String,
38    // Index in imports[] of the module import function.
39    import: ImportId,
40    // Index in functions[] of the corespoinding import
41    import_func: InputFuncId,
42    // Index in exports[] of the module export function.
43    export: ExportId,
44    // Index in functions[] of corespoiding export
45    export_func: InputFuncId,
46}
47
48impl SplitPoint {
49    pub fn import_func(&self) -> InputFuncId {
50        self.import_func
51    }
52    pub fn export_func(&self) -> InputFuncId {
53        self.export_func
54    }
55}
56
57pub(crate) fn parser<'a>(name: &'a str, prefix: &str, postfix: &str) -> Option<(&'a str, &'a str)> {
58    if !name.starts_with(prefix) {
59        return None;
60    }
61    let name = &name[prefix.len()..];
62    let postfix_index = name.find(postfix)?;
63    let module_name = &name[..postfix_index];
64    let fn_name = &name[postfix_index + postfix.len()..];
65
66    Some((module_name, fn_name))
67}
68
69pub(crate) const SPLIT_IMPORT_POSTFIX: &str = "00_import_";
70pub(crate) const SPLIT_EXPORT_POSTFIX: &str = "00_export_";
71fn find_split_points_with_prefix(
72    info: &analysis::ModuleInfo,
73    prefix: &str,
74) -> anyhow::Result<Vec<SplitPoint>> {
75    macro_rules! process_imports_or_exports {
76        ($postfix: expr, $map:ident, $member:ident, $id_ty:ty) => {
77            let $map = info
78                .wasm
79                .$member
80                .iter()
81                .filter_map(|(id, item)| {
82                    if let Some((module_name, unique_id)) = parser(&item.name, prefix, $postfix) {
83                        Some(((module_name.into(), unique_id.into()), id))
84                    } else {
85                        None
86                    }
87                })
88                .collect::<BTreeMap<(String, String), $id_ty>>();
89        };
90    }
91
92    process_imports_or_exports!(SPLIT_IMPORT_POSTFIX, import_map, imports, ImportId);
93    process_imports_or_exports!(SPLIT_EXPORT_POSTFIX, export_map, exports, ExportId);
94    let mut export_map = export_map;
95
96    let split_points = import_map
97        .into_iter()
98        .map(|(key, import_id)| -> anyhow::Result<SplitPoint> {
99            let export_id = export_map.remove(&key).ok_or_else(|| {
100                anyhow::anyhow!("No corresponding export for split import {key:?}")
101            })?;
102            let export = info.wasm.exports[export_id];
103            let wasmparser::Export {
104                kind: wasmparser::ExternalKind::Func,
105                index,
106                ..
107            } = export
108            else {
109                bail!("Expected exported function but received: {export:?}");
110            };
111            let &import_func = info
112                .import_info
113                .imported_func_map
114                .get(import_id)
115                .ok_or_else(|| {
116                    anyhow!(
117                        "Expected imported function but received: {:?}",
118                        &info.wasm.imports[import_id]
119                    )
120                })?;
121            Ok(SplitPoint {
122                module_name: key.0,
123                unique_id: key.1,
124                import: import_id,
125                import_func,
126                export: export_id,
127                export_func: InputFuncId::from_index(index),
128            })
129        })
130        .collect::<anyhow::Result<Vec<SplitPoint>>>()?;
131
132    if let Some((key, _)) = export_map.iter().next() {
133        log::error!(
134            "No corresponding import for split export {key:?} hash {key_hash:?}. Maybe split module is defined but not used.",
135            key_hash = key.1,
136            key = key.0
137        );
138    }
139
140    Ok(split_points)
141}
142
143/// Search for _wasm_split_00<module_name>00_import_<import_id> and
144/// _wasm_split_00<module_name>00_export_<export_id> functions
145/// and extract them as SplitPoints.
146pub fn find_split_points_legacy(info: &analysis::ModuleInfo) -> anyhow::Result<Vec<SplitPoint>> {
147    find_split_points_with_prefix(info, "__wasm_split_00")
148}
149pub(crate) const WAMEX_ENTRY_PREFIX: &str = "__wamex_00";
150/// Search for __wamex_00<module_name>00_import_<import_id> and
151/// __wamex_00<module_name>00_export_<export_id> functions
152/// and extract them as SplitPoints.
153fn find_split_points_wamex(info: &analysis::ModuleInfo) -> anyhow::Result<Vec<SplitPoint>> {
154    find_split_points_with_prefix(info, WAMEX_ENTRY_PREFIX)
155}
156
157pub fn find_split_points(
158    info: &analysis::ModuleInfo,
159    split_point_type: SplitPointExtractor,
160) -> anyhow::Result<Vec<SplitPoint>> {
161    match split_point_type {
162        SplitPointExtractor::Legacy => find_split_points_legacy(info),
163        SplitPointExtractor::Wamex => find_split_points_wamex(info),
164    }
165}
166
167#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone)]
168pub enum ModuleIdentifier {
169    Main,
170    Split(String),
171}
172
173#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone)]
174pub struct SharedModuleIdentifier(pub Vec<ModuleIdentifier>);
175
176impl Display for SharedModuleIdentifier {
177    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
178        let mut components = self.0.iter();
179        let Some(first) = components.next() else {
180            return Ok(());
181        };
182
183        write!(f, "{}", first)?;
184        components.try_for_each(|n| write!(f, "_{}", n))?;
185        Ok(())
186    }
187}
188
189impl SharedModuleIdentifier {
190    pub fn contains(&self, module: &ModuleIdentifier) -> bool {
191        self.0.iter().any(|m| m == module)
192    }
193    // Return true if a module was removed.
194    pub fn remove(&mut self, module: &ModuleIdentifier) -> bool {
195        let original_len = self.0.len();
196        self.0.retain(|m| m != module);
197        original_len != self.0.len()
198    }
199    pub fn includes(&self, other: &SplitModuleIdentifier) -> bool {
200        match other {
201            SplitModuleIdentifier::Single(name) => self.contains(name),
202            SplitModuleIdentifier::Shared(shared) => {
203                shared.0.iter().all(|name| self.contains(name))
204            }
205        }
206    }
207}
208
209impl PartialEq<SplitModuleIdentifier> for SharedModuleIdentifier {
210    fn eq(&self, other: &SplitModuleIdentifier) -> bool {
211        match other {
212            SplitModuleIdentifier::Single(_) => false,
213            SplitModuleIdentifier::Shared(shared) => shared == self,
214        }
215    }
216}
217
218impl<'a> IntoIterator for &'a SharedModuleIdentifier {
219    type Item = &'a ModuleIdentifier;
220    type IntoIter = std::slice::Iter<'a, ModuleIdentifier>;
221
222    fn into_iter(self) -> Self::IntoIter {
223        self.0.iter()
224    }
225}
226
227#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone)]
228pub enum SplitModuleIdentifier {
229    Single(ModuleIdentifier),
230    Shared(SharedModuleIdentifier),
231}
232
233impl Display for ModuleIdentifier {
234    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
235        match self {
236            Self::Main => write!(f, "main"),
237            Self::Split(name) => write!(f, "{}", name),
238        }
239    }
240}
241
242impl Display for SplitModuleIdentifier {
243    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
244        match self {
245            Self::Single(name) => Display::fmt(name, f),
246            Self::Shared(name) => Display::fmt(name, f),
247        }
248    }
249}
250
251impl SplitModuleIdentifier {
252    pub fn as_single(&self) -> Option<&ModuleIdentifier> {
253        match self {
254            Self::Single(name) => Some(name),
255            Self::Shared(_) => None,
256        }
257    }
258    pub fn as_shared(&self) -> Option<&SharedModuleIdentifier> {
259        match self {
260            Self::Single(_) => None,
261            Self::Shared(name) => Some(name),
262        }
263    }
264    pub fn is_shared(&self) -> bool {
265        matches!(self, Self::Shared(_))
266    }
267    pub fn is_main(&self) -> bool {
268        matches!(self, Self::Single(ModuleIdentifier::Main))
269    }
270
271    /// Check if this split module identifier includes the given module identifier.
272    pub fn is_part_of(&self, other: &SharedModuleIdentifier) -> bool {
273        match self {
274            Self::Single(name) => other.contains(name),
275            Self::Shared(shared) => shared.0.iter().all(|name| other.contains(name)),
276        }
277    }
278
279    // List all shared modules which name include this split module.
280    // This modules are (directly or indirectly) called by the current module.
281    // NOTE: This method can return modules that are not directly connected to this module.
282    //
283    // Example, consider next tree deps:
284    // Single(A) -> Shared(A,B)
285    // Single(B) -> Shared(A,B)
286    // Single(C) -> Shared(B,C)
287    // Shared(A,B) -> Shared(A,B,C);
288    //
289    // This method will return:
290    // For Single(A) -> [Shared(A,B), Shared(A,B,C)]
291    // For Shared(A,B) -> [Shared(A,B,C)]
292    // ...
293    pub fn collect_deps(
294        &self,
295        shared_modules: &[SharedModuleIdentifier],
296    ) -> Vec<SharedModuleIdentifier> {
297        let mut result = Vec::new();
298        for shared_module in shared_modules {
299            if matches!(&self, SplitModuleIdentifier::Shared(our_module) if shared_module == our_module)
300            {
301                continue; // skip self
302            }
303            if self.is_part_of(shared_module) {
304                result.push(shared_module.clone());
305            }
306        }
307        result
308    }
309}
310
311#[derive(Debug, Default)]
312pub struct SplitProgramInfo {
313    pub output_modules: Vec<(SplitModuleIdentifier, OutputModuleInfo)>,
314    pub symbol_output_module: IdMap<SymbolId, usize>,
315}
316
317impl SplitProgramInfo {
318    // Add start_func, exports and imports
319    // Filter-out all split points related functions.
320    fn get_main_module_roots(
321        info: &analysis::ModuleInfo,
322        split_points: &[SplitPoint],
323        wbg_descriptors: &MiniSet<SymbolId>,
324    ) -> DepSet {
325        let mut roots: DepSet = DepSet::new();
326        if let Some(id) = info.wasm.code.section_payload.start_func {
327            roots.insert(info.symbols.get_function_symbol(id).unwrap());
328        }
329        for (_id, export) in info.wasm.exports.iter() {
330            let wasmparser::Export {
331                index,
332                kind: wasmparser::ExternalKind::Func,
333                ..
334            } = export
335            else {
336                continue;
337            };
338            roots.insert(
339                info.symbols
340                    .get_function_symbol(InputFuncId::from_index(*index))
341                    .unwrap(),
342            );
343        }
344
345        // TODO: Only wasm_bindgen imports are important - rest COULD be imported by loader.
346        for (index, (_, import)) in info.wasm.imports.iter().enumerate() {
347            let wasmparser::Import {
348                ty: wasmparser::TypeRef::Func(_),
349                ..
350            } = import
351            else {
352                continue;
353            };
354
355            roots.insert(
356                info.symbols
357                    .get_function_symbol(InputFuncId::from_index(index))
358                    .unwrap(),
359            );
360        }
361
362        for descriptor in wbg_descriptors {
363            roots.insert(*descriptor);
364        }
365
366        for split_point in split_points.iter() {
367            roots.remove(
368                &info
369                    .symbols
370                    .get_function_symbol(split_point.export_func)
371                    .unwrap(),
372            );
373            roots.remove(
374                &info
375                    .symbols
376                    .get_function_symbol(split_point.import_func)
377                    .unwrap(),
378            );
379        }
380        roots
381    }
382
383    fn is_wasm_bindgen_cast(name: &str) -> bool {
384        name == "__wbindgen_describe_closure" || name == "__wbindgen_describe_cast" // in later version closure was replaced by more generic cast
385        // || name == "__wbindgen_describe"
386    }
387
388    // Find all wasm-bindgen closures
389    //
390    // Most of wasm-bindgen descriptors are exported functions and handling during `find_main_entrypoints`.
391    // However, some are made for closures and are not exported directly.
392    //
393    // wasm-bindgen do dfs to find all `__wbindgen_describe_closure` (check out interpret_closure_descriptor in wasm-bindgen source).
394    // This are entrypoints for closures.
395    // Within this entrypoints, wasm-bindgen finds and dynamic `describe` declaration and process it. For entrpoints imports are generated.
396    //
397    pub fn wbg_closures(module: &analysis::ModuleInfo, graph: &DepGraph) -> MiniSet<SymbolId> {
398        let wbg_fns: BTreeSet<_> = module
399            .symbols
400            .iter()
401            .filter(|(_id, sym)| Self::is_wasm_bindgen_cast(&sym.name))
402            .map(|(id, _name)| id)
403            .collect();
404
405        let mut wbg_descriptors = BTreeSet::new();
406        for id in wbg_fns.iter().cloned() {
407            wbg_descriptors.insert(id);
408            if let Some(parents) = graph.get_parents(id) {
409                for parent in parents {
410                    debug_assert!(matches!(
411                        module.symbols.get(*parent).unwrap().kind,
412                        SymbolKind::Func { .. }
413                    ));
414                    wbg_descriptors.insert(*parent);
415                }
416            }
417        }
418        wbg_descriptors.into_iter().collect()
419    }
420
421    pub fn merge_split_points_by_name(
422        split_points: &[SplitPoint],
423    ) -> BTreeMap<String, Vec<&SplitPoint>> {
424        let mut result = BTreeMap::<String, Vec<&SplitPoint>>::new();
425
426        for split_point in split_points {
427            result
428                .entry(split_point.module_name.clone())
429                .or_default()
430                .push(split_point);
431        }
432        for results in result.values_mut() {
433            results.sort_by_key(|sp| sp.unique_id.clone());
434        }
435        result
436    }
437
438    pub fn compute_split_modules(
439        info: &analysis::ModuleInfo,
440        dep_graph: &DepGraph,
441        split_points: &[SplitPoint],
442        wbg_descriptors: &MiniSet<SymbolId>,
443    ) -> anyhow::Result<SplitProgramInfo> {
444        let split_points_by_module = Self::merge_split_points_by_name(split_points);
445
446        let main_roots = Self::get_main_module_roots(info, split_points, wbg_descriptors);
447
448        // graph root -> dep -> dep
449        let main_deps = find_reachable_deps(dep_graph, &main_roots);
450
451        let mut named_modules = vec![NamedGraph::new(ModuleIdentifier::Main, main_deps.clone())];
452
453        // Determine reachable symbols (excluding main module symbols) for each
454        // split module. Symbols may be reachable from more than one split module;
455        // these symbols will be moved to a separate module.
456        for (module_name, entry_points) in split_points_by_module.iter() {
457            let mut roots = DepSet::new();
458            for entry_point in entry_points.iter() {
459                roots.insert(
460                    info.symbols
461                        .get_function_symbol(entry_point.export_func)
462                        .unwrap(),
463                );
464            }
465
466            let split_functions = find_reachable_deps(dep_graph, &roots);
467
468            named_modules.push(NamedGraph::new(
469                ModuleIdentifier::Split(module_name.clone()),
470                split_functions,
471            ));
472        }
473
474        // Calculate shared deps.
475        let shared_deps = NamedGraph::calculate_shared_modules(&mut named_modules, dep_graph);
476
477        let mut split_module_contents = BTreeMap::<SplitModuleIdentifier, OutputModuleInfo>::new();
478
479        split_module_contents.extend(named_modules.into_iter().map(|named_graph| {
480            let imports = named_graph.imports().clone();
481            // TODO: Rewrite this
482            let split_points = split_points_by_module
483                .get(&named_graph.module.to_string())
484                .iter()
485                .copied()
486                .flatten()
487                .copied()
488                .cloned()
489                .collect();
490            (
491                SplitModuleIdentifier::Single(named_graph.module),
492                OutputModuleInfo {
493                    defined_symbols: named_graph.reachable,
494                    imports,
495                    split_points,
496                    // Module can only import symbols from shared modules.
497                    exports: DepMiniSet::new(),
498                },
499            )
500        }));
501
502        for shared in shared_deps {
503            split_module_contents.insert(
504                SplitModuleIdentifier::Shared(SharedModuleIdentifier(shared.module_names.clone())),
505                OutputModuleInfo {
506                    defined_symbols: shared.shared_deps,
507                    exports: shared.exports,
508                    imports: shared.imports,
509                    split_points: vec![],
510                },
511            );
512        }
513
514        let symbol_output_module = split_module_contents
515            .iter()
516            .enumerate()
517            .flat_map(|(output_index, (_, info))| {
518                info.defined_symbols
519                    .iter()
520                    .map(move |symbol| (*symbol, output_index))
521            })
522            .collect();
523
524        Ok(SplitProgramInfo {
525            output_modules: split_module_contents.into_iter().collect(),
526            symbol_output_module,
527        })
528    }
529}
530
531#[cfg(test)]
532mod tests {
533    use super::*;
534
535    #[test]
536    fn test_is_part_of() {
537        let shared = SharedModuleIdentifier(vec![
538            ModuleIdentifier::Main,
539            ModuleIdentifier::Split("a".into()),
540            ModuleIdentifier::Split("c".into()),
541        ]);
542        let single_main = SplitModuleIdentifier::Single(ModuleIdentifier::Main);
543        let single_a = SplitModuleIdentifier::Single(ModuleIdentifier::Split("a".into()));
544        let single_b = SplitModuleIdentifier::Single(ModuleIdentifier::Split("b".into()));
545        let shared_ab = SplitModuleIdentifier::Shared(SharedModuleIdentifier(vec![
546            ModuleIdentifier::Split("a".into()),
547            ModuleIdentifier::Split("b".into()),
548        ]));
549        let single_c = SplitModuleIdentifier::Single(ModuleIdentifier::Split("c".into()));
550        let shared_ac = SplitModuleIdentifier::Shared(SharedModuleIdentifier(vec![
551            ModuleIdentifier::Split("a".into()),
552            ModuleIdentifier::Split("c".into()),
553        ]));
554
555        assert!(single_main.is_part_of(&shared));
556        assert!(single_a.is_part_of(&shared));
557        assert!(single_c.is_part_of(&shared));
558        assert!(shared_ac.is_part_of(&shared));
559        assert!(!single_b.is_part_of(&shared));
560        assert!(!shared_ab.is_part_of(&shared));
561    }
562
563    //     #[test]
564    //     fn from_example() {
565    //         "e_data_9382861128153349529"
566    //         "lazy_data_2021541099659736428"
567
568    //          "view_c_view_16564031152823166319_view_d_view_3929403835768869397_view_e_view_16839038780052115883"
569    // "e_data_9382861128153349529_lazy_data_2021541099659736428"
570    //     }
571}