dampen_core/codegen/
bindings.rs

1//! Binding expression inlining for code generation
2//!
3//! This module provides functions to generate pure Rust code from binding expressions,
4//! eliminating runtime interpretation overhead in production builds.
5
6use crate::CodegenError;
7use crate::expr::ast::{
8    BinaryOp, BinaryOpExpr, ConditionalExpr, Expr, FieldAccessExpr, LiteralExpr, MethodCallExpr,
9    SharedFieldAccessExpr, UnaryOp, UnaryOpExpr,
10};
11use proc_macro2::TokenStream;
12use quote::{format_ident, quote};
13
14/// Generate Rust code for a binding expression
15///
16/// This function converts binding expressions from the AST into TokenStream
17/// for direct field access without runtime evaluation.
18///
19/// # Arguments
20/// * `expr` - The expression to generate code for
21///
22/// # Returns
23/// Generated code as a TokenStream that produces a String when interpolated
24pub fn generate_expr(expr: &Expr) -> TokenStream {
25    match expr {
26        Expr::FieldAccess(field_access) => generate_field_access(field_access),
27        Expr::SharedFieldAccess(shared_access) => generate_shared_field_access(shared_access),
28        Expr::MethodCall(method_call) => generate_method_call(method_call),
29        Expr::BinaryOp(binary_op) => generate_binary_op(binary_op),
30        Expr::UnaryOp(unary_op) => generate_unary_op(unary_op),
31        Expr::Conditional(conditional) => generate_conditional(conditional),
32        Expr::Literal(literal) => generate_literal(literal),
33    }
34}
35
36/// Generate Rust code for a boolean expression
37///
38/// This function is like generate_expr but produces native boolean values
39/// instead of converting to String. Use for conditions like `enabled="{count > 0}"`.
40///
41/// # Arguments
42/// * `expr` - The expression to generate code for
43///
44/// # Returns
45/// Generated code as a TokenStream that produces a bool
46pub fn generate_bool_expr(expr: &Expr) -> TokenStream {
47    match expr {
48        Expr::FieldAccess(field_access) => generate_field_access_raw(field_access),
49        Expr::SharedFieldAccess(shared_access) => generate_shared_field_access_raw(shared_access),
50        Expr::MethodCall(method_call) => generate_method_call_raw(method_call),
51        Expr::BinaryOp(binary_op) => generate_binary_op_raw(binary_op),
52        Expr::UnaryOp(unary_op) => generate_unary_op_raw(unary_op),
53        Expr::Conditional(conditional) => generate_conditional_raw(conditional),
54        Expr::Literal(literal) => generate_literal_raw(literal),
55    }
56}
57
58/// Validate that an expression can be inlined
59///
60/// Returns Err if the expression contains unsupported constructs for codegen
61pub fn validate_expression_inlinable(expr: &Expr) -> Result<(), CodegenError> {
62    match expr {
63        Expr::FieldAccess(_) => Ok(()),
64        Expr::SharedFieldAccess(_) => Ok(()), // Shared field access is inlinable
65        Expr::MethodCall(method_expr) => {
66            validate_expression_inlinable(&method_expr.receiver)?;
67            for arg in &method_expr.args {
68                validate_expression_inlinable(arg)?;
69            }
70            Ok(())
71        }
72        Expr::BinaryOp(binary_expr) => {
73            validate_expression_inlinable(&binary_expr.left)?;
74            validate_expression_inlinable(&binary_expr.right)?;
75            Ok(())
76        }
77        Expr::UnaryOp(unary_expr) => {
78            validate_expression_inlinable(&unary_expr.operand)?;
79            Ok(())
80        }
81        Expr::Conditional(cond_expr) => {
82            validate_expression_inlinable(&cond_expr.condition)?;
83            validate_expression_inlinable(&cond_expr.then_branch)?;
84            validate_expression_inlinable(&cond_expr.else_branch)?;
85            Ok(())
86        }
87        Expr::Literal(_) => Ok(()),
88    }
89}
90
91/// Generate code for a field access expression
92///
93/// # Arguments
94/// * `expr` - Field access with path components
95///
96/// # Returns
97/// TokenStream generating `model.field.to_string()`
98fn generate_field_access(expr: &FieldAccessExpr) -> TokenStream {
99    if expr.path.is_empty() {
100        return quote! { String::new() };
101    }
102
103    let field_access: Vec<_> = expr.path.iter().map(|s| format_ident!("{}", s)).collect();
104
105    quote! { model.#(#field_access).*.to_string() }
106}
107
108/// Generate code for a shared field access expression
109///
110/// # Arguments
111/// * `expr` - Shared field access with path components (after "shared.")
112///
113/// # Returns
114/// TokenStream generating `shared.field.to_string()` where `shared` refers to
115/// the shared context's read guard
116fn generate_shared_field_access(expr: &SharedFieldAccessExpr) -> TokenStream {
117    if expr.path.is_empty() {
118        return quote! { String::new() };
119    }
120
121    // Generate access to the shared context
122    // The generated code assumes a `shared` variable is in scope (the read guard)
123    let field_access: Vec<_> = expr.path.iter().map(|s| format_ident!("{}", s)).collect();
124
125    quote! { shared.#(#field_access).*.to_string() }
126}
127
128/// Generate code for a method call expression
129///
130/// # Arguments
131/// * `expr` - Method call with receiver and arguments
132///
133/// # Returns
134/// TokenStream generating `model.receiver.method(args).to_string()`
135fn generate_method_call(expr: &MethodCallExpr) -> TokenStream {
136    let receiver_tokens = generate_expr(&expr.receiver);
137    let method_ident = format_ident!("{}", expr.method);
138
139    if expr.args.is_empty() {
140        quote! { #receiver_tokens.#method_ident().to_string() }
141    } else {
142        let arg_tokens: Vec<TokenStream> = expr.args.iter().map(generate_expr).collect();
143        quote! { #receiver_tokens.#method_ident(#(#arg_tokens),*).to_string() }
144    }
145}
146
147/// Generate code for a binary operation expression
148///
149/// # Arguments
150/// * `expr` - Binary operation with left, op, right
151///
152/// # Returns
153/// TokenStream generating `(left op right).to_string()`
154fn generate_binary_op(expr: &BinaryOpExpr) -> TokenStream {
155    let left = generate_expr(&expr.left);
156    let right = generate_expr(&expr.right);
157    let op = match expr.op {
158        BinaryOp::Eq => quote! { == },
159        BinaryOp::Ne => quote! { != },
160        BinaryOp::Lt => quote! { < },
161        BinaryOp::Le => quote! { <= },
162        BinaryOp::Gt => quote! { > },
163        BinaryOp::Ge => quote! { >= },
164        BinaryOp::And => quote! { && },
165        BinaryOp::Or => quote! { || },
166        BinaryOp::Add => quote! { + },
167        BinaryOp::Sub => quote! { - },
168        BinaryOp::Mul => quote! { * },
169        BinaryOp::Div => quote! { / },
170    };
171
172    quote! { (#left #op #right).to_string() }
173}
174
175/// Generate code for a unary operation expression
176///
177/// # Arguments
178/// * `expr` - Unary operation with operator and operand
179///
180/// # Returns
181/// TokenStream generating `(!operand).to_string()` or `(-operand).to_string()`
182fn generate_unary_op(expr: &UnaryOpExpr) -> TokenStream {
183    let operand = generate_expr(&expr.operand);
184    let op = match expr.op {
185        UnaryOp::Not => quote! { ! },
186        UnaryOp::Neg => quote! { - },
187    };
188
189    quote! { (#op #operand).to_string() }
190}
191
192/// Generate code for a conditional expression
193///
194/// # Arguments
195/// * `expr` - Conditional with condition, then_branch, else_branch
196///
197/// # Returns
198/// TokenStream generating `if condition { then } else { else }.to_string()`
199fn generate_conditional(expr: &ConditionalExpr) -> TokenStream {
200    let condition = generate_expr(&expr.condition);
201    let then_branch = generate_expr(&expr.then_branch);
202    let else_branch = generate_expr(&expr.else_branch);
203
204    quote! {
205        {
206            let __cond = #condition;
207            let __then = #then_branch;
208            let __else = #else_branch;
209            if __cond.trim() == "true" || __cond.parse::<bool>().unwrap_or(false) {
210                __then
211            } else {
212                __else
213            }
214        }
215    }
216}
217
218/// Generate code for a literal expression
219///
220/// # Arguments
221/// * `expr` - Literal value
222///
223/// # Returns
224/// TokenStream generating the literal value as a string
225fn generate_literal(expr: &LiteralExpr) -> TokenStream {
226    match expr {
227        LiteralExpr::String(s) => {
228            let lit = proc_macro2::Literal::string(s);
229            quote! { #lit.to_string() }
230        }
231        LiteralExpr::Integer(n) => {
232            let lit = proc_macro2::Literal::i64_unsuffixed(*n);
233            quote! { #lit.to_string() }
234        }
235        LiteralExpr::Float(f) => {
236            let lit = proc_macro2::Literal::f64_unsuffixed(*f);
237            quote! { #lit.to_string() }
238        }
239        LiteralExpr::Bool(b) => {
240            let val = if *b { "true" } else { "false" };
241            let lit = proc_macro2::Literal::string(val);
242            quote! { #lit.to_string() }
243        }
244    }
245}
246
247/// Generate Rust code for interpolated strings
248///
249/// Converts interpolated strings like "Count: {count}" into format! macro calls.
250///
251/// # Arguments
252/// * `parts` - Alternating literal strings and binding expressions
253///
254/// # Returns
255/// TokenStream generating format! macro invocation
256///
257/// # Examples
258/// ```ignore
259/// // "Count: {count}"
260/// generate_interpolated(...) -> quote! { format!("Count: {}", count) }
261/// ```
262pub fn generate_interpolated(parts: &[String]) -> TokenStream {
263    if parts.is_empty() {
264        return quote! { String::new() };
265    }
266
267    let mut format_args = Vec::new();
268    let mut arg_exprs = Vec::new();
269
270    for part in parts {
271        if part.starts_with('{') && part.ends_with('}') {
272            let binding_name = &part[1..part.len() - 1];
273            let field_parts: Vec<_> = binding_name
274                .split('.')
275                .map(|s| format_ident!("{}", s))
276                .collect();
277            format_args.push("{}");
278            arg_exprs.push(quote! { #(#field_parts).*.to_string() });
279        } else {
280            format_args.push(part);
281        }
282    }
283
284    let format_string = format_args.join("");
285    let lit = proc_macro2::Literal::string(&format_string);
286
287    quote! { format!(#lit, #(#arg_exprs),*) }
288}
289
290// ============================================================================
291// Raw value generation (without .to_string() conversion)
292// Used for boolean expressions in conditions like enabled="{count > 0}"
293// ============================================================================
294
295/// Generate field access without .to_string() conversion
296fn generate_field_access_raw(expr: &FieldAccessExpr) -> TokenStream {
297    if expr.path.is_empty() {
298        return quote! { false };
299    }
300
301    let field_access: Vec<_> = expr.path.iter().map(|s| format_ident!("{}", s)).collect();
302    quote! { model.#(#field_access).* }
303}
304
305/// Generate shared field access without .to_string() conversion
306fn generate_shared_field_access_raw(expr: &SharedFieldAccessExpr) -> TokenStream {
307    if expr.path.is_empty() {
308        return quote! { false };
309    }
310
311    let field_access: Vec<_> = expr.path.iter().map(|s| format_ident!("{}", s)).collect();
312    quote! { shared.#(#field_access).* }
313}
314
315/// Generate method call without .to_string() conversion
316fn generate_method_call_raw(expr: &MethodCallExpr) -> TokenStream {
317    let receiver_tokens = generate_bool_expr(&expr.receiver);
318    let method_ident = format_ident!("{}", &expr.method);
319    let arg_tokens: Vec<_> = expr.args.iter().map(generate_bool_expr).collect();
320
321    quote! { #receiver_tokens.#method_ident(#(#arg_tokens),*) }
322}
323
324/// Generate binary operation without .to_string() conversion
325fn generate_binary_op_raw(expr: &BinaryOpExpr) -> TokenStream {
326    let left = generate_bool_expr(&expr.left);
327    let right = generate_bool_expr(&expr.right);
328    let op = match expr.op {
329        BinaryOp::Eq => quote! { == },
330        BinaryOp::Ne => quote! { != },
331        BinaryOp::Lt => quote! { < },
332        BinaryOp::Le => quote! { <= },
333        BinaryOp::Gt => quote! { > },
334        BinaryOp::Ge => quote! { >= },
335        BinaryOp::And => quote! { && },
336        BinaryOp::Or => quote! { || },
337        BinaryOp::Add => quote! { + },
338        BinaryOp::Sub => quote! { - },
339        BinaryOp::Mul => quote! { * },
340        BinaryOp::Div => quote! { / },
341    };
342
343    quote! { #left #op #right }
344}
345
346/// Generate unary operation without .to_string() conversion
347fn generate_unary_op_raw(expr: &UnaryOpExpr) -> TokenStream {
348    let operand = generate_bool_expr(&expr.operand);
349    let op = match expr.op {
350        UnaryOp::Not => quote! { ! },
351        UnaryOp::Neg => quote! { - },
352    };
353
354    quote! { #op #operand }
355}
356
357/// Generate conditional expression without .to_string() conversion
358fn generate_conditional_raw(expr: &ConditionalExpr) -> TokenStream {
359    let condition = generate_bool_expr(&expr.condition);
360    let then_branch = generate_bool_expr(&expr.then_branch);
361    let else_branch = generate_bool_expr(&expr.else_branch);
362
363    quote! {
364        if #condition {
365            #then_branch
366        } else {
367            #else_branch
368        }
369    }
370}
371
372/// Generate literal without .to_string() conversion
373fn generate_literal_raw(expr: &LiteralExpr) -> TokenStream {
374    match expr {
375        LiteralExpr::String(s) => {
376            let lit = proc_macro2::Literal::string(s);
377            quote! { #lit }
378        }
379        LiteralExpr::Integer(n) => {
380            let lit = proc_macro2::Literal::i64_unsuffixed(*n);
381            quote! { #lit }
382        }
383        LiteralExpr::Float(f) => {
384            let lit = proc_macro2::Literal::f64_unsuffixed(*f);
385            quote! { #lit }
386        }
387        LiteralExpr::Bool(b) => {
388            quote! { #b }
389        }
390    }
391}