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 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 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
407mod python_ast {
410
411 use std::fmt::{Display, Error, Formatter};
412
413 use smol_str::SmolStr;
414
415 #[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
578pub 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}