Skip to main content

i_slint_compiler/passes/
collect_globals.rs

1// Copyright © SixtyFPS GmbH <info@slint.dev>
2// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
3
4//! This pass fills the root component used_types.globals
5
6#![allow(clippy::mutable_key_type)] // NamedReference/Component keys intentionally use interior mutability
7
8use by_address::ByAddress;
9use smol_str::format_smolstr;
10
11use crate::diagnostics::BuildDiagnostics;
12use crate::expression_tree::NamedReference;
13use crate::object_tree::*;
14use std::collections::HashSet;
15use std::rc::Rc;
16
17/// Fill the root_component's used_types.globals
18pub fn collect_globals(doc: &Document, _diag: &mut BuildDiagnostics) {
19    doc.used_types.borrow_mut().globals.clear();
20    let mut set = HashSet::new();
21    let mut sorted_globals = Vec::new();
22    for (_, ty) in &*doc.exports {
23        if let Some(c) = ty.as_ref().left()
24            && c.is_global()
25            && set.insert(ByAddress(c.clone()))
26        {
27            collect_in_component(c, &mut set, &mut sorted_globals);
28            sorted_globals.push(c.clone());
29        }
30    }
31    doc.visit_all_used_components(|component| {
32        collect_in_component(component, &mut set, &mut sorted_globals)
33    });
34
35    doc.used_types.borrow_mut().globals = sorted_globals;
36}
37
38/// Properties of library-imported globals may be modified by the library at runtime via bindings or
39/// public API. Mark the properties as externally set so that they do not get treated as constants
40/// by future passes.
41/// Additionally register the globals as imported from external libraries.
42pub fn mark_library_globals(doc: &Document) {
43    let mut used_types = doc.used_types.borrow_mut();
44    used_types.globals.clone().iter().for_each(|component| {
45        if let Some(library_info) = doc.library_exports.get(component.id.as_str()) {
46            component.from_library.set(true);
47            let root = component.root_element.borrow();
48            let mut analysis = root.property_analysis.borrow_mut();
49            for name in root.property_declarations.keys() {
50                let entry = analysis.entry(name.clone()).or_default();
51                entry.is_set_externally = true;
52                entry.is_read_externally = true;
53            }
54            used_types.library_types_imports.push((component.id.clone(), library_info.clone()));
55            used_types
56                .library_types_imports
57                .push((format_smolstr!("Inner{}", component.id.clone()), library_info.clone()));
58        }
59    });
60}
61
62fn collect_in_component(
63    component: &Rc<Component>,
64    global_set: &mut HashSet<ByAddress<Rc<Component>>>,
65    sorted_globals: &mut Vec<Rc<Component>>,
66) {
67    let mut maybe_collect_global = |nr: &mut NamedReference| {
68        let element = nr.element();
69        let global_component = element.borrow().enclosing_component.upgrade().unwrap();
70        if global_component.is_global() && global_set.insert(ByAddress(global_component.clone())) {
71            collect_in_component(&global_component, global_set, sorted_globals);
72            sorted_globals.push(global_component);
73        }
74    };
75    visit_all_named_references(component, &mut maybe_collect_global);
76}
77
78#[cfg(test)]
79mod tests {
80    use super::*;
81
82    /// Checks that `mark_library_globals` sets `is_set_externally` and
83    /// `is_read_externally` on every property of a library-imported global,
84    /// regardless of visibility.
85    ///
86    /// Regression for the bug where consumer bindings reading from a library global
87    /// were inlined to the global's compile-time default.
88    #[test]
89    fn mark_library_globals_marks_properties_as_externally_used() {
90        let mut compiler_config =
91            crate::CompilerConfiguration::new(crate::generator::OutputFormat::Interpreter);
92        compiler_config.style = Some("fluent".into());
93        let mut diag = crate::diagnostics::BuildDiagnostics::default();
94        let doc_node = crate::parser::parse(
95            r#"
96export global LibGlobal {
97    in-out property <int> value: 0;
98    in property <int> count: 5;
99    out property <int> ready: 9;
100}
101export component App {
102    out property <string> text-out: "\{LibGlobal.value} \{LibGlobal.count} \{LibGlobal.ready}";
103}
104"#
105            .into(),
106            Some(std::path::Path::new("test.slint")),
107            &mut diag,
108        );
109        let (mut doc, diag, _) =
110            spin_on::spin_on(crate::compile_syntax_node(doc_node, diag, compiler_config));
111        assert!(!diag.has_errors(), "compile error: {:?}", diag.to_string_vec());
112
113        let global = doc
114            .used_types
115            .borrow()
116            .globals
117            .iter()
118            .find(|g| g.id == "LibGlobal")
119            .expect("LibGlobal not found")
120            .clone();
121
122        // Simulate a library import by adding `LibGlobal` to `library_exports`
123        // and re-running `mark_library_globals`. Clear `expose_in_public_api`
124        // so the `is_constant_impl` short-circuit doesn't mask the result.
125        doc.library_exports.insert(
126            "LibGlobal".to_string(),
127            crate::typeloader::LibraryInfo {
128                name: "Lib".into(),
129                package: "lib".into(),
130                module: None,
131                exports: Vec::new(),
132            },
133        );
134        for (_, d) in global.root_element.borrow_mut().property_declarations.iter_mut() {
135            d.expose_in_public_api = false;
136        }
137        global.root_element.borrow().property_analysis.borrow_mut().clear();
138
139        mark_library_globals(&doc);
140
141        let root = global.root_element.borrow();
142        let analysis = root.property_analysis.borrow();
143        // `private` is omitted — `remove_unused_properties` strips it before
144        // we re-invoke `mark_library_globals`; `bapp` covers that path.
145        for prop in ["value", "count", "ready"] {
146            let a = analysis
147                .get(prop)
148                .unwrap_or_else(|| panic!("{prop}: no analysis entry for library global property"));
149            assert!(
150                a.is_set_externally,
151                "{prop}: every property on a library global must be marked is_set_externally \
152                 — the library or its host code may write it at runtime regardless of visibility"
153            );
154            assert!(
155                a.is_read_externally,
156                "{prop}: every property on a library global must be marked is_read_externally \
157                 — the library's bindings or host code may read it"
158            );
159        }
160        drop(analysis);
161        drop(root);
162
163        for prop in ["value", "count", "ready"] {
164            assert!(
165                !NamedReference::new(&global.root_element, prop.into()).is_constant(),
166                "{prop} on a library global must never be constant"
167            );
168        }
169    }
170}