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}