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::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    pub(crate) 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            // Bump whenever the meaning of any annotation produced by
319            // `python_type_name` changes (e.g. Type::Int32 → "int" in 2.0).
320            // A previously-generated wrapper that carries an older version
321            // is treated as incompatible by `changed_version`.
322            version: SmolStr::new_static("2.1"),
323            globals: Default::default(),
324            components: Default::default(),
325            structs_and_enums: Default::default(),
326        }
327    }
328}
329
330impl PyModule {
331    pub fn load_from_json(json: &str) -> Result<Self, String> {
332        serde_json::from_str(json).map_err(|e| format!("{}", e))
333    }
334}
335
336pub fn generate_py_module(
337    doc: &Document,
338    compiler_config: &CompilerConfiguration,
339) -> std::io::Result<PyModule> {
340    let mut module = PyModule::default();
341
342    let mut compo_aliases: HashMap<SmolStr, Vec<SmolStr>> = Default::default();
343    let mut struct_aliases: HashMap<SmolStr, Vec<SmolStr>> = Default::default();
344    let mut enum_aliases: HashMap<SmolStr, Vec<SmolStr>> = Default::default();
345
346    for export in doc.exports.iter() {
347        match &export.1 {
348            Either::Left(component) if !component.is_global() && export.0.name != component.id => {
349                compo_aliases.entry(component.id.clone()).or_default().push(export.0.name.clone());
350            }
351            Either::Right(ty) => match &ty {
352                Type::Struct(s) if s.node().is_some() => {
353                    if let StructName::User { name: orig_name, .. } = &s.name
354                        && export.0.name != *orig_name
355                    {
356                        struct_aliases
357                            .entry(orig_name.clone())
358                            .or_default()
359                            .push(export.0.name.clone());
360                    }
361                }
362                Type::Enumeration(en) if export.0.name != en.name => {
363                    enum_aliases.entry(en.name.clone()).or_default().push(export.0.name.clone());
364                }
365                _ => {}
366            },
367            _ => {}
368        }
369    }
370
371    for ty in &doc.used_types.borrow().structs_and_enums {
372        match ty {
373            Type::Struct(s) => module.structs_and_enums.extend(
374                PyStruct::try_from(s).ok().and_then(|mut pystruct| {
375                    let StructName::User { name, .. } = &s.name else {
376                        return None;
377                    };
378                    pystruct.aliases = struct_aliases.remove(name).unwrap_or_default();
379                    Some(PyStructOrEnum::Struct(pystruct))
380                }),
381            ),
382            Type::Enumeration(en) => {
383                module.structs_and_enums.push({
384                    let mut pyenum = PyEnum::from(en);
385                    pyenum.aliases = enum_aliases.remove(&en.name).unwrap_or_default();
386                    PyStructOrEnum::Enum(pyenum)
387                });
388            }
389            _ => {}
390        }
391    }
392
393    let llr = llr::lower_to_item_tree::lower_to_item_tree(doc, compiler_config);
394
395    let globals = llr.globals.iter().filter(|glob| glob.exported && glob.must_generate());
396
397    module.globals.extend(globals.clone().map(PyComponent::from));
398    module.components.extend(llr.public_components.iter().map(|llr_compo| {
399        let mut pycompo = PyComponent::from(llr_compo);
400        pycompo.aliases = compo_aliases.remove(&llr_compo.name).unwrap_or_default();
401        pycompo
402    }));
403
404    Ok(module)
405}
406
407/// This module contains some data structures that helps represent a Python file.
408/// It is then rendered into an actual Python code using the Display trait
409mod python_ast {
410
411    use std::fmt::{Display, Error, Formatter};
412
413    use smol_str::SmolStr;
414
415    ///A full Python file
416    #[derive(Default, Debug)]
417    pub struct File {
418        pub imports: Vec<SmolStr>,
419        pub declarations: Vec<Declaration>,
420        pub trailing_code: Vec<SmolStr>,
421    }
422
423    impl Display for File {
424        fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), Error> {
425            writeln!(f, "# This file is auto-generated\n")?;
426            for import in &self.imports {
427                writeln!(f, "import {}", import)?;
428            }
429            writeln!(f)?;
430            for decl in &self.declarations {
431                writeln!(f, "{}", decl)?;
432            }
433            for code in &self.trailing_code {
434                writeln!(f, "{}", code)?;
435            }
436            Ok(())
437        }
438    }
439
440    #[derive(Debug, derive_more::Display)]
441    pub enum Declaration {
442        Class(Class),
443        Variable(Variable),
444    }
445
446    #[derive(Debug, Default)]
447    pub struct Class {
448        pub name: SmolStr,
449        pub super_class: Option<SmolStr>,
450        pub fields: Vec<Field>,
451        pub function_declarations: Vec<FunctionDeclaration>,
452    }
453
454    impl Display for Class {
455        fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
456            if let Some(super_class) = self.super_class.as_ref() {
457                writeln!(f, "class {}({}):", self.name, super_class)?;
458            } else {
459                writeln!(f, "class {}:", self.name)?;
460            }
461            if self.fields.is_empty() && self.function_declarations.is_empty() {
462                writeln!(f, "    pass")?;
463                return Ok(());
464            }
465
466            for field in &self.fields {
467                writeln!(f, "    {}", field)?;
468            }
469
470            if !self.fields.is_empty() {
471                writeln!(f)?;
472            }
473
474            for fundecl in &self.function_declarations {
475                writeln!(f, "    {}", fundecl)?;
476            }
477
478            Ok(())
479        }
480    }
481
482    #[derive(Debug)]
483    pub struct Variable {
484        pub name: SmolStr,
485        pub value: SmolStr,
486    }
487
488    impl Display for Variable {
489        fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
490            writeln!(f, "{} = {}", self.name, self.value)
491        }
492    }
493
494    #[derive(Debug, Clone)]
495    pub struct PyType {
496        pub name: SmolStr,
497        pub optional: bool,
498    }
499
500    impl Display for PyType {
501        fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
502            if self.optional {
503                write!(f, "typing.Optional[{}]", self.name)
504            } else {
505                write!(f, "{}", self.name)
506            }
507        }
508    }
509
510    #[derive(Debug, Clone)]
511    pub struct Field {
512        pub name: SmolStr,
513        pub ty: Option<PyType>,
514        pub default_value: Option<SmolStr>,
515    }
516
517    impl Display for Field {
518        fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
519            write!(f, "{}", self.name)?;
520            if let Some(ty) = &self.ty {
521                write!(f, ": {}", ty)?;
522            }
523            if let Some(default_value) = &self.default_value {
524                write!(f, " = {}", default_value)?
525            }
526            Ok(())
527        }
528    }
529
530    #[derive(Debug)]
531    pub struct FunctionDeclaration {
532        pub name: SmolStr,
533        pub positional_parameters: Vec<SmolStr>,
534        pub keyword_parameters: Vec<Field>,
535        pub return_type: Option<PyType>,
536    }
537
538    impl Display for FunctionDeclaration {
539        fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
540            write!(f, "def {}(self", self.name)?;
541
542            if !self.positional_parameters.is_empty() {
543                write!(f, ", {}", self.positional_parameters.join(","))?;
544            }
545
546            if !self.keyword_parameters.is_empty() {
547                write!(f, ", *")?;
548                write!(
549                    f,
550                    ", {}",
551                    self.keyword_parameters
552                        .iter()
553                        .map(ToString::to_string)
554                        .collect::<Vec<_>>()
555                        .join(", ")
556                )?;
557            }
558            writeln!(
559                f,
560                ") -> {}: ...",
561                self.return_type.as_ref().map_or(std::borrow::Cow::Borrowed("None"), |ty| {
562                    std::borrow::Cow::Owned(ty.to_string())
563                })
564            )?;
565            Ok(())
566        }
567    }
568}
569
570use crate::langtype::{StructName, Type};
571
572use crate::CompilerConfiguration;
573use crate::llr;
574use crate::object_tree::Document;
575use itertools::{Either, Itertools};
576use python_ast::*;
577
578/// Returns the text of the Python code produced by the given root component
579pub fn generate(
580    doc: &Document,
581    compiler_config: &CompilerConfiguration,
582    destination_path: Option<&std::path::Path>,
583) -> std::io::Result<File> {
584    let mut file = File { ..Default::default() };
585    file.imports.push(SmolStr::new_static("slint"));
586    file.imports.push(SmolStr::new_static("typing"));
587
588    let pymodule = generate_py_module(doc, compiler_config)?;
589
590    if pymodule.structs_and_enums.iter().any(|se| matches!(se, PyStructOrEnum::Enum(_))) {
591        file.imports.push(SmolStr::new_static("enum"));
592    }
593
594    file.declarations.extend(pymodule.structs_and_enums.iter().map(From::from));
595
596    for global in &pymodule.globals {
597        global.generate(ComponentType::Global, &mut file);
598    }
599
600    for public_component in &pymodule.components {
601        public_component.generate(
602            ComponentType::Component { associated_globals: &pymodule.globals },
603            &mut file,
604        );
605    }
606
607    for struct_or_enum in &pymodule.structs_and_enums {
608        struct_or_enum.generate_aliases(&mut file);
609    }
610
611    let main_file = std::path::absolute(
612        doc.node
613            .as_ref()
614            .ok_or_else(|| std::io::Error::other("Cannot determine path of the main file"))?
615            .source_file
616            .path(),
617    )
618    .unwrap();
619
620    let destination_path = destination_path.and_then(|maybe_relative_destination_path| {
621        std::fs::canonicalize(maybe_relative_destination_path)
622            .ok()
623            .and_then(|p| p.parent().map(std::path::PathBuf::from))
624    });
625
626    let relative_path_from_destination_to_main_file =
627        destination_path.and_then(|destination_path| {
628            pathdiff::diff_paths(main_file.parent().unwrap(), destination_path)
629        });
630
631    if let Some(relative_path_from_destination_to_main_file) =
632        relative_path_from_destination_to_main_file
633    {
634        use base64::engine::Engine;
635        use std::io::Write;
636
637        let mut api_str_compressor =
638            flate2::write::GzEncoder::new(Vec::new(), flate2::Compression::default());
639        api_str_compressor.write_all(serde_json::to_string(&pymodule).unwrap().as_bytes())?;
640        let compressed_api_str = api_str_compressor.finish()?;
641        let base64_api_str = base64::engine::general_purpose::STANDARD.encode(&compressed_api_str);
642
643        file.imports.push(SmolStr::new_static("os"));
644        file.trailing_code.push(format_smolstr!(
645            "globals().update(vars(slint._load_file_checked(path=os.path.join(os.path.dirname(__file__), r'{}'), expected_api_base64_compressed=r'{}', generated_file=__file__)))",
646            relative_path_from_destination_to_main_file.join(main_file.file_name().unwrap()).to_string_lossy(),
647            base64_api_str
648        ));
649    }
650
651    Ok(file)
652}
653
654fn python_type_name(ty: &Type) -> SmolStr {
655    match ty {
656        Type::Invalid => panic!("Invalid type encountered in llr output"),
657        Type::Void => SmolStr::new_static("None"),
658        Type::String => SmolStr::new_static("str"),
659        Type::Color => SmolStr::new_static("slint.Color"),
660        Type::Int32 => SmolStr::new_static("int"),
661        Type::Float32
662        | Type::Duration
663        | Type::Angle
664        | Type::PhysicalLength
665        | Type::LogicalLength
666        | Type::Percent
667        | Type::Rem
668        | Type::UnitProduct(_) => SmolStr::new_static("float"),
669        Type::Image => SmolStr::new_static("slint.Image"),
670        Type::Bool => SmolStr::new_static("bool"),
671        Type::Brush => SmolStr::new_static("slint.Brush"),
672        Type::StyledText => SmolStr::new_static("slint.StyledText"),
673        Type::Array(elem_type) => format_smolstr!("slint.Model[{}]", python_type_name(elem_type)),
674        Type::Struct(s) => match &s.name {
675            StructName::User { name, .. } => ident(name),
676            StructName::Builtin(crate::langtype::BuiltinStruct::LogicalPosition) => {
677                SmolStr::new_static("slint.LogicalPosition")
678            }
679            StructName::Builtin(crate::langtype::BuiltinStruct::LogicalSize) => {
680                SmolStr::new_static("slint.LogicalSize")
681            }
682            StructName::Builtin(crate::langtype::BuiltinStruct::Color) | StructName::None => {
683                let tuple_types = s.fields.values().map(python_type_name).collect::<Vec<_>>();
684                format_smolstr!("typing.Tuple[{}]", tuple_types.join(", "))
685            }
686            StructName::Builtin(builtin_struct) if builtin_struct.is_public() => {
687                let name: &'static str = builtin_struct.into();
688                format_smolstr!("slint.language.{}", name)
689            }
690            StructName::Builtin(_) => SmolStr::new_static("None"),
691        },
692        Type::Enumeration(enumeration) => {
693            if enumeration.node.is_some() {
694                ident(&enumeration.name)
695            } else {
696                SmolStr::new_static("None")
697            }
698        }
699        Type::Callback(function) | Type::Function(function) => {
700            format_smolstr!(
701                "typing.Callable[[{}], {}]",
702                function.args.iter().map(python_type_name).join(", "),
703                python_type_name(&function.return_type)
704            )
705        }
706        Type::Keys => SmolStr::new_static("slint.Keys"),
707        Type::DataTransfer => SmolStr::new_static("slint.DataTransfer"),
708        ty => unimplemented!("implemented type conversion {:#?}", ty),
709    }
710}