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