1use 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
19fn 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
403mod python_ast {
406
407 use std::fmt::{Display, Error, Formatter};
408
409 use smol_str::SmolStr;
410
411 #[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
574pub 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}