Skip to main content

telltale_runtime/compiler/
effects_codegen.rs

1// Code generation for free algebra choreographic protocols
2//
3// This module generates protocol implementations that build
4// effect programs using a free algebra approach.
5
6use crate::ast::{Choreography, Condition, Protocol, Role};
7use proc_macro2::TokenStream;
8use quote::{format_ident, quote};
9
10#[path = "effects_protocol_types.rs"]
11mod protocol_types;
12use protocol_types::{generate_label_type, generate_message_types};
13
14/// Generate annotation-aware effect metadata for a protocol node
15fn generate_effect_metadata_from_annotations(protocol: &Protocol, _role: &Role) -> TokenStream {
16    use crate::ast::ProtocolAnnotation;
17
18    let annotations = protocol.get_annotations();
19
20    if annotations.is_empty() {
21        return quote! {};
22    }
23
24    let metadata_items: Vec<TokenStream> = annotations
25        .iter()
26        .filter_map(|annotation| {
27            match annotation {
28                ProtocolAnnotation::Priority(value) => Some(quote! { .with_priority(#value) }),
29                ProtocolAnnotation::RuntimeTimeout(dur) => {
30                    let secs = dur.as_secs();
31                    Some(quote! { .with_timeout(std::time::Duration::from_secs(#secs)) })
32                }
33                ProtocolAnnotation::Retry { max_attempts, .. } => {
34                    Some(quote! { .with_retry(#max_attempts) })
35                }
36                ProtocolAnnotation::Custom { key, value } => {
37                    // Generic annotation - add as metadata
38                    Some(quote! { .with_annotation(#key, #value) })
39                }
40                // Skip annotations that don't generate effect metadata
41                // (these are handled elsewhere or are purely structural)
42                ProtocolAnnotation::TimedChoice { .. }
43                | ProtocolAnnotation::Idempotent
44                | ProtocolAnnotation::Trace { .. }
45                | ProtocolAnnotation::Heartbeat { .. }
46                | ProtocolAnnotation::Parallel
47                | ProtocolAnnotation::Ordered
48                | ProtocolAnnotation::MinResponses(_) => None,
49            }
50        })
51        .collect();
52
53    quote! { #(#metadata_items)* }
54}
55
56/// Generate effect-based protocol implementation
57#[must_use]
58pub fn generate_effects_protocol(choreography: &Choreography) -> TokenStream {
59    let protocol_name = &choreography.name;
60    let roles = generate_role_enum(&choreography.roles);
61    let labels = generate_label_type(&choreography.protocol);
62    let messages = generate_message_types(&choreography.protocol);
63    let role_functions = generate_role_functions(choreography);
64    let endpoint_type = generate_endpoint_type(protocol_name);
65
66    quote! {
67        use telltale_runtime::{
68            ChoreoHandler, Result, Program, Effect, LabelId, RoleId, RoleName,
69            interpret, InterpretResult, ProgramMessage
70        };
71        use serde::{Serialize, Deserialize};
72
73        // Common message trait for this choreography
74        #[derive(Clone, Debug, Serialize, Deserialize)]
75        pub enum Message {
76            // Generated message variants would go here
77            Default,
78        }
79
80        impl ProgramMessage for Message {}
81
82        #roles
83
84        #endpoint_type
85
86        #labels
87
88        #messages
89
90        #role_functions
91    }
92}
93
94fn generate_role_enum(roles: &[Role]) -> TokenStream {
95    let role_names: Vec<_> = roles.iter().map(|r| r.name()).collect();
96    let role_match_arms = roles.iter().map(|role| {
97        let role_name = role.name();
98        let role_str = role.name().to_string();
99        quote! { Role::#role_name => RoleName::from_static(#role_str) }
100    });
101
102    quote! {
103        #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
104        pub enum Role {
105            #(#role_names),*
106        }
107
108        impl RoleId for Role {
109            type Label = Label;
110
111            fn role_name(&self) -> RoleName {
112                match self {
113                    #(#role_match_arms),*
114                }
115            }
116        }
117    }
118}
119
120fn generate_endpoint_type(protocol_name: &proc_macro2::Ident) -> TokenStream {
121    let ep_name = format_ident!("{}Endpoint", protocol_name);
122
123    quote! {
124        pub struct #ep_name {
125            // Protocol-specific endpoint state
126        }
127
128        impl telltale::effects::Endpoint for #ep_name {}
129    }
130}
131
132fn generate_role_functions(choreography: &Choreography) -> TokenStream {
133    choreography
134        .roles
135        .iter()
136        .map(|role| {
137            let role_name_str = role.name().to_string().to_lowercase();
138            let program_fn_name = format_ident!("{}_program", role_name_str);
139            let run_fn_name = format_ident!("run_{}", role_name_str);
140            let protocol_name = &choreography.name;
141            let endpoint_type = format_ident!("{}Endpoint", protocol_name);
142
143            let body = generate_role_body(&choreography.protocol, role);
144
145            quote! {
146                /// Generate the choreographic program for this role
147                pub fn #program_fn_name() -> Program<Role, Message> {
148                    #body
149                }
150
151                /// Run the choreographic program for this role using a handler
152                pub async fn #run_fn_name<H: ChoreoHandler<Role = Role, Endpoint = #endpoint_type>>(
153                    handler: &mut H,
154                    endpoint: &mut #endpoint_type,
155                ) -> Result<InterpretResult<Message>> {
156                    let program = #program_fn_name();
157                    interpret(handler, endpoint, program).await
158                }
159            }
160        })
161        .collect()
162}
163
164fn generate_role_body(protocol: &Protocol, role: &Role) -> TokenStream {
165    generate_program_builder(protocol, role)
166}
167
168/// Generate program builder code for a protocol from the perspective of a specific role
169fn generate_program_builder(protocol: &Protocol, role: &Role) -> TokenStream {
170    let program_effects = generate_program_effects(protocol, role);
171
172    quote! {
173        use telltale_runtime::{Program, Effect};
174
175        Program::new()
176            #program_effects
177            .end()
178    }
179}
180
181/// Generate effect builder calls for a protocol
182#[allow(clippy::cognitive_complexity)]
183#[allow(clippy::too_many_lines)]
184// RECURSION_SAFE: structural recursion over finite protocol AST depth.
185fn generate_program_effects(protocol: &Protocol, role: &Role) -> TokenStream {
186    match protocol {
187        Protocol::End => {
188            quote! {}
189        }
190        Protocol::Begin { continuation, .. }
191        | Protocol::Await { continuation, .. }
192        | Protocol::Resolve { continuation, .. }
193        | Protocol::Invalidate { continuation, .. }
194        | Protocol::Let { continuation, .. }
195        | Protocol::Publish { continuation, .. }
196        | Protocol::PublishAuthority { continuation, .. }
197        | Protocol::Materialize { continuation, .. }
198        | Protocol::Handoff { continuation, .. }
199        | Protocol::DependentWork { continuation, .. } => {
200            generate_program_effects(continuation, role)
201        }
202        Protocol::Case { branches, .. } => {
203            let branch_effects: Vec<_> = branches
204                .iter()
205                .map(|branch| generate_program_effects(&branch.protocol, role))
206                .collect();
207            quote! { #(#branch_effects)* }
208        }
209        Protocol::Timeout {
210            body,
211            on_timeout,
212            on_cancel,
213            ..
214        } => {
215            let body_effects = generate_program_effects(body, role);
216            let timeout_effects = generate_program_effects(on_timeout, role);
217            let cancel_effects = on_cancel
218                .as_deref()
219                .map(|branch| generate_program_effects(branch, role))
220                .unwrap_or_default();
221            quote! {
222                #body_effects
223                #timeout_effects
224                #cancel_effects
225            }
226        }
227        Protocol::Send {
228            from,
229            to,
230            message,
231            continuation,
232            ..
233        } => {
234            let continuation_effects = generate_program_effects(continuation, role);
235
236            if from == role {
237                // This role is sending
238                let message_type = &message.name;
239                let to_ident = to.name();
240                let send_metadata = generate_effect_metadata_from_annotations(protocol, role);
241
242                quote! {
243                    .send(Role::#to_ident, #message_type::default())
244                    #send_metadata
245                    #continuation_effects
246                }
247            } else if to == role {
248                // This role is receiving
249                let message_type = &message.name;
250                let from_ident = from.name();
251                let recv_metadata = generate_effect_metadata_from_annotations(protocol, role);
252
253                quote! {
254                    .recv::<#message_type>(Role::#from_ident)
255                    #recv_metadata
256                    #continuation_effects
257                }
258            } else {
259                // This role is not involved in this step
260                continuation_effects
261            }
262        }
263        Protocol::Choice {
264            role: choice_role,
265            branches,
266            annotations,
267        } => {
268            // Check for timed_choice annotation using typed accessor
269            let timed_choice_duration = annotations.timed_choice();
270            let is_timed_choice = timed_choice_duration.is_some();
271            let timeout_ms = timed_choice_duration
272                .map(|d| u64::try_from(d.as_millis()).unwrap_or(u64::MAX))
273                .unwrap_or(5000); // Default 5 seconds
274
275            // Generate Branch effect with all possible continuations
276            let choice_role_name = choice_role.name();
277
278            // Generate all branch continuations
279            let branch_programs: Vec<_> = branches
280                .iter()
281                .map(|branch| {
282                    let label_ident = &branch.label;
283                    let branch_effects = generate_program_effects(&branch.protocol, role);
284
285                    quote! {
286                        (Label::#label_ident, Program::new()#branch_effects.end())
287                    }
288                })
289                .collect();
290
291            if choice_role == role {
292                // This role is making the choice
293                if is_timed_choice {
294                    // For timed choice: the actor races operations against a timeout
295                    // The first branch is executed if action completes in time (OnTime)
296                    // The second branch is executed if timeout fires first (TimedOut)
297                    //
298                    // Generated code wraps the choice in a timeout effect:
299                    // .with_timeout(duration, normal_program)
300                    // The handler will use tokio::select! internally
301
302                    // Find OnTime and TimedOut branches
303                    let on_time_branch = branches.iter().find(|b| b.label == "OnTime");
304                    let timed_out_branch = branches.iter().find(|b| b.label == "TimedOut");
305
306                    match (on_time_branch, timed_out_branch) {
307                        (Some(on_time), Some(timed_out)) => {
308                            let on_time_effects = generate_program_effects(&on_time.protocol, role);
309                            let timed_out_effects =
310                                generate_program_effects(&timed_out.protocol, role);
311
312                            quote! {
313                                .with_timed_choice(
314                                    Role::#choice_role_name,
315                                    std::time::Duration::from_millis(#timeout_ms),
316                                    // OnTime branch - executed if no timeout
317                                    Program::new()
318                                        .choose(Role::#choice_role_name, Label::OnTime)
319                                        #on_time_effects
320                                        .end(),
321                                    // TimedOut branch - executed on timeout
322                                    Program::new()
323                                        .choose(Role::#choice_role_name, Label::TimedOut)
324                                        #timed_out_effects
325                                        .end()
326                                )
327                            }
328                        }
329                        _ => {
330                            // Fall back to regular choice if OnTime/TimedOut not found
331                            let first_branch = branches.first();
332                            let label_ident = &first_branch.label;
333                            quote! {
334                                .with_timed_choice(
335                                    Role::#choice_role_name,
336                                    std::time::Duration::from_millis(#timeout_ms),
337                                    Program::new()
338                                        .choose(Role::#choice_role_name, Label::#label_ident)
339                                        .branch(Role::#choice_role_name, vec![#(#branch_programs),*])
340                                        .end(),
341                                    // Timeout takes first branch as fallback
342                                    Program::new()
343                                        .choose(Role::#choice_role_name, Label::#label_ident)
344                                        .end()
345                                )
346                            }
347                        }
348                    }
349                } else {
350                    // Standard choice without timeout
351                    // Check if branches have guards - if so, generate guard evaluation
352                    // Otherwise, generate code that takes the first valid branch
353                    let has_guards = branches.iter().any(|b| b.guard.is_some());
354
355                    if has_guards {
356                        // Generate guard evaluation logic
357                        let guard_checks: Vec<TokenStream> = branches
358                            .iter()
359                            .map(|branch| {
360                                let label_ident = &branch.label;
361                                if let Some(ref guard) = branch.guard {
362                                    let guard_ts = match guard {
363                                        crate::ast::ChoiceGuard::Predicate(tokens) => {
364                                            quote!(#tokens)
365                                        }
366                                        crate::ast::ChoiceGuard::Evidence { .. } => quote!(true),
367                                    };
368                                    quote! {
369                                        if #guard_ts {
370                                            Label::#label_ident
371                                        }
372                                    }
373                                } else {
374                                    quote! {
375                                        // No guard - default fallback
376                                        { Label::#label_ident }
377                                    }
378                                }
379                            })
380                            .collect();
381
382                        // Generate a choice selection expression using guards
383                        let first_label = branches.first();
384                        let first_label_ident = &first_label.label;
385                        quote! {
386                            .choose(Role::#choice_role_name, {
387                                // Evaluate guards to determine which branch to choose
388                                #(#guard_checks else)* Label::#first_label_ident
389                            })
390                            .branch(Role::#choice_role_name, vec![#(#branch_programs),*])
391                        }
392                    } else {
393                        // No guards - default to first branch or allow runtime decision
394                        let first_branch = branches.first();
395                        let label_ident = &first_branch.label;
396
397                        quote! {
398                            .choose(Role::#choice_role_name, Label::#label_ident)
399                            .branch(Role::#choice_role_name, vec![#(#branch_programs),*])
400                        }
401                    }
402                }
403            } else {
404                // This role is offering/waiting for choice
405                // It will receive the label and execute the matching branch
406                if is_timed_choice {
407                    // For timed choice receivers: they still wait for the choice
408                    // The timeout is managed by the choice maker, so receivers
409                    // just offer as normal. They'll receive either OnTime or TimedOut.
410                    quote! {
411                        .offer(Role::#choice_role_name)
412                        .branch(Role::#choice_role_name, vec![#(#branch_programs),*])
413                    }
414                } else {
415                    quote! {
416                        .offer(Role::#choice_role_name)
417                        .branch(Role::#choice_role_name, vec![#(#branch_programs),*])
418                    }
419                }
420            }
421        }
422        Protocol::Loop { body, condition } => {
423            let body_effects = generate_program_effects(body, role);
424
425            // Generate Loop effect with runtime iteration control
426            match condition {
427                Some(Condition::Count(n)) => {
428                    // Fixed iteration count - use loop_n
429                    quote! {
430                        .loop_n(#n, Program::new()#body_effects.end())
431                    }
432                }
433                Some(Condition::RoleDecides(deciding_role)) => {
434                    // Role-based loop control via choice mechanism
435                    // The deciding role uses choices to signal continue/break
436                    // Other roles follow the decision
437
438                    if deciding_role == role {
439                        // This role decides - wrap body in a choice-controlled loop
440                        // The choice determines whether to continue or break
441                        quote! {
442                            // Loop controlled by this role via choices
443                            // Check condition and choose "continue" or "break"
444                            // Execute once (implicit "break" choice in this generation)
445                            .loop_n(1, Program::new()#body_effects.end())
446                        }
447                    } else {
448                        // This role follows the deciding role's decision
449                        quote! {
450                            // Loop follows Role::#deciding_role_name's decision
451                            // Receives "continue" or "break" choice from deciding role
452                            .loop_n(1, Program::new()#body_effects.end())
453                        }
454                    }
455                }
456                Some(Condition::Custom(_expr)) => {
457                    // Custom condition - evaluate expression at runtime
458                    // The expression determines loop iteration count or termination
459                    quote! {
460                        // Loop with custom condition: #expr
461                        // Condition is evaluated to determine iteration count
462                        .loop_n({
463                            // Evaluate custom condition to get iteration count
464                            // Default to 1 if condition doesn't produce a count
465                            let count: usize = 1; // Custom expr evaluation would go here
466                            count
467                        }, Program::new()#body_effects.end())
468                    }
469                }
470                Some(Condition::Fuel(n)) => {
471                    // Fuel-based bounding - max iterations
472                    quote! {
473                        .loop_n(#n, Program::new()#body_effects.end())
474                    }
475                }
476                Some(Condition::YieldAfter(n)) => {
477                    // Yield after N communication steps
478                    // Execute up to N iterations then yield
479                    quote! {
480                        .loop_n(#n, Program::new()#body_effects.end())
481                    }
482                }
483                Some(Condition::YieldWhen(_condition)) => {
484                    // YieldWhen executes once then yields (condition captured for observability)
485                    quote! {
486                        .loop_n(1, Program::new()#body_effects.end())
487                    }
488                }
489                None => {
490                    // No explicit condition - execute once
491                    quote! {
492                        .loop_n(1, Program::new()#body_effects.end())
493                    }
494                }
495            }
496        }
497        Protocol::Parallel { protocols } => {
498            // For simplicity, execute sequentially in program building
499            let parallel_effects: Vec<TokenStream> = protocols
500                .iter()
501                .map(|p| generate_program_effects(p, role))
502                .collect();
503
504            quote! {
505                #(#parallel_effects)*
506            }
507        }
508        Protocol::Rec { label: _, body } => {
509            // For simplicity, treat recursion as a simple body
510            generate_program_effects(body, role)
511        }
512        Protocol::Broadcast {
513            from,
514            to_all,
515            message,
516            continuation,
517            ..
518        } => {
519            let continuation_effects = generate_program_effects(continuation, role);
520            let message_type = &message.name;
521
522            if from == role {
523                // This role is broadcasting - send to all recipients
524                let sends: Vec<TokenStream> = to_all
525                    .iter()
526                    .map(|to| {
527                        let to_ident = to.name();
528                        quote! {
529                            .send(Role::#to_ident, #message_type::default())
530                        }
531                    })
532                    .collect();
533
534                quote! {
535                    #(#sends)*
536                    #continuation_effects
537                }
538            } else if to_all.contains(role) {
539                // This role is receiving the broadcast
540                let from_ident = from.name();
541
542                quote! {
543                    .recv::<#message_type>(Role::#from_ident)
544                    #continuation_effects
545                }
546            } else {
547                // This role doesn't participate in the broadcast
548                quote! {
549                    #continuation_effects
550                }
551            }
552        }
553        Protocol::Var(_label) => {
554            // Variable reference for recursion - refers back to a Rec label
555            // This creates a recursive call/loop back to the labeled protocol point
556            quote! {
557                // Recursion to recursive label
558                // This represents a jump back to the Rec point
559                // In a runtime implementation, this would:
560                // 1. Reset state to the Rec entry point
561                // 2. Continue execution from the beginning of the Rec body
562                // 3. Maintain any accumulated state/messages
563                // For code generation, this is typically handled by the containing Rec block
564                // which wraps the body in an actual loop construct
565            }
566        }
567
568        Protocol::Extension {
569            extension,
570            continuation,
571            ..
572        } => {
573            // Generate code for the extension and then continue with the rest
574            let extension_effects =
575                extension.generate_code(&crate::extensions::CodegenContext::default());
576            let continuation_effects = generate_program_effects(continuation, role);
577
578            quote! {
579                #extension_effects
580                #continuation_effects
581            }
582        }
583    }
584}
585
586#[cfg(test)]
587mod tests {
588    include!("../../tests/unit/compiler/effects_codegen_tests.rs");
589}