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
8
9use std::collections::HashMap;
10use std::sync::OnceLock;
11use std::{collections::HashSet, rc::Rc};
12
13use smol_str::{SmolStr, StrExt, format_smolstr};
14
15use serde::{Deserialize, Serialize};
16
17mod diff;
18
19// Check if word is one of Python keywords
20// (https://docs.python.org/3/reference/lexical_analysis.html#keywords)
21fn is_python_keyword(word: &str) -> bool {
22    static PYTHON_KEYWORDS: OnceLock<HashSet<&'static str>> = OnceLock::new();
23    let keywords = PYTHON_KEYWORDS.get_or_init(|| {
24        let keywords: HashSet<&str> = HashSet::from([
25            "False", "await", "else", "import", "pass", "None", "break", "except", "in", "raise",
26            "True", "class", "finally", "is", "return", "and", "continue", "for", "lambda", "try",
27            "as", "def", "from", "nonlocal", "while", "assert", "del", "global", "not", "with",
28            "async", "elif", "if", "or", "yield",
29        ]);
30        keywords
31    });
32    keywords.contains(word)
33}
34
35pub fn ident(ident: &str) -> SmolStr {
36    let mut new_ident = SmolStr::from(ident);
37    if ident.contains('-') {
38        new_ident = ident.replace_smolstr("-", "_");
39    }
40    if is_python_keyword(new_ident.as_str()) {
41        new_ident = format_smolstr!("{}_", new_ident);
42    }
43    new_ident
44}
45
46#[derive(Clone, PartialEq, Serialize, Deserialize, Debug)]
47pub struct PyProperty {
48    name: SmolStr,
49    ty: SmolStr,
50}
51
52impl From<&PyProperty> for python_ast::Field {
53    fn from(prop: &PyProperty) -> Self {
54        Field {
55            name: prop.name.clone(),
56            ty: Some(PyType { name: prop.ty.clone(), optional: false }),
57            default_value: None,
58        }
59    }
60}
61
62impl From<&llr::PublicProperty> for PyProperty {
63    fn from(llr_prop: &llr::PublicProperty) -> Self {
64        Self { name: ident(&llr_prop.name), ty: python_type_name(&llr_prop.ty) }
65    }
66}
67
68enum ComponentType<'a> {
69    Global,
70    Component { associated_globals: &'a [PyComponent] },
71}
72
73#[derive(Serialize, Deserialize)]
74pub struct PyComponent {
75    name: SmolStr,
76    properties: Vec<PyProperty>,
77    aliases: Vec<SmolStr>,
78}
79
80impl PyComponent {
81    fn generate(&self, ty: ComponentType<'_>, file: &mut File) {
82        let mut class = Class {
83            name: self.name.clone(),
84            super_class: if matches!(ty, ComponentType::Global) {
85                None
86            } else {
87                Some(SmolStr::new_static("slint.Component"))
88            },
89            ..Default::default()
90        };
91
92        class.fields = self
93            .properties
94            .iter()
95            .map(From::from)
96            .chain(
97                match ty {
98                    ComponentType::Global => None,
99                    ComponentType::Component { associated_globals } => Some(associated_globals),
100                }
101                .into_iter()
102                .flat_map(|globals| globals.iter())
103                .map(|glob| Field {
104                    name: glob.name.clone(),
105                    ty: Some(PyType { name: glob.name.clone(), optional: false }),
106                    default_value: None,
107                }),
108            )
109            .collect();
110
111        file.declarations.push(python_ast::Declaration::Class(class));
112
113        file.declarations.extend(self.aliases.iter().map(|exported_name| {
114            python_ast::Declaration::Variable(Variable {
115                name: ident(&exported_name),
116                value: self.name.clone(),
117            })
118        }))
119    }
120}
121
122impl From<&llr::PublicComponent> for PyComponent {
123    fn from(llr_compo: &llr::PublicComponent) -> Self {
124        Self {
125            name: ident(&llr_compo.name),
126            properties: llr_compo.public_properties.iter().map(From::from).collect(),
127            aliases: Vec::new(),
128        }
129    }
130}
131
132impl From<&llr::GlobalComponent> for PyComponent {
133    fn from(llr_global: &llr::GlobalComponent) -> Self {
134        Self {
135            name: ident(&llr_global.name),
136            properties: llr_global.public_properties.iter().map(From::from).collect(),
137            aliases: llr_global.aliases.iter().map(|exported_name| ident(&exported_name)).collect(),
138        }
139    }
140}
141
142#[derive(Clone, PartialEq, Debug, Serialize, Deserialize)]
143pub struct PyStructField {
144    name: SmolStr,
145    ty: SmolStr,
146}
147
148#[derive(Serialize, Deserialize)]
149pub struct PyStruct {
150    name: SmolStr,
151    fields: Vec<PyStructField>,
152    aliases: Vec<SmolStr>,
153}
154
155pub struct AnonymousStruct;
156
157impl TryFrom<&Rc<crate::langtype::Struct>> for PyStruct {
158    type Error = AnonymousStruct;
159
160    fn try_from(structty: &Rc<crate::langtype::Struct>) -> Result<Self, Self::Error> {
161        let StructName::User { name, .. } = &structty.name else {
162            return Err(AnonymousStruct);
163        };
164        Ok(Self {
165            name: ident(&name),
166            fields: structty
167                .fields
168                .iter()
169                .map(|(name, ty)| PyStructField { name: ident(&name), ty: python_type_name(ty) })
170                .collect(),
171            aliases: Vec::new(),
172        })
173    }
174}
175
176impl From<&PyStruct> for python_ast::Declaration {
177    fn from(py_struct: &PyStruct) -> Self {
178        let py_fields = py_struct
179            .fields
180            .iter()
181            .map(|field| Field {
182                name: field.name.clone(),
183                ty: Some(PyType { name: field.ty.clone(), optional: false }),
184                default_value: None,
185            })
186            .collect::<Vec<_>>();
187
188        let ctor = FunctionDeclaration {
189            name: SmolStr::new_static("__init__"),
190            positional_parameters: Vec::default(),
191            keyword_parameters: py_fields
192                .iter()
193                .cloned()
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<&Rc<crate::langtype::Enumeration>> for PyEnum {
239    fn from(enumty: &Rc<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    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            version: SmolStr::new_static("1.0"),
320            globals: Default::default(),
321            components: Default::default(),
322            structs_and_enums: Default::default(),
323        }
324    }
325}
326
327impl PyModule {
328    pub fn load_from_json(json: &str) -> Result<Self, String> {
329        serde_json::from_str(json).map_err(|e| format!("{}", e))
330    }
331}
332
333pub fn generate_py_module(
334    doc: &Document,
335    compiler_config: &CompilerConfiguration,
336) -> std::io::Result<PyModule> {
337    let mut module = PyModule::default();
338
339    let mut compo_aliases: HashMap<SmolStr, Vec<SmolStr>> = Default::default();
340    let mut struct_aliases: HashMap<SmolStr, Vec<SmolStr>> = Default::default();
341    let mut enum_aliases: HashMap<SmolStr, Vec<SmolStr>> = Default::default();
342
343    for export in doc.exports.iter() {
344        match &export.1 {
345            Either::Left(component) if !component.is_global() => {
346                if export.0.name != component.id {
347                    compo_aliases
348                        .entry(component.id.clone())
349                        .or_default()
350                        .push(export.0.name.clone());
351                }
352            }
353            Either::Right(ty) => match &ty {
354                Type::Struct(s) if s.node().is_some() => {
355                    if let StructName::User { name: orig_name, .. } = &s.name {
356                        if export.0.name != *orig_name {
357                            struct_aliases
358                                .entry(orig_name.clone())
359                                .or_default()
360                                .push(export.0.name.clone());
361                        }
362                    }
363                }
364                Type::Enumeration(en) => {
365                    if export.0.name != en.name {
366                        enum_aliases
367                            .entry(en.name.clone())
368                            .or_default()
369                            .push(export.0.name.clone());
370                    }
371                }
372                _ => {}
373            },
374            _ => {}
375        }
376    }
377
378    for ty in &doc.used_types.borrow().structs_and_enums {
379        match ty {
380            Type::Struct(s) => module.structs_and_enums.extend(
381                PyStruct::try_from(s).ok().and_then(|mut pystruct| {
382                    let StructName::User { name, .. } = &s.name else {
383                        return None;
384                    };
385                    pystruct.aliases = struct_aliases.remove(name).unwrap_or_default();
386                    Some(PyStructOrEnum::Struct(pystruct))
387                }),
388            ),
389            Type::Enumeration(en) => {
390                module.structs_and_enums.push({
391                    let mut pyenum = PyEnum::from(en);
392                    pyenum.aliases = enum_aliases.remove(&en.name).unwrap_or_default();
393                    PyStructOrEnum::Enum(pyenum)
394                });
395            }
396            _ => {}
397        }
398    }
399
400    let llr = llr::lower_to_item_tree::lower_to_item_tree(doc, compiler_config);
401
402    let globals = llr.globals.iter().filter(|glob| glob.exported && glob.must_generate());
403
404    module.globals.extend(globals.clone().map(PyComponent::from));
405    module.components.extend(llr.public_components.iter().map(|llr_compo| {
406        let mut pycompo = PyComponent::from(llr_compo);
407        pycompo.aliases = compo_aliases.remove(&llr_compo.name).unwrap_or_default();
408        pycompo
409    }));
410
411    Ok(module)
412}
413
414/// This module contains some data structures that helps represent a Python file.
415/// It is then rendered into an actual Python code using the Display trait
416mod python_ast {
417
418    use std::fmt::{Display, Error, Formatter};
419
420    use smol_str::SmolStr;
421
422    ///A full Python file
423    #[derive(Default, Debug)]
424    pub struct File {
425        pub imports: Vec<SmolStr>,
426        pub declarations: Vec<Declaration>,
427        pub trailing_code: Vec<SmolStr>,
428    }
429
430    impl Display for File {
431        fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), Error> {
432            writeln!(f, "# This file is auto-generated\n")?;
433            for import in &self.imports {
434                writeln!(f, "import {}", import)?;
435            }
436            writeln!(f)?;
437            for decl in &self.declarations {
438                writeln!(f, "{}", decl)?;
439            }
440            for code in &self.trailing_code {
441                writeln!(f, "{}", code)?;
442            }
443            Ok(())
444        }
445    }
446
447    #[derive(Debug, derive_more::Display)]
448    pub enum Declaration {
449        Class(Class),
450        Variable(Variable),
451    }
452
453    #[derive(Debug, Default)]
454    pub struct Class {
455        pub name: SmolStr,
456        pub super_class: Option<SmolStr>,
457        pub fields: Vec<Field>,
458        pub function_declarations: Vec<FunctionDeclaration>,
459    }
460
461    impl Display for Class {
462        fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
463            if let Some(super_class) = self.super_class.as_ref() {
464                writeln!(f, "class {}({}):", self.name, super_class)?;
465            } else {
466                writeln!(f, "class {}:", self.name)?;
467            }
468            if self.fields.is_empty() && self.function_declarations.is_empty() {
469                writeln!(f, "    pass")?;
470                return Ok(());
471            }
472
473            for field in &self.fields {
474                writeln!(f, "    {}", field)?;
475            }
476
477            if !self.fields.is_empty() {
478                writeln!(f)?;
479            }
480
481            for fundecl in &self.function_declarations {
482                writeln!(f, "    {}", fundecl)?;
483            }
484
485            Ok(())
486        }
487    }
488
489    #[derive(Debug)]
490    pub struct Variable {
491        pub name: SmolStr,
492        pub value: SmolStr,
493    }
494
495    impl Display for Variable {
496        fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
497            writeln!(f, "{} = {}", self.name, self.value)
498        }
499    }
500
501    #[derive(Debug, Clone)]
502    pub struct PyType {
503        pub name: SmolStr,
504        pub optional: bool,
505    }
506
507    impl Display for PyType {
508        fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
509            if self.optional {
510                write!(f, "typing.Optional[{}]", self.name)
511            } else {
512                write!(f, "{}", self.name)
513            }
514        }
515    }
516
517    #[derive(Debug, Clone)]
518    pub struct Field {
519        pub name: SmolStr,
520        pub ty: Option<PyType>,
521        pub default_value: Option<SmolStr>,
522    }
523
524    impl Display for Field {
525        fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
526            write!(f, "{}", self.name)?;
527            if let Some(ty) = &self.ty {
528                write!(f, ": {}", ty)?;
529            }
530            if let Some(default_value) = &self.default_value {
531                write!(f, " = {}", default_value)?
532            }
533            Ok(())
534        }
535    }
536
537    #[derive(Debug)]
538    pub struct FunctionDeclaration {
539        pub name: SmolStr,
540        pub positional_parameters: Vec<SmolStr>,
541        pub keyword_parameters: Vec<Field>,
542        pub return_type: Option<PyType>,
543    }
544
545    impl Display for FunctionDeclaration {
546        fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
547            write!(f, "def {}(self", self.name)?;
548
549            if !self.positional_parameters.is_empty() {
550                write!(f, ", {}", self.positional_parameters.join(","))?;
551            }
552
553            if !self.keyword_parameters.is_empty() {
554                write!(f, ", *")?;
555                write!(
556                    f,
557                    ", {}",
558                    self.keyword_parameters
559                        .iter()
560                        .map(ToString::to_string)
561                        .collect::<Vec<_>>()
562                        .join(", ")
563                )?;
564            }
565            writeln!(
566                f,
567                ") -> {}: ...",
568                self.return_type.as_ref().map_or(std::borrow::Cow::Borrowed("None"), |ty| {
569                    std::borrow::Cow::Owned(ty.to_string())
570                })
571            )?;
572            Ok(())
573        }
574    }
575}
576
577use crate::langtype::{StructName, Type};
578
579use crate::CompilerConfiguration;
580use crate::llr;
581use crate::object_tree::Document;
582use itertools::{Either, Itertools};
583use python_ast::*;
584
585/// Returns the text of the Python code produced by the given root component
586pub fn generate(
587    doc: &Document,
588    compiler_config: &CompilerConfiguration,
589    destination_path: Option<&std::path::Path>,
590) -> std::io::Result<File> {
591    let mut file = File { ..Default::default() };
592    file.imports.push(SmolStr::new_static("slint"));
593    file.imports.push(SmolStr::new_static("typing"));
594
595    let pymodule = generate_py_module(doc, compiler_config)?;
596
597    if pymodule.structs_and_enums.iter().any(|se| matches!(se, PyStructOrEnum::Enum(_))) {
598        file.imports.push(SmolStr::new_static("enum"));
599    }
600
601    file.declarations.extend(pymodule.structs_and_enums.iter().map(From::from));
602
603    for global in &pymodule.globals {
604        global.generate(ComponentType::Global, &mut file);
605    }
606
607    for public_component in &pymodule.components {
608        public_component.generate(
609            ComponentType::Component { associated_globals: &pymodule.globals },
610            &mut file,
611        );
612    }
613
614    for struct_or_enum in &pymodule.structs_and_enums {
615        struct_or_enum.generate_aliases(&mut file);
616    }
617
618    let main_file = std::path::absolute(
619        doc.node
620            .as_ref()
621            .ok_or_else(|| std::io::Error::other("Cannot determine path of the main file"))?
622            .source_file
623            .path(),
624    )
625    .unwrap();
626
627    let destination_path = destination_path.and_then(|maybe_relative_destination_path| {
628        std::fs::canonicalize(maybe_relative_destination_path)
629            .ok()
630            .and_then(|p| p.parent().map(std::path::PathBuf::from))
631    });
632
633    let relative_path_from_destination_to_main_file =
634        destination_path.and_then(|destination_path| {
635            pathdiff::diff_paths(main_file.parent().unwrap(), destination_path)
636        });
637
638    if let Some(relative_path_from_destination_to_main_file) =
639        relative_path_from_destination_to_main_file
640    {
641        use base64::engine::Engine;
642        use std::io::Write;
643
644        let mut api_str_compressor =
645            flate2::write::GzEncoder::new(Vec::new(), flate2::Compression::default());
646        api_str_compressor.write_all(serde_json::to_string(&pymodule).unwrap().as_bytes())?;
647        let compressed_api_str = api_str_compressor.finish()?;
648        let base64_api_str = base64::engine::general_purpose::STANDARD.encode(&compressed_api_str);
649
650        file.imports.push(SmolStr::new_static("os"));
651        file.trailing_code.push(format_smolstr!(
652            "globals().update(vars(slint._load_file_checked(path=os.path.join(os.path.dirname(__file__), r'{}'), expected_api_base64_compressed=r'{}', generated_file=__file__)))",
653            relative_path_from_destination_to_main_file.join(main_file.file_name().unwrap()).to_string_lossy(),
654            base64_api_str
655        ));
656    }
657
658    Ok(file)
659}
660
661fn python_type_name(ty: &Type) -> SmolStr {
662    match ty {
663        Type::Invalid => panic!("Invalid type encountered in llr output"),
664        Type::Void => SmolStr::new_static("None"),
665        Type::String => SmolStr::new_static("str"),
666        Type::Color => SmolStr::new_static("slint.Color"),
667        Type::Float32
668        | Type::Int32
669        | Type::Duration
670        | Type::Angle
671        | Type::PhysicalLength
672        | Type::LogicalLength
673        | Type::Percent
674        | Type::Rem
675        | Type::UnitProduct(_) => SmolStr::new_static("float"),
676        Type::Image => SmolStr::new_static("slint.Image"),
677        Type::Bool => SmolStr::new_static("bool"),
678        Type::Brush => SmolStr::new_static("slint.Brush"),
679        Type::Array(elem_type) => format_smolstr!("slint.Model[{}]", python_type_name(elem_type)),
680        Type::Struct(s) => match &s.name {
681            StructName::User { name, .. } => ident(name),
682            StructName::BuiltinPrivate(_) => SmolStr::new_static("None"),
683            StructName::BuiltinPublic(_) | StructName::None => {
684                let tuple_types =
685                    s.fields.values().map(|ty| python_type_name(ty)).collect::<Vec<_>>();
686                format_smolstr!("typing.Tuple[{}]", tuple_types.join(", "))
687            }
688        },
689        Type::Enumeration(enumeration) => {
690            if enumeration.node.is_some() {
691                ident(&enumeration.name)
692            } else {
693                SmolStr::new_static("None")
694            }
695        }
696        Type::Callback(function) | Type::Function(function) => {
697            format_smolstr!(
698                "typing.Callable[[{}], {}]",
699                function.args.iter().map(|arg_ty| python_type_name(arg_ty)).join(", "),
700                python_type_name(&function.return_type)
701            )
702        }
703        ty @ _ => unimplemented!("implemented type conversion {:#?}", ty),
704    }
705}