dampen_core/codegen/
mod.rs

1//! Code generation for production builds
2//!
3//! This module generates static Rust code from Dampen UI definitions.
4//! The generated code has zero runtime overhead compared to hand-written Iced code.
5//!
6//! # Overview
7//!
8//! Code generation is used in production mode to eliminate runtime parsing:
9//! 1. Parse `.dampen` files at build time
10//! 2. Generate Rust code with `generate_application()`
11//! 3. Include generated code via `include!(concat!(env!("OUT_DIR"), "/ui_generated.rs"))`
12//!
13//! # Example
14//!
15//! ```rust,ignore
16//! // At build time:
17//! let doc = parse(ui_xml)?;
18//! let output = generate_application(&doc, "Model", "Message", &handlers)?;
19//! fs::write("ui_generated.rs", output.code)?;
20//!
21//! // At runtime:
22//! include!("ui_generated.rs");
23//! // No XML parsing, no runtime overhead!
24//! ```
25//!
26//! # Generated Code Structure
27//!
28//! The generated code includes:
29//! - Message enum from handler signatures
30//! - `impl Application for Model` with `view()` and `update()` methods
31//! - Inlined widget tree with evaluated bindings
32
33pub mod application;
34pub mod bindings;
35pub mod config;
36pub mod handlers;
37pub mod inventory;
38pub mod status_mapping;
39pub mod subscription;
40pub mod theme;
41pub mod update;
42pub mod view;
43
44use crate::DampenDocument;
45use crate::HandlerSignature;
46use proc_macro2::TokenStream;
47use quote::quote;
48use std::path::PathBuf;
49use std::time::SystemTime;
50
51/// Handler signature classification for code generation
52#[derive(Debug, Clone, Copy, PartialEq, Eq)]
53pub enum HandlerSignatureType {
54    /// fn(&mut Model) -> ()
55    /// Simple handler with no additional parameters
56    Simple,
57
58    /// fn(&mut Model, T) -> ()
59    /// Handler that receives a value from the UI element
60    WithValue,
61
62    /// fn(&mut Model) -> Command<Message>
63    /// Handler that returns a command for side effects
64    WithCommand,
65}
66
67/// Metadata structure emitted by #[ui_handler] macro for build-time registration
68#[derive(Debug, Clone)]
69pub struct HandlerInfo {
70    /// Unique handler name referenced in XML
71    pub name: &'static str,
72
73    /// Handler signature classification
74    pub signature_type: HandlerSignatureType,
75
76    /// Parameter type names for validation
77    pub param_types: &'static [&'static str],
78
79    /// Return type name
80    pub return_type: &'static str,
81
82    /// Source file location for error messages
83    pub source_file: &'static str,
84
85    /// Source line number
86    pub source_line: u32,
87}
88
89impl HandlerInfo {
90    /// Convert HandlerInfo to HandlerSignature for code generation.
91    ///
92    /// This bridges the metadata emitted by #[ui_handler] with the signature
93    /// format expected by the code generator.
94    ///
95    /// # Returns
96    ///
97    /// A HandlerSignature suitable for use with generate_application()
98    pub fn to_signature(&self) -> HandlerSignature {
99        // Determine param_type based on signature type
100        let param_type = match self.signature_type {
101            HandlerSignatureType::Simple => None,
102            HandlerSignatureType::WithValue => {
103                // Extract the second parameter type (first is &mut Model)
104                if self.param_types.len() > 1 {
105                    Some(self.param_types[1].to_string())
106                } else {
107                    None
108                }
109            }
110            HandlerSignatureType::WithCommand => None,
111        };
112
113        let returns_command = matches!(self.signature_type, HandlerSignatureType::WithCommand);
114
115        HandlerSignature {
116            name: self.name.to_string(),
117            param_type,
118            returns_command,
119        }
120    }
121}
122
123/// Result of code generation
124///
125/// Contains the generated Rust code and any warnings or notes.
126#[derive(Debug)]
127pub struct CodegenOutput {
128    /// Generated Rust code
129    pub code: String,
130    /// Any warnings or notes
131    pub warnings: Vec<String>,
132}
133
134/// Output structure from code generation
135#[derive(Debug, Clone)]
136pub struct GeneratedApplication {
137    /// Generated Rust code as string
138    pub code: String,
139
140    /// List of handler names discovered
141    pub handlers: Vec<String>,
142
143    /// List of widget types used
144    pub widgets: Vec<String>,
145
146    /// Any warnings generated during code gen
147    pub warnings: Vec<String>,
148}
149
150/// Container for generated Rust code with metadata
151///
152/// This structure holds generated code along with metadata about the source
153/// file and validation status. It provides methods for validating and formatting
154/// the generated code.
155#[derive(Debug, Clone)]
156pub struct GeneratedCode {
157    /// Generated Rust source code
158    pub code: String,
159
160    /// Module path (e.g., "ui_window")
161    pub module_name: String,
162
163    /// Source .dampen file path
164    pub source_file: PathBuf,
165
166    /// Generated at timestamp
167    pub timestamp: SystemTime,
168
169    /// Validation status
170    pub validated: bool,
171}
172
173impl GeneratedCode {
174    /// Create a new GeneratedCode instance
175    ///
176    /// # Arguments
177    /// * `code` - The generated Rust source code
178    /// * `module_name` - Module name (e.g., "ui_window")
179    /// * `source_file` - Path to the source .dampen file
180    ///
181    /// # Returns
182    /// A new GeneratedCode instance with validated set to false
183    pub fn new(code: String, module_name: String, source_file: PathBuf) -> Self {
184        Self {
185            code,
186            module_name,
187            source_file,
188            timestamp: SystemTime::now(),
189            validated: false,
190        }
191    }
192
193    /// Validate syntax by parsing with syn
194    ///
195    /// # Returns
196    /// Ok(()) if the code is valid Rust, Err with message otherwise
197    pub fn validate(&mut self) -> Result<(), String> {
198        match syn::parse_file(&self.code) {
199            Ok(_) => {
200                self.validated = true;
201                Ok(())
202            }
203            Err(e) => Err(format!("Syntax validation failed: {}", e)),
204        }
205    }
206
207    /// Format code with prettyplease
208    ///
209    /// # Returns
210    /// Ok(()) if formatting succeeded, Err with message otherwise
211    pub fn format(&mut self) -> Result<(), String> {
212        // First parse the code
213        match syn::parse_file(&self.code) {
214            Ok(syntax_tree) => {
215                // Format using prettyplease
216                self.code = prettyplease::unparse(&syntax_tree);
217                Ok(())
218            }
219            Err(e) => Err(format!("Failed to parse code for formatting: {}", e)),
220        }
221    }
222
223    /// Write to output directory
224    ///
225    /// # Arguments
226    /// * `path` - Path to write the generated code to
227    ///
228    /// # Returns
229    /// Ok(()) if write succeeded, Err with IO error otherwise
230    pub fn write_to_file(&self, path: &std::path::Path) -> std::io::Result<()> {
231        std::fs::write(path, &self.code)
232    }
233}
234
235/// Generate complete application code from a Dampen document
236///
237/// This is the main entry point for code generation. It orchestrates
238/// the generation of all necessary components.
239///
240/// # Arguments
241///
242/// * `document` - Parsed Dampen document
243/// * `model_name` - Name of the model struct (e.g., "Model")
244/// * `message_name` - Name of the message enum (e.g., "Message")
245/// * `handlers` - List of handler signatures
246///
247/// # Returns
248///
249/// `Ok(CodegenOutput)` with generated code and warnings
250///
251/// # Errors
252///
253/// Returns `CodegenError` if:
254/// - Handler validation fails
255/// - Widget generation fails
256pub fn generate_application(
257    document: &DampenDocument,
258    model_name: &str,
259    message_name: &str,
260    handlers: &[HandlerSignature],
261) -> Result<CodegenOutput, CodegenError> {
262    let warnings = Vec::new();
263
264    let message_enum = generate_message_enum(handlers);
265
266    let view_fn = view::generate_view(document, model_name, message_name)?;
267
268    let update_arms = update::generate_arms(handlers, message_name)?;
269
270    let model_ident = syn::Ident::new(model_name, proc_macro2::Span::call_site());
271    let message_ident = syn::Ident::new(message_name, proc_macro2::Span::call_site());
272
273    let combined = quote! {
274        use iced::{Element, Task};
275        use crate::ui::window::*;
276
277        #message_enum
278
279        pub fn new_model() -> (#model_ident, Task<#message_ident>) {
280            (#model_ident::default(), Task::none())
281        }
282
283        pub fn update_model(model: &mut #model_ident, message: #message_ident) -> Task<#message_ident> {
284            match message {
285                #update_arms
286            }
287        }
288
289        pub fn view_model(model: &#model_ident) -> Element<'_, #message_ident> {
290            #view_fn
291        }
292    };
293
294    Ok(CodegenOutput {
295        code: combined.to_string(),
296        warnings,
297    })
298}
299
300use crate::ir::theme::ThemeDocument;
301
302/// Generate complete application code with theme and subscription support
303///
304/// This is the full-featured entry point for code generation that includes:
305/// - Message enum with system theme variant (if follow_system is enabled)
306/// - Subscription function for system theme detection
307/// - Theme code generation
308/// - View and update functions
309///
310/// # Arguments
311///
312/// * `document` - Parsed Dampen document
313/// * `model_name` - Name of the model struct (e.g., "Model")
314/// * `message_name` - Name of the message enum (e.g., "Message")
315/// * `handlers` - List of handler signatures
316/// * `theme_document` - Optional theme document for theme code generation
317///
318/// # Returns
319///
320/// `Ok(CodegenOutput)` with generated code and warnings
321pub fn generate_application_with_theme_and_subscriptions(
322    document: &DampenDocument,
323    model_name: &str,
324    message_name: &str,
325    handlers: &[HandlerSignature],
326    theme_document: Option<&ThemeDocument>,
327) -> Result<CodegenOutput, CodegenError> {
328    let warnings = Vec::new();
329
330    // Create subscription config from theme document
331    let sub_config =
332        subscription::SubscriptionConfig::from_theme_document(theme_document, message_name);
333
334    // Generate message enum with system theme variant if needed
335    let message_enum = generate_message_enum_with_subscription(handlers, Some(&sub_config));
336
337    let view_fn = view::generate_view(document, model_name, message_name)?;
338
339    let update_arms = update::generate_arms(handlers, message_name)?;
340
341    // Generate system theme update arm if needed
342    let system_theme_arm = subscription::generate_system_theme_update_arm(&sub_config);
343
344    let model_ident = syn::Ident::new(model_name, proc_macro2::Span::call_site());
345    let message_ident = syn::Ident::new(message_name, proc_macro2::Span::call_site());
346
347    // Generate theme code if theme document is provided
348    let theme_code = if let Some(theme_doc) = theme_document {
349        match theme::generate_theme_code(theme_doc, &document.style_classes, "app") {
350            Ok(generated) => {
351                let code_str = generated.code;
352                syn::parse_str::<TokenStream>(&code_str).unwrap_or_default()
353            }
354            Err(e) => {
355                return Err(CodegenError::ThemeError(e));
356            }
357        }
358    } else {
359        TokenStream::new()
360    };
361
362    // Generate subscription function
363    let subscription_fn = subscription::generate_subscription_function(&sub_config);
364
365    let has_theme = theme_document.is_some();
366    let theme_method = if has_theme {
367        quote! {
368            pub fn theme(_model: &#model_ident) -> iced::Theme {
369                app_theme()
370            }
371        }
372    } else {
373        quote! {}
374    };
375
376    let combined = quote! {
377        use iced::{Element, Task, Theme};
378        use crate::ui::window::*;
379        use std::collections::HashMap;
380
381        #theme_code
382
383        #message_enum
384
385        pub fn new_model() -> (#model_ident, Task<#message_ident>) {
386            (#model_ident::default(), Task::none())
387        }
388
389        pub fn update_model(model: &mut #model_ident, message: #message_ident) -> Task<#message_ident> {
390            match message {
391                #update_arms
392                #system_theme_arm
393            }
394        }
395
396        pub fn view_model(model: &#model_ident) -> Element<'_, #message_ident> {
397            #view_fn
398        }
399
400        #theme_method
401
402        #subscription_fn
403    };
404
405    Ok(CodegenOutput {
406        code: combined.to_string(),
407        warnings,
408    })
409}
410
411/// Generate Message enum from handler signatures
412fn generate_message_enum(handlers: &[HandlerSignature]) -> TokenStream {
413    generate_message_enum_with_subscription(handlers, None)
414}
415
416/// Generate Message enum from handler signatures with optional subscription config
417fn generate_message_enum_with_subscription(
418    handlers: &[HandlerSignature],
419    sub_config: Option<&subscription::SubscriptionConfig>,
420) -> TokenStream {
421    let handler_variants: Vec<_> = handlers
422        .iter()
423        .map(|h| {
424            // Convert snake_case to UpperCamelCase
425            let variant_name = to_upper_camel_case(&h.name);
426            let ident = syn::Ident::new(&variant_name, proc_macro2::Span::call_site());
427
428            if let Some(param_type) = &h.param_type {
429                let type_ident = syn::Ident::new(param_type, proc_macro2::Span::call_site());
430                quote! { #ident(#type_ident) }
431            } else {
432                quote! { #ident }
433            }
434        })
435        .collect();
436
437    // Add system theme variant if configured
438    let system_theme_variant = sub_config.and_then(subscription::generate_system_theme_variant);
439
440    let all_variants: Vec<TokenStream> = handler_variants
441        .into_iter()
442        .chain(system_theme_variant)
443        .collect();
444
445    if all_variants.is_empty() {
446        return quote! {
447            #[derive(Clone, Debug)]
448            pub enum Message {}
449        };
450    }
451
452    quote! {
453        #[derive(Clone, Debug)]
454        pub enum Message {
455            #(#all_variants),*
456        }
457    }
458}
459
460/// Convert snake_case to UpperCamelCase
461fn to_upper_camel_case(s: &str) -> String {
462    let mut result = String::new();
463    let mut capitalize_next = true;
464    for c in s.chars() {
465        if c == '_' {
466            capitalize_next = true;
467        } else if capitalize_next {
468            result.push(c.to_ascii_uppercase());
469            capitalize_next = false;
470        } else {
471            result.push(c);
472        }
473    }
474    result
475}
476
477/// Basic constant folding optimizations for generated code
478///
479/// Performs simple optimizations:
480/// - Removes consecutive blank lines
481/// - Trims trailing whitespace from lines
482/// - Normalizes multiple spaces to single space
483pub fn constant_folding(code: &str) -> String {
484    let mut result = String::with_capacity(code.len());
485
486    for line in code.lines() {
487        let trimmed = line.trim_end();
488        if !trimmed.is_empty() {
489            result.push_str(trimmed);
490            result.push('\n');
491        }
492    }
493
494    result
495}
496
497/// Validate that all handlers referenced in the document exist
498pub fn validate_handlers(
499    document: &DampenDocument,
500    available_handlers: &[HandlerSignature],
501) -> Result<(), CodegenError> {
502    let handler_names: Vec<_> = available_handlers.iter().map(|h| h.name.clone()).collect();
503
504    // Collect all handler references from the document
505    fn collect_handlers(node: &crate::WidgetNode, handlers: &mut Vec<String>) {
506        for event in &node.events {
507            handlers.push(event.handler.clone());
508        }
509        for child in &node.children {
510            collect_handlers(child, handlers);
511        }
512    }
513
514    let mut referenced_handlers = Vec::new();
515    collect_handlers(&document.root, &mut referenced_handlers);
516
517    // Check each referenced handler exists
518    for handler in referenced_handlers {
519        if !handler_names.contains(&handler) {
520            return Err(CodegenError::MissingHandler(handler));
521        }
522    }
523
524    Ok(())
525}
526
527/// Errors that can occur during code generation
528#[derive(Debug, thiserror::Error)]
529pub enum CodegenError {
530    #[error("Handler '{0}' is referenced but not defined")]
531    MissingHandler(String),
532
533    #[error("Invalid widget kind for code generation: {0}")]
534    InvalidWidget(String),
535
536    #[error("Binding expression error: {0}")]
537    BindingError(String),
538
539    #[error("Theme code generation error: {0}")]
540    ThemeError(String),
541
542    #[error("IO error: {0}")]
543    IoError(#[from] std::io::Error),
544}
545
546#[cfg(test)]
547mod tests {
548    use super::*;
549    use crate::parse;
550
551    #[test]
552    fn test_message_enum_generation() {
553        let handlers = vec![
554            HandlerSignature {
555                name: "increment".to_string(),
556                param_type: None,
557                returns_command: false,
558            },
559            HandlerSignature {
560                name: "update_value".to_string(),
561                param_type: Some("String".to_string()),
562                returns_command: false,
563            },
564        ];
565
566        let tokens = generate_message_enum(&handlers);
567        let code = tokens.to_string();
568
569        assert!(code.contains("Increment"));
570        assert!(code.contains("UpdateValue"));
571    }
572
573    #[test]
574    fn test_handler_validation() {
575        let xml = r#"<column><button on_click="increment" /></column>"#;
576        let doc = parse(xml).unwrap();
577
578        let handlers = vec![HandlerSignature {
579            name: "increment".to_string(),
580            param_type: None,
581            returns_command: false,
582        }];
583
584        assert!(validate_handlers(&doc, &handlers).is_ok());
585
586        // Missing handler should fail
587        let handlers_empty: Vec<HandlerSignature> = vec![];
588        assert!(validate_handlers(&doc, &handlers_empty).is_err());
589    }
590}