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 .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
414mod python_ast {
417
418 use std::fmt::{Display, Error, Formatter};
419
420 use smol_str::SmolStr;
421
422 #[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
585pub 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}