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                .map(|field| {
194                    let mut kw_field = field.clone();
195                    kw_field.ty.as_mut().unwrap().optional = true;
196                    kw_field.default_value = Some(SmolStr::new_static("None"));
197                    kw_field
198                })
199                .collect(),
200            return_type: None,
201        };
202
203        let struct_class = Class {
204            name: py_struct.name.clone(),
205            fields: py_fields,
206            function_declarations: vec![ctor],
207            ..Default::default()
208        };
209        python_ast::Declaration::Class(struct_class)
210    }
211}
212
213impl PyStruct {
214    fn generate_aliases(&self) -> impl ExactSizeIterator<Item = python_ast::Declaration> + use<'_> {
215        self.aliases.iter().map(|alias| {
216            python_ast::Declaration::Variable(Variable {
217                name: alias.clone(),
218                value: self.name.clone(),
219            })
220        })
221    }
222}
223
224#[derive(Serialize, Deserialize)]
225pub struct PyEnumVariant {
226    name: SmolStr,
227    strvalue: SmolStr,
228}
229
230#[derive(Serialize, Deserialize)]
231pub struct PyEnum {
232    name: SmolStr,
233    variants: Vec<PyEnumVariant>,
234    aliases: Vec<SmolStr>,
235}
236
237impl From<&Rc<crate::langtype::Enumeration>> for PyEnum {
238    fn from(enumty: &Rc<crate::langtype::Enumeration>) -> Self {
239        Self {
240            name: ident(&enumty.name),
241            variants: enumty
242                .values
243                .iter()
244                .map(|val| PyEnumVariant { name: ident(val), strvalue: val.clone() })
245                .collect(),
246            aliases: Vec::new(),
247        }
248    }
249}
250
251impl From<&PyEnum> for python_ast::Declaration {
252    fn from(py_enum: &PyEnum) -> Self {
253        python_ast::Declaration::Class(Class {
254            name: py_enum.name.clone(),
255            super_class: Some(SmolStr::new_static("enum.StrEnum")),
256            fields: py_enum
257                .variants
258                .iter()
259                .map(|variant| Field {
260                    name: variant.name.clone(),
261                    ty: None,
262                    default_value: Some(format_smolstr!("\"{}\"", variant.strvalue)),
263                })
264                .collect(),
265            function_declarations: Vec::new(),
266        })
267    }
268}
269
270impl PyEnum {
271    fn generate_aliases(&self) -> impl ExactSizeIterator<Item = python_ast::Declaration> + use<'_> {
272        self.aliases.iter().map(|alias| {
273            python_ast::Declaration::Variable(Variable {
274                name: alias.clone(),
275                value: self.name.clone(),
276            })
277        })
278    }
279}
280
281#[derive(Serialize, Deserialize)]
282pub enum PyStructOrEnum {
283    Struct(PyStruct),
284    Enum(PyEnum),
285}
286
287impl From<&PyStructOrEnum> for python_ast::Declaration {
288    fn from(struct_or_enum: &PyStructOrEnum) -> Self {
289        match struct_or_enum {
290            PyStructOrEnum::Struct(py_struct) => py_struct.into(),
291            PyStructOrEnum::Enum(py_enum) => py_enum.into(),
292        }
293    }
294}
295
296impl PyStructOrEnum {
297    fn generate_aliases(&self, file: &mut File) {
298        match self {
299            PyStructOrEnum::Struct(py_struct) => {
300                file.declarations.extend(py_struct.generate_aliases())
301            }
302            PyStructOrEnum::Enum(py_enum) => file.declarations.extend(py_enum.generate_aliases()),
303        }
304    }
305}
306
307#[derive(Serialize, Deserialize)]
308pub struct PyModule {
309    version: SmolStr,
310    globals: Vec<PyComponent>,
311    components: Vec<PyComponent>,
312    structs_and_enums: Vec<PyStructOrEnum>,
313}
314
315impl Default for PyModule {
316    fn default() -> Self {
317        Self {
318            version: SmolStr::new_static("1.0"),
319            globals: Default::default(),
320            components: Default::default(),
321            structs_and_enums: Default::default(),
322        }
323    }
324}
325
326impl PyModule {
327    pub fn load_from_json(json: &str) -> Result<Self, String> {
328        serde_json::from_str(json).map_err(|e| format!("{}", e))
329    }
330}
331
332pub fn generate_py_module(
333    doc: &Document,
334    compiler_config: &CompilerConfiguration,
335) -> std::io::Result<PyModule> {
336    let mut module = PyModule::default();
337
338    let mut compo_aliases: HashMap<SmolStr, Vec<SmolStr>> = Default::default();
339    let mut struct_aliases: HashMap<SmolStr, Vec<SmolStr>> = Default::default();
340    let mut enum_aliases: HashMap<SmolStr, Vec<SmolStr>> = Default::default();
341
342    for export in doc.exports.iter() {
343        match &export.1 {
344            Either::Left(component) if !component.is_global() && export.0.name != component.id => {
345                compo_aliases.entry(component.id.clone()).or_default().push(export.0.name.clone());
346            }
347            Either::Right(ty) => match &ty {
348                Type::Struct(s) if s.node().is_some() => {
349                    if let StructName::User { name: orig_name, .. } = &s.name
350                        && export.0.name != *orig_name
351                    {
352                        struct_aliases
353                            .entry(orig_name.clone())
354                            .or_default()
355                            .push(export.0.name.clone());
356                    }
357                }
358                Type::Enumeration(en) if export.0.name != en.name => {
359                    enum_aliases.entry(en.name.clone()).or_default().push(export.0.name.clone());
360                }
361                _ => {}
362            },
363            _ => {}
364        }
365    }
366
367    for ty in &doc.used_types.borrow().structs_and_enums {
368        match ty {
369            Type::Struct(s) => module.structs_and_enums.extend(
370                PyStruct::try_from(s).ok().and_then(|mut pystruct| {
371                    let StructName::User { name, .. } = &s.name else {
372                        return None;
373                    };
374                    pystruct.aliases = struct_aliases.remove(name).unwrap_or_default();
375                    Some(PyStructOrEnum::Struct(pystruct))
376                }),
377            ),
378            Type::Enumeration(en) => {
379                module.structs_and_enums.push({
380                    let mut pyenum = PyEnum::from(en);
381                    pyenum.aliases = enum_aliases.remove(&en.name).unwrap_or_default();
382                    PyStructOrEnum::Enum(pyenum)
383                });
384            }
385            _ => {}
386        }
387    }
388
389    let llr = llr::lower_to_item_tree::lower_to_item_tree(doc, compiler_config);
390
391    let globals = llr.globals.iter().filter(|glob| glob.exported && glob.must_generate());
392
393    module.globals.extend(globals.clone().map(PyComponent::from));
394    module.components.extend(llr.public_components.iter().map(|llr_compo| {
395        let mut pycompo = PyComponent::from(llr_compo);
396        pycompo.aliases = compo_aliases.remove(&llr_compo.name).unwrap_or_default();
397        pycompo
398    }));
399
400    Ok(module)
401}
402
403/// This module contains some data structures that helps represent a Python file.
404/// It is then rendered into an actual Python code using the Display trait
405mod python_ast {
406
407    use std::fmt::{Display, Error, Formatter};
408
409    use smol_str::SmolStr;
410
411    ///A full Python file
412    #[derive(Default, Debug)]
413    pub struct File {
414        pub imports: Vec<SmolStr>,
415        pub declarations: Vec<Declaration>,
416        pub trailing_code: Vec<SmolStr>,
417    }
418
419    impl Display for File {
420        fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), Error> {
421            writeln!(f, "# This file is auto-generated\n")?;
422            for import in &self.imports {
423                writeln!(f, "import {}", import)?;
424            }
425            writeln!(f)?;
426            for decl in &self.declarations {
427                writeln!(f, "{}", decl)?;
428            }
429            for code in &self.trailing_code {
430                writeln!(f, "{}", code)?;
431            }
432            Ok(())
433        }
434    }
435
436    #[derive(Debug, derive_more::Display)]
437    pub enum Declaration {
438        Class(Class),
439        Variable(Variable),
440    }
441
442    #[derive(Debug, Default)]
443    pub struct Class {
444        pub name: SmolStr,
445        pub super_class: Option<SmolStr>,
446        pub fields: Vec<Field>,
447        pub function_declarations: Vec<FunctionDeclaration>,
448    }
449
450    impl Display for Class {
451        fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
452            if let Some(super_class) = self.super_class.as_ref() {
453                writeln!(f, "class {}({}):", self.name, super_class)?;
454            } else {
455                writeln!(f, "class {}:", self.name)?;
456            }
457            if self.fields.is_empty() && self.function_declarations.is_empty() {
458                writeln!(f, "    pass")?;
459                return Ok(());
460            }
461
462            for field in &self.fields {
463                writeln!(f, "    {}", field)?;
464            }
465
466            if !self.fields.is_empty() {
467                writeln!(f)?;
468            }
469
470            for fundecl in &self.function_declarations {
471                writeln!(f, "    {}", fundecl)?;
472            }
473
474            Ok(())
475        }
476    }
477
478    #[derive(Debug)]
479    pub struct Variable {
480        pub name: SmolStr,
481        pub value: SmolStr,
482    }
483
484    impl Display for Variable {
485        fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
486            writeln!(f, "{} = {}", self.name, self.value)
487        }
488    }
489
490    #[derive(Debug, Clone)]
491    pub struct PyType {
492        pub name: SmolStr,
493        pub optional: bool,
494    }
495
496    impl Display for PyType {
497        fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
498            if self.optional {
499                write!(f, "typing.Optional[{}]", self.name)
500            } else {
501                write!(f, "{}", self.name)
502            }
503        }
504    }
505
506    #[derive(Debug, Clone)]
507    pub struct Field {
508        pub name: SmolStr,
509        pub ty: Option<PyType>,
510        pub default_value: Option<SmolStr>,
511    }
512
513    impl Display for Field {
514        fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
515            write!(f, "{}", self.name)?;
516            if let Some(ty) = &self.ty {
517                write!(f, ": {}", ty)?;
518            }
519            if let Some(default_value) = &self.default_value {
520                write!(f, " = {}", default_value)?
521            }
522            Ok(())
523        }
524    }
525
526    #[derive(Debug)]
527    pub struct FunctionDeclaration {
528        pub name: SmolStr,
529        pub positional_parameters: Vec<SmolStr>,
530        pub keyword_parameters: Vec<Field>,
531        pub return_type: Option<PyType>,
532    }
533
534    impl Display for FunctionDeclaration {
535        fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
536            write!(f, "def {}(self", self.name)?;
537
538            if !self.positional_parameters.is_empty() {
539                write!(f, ", {}", self.positional_parameters.join(","))?;
540            }
541
542            if !self.keyword_parameters.is_empty() {
543                write!(f, ", *")?;
544                write!(
545                    f,
546                    ", {}",
547                    self.keyword_parameters
548                        .iter()
549                        .map(ToString::to_string)
550                        .collect::<Vec<_>>()
551                        .join(", ")
552                )?;
553            }
554            writeln!(
555                f,
556                ") -> {}: ...",
557                self.return_type.as_ref().map_or(std::borrow::Cow::Borrowed("None"), |ty| {
558                    std::borrow::Cow::Owned(ty.to_string())
559                })
560            )?;
561            Ok(())
562        }
563    }
564}
565
566use crate::langtype::{StructName, Type};
567
568use crate::CompilerConfiguration;
569use crate::llr;
570use crate::object_tree::Document;
571use itertools::{Either, Itertools};
572use python_ast::*;
573
574/// Returns the text of the Python code produced by the given root component
575pub fn generate(
576    doc: &Document,
577    compiler_config: &CompilerConfiguration,
578    destination_path: Option<&std::path::Path>,
579) -> std::io::Result<File> {
580    let mut file = File { ..Default::default() };
581    file.imports.push(SmolStr::new_static("slint"));
582    file.imports.push(SmolStr::new_static("typing"));
583
584    let pymodule = generate_py_module(doc, compiler_config)?;
585
586    if pymodule.structs_and_enums.iter().any(|se| matches!(se, PyStructOrEnum::Enum(_))) {
587        file.imports.push(SmolStr::new_static("enum"));
588    }
589
590    file.declarations.extend(pymodule.structs_and_enums.iter().map(From::from));
591
592    for global in &pymodule.globals {
593        global.generate(ComponentType::Global, &mut file);
594    }
595
596    for public_component in &pymodule.components {
597        public_component.generate(
598            ComponentType::Component { associated_globals: &pymodule.globals },
599            &mut file,
600        );
601    }
602
603    for struct_or_enum in &pymodule.structs_and_enums {
604        struct_or_enum.generate_aliases(&mut file);
605    }
606
607    let main_file = std::path::absolute(
608        doc.node
609            .as_ref()
610            .ok_or_else(|| std::io::Error::other("Cannot determine path of the main file"))?
611            .source_file
612            .path(),
613    )
614    .unwrap();
615
616    let destination_path = destination_path.and_then(|maybe_relative_destination_path| {
617        std::fs::canonicalize(maybe_relative_destination_path)
618            .ok()
619            .and_then(|p| p.parent().map(std::path::PathBuf::from))
620    });
621
622    let relative_path_from_destination_to_main_file =
623        destination_path.and_then(|destination_path| {
624            pathdiff::diff_paths(main_file.parent().unwrap(), destination_path)
625        });
626
627    if let Some(relative_path_from_destination_to_main_file) =
628        relative_path_from_destination_to_main_file
629    {
630        use base64::engine::Engine;
631        use std::io::Write;
632
633        let mut api_str_compressor =
634            flate2::write::GzEncoder::new(Vec::new(), flate2::Compression::default());
635        api_str_compressor.write_all(serde_json::to_string(&pymodule).unwrap().as_bytes())?;
636        let compressed_api_str = api_str_compressor.finish()?;
637        let base64_api_str = base64::engine::general_purpose::STANDARD.encode(&compressed_api_str);
638
639        file.imports.push(SmolStr::new_static("os"));
640        file.trailing_code.push(format_smolstr!(
641            "globals().update(vars(slint._load_file_checked(path=os.path.join(os.path.dirname(__file__), r'{}'), expected_api_base64_compressed=r'{}', generated_file=__file__)))",
642            relative_path_from_destination_to_main_file.join(main_file.file_name().unwrap()).to_string_lossy(),
643            base64_api_str
644        ));
645    }
646
647    Ok(file)
648}
649
650fn python_type_name(ty: &Type) -> SmolStr {
651    match ty {
652        Type::Invalid => panic!("Invalid type encountered in llr output"),
653        Type::Void => SmolStr::new_static("None"),
654        Type::String => SmolStr::new_static("str"),
655        Type::Color => SmolStr::new_static("slint.Color"),
656        Type::Float32
657        | Type::Int32
658        | Type::Duration
659        | Type::Angle
660        | Type::PhysicalLength
661        | Type::LogicalLength
662        | Type::Percent
663        | Type::Rem
664        | Type::UnitProduct(_) => SmolStr::new_static("float"),
665        Type::Image => SmolStr::new_static("slint.Image"),
666        Type::Bool => SmolStr::new_static("bool"),
667        Type::Brush => SmolStr::new_static("slint.Brush"),
668        Type::Array(elem_type) => format_smolstr!("slint.Model[{}]", python_type_name(elem_type)),
669        Type::Struct(s) => match &s.name {
670            StructName::User { name, .. } => ident(name),
671            StructName::BuiltinPrivate(_) => SmolStr::new_static("None"),
672            StructName::BuiltinPublic(
673                crate::langtype::BuiltinPublicStruct::Color
674                | crate::langtype::BuiltinPublicStruct::LogicalPosition
675                | crate::langtype::BuiltinPublicStruct::LogicalSize,
676            )
677            | StructName::None => {
678                let tuple_types = s.fields.values().map(python_type_name).collect::<Vec<_>>();
679                format_smolstr!("typing.Tuple[{}]", tuple_types.join(", "))
680            }
681            StructName::BuiltinPublic(builtin_public_struct) => {
682                let name: &'static str = builtin_public_struct.into();
683                format_smolstr!("slint.language.{}", name)
684            }
685        },
686        Type::Enumeration(enumeration) => {
687            if enumeration.node.is_some() {
688                ident(&enumeration.name)
689            } else {
690                SmolStr::new_static("None")
691            }
692        }
693        Type::Callback(function) | Type::Function(function) => {
694            format_smolstr!(
695                "typing.Callable[[{}], {}]",
696                function.args.iter().map(python_type_name).join(", "),
697                python_type_name(&function.return_type)
698            )
699        }
700        Type::Keys => SmolStr::new_static("slint.Keys"),
701        ty => unimplemented!("implemented type conversion {:#?}", ty),
702    }
703}