Skip to main content

i_slint_compiler/generator/
python.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/*! module for the C++ code generator
5*/
6
7// cSpell:ignore cmath constexpr cstdlib decltype intptr itertools nullptr prepended struc subcomponent uintptr vals enumty fundecl pycompo pyenum structty
8
9use std::collections::HashMap;
10use std::collections::HashSet;
11use std::sync::Arc;
12use std::sync::OnceLock;
13
14use smol_str::{SmolStr, StrExt, format_smolstr};
15
16use serde::{Deserialize, Serialize};
17
18mod diff;
19
20// Check if word is one of Python keywords
21// (https://docs.python.org/3/reference/lexical_analysis.html#keywords)
22fn is_python_keyword(word: &str) -> bool {
23    static PYTHON_KEYWORDS: OnceLock<HashSet<&'static str>> = OnceLock::new();
24    let keywords = PYTHON_KEYWORDS.get_or_init(|| {
25        let keywords: HashSet<&str> = HashSet::from([
26            "False", "await", "else", "import", "pass", "None", "break", "except", "in", "raise",
27            "True", "class", "finally", "is", "return", "and", "continue", "for", "lambda", "try",
28            "as", "def", "from", "nonlocal", "while", "assert", "del", "global", "not", "with",
29            "async", "elif", "if", "or", "yield",
30        ]);
31        keywords
32    });
33    keywords.contains(word)
34}
35
36pub fn ident(ident: &str) -> SmolStr {
37    let mut new_ident = SmolStr::from(ident);
38    if ident.contains('-') {
39        new_ident = ident.replace_smolstr("-", "_");
40    }
41    if is_python_keyword(new_ident.as_str()) {
42        new_ident = format_smolstr!("{}_", new_ident);
43    }
44    new_ident
45}
46
47#[derive(Clone, PartialEq, Serialize, Deserialize, Debug)]
48pub struct PyProperty {
49    name: SmolStr,
50    ty: SmolStr,
51}
52
53impl From<&PyProperty> for python_ast::Field {
54    fn from(prop: &PyProperty) -> Self {
55        Field {
56            name: prop.name.clone(),
57            ty: Some(PyType { name: prop.ty.clone(), optional: false }),
58            default_value: None,
59        }
60    }
61}
62
63impl From<(&SmolStr, &llr::PublicProperty)> for PyProperty {
64    fn from((name, llr_prop): (&SmolStr, &llr::PublicProperty)) -> Self {
65        Self { name: ident(name), ty: python_type_name(&llr_prop.ty) }
66    }
67}
68
69enum ComponentType<'a> {
70    Global,
71    Component { associated_globals: &'a [PyComponent] },
72}
73
74#[derive(Serialize, Deserialize)]
75pub struct PyComponent {
76    name: SmolStr,
77    properties: Vec<PyProperty>,
78    aliases: Vec<SmolStr>,
79}
80
81impl PyComponent {
82    fn generate(&self, ty: ComponentType<'_>, file: &mut File) {
83        let mut class = Class {
84            name: self.name.clone(),
85            super_class: if matches!(ty, ComponentType::Global) {
86                None
87            } else {
88                Some(SmolStr::new_static("slint.Component"))
89            },
90            ..Default::default()
91        };
92
93        class.fields = self
94            .properties
95            .iter()
96            .map(From::from)
97            .chain(
98                match ty {
99                    ComponentType::Global => None,
100                    ComponentType::Component { associated_globals } => Some(associated_globals),
101                }
102                .into_iter()
103                .flat_map(|globals| globals.iter())
104                .map(|glob| Field {
105                    name: glob.name.clone(),
106                    ty: Some(PyType { name: glob.name.clone(), optional: false }),
107                    default_value: None,
108                }),
109            )
110            .collect();
111
112        file.declarations.push(python_ast::Declaration::Class(class));
113
114        file.declarations.extend(self.aliases.iter().map(|exported_name| {
115            python_ast::Declaration::Variable(Variable {
116                name: ident(exported_name),
117                value: self.name.clone(),
118            })
119        }))
120    }
121}
122
123impl From<&llr::PublicComponent> for PyComponent {
124    fn from(llr_compo: &llr::PublicComponent) -> Self {
125        Self {
126            name: ident(&llr_compo.name),
127            properties: llr_compo.public_properties.iter().map(From::from).collect(),
128            aliases: Vec::new(),
129        }
130    }
131}
132
133impl From<&llr::GlobalComponent> for PyComponent {
134    fn from(llr_global: &llr::GlobalComponent) -> Self {
135        Self {
136            name: ident(&llr_global.name),
137            properties: llr_global.public_properties.iter().map(From::from).collect(),
138            aliases: llr_global.aliases.iter().map(|exported_name| ident(exported_name)).collect(),
139        }
140    }
141}
142
143#[derive(Clone, PartialEq, Debug, Serialize, Deserialize)]
144pub struct PyStructField {
145    name: SmolStr,
146    ty: SmolStr,
147}
148
149#[derive(Serialize, Deserialize)]
150pub struct PyStruct {
151    name: SmolStr,
152    fields: Vec<PyStructField>,
153    aliases: Vec<SmolStr>,
154}
155
156pub struct AnonymousStruct;
157
158impl TryFrom<&Arc<crate::langtype::Struct>> for PyStruct {
159    type Error = AnonymousStruct;
160
161    fn try_from(structty: &Arc<crate::langtype::Struct>) -> Result<Self, Self::Error> {
162        let StructName::User { name, .. } = &structty.name else {
163            return Err(AnonymousStruct);
164        };
165        Ok(Self {
166            name: ident(name),
167            fields: structty
168                .fields
169                .iter()
170                .map(|(name, ty)| PyStructField { name: ident(name), ty: python_type_name(ty) })
171                .collect(),
172            aliases: Vec::new(),
173        })
174    }
175}
176
177impl From<&PyStruct> for python_ast::Declaration {
178    fn from(py_struct: &PyStruct) -> Self {
179        let py_fields = py_struct
180            .fields
181            .iter()
182            .map(|field| Field {
183                name: field.name.clone(),
184                ty: Some(PyType { name: field.ty.clone(), optional: false }),
185                default_value: None,
186            })
187            .collect::<Vec<_>>();
188
189        let ctor = FunctionDeclaration {
190            name: SmolStr::new_static("__init__"),
191            positional_parameters: Vec::default(),
192            keyword_parameters: py_fields
193                .iter()
194                .map(|field| {
195                    let mut kw_field = field.clone();
196                    kw_field.ty.as_mut().unwrap().optional = true;
197                    kw_field.default_value = Some(SmolStr::new_static("None"));
198                    kw_field
199                })
200                .collect(),
201            return_type: None,
202        };
203
204        let struct_class = Class {
205            name: py_struct.name.clone(),
206            fields: py_fields,
207            function_declarations: vec![ctor],
208            ..Default::default()
209        };
210        python_ast::Declaration::Class(struct_class)
211    }
212}
213
214impl PyStruct {
215    fn generate_aliases(&self) -> impl ExactSizeIterator<Item = python_ast::Declaration> + use<'_> {
216        self.aliases.iter().map(|alias| {
217            python_ast::Declaration::Variable(Variable {
218                name: alias.clone(),
219                value: self.name.clone(),
220            })
221        })
222    }
223}
224
225#[derive(Serialize, Deserialize)]
226pub struct PyEnumVariant {
227    name: SmolStr,
228    strvalue: SmolStr,
229}
230
231#[derive(Serialize, Deserialize)]
232pub struct PyEnum {
233    name: SmolStr,
234    variants: Vec<PyEnumVariant>,
235    aliases: Vec<SmolStr>,
236}
237
238impl From<&Arc<crate::langtype::Enumeration>> for PyEnum {
239    fn from(enumty: &Arc<crate::langtype::Enumeration>) -> Self {
240        Self {
241            name: ident(&enumty.name),
242            variants: enumty
243                .values
244                .iter()
245                .map(|val| PyEnumVariant { name: ident(val), strvalue: val.clone() })
246                .collect(),
247            aliases: Vec::new(),
248        }
249    }
250}
251
252impl From<&PyEnum> for python_ast::Declaration {
253    fn from(py_enum: &PyEnum) -> Self {
254        python_ast::Declaration::Class(Class {
255            name: py_enum.name.clone(),
256            super_class: Some(SmolStr::new_static("enum.StrEnum")),
257            fields: py_enum
258                .variants
259                .iter()
260                .map(|variant| Field {
261                    name: variant.name.clone(),
262                    ty: None,
263                    default_value: Some(format_smolstr!("\"{}\"", variant.strvalue)),
264                })
265                .collect(),
266            function_declarations: Vec::new(),
267        })
268    }
269}
270
271impl PyEnum {
272    fn generate_aliases(&self) -> impl ExactSizeIterator<Item = python_ast::Declaration> + use<'_> {
273        self.aliases.iter().map(|alias| {
274            python_ast::Declaration::Variable(Variable {
275                name: alias.clone(),
276                value: self.name.clone(),
277            })
278        })
279    }
280}
281
282#[derive(Serialize, Deserialize)]
283pub enum PyStructOrEnum {
284    Struct(PyStruct),
285    Enum(PyEnum),
286}
287
288impl From<&PyStructOrEnum> for python_ast::Declaration {
289    fn from(struct_or_enum: &PyStructOrEnum) -> Self {
290        match struct_or_enum {
291            PyStructOrEnum::Struct(py_struct) => py_struct.into(),
292            PyStructOrEnum::Enum(py_enum) => py_enum.into(),
293        }
294    }
295}
296
297impl PyStructOrEnum {
298    fn generate_aliases(&self, file: &mut File) {
299        match self {
300            PyStructOrEnum::Struct(py_struct) => {
301                file.declarations.extend(py_struct.generate_aliases())
302            }
303            PyStructOrEnum::Enum(py_enum) => file.declarations.extend(py_enum.generate_aliases()),
304        }
305    }
306}
307
308#[derive(Serialize, Deserialize)]
309pub struct PyModule {
310    pub(crate) version: SmolStr,
311    globals: Vec<PyComponent>,
312    components: Vec<PyComponent>,
313    structs_and_enums: Vec<PyStructOrEnum>,
314}
315
316impl Default for PyModule {
317    fn default() -> Self {
318        Self {
319            // Bump whenever the meaning of any annotation produced by
320            // `python_type_name` changes (e.g. Type::Int32 → "int" in 2.0).
321            // A previously-generated wrapper that carries an older version
322            // is treated as incompatible by `changed_version`.
323            version: SmolStr::new_static("2.1"),
324            globals: Default::default(),
325            components: Default::default(),
326            structs_and_enums: Default::default(),
327        }
328    }
329}
330
331impl PyModule {
332    pub fn load_from_json(json: &str) -> Result<Self, String> {
333        serde_json::from_str(json).map_err(|e| format!("{}", e))
334    }
335}
336
337/// Build the Python API description from the lowered LLR plus the source-level
338/// `structs_and_enums`, which the LLR does not carry. Both are available without
339/// the object-tree `Document`, so the interpreter can generate the API from a
340/// `CompilationResult` alone.
341pub fn generate_py_module(unit: &llr::CompilationUnit, structs_and_enums: &[Type]) -> PyModule {
342    let mut module = PyModule::default();
343
344    let mut aliases: HashMap<&str, Vec<SmolStr>> = Default::default();
345    for export in unit.type_exports.iter().filter(|e| e.is_alias()) {
346        aliases
347            .entry(export.internal_name.as_str())
348            .or_default()
349            .push(export.exported_name.clone());
350    }
351    let aliases_of = |name: &str| aliases.get(name).cloned().unwrap_or_default();
352
353    for ty in structs_and_enums {
354        match ty {
355            Type::Struct(s) => module.structs_and_enums.extend(
356                PyStruct::try_from(s).ok().and_then(|mut pystruct| {
357                    let StructName::User { name, .. } = &s.name else {
358                        return None;
359                    };
360                    pystruct.aliases = aliases_of(name);
361                    Some(PyStructOrEnum::Struct(pystruct))
362                }),
363            ),
364            Type::Enumeration(en) => {
365                module.structs_and_enums.push({
366                    let mut pyenum = PyEnum::from(en);
367                    pyenum.aliases = aliases_of(&en.name);
368                    PyStructOrEnum::Enum(pyenum)
369                });
370            }
371            _ => {}
372        }
373    }
374
375    module.globals.extend(
376        unit.globals
377            .iter()
378            .filter(|glob| glob.exported && glob.must_generate())
379            .map(PyComponent::from),
380    );
381    module.components.extend(unit.public_components.iter().map(|llr_compo| {
382        let mut pycompo = PyComponent::from(llr_compo);
383        pycompo.aliases = aliases_of(&llr_compo.name);
384        pycompo
385    }));
386
387    module
388}
389
390/// This module contains some data structures that helps represent a Python file.
391/// It is then rendered into an actual Python code using the Display trait
392mod python_ast {
393
394    use std::fmt::{Display, Error, Formatter};
395
396    use smol_str::SmolStr;
397
398    ///A full Python file
399    #[derive(Default, Debug)]
400    pub struct File {
401        pub imports: Vec<SmolStr>,
402        pub declarations: Vec<Declaration>,
403        pub trailing_code: Vec<SmolStr>,
404    }
405
406    impl Display for File {
407        fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), Error> {
408            writeln!(f, "# This file is auto-generated\n")?;
409            for import in &self.imports {
410                writeln!(f, "import {}", import)?;
411            }
412            writeln!(f)?;
413            for decl in &self.declarations {
414                writeln!(f, "{}", decl)?;
415            }
416            for code in &self.trailing_code {
417                writeln!(f, "{}", code)?;
418            }
419            Ok(())
420        }
421    }
422
423    #[derive(Debug, derive_more::Display)]
424    pub enum Declaration {
425        Class(Class),
426        Variable(Variable),
427    }
428
429    #[derive(Debug, Default)]
430    pub struct Class {
431        pub name: SmolStr,
432        pub super_class: Option<SmolStr>,
433        pub fields: Vec<Field>,
434        pub function_declarations: Vec<FunctionDeclaration>,
435    }
436
437    impl Display for Class {
438        fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
439            if let Some(super_class) = self.super_class.as_ref() {
440                writeln!(f, "class {}({}):", self.name, super_class)?;
441            } else {
442                writeln!(f, "class {}:", self.name)?;
443            }
444            if self.fields.is_empty() && self.function_declarations.is_empty() {
445                writeln!(f, "    pass")?;
446                return Ok(());
447            }
448
449            for field in &self.fields {
450                writeln!(f, "    {}", field)?;
451            }
452
453            if !self.fields.is_empty() {
454                writeln!(f)?;
455            }
456
457            for fundecl in &self.function_declarations {
458                writeln!(f, "    {}", fundecl)?;
459            }
460
461            Ok(())
462        }
463    }
464
465    #[derive(Debug)]
466    pub struct Variable {
467        pub name: SmolStr,
468        pub value: SmolStr,
469    }
470
471    impl Display for Variable {
472        fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
473            writeln!(f, "{} = {}", self.name, self.value)
474        }
475    }
476
477    #[derive(Debug, Clone)]
478    pub struct PyType {
479        pub name: SmolStr,
480        pub optional: bool,
481    }
482
483    impl Display for PyType {
484        fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
485            if self.optional {
486                write!(f, "typing.Optional[{}]", self.name)
487            } else {
488                write!(f, "{}", self.name)
489            }
490        }
491    }
492
493    #[derive(Debug, Clone)]
494    pub struct Field {
495        pub name: SmolStr,
496        pub ty: Option<PyType>,
497        pub default_value: Option<SmolStr>,
498    }
499
500    impl Display for Field {
501        fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
502            write!(f, "{}", self.name)?;
503            if let Some(ty) = &self.ty {
504                write!(f, ": {}", ty)?;
505            }
506            if let Some(default_value) = &self.default_value {
507                write!(f, " = {}", default_value)?
508            }
509            Ok(())
510        }
511    }
512
513    #[derive(Debug)]
514    pub struct FunctionDeclaration {
515        pub name: SmolStr,
516        pub positional_parameters: Vec<SmolStr>,
517        pub keyword_parameters: Vec<Field>,
518        pub return_type: Option<PyType>,
519    }
520
521    impl Display for FunctionDeclaration {
522        fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
523            write!(f, "def {}(self", self.name)?;
524
525            if !self.positional_parameters.is_empty() {
526                write!(f, ", {}", self.positional_parameters.join(","))?;
527            }
528
529            if !self.keyword_parameters.is_empty() {
530                write!(f, ", *")?;
531                write!(
532                    f,
533                    ", {}",
534                    self.keyword_parameters
535                        .iter()
536                        .map(ToString::to_string)
537                        .collect::<Vec<_>>()
538                        .join(", ")
539                )?;
540            }
541            writeln!(
542                f,
543                ") -> {}: ...",
544                self.return_type.as_ref().map_or(std::borrow::Cow::Borrowed("None"), |ty| {
545                    std::borrow::Cow::Owned(ty.to_string())
546                })
547            )?;
548            Ok(())
549        }
550    }
551}
552
553use crate::langtype::{StructName, Type};
554
555use crate::CompilerConfiguration;
556use crate::llr;
557use crate::object_tree::Document;
558use itertools::Itertools;
559use python_ast::*;
560
561/// Returns the text of the Python code produced by the given root component
562pub fn generate(
563    doc: &Document,
564    compiler_config: &CompilerConfiguration,
565    destination_path: Option<&std::path::Path>,
566) -> std::io::Result<File> {
567    let mut file = File { ..Default::default() };
568    file.imports.push(SmolStr::new_static("slint"));
569    file.imports.push(SmolStr::new_static("typing"));
570
571    let unit = llr::lower_to_item_tree::lower_to_item_tree(doc, compiler_config);
572    let pymodule = generate_py_module(&unit, &doc.used_types.borrow().structs_and_enums);
573
574    if pymodule.structs_and_enums.iter().any(|se| matches!(se, PyStructOrEnum::Enum(_))) {
575        file.imports.push(SmolStr::new_static("enum"));
576    }
577
578    file.declarations.extend(pymodule.structs_and_enums.iter().map(From::from));
579
580    for global in &pymodule.globals {
581        global.generate(ComponentType::Global, &mut file);
582    }
583
584    for public_component in &pymodule.components {
585        public_component.generate(
586            ComponentType::Component { associated_globals: &pymodule.globals },
587            &mut file,
588        );
589    }
590
591    for struct_or_enum in &pymodule.structs_and_enums {
592        struct_or_enum.generate_aliases(&mut file);
593    }
594
595    let main_file = std::path::absolute(
596        doc.node
597            .as_ref()
598            .ok_or_else(|| std::io::Error::other("Cannot determine path of the main file"))?
599            .source_file
600            .path(),
601    )
602    .unwrap();
603
604    let destination_path = destination_path.and_then(|maybe_relative_destination_path| {
605        std::fs::canonicalize(maybe_relative_destination_path)
606            .ok()
607            .and_then(|p| p.parent().map(std::path::PathBuf::from))
608    });
609
610    let relative_path_from_destination_to_main_file =
611        destination_path.and_then(|destination_path| {
612            pathdiff::diff_paths(main_file.parent().unwrap(), destination_path)
613        });
614
615    if let Some(relative_path_from_destination_to_main_file) =
616        relative_path_from_destination_to_main_file
617    {
618        use base64::engine::Engine;
619        use std::io::Write;
620
621        let mut api_str_compressor =
622            flate2::write::GzEncoder::new(Vec::new(), flate2::Compression::default());
623        api_str_compressor.write_all(serde_json::to_string(&pymodule).unwrap().as_bytes())?;
624        let compressed_api_str = api_str_compressor.finish()?;
625        let base64_api_str = base64::engine::general_purpose::STANDARD.encode(&compressed_api_str);
626
627        file.imports.push(SmolStr::new_static("os"));
628        file.trailing_code.push(format_smolstr!(
629            "globals().update(vars(slint._load_file_checked(path=os.path.join(os.path.dirname(__file__), r'{}'), expected_api_base64_compressed=r'{}', generated_file=__file__)))",
630            relative_path_from_destination_to_main_file.join(main_file.file_name().unwrap()).to_string_lossy(),
631            base64_api_str
632        ));
633    }
634
635    Ok(file)
636}
637
638fn python_type_name(ty: &Type) -> SmolStr {
639    match ty {
640        Type::Invalid => panic!("Invalid type encountered in llr output"),
641        Type::Void => SmolStr::new_static("None"),
642        Type::String => SmolStr::new_static("str"),
643        Type::Color => SmolStr::new_static("slint.Color"),
644        Type::Int32 => SmolStr::new_static("int"),
645        Type::Float32
646        | Type::Duration
647        | Type::Angle
648        | Type::PhysicalLength
649        | Type::LogicalLength
650        | Type::Percent
651        | Type::Rem
652        | Type::UnitProduct(_) => SmolStr::new_static("float"),
653        Type::Image => SmolStr::new_static("slint.Image"),
654        Type::Bool => SmolStr::new_static("bool"),
655        Type::Brush => SmolStr::new_static("slint.Brush"),
656        Type::StyledText => SmolStr::new_static("slint.StyledText"),
657        Type::Array(elem_type) => format_smolstr!("slint.Model[{}]", python_type_name(elem_type)),
658        Type::Struct(s) => match &s.name {
659            StructName::User { name, .. } => ident(name),
660            StructName::Builtin(crate::langtype::BuiltinStruct::LogicalPosition) => {
661                SmolStr::new_static("slint.LogicalPosition")
662            }
663            StructName::Builtin(crate::langtype::BuiltinStruct::LogicalSize) => {
664                SmolStr::new_static("slint.LogicalSize")
665            }
666            StructName::Builtin(crate::langtype::BuiltinStruct::Color) | StructName::None => {
667                let tuple_types = s.fields.values().map(python_type_name).collect::<Vec<_>>();
668                format_smolstr!("typing.Tuple[{}]", tuple_types.join(", "))
669            }
670            StructName::Builtin(builtin_struct) if builtin_struct.is_public() => {
671                let name: &'static str = builtin_struct.into();
672                format_smolstr!("slint.language.{}", name)
673            }
674            StructName::Builtin(_) => SmolStr::new_static("None"),
675        },
676        Type::Enumeration(enumeration) => {
677            if enumeration.node.is_some() {
678                ident(&enumeration.name)
679            } else {
680                SmolStr::new_static("None")
681            }
682        }
683        Type::Callback(function) | Type::Function(function) => {
684            format_smolstr!(
685                "typing.Callable[[{}], {}]",
686                function.args.iter().map(python_type_name).join(", "),
687                python_type_name(&function.return_type)
688            )
689        }
690        Type::Keys => SmolStr::new_static("slint.Keys"),
691        Type::DataTransfer => SmolStr::new_static("slint.DataTransfer"),
692        Type::MouseCursor => SmolStr::new_static("None"),
693        ty => unimplemented!("implemented type conversion {:#?}", ty),
694    }
695}