1use std::collections::HashMap;
13use std::fmt::Write;
14use std::path::PathBuf;
15
16use bock_air::{AIRNode, AirInterpolationPart, EnumVariantPayload, NodeKind, ResultVariant};
17use bock_ast::{AssignOp, BinOp, ImportItems, Literal, TypeExpr, UnaryOp, Visibility};
18use bock_types::AIRModule;
19
20use crate::error::CodegenError;
21use crate::generator::{CodeGenerator, GeneratedCode, OutputFile, SourceMap};
22use crate::profile::TargetProfile;
23
24fn py_module_uses_concurrency(items: &[AIRNode]) -> bool {
30 items.iter().any(|n| {
31 let s = format!("{n:?}");
32 s.contains("\"Channel\"") || s.contains("\"spawn\"")
33 })
34}
35
36const CONCURRENCY_RUNTIME_PY: &str = "\
37# ── Bock concurrency runtime ──
38import asyncio as __bock_asyncio
39
40class __BockChannel:
41 __slots__ = ('_q',)
42 def __init__(self):
43 self._q = __bock_asyncio.Queue()
44 def send(self, v):
45 self._q.put_nowait(v)
46 async def recv(self):
47 return await self._q.get()
48 def close(self):
49 pass
50
51def __bock_channel_new():
52 ch = __BockChannel()
53 return (ch, ch)
54
55def __bock_spawn(x):
56 # If already a coroutine, wrap it in a Task so it starts eagerly.
57 if __bock_asyncio.iscoroutine(x):
58 return __bock_asyncio.create_task(x)
59 return x
60";
61
62#[derive(Debug)]
64pub struct PyGenerator {
65 profile: TargetProfile,
66}
67
68impl PyGenerator {
69 #[must_use]
71 pub fn new() -> Self {
72 Self {
73 profile: TargetProfile::python(),
74 }
75 }
76}
77
78impl Default for PyGenerator {
79 fn default() -> Self {
80 Self::new()
81 }
82}
83
84impl CodeGenerator for PyGenerator {
85 fn target(&self) -> &TargetProfile {
86 &self.profile
87 }
88
89 fn generate_module(&self, module: &AIRModule) -> Result<GeneratedCode, CodegenError> {
90 let mut ctx = PyEmitCtx::new();
91 ctx.emit_node(module)?;
92 let content = ctx.finish();
93 let source_map = SourceMap {
94 generated_file: "output.py".to_string(),
95 ..Default::default()
96 };
97 Ok(GeneratedCode {
98 files: vec![OutputFile {
99 path: PathBuf::from("output.py"),
100 content,
101 }],
102 source_map: Some(source_map),
103 })
104 }
105
106 fn entry_invocation(&self, main_is_async: bool) -> Option<String> {
107 if main_is_async {
108 Some(
109 "if __name__ == \"__main__\":\n asyncio.run(main())\n"
110 .to_string(),
111 )
112 } else {
113 Some("if __name__ == \"__main__\":\n main()\n".to_string())
114 }
115 }
116}
117
118struct PyEmitCtx {
122 buf: String,
123 indent: usize,
124 needs_dataclass_import: bool,
125 needs_abc_import: bool,
126 needs_asyncio_import: bool,
130 needs_time_import: bool,
132 task_bound_names: std::collections::HashSet<String>,
136 effect_ops: HashMap<String, String>,
138 current_handler_vars: HashMap<String, String>,
140 fn_effects: HashMap<String, Vec<String>>,
142 composite_effects: HashMap<String, Vec<String>>,
144 handling_counter: usize,
149 impls_by_target: HashMap<String, Vec<AIRNode>>,
154}
155
156impl PyEmitCtx {
157 fn new() -> Self {
158 Self {
159 buf: String::with_capacity(4096),
160 indent: 0,
161 needs_dataclass_import: false,
162 needs_abc_import: false,
163 needs_asyncio_import: false,
164 needs_time_import: false,
165 task_bound_names: std::collections::HashSet::new(),
166 effect_ops: HashMap::new(),
167 current_handler_vars: HashMap::new(),
168 fn_effects: HashMap::new(),
169 composite_effects: HashMap::new(),
170 handling_counter: 0,
171 impls_by_target: HashMap::new(),
172 }
173 }
174
175 fn finish(mut self) -> String {
176 let mut preamble = String::new();
177 if self.needs_asyncio_import {
178 preamble.push_str("import asyncio\n");
179 }
180 if self.needs_time_import {
181 preamble.push_str("import time\n");
182 }
183 if self.needs_abc_import {
184 preamble.push_str("from abc import ABC, abstractmethod\n");
185 }
186 if self.needs_dataclass_import {
187 preamble.push_str("from dataclasses import dataclass\n");
188 }
189 if !preamble.is_empty() {
190 preamble.push('\n');
191 self.buf.insert_str(0, &preamble);
192 }
193 self.buf
194 }
195
196 fn indent_str(&self) -> String {
197 " ".repeat(self.indent)
198 }
199
200 fn write_indent(&mut self) {
201 let indent = self.indent_str();
202 self.buf.push_str(&indent);
203 }
204
205 fn writeln(&mut self, s: &str) {
206 self.write_indent();
207 self.buf.push_str(s);
208 self.buf.push('\n');
209 }
210
211 fn expr_to_string(&mut self, node: &AIRNode) -> Result<String, CodegenError> {
215 let start = self.buf.len();
216 self.emit_expr(node)?;
217 let s = self.buf[start..].to_string();
218 self.buf.truncate(start);
219 Ok(s)
220 }
221
222 fn map_prelude_call(
224 &mut self,
225 callee: &AIRNode,
226 args: &[bock_air::AirArg],
227 ) -> Result<Option<String>, CodegenError> {
228 let name = match &callee.kind {
229 NodeKind::Identifier { name } => name.name.as_str(),
230 _ => return Ok(None),
231 };
232 let arg_strs: Vec<String> = args
233 .iter()
234 .map(|a| self.expr_to_string(&a.value))
235 .collect::<Result<_, _>>()?;
236 let code = match name {
237 "println" => {
238 let a = arg_strs.first().map_or(String::new(), |s| s.clone());
239 format!("print({a})")
240 }
241 "print" => {
242 let a = arg_strs.first().map_or(String::new(), |s| s.clone());
243 format!("print({a}, end=\"\")")
244 }
245 "debug" => {
246 let a = arg_strs.first().map_or(String::new(), |s| s.clone());
247 format!("print(repr({a}))")
248 }
249 "assert" => {
250 let a = arg_strs.first().map_or(String::new(), |s| s.clone());
251 format!("assert {a}")
252 }
253 "todo" => "raise NotImplementedError()".to_string(),
254 "unreachable" => "raise RuntimeError(\"unreachable\")".to_string(),
255 "sleep" => {
256 self.needs_asyncio_import = true;
257 let a = arg_strs.first().map_or(String::new(), |s| s.clone());
258 format!("asyncio.sleep(({a}) / 1_000_000_000)")
260 }
261 _ => return Ok(None),
262 };
263 Ok(Some(code))
264 }
265
266 fn try_emit_time_assoc_call(
270 &mut self,
271 callee: &AIRNode,
272 args: &[bock_air::AirArg],
273 ) -> Result<bool, CodegenError> {
274 let NodeKind::FieldAccess { object, field } = &callee.kind else {
275 return Ok(false);
276 };
277 let NodeKind::Identifier { name: type_name } = &object.kind else {
278 return Ok(false);
279 };
280 let arg_strs: Vec<String> = args
281 .iter()
282 .map(|a| self.expr_to_string(&a.value))
283 .collect::<Result<_, _>>()?;
284 let arg0 = || arg_strs.first().cloned().unwrap_or_default();
285 let code = match (type_name.name.as_str(), field.name.as_str()) {
286 ("Duration", "zero") => "0".to_string(),
287 ("Duration", "nanos") => arg0(),
288 ("Duration", "micros") => format!("(({}) * 1_000)", arg0()),
289 ("Duration", "millis") => format!("(({}) * 1_000_000)", arg0()),
290 ("Duration", "seconds") => format!("(({}) * 1_000_000_000)", arg0()),
291 ("Duration", "minutes") => format!("(({}) * 60_000_000_000)", arg0()),
292 ("Duration", "hours") => format!("(({}) * 3_600_000_000_000)", arg0()),
293 ("Instant", "now") => {
294 self.needs_time_import = true;
295 "time.monotonic_ns()".to_string()
296 }
297 _ => return Ok(false),
298 };
299 self.buf.push_str(&code);
300 Ok(true)
301 }
302
303 fn try_emit_concurrency_call(
307 &mut self,
308 callee: &AIRNode,
309 args: &[bock_air::AirArg],
310 ) -> Result<bool, CodegenError> {
311 if let NodeKind::Identifier { name } = &callee.kind {
312 if name.name == "spawn" {
313 self.buf.push_str("__bock_spawn(");
314 for (i, arg) in args.iter().enumerate() {
315 if i > 0 {
316 self.buf.push_str(", ");
317 }
318 self.emit_expr(&arg.value)?;
319 }
320 self.buf.push(')');
321 return Ok(true);
322 }
323 }
324 let NodeKind::FieldAccess { object, field } = &callee.kind else {
325 return Ok(false);
326 };
327 if let NodeKind::Identifier { name: type_name } = &object.kind {
328 if type_name.name == "Channel" && field.name == "new" {
329 self.buf.push_str("__bock_channel_new()");
330 return Ok(true);
331 }
332 }
333 if matches!(field.name.as_str(), "send" | "recv" | "close") {
334 self.emit_expr(object)?;
335 let _ = write!(self.buf, ".{}", field.name);
336 self.buf.push('(');
337 for (i, arg) in args.iter().skip(1).enumerate() {
338 if i > 0 {
339 self.buf.push_str(", ");
340 }
341 self.emit_expr(&arg.value)?;
342 }
343 self.buf.push(')');
344 return Ok(true);
345 }
346 Ok(false)
347 }
348
349 fn try_emit_time_desugared_method(
352 &mut self,
353 callee: &AIRNode,
354 args: &[bock_air::AirArg],
355 ) -> Result<bool, CodegenError> {
356 let NodeKind::FieldAccess { object, field } = &callee.kind else {
357 return Ok(false);
358 };
359 if let NodeKind::Identifier { name } = &object.kind {
360 if matches!(name.name.as_str(), "Duration" | "Instant") {
361 return Ok(false);
362 }
363 }
364 if !is_time_method_name(&field.name) {
365 return Ok(false);
366 }
367 let remaining: Vec<bock_air::AirArg> = args.iter().skip(1).cloned().collect();
368 self.try_emit_time_method(object, &field.name, &remaining)
369 }
370
371 fn try_emit_time_method(
374 &mut self,
375 receiver: &AIRNode,
376 method: &str,
377 args: &[bock_air::AirArg],
378 ) -> Result<bool, CodegenError> {
379 let recv_str = self.expr_to_string(receiver)?;
380 let arg_strs: Vec<String> = args
381 .iter()
382 .map(|a| self.expr_to_string(&a.value))
383 .collect::<Result<_, _>>()?;
384 let code = match method {
385 "as_nanos" => format!("({recv_str})"),
386 "as_millis" => format!("(({recv_str}) // 1_000_000)"),
387 "as_seconds" => format!("(({recv_str}) // 1_000_000_000)"),
388 "is_zero" => format!("(({recv_str}) == 0)"),
389 "is_negative" => format!("(({recv_str}) < 0)"),
390 "abs" => format!("abs({recv_str})"),
391 "elapsed" => {
392 self.needs_time_import = true;
393 format!("(time.monotonic_ns() - ({recv_str}))")
394 }
395 "duration_since" => {
396 let other = arg_strs.first().cloned().unwrap_or_default();
397 format!("(({recv_str}) - ({other}))")
398 }
399 _ => return Ok(false),
400 };
401 self.buf.push_str(&code);
402 Ok(true)
403 }
404
405 fn emit_node(&mut self, node: &AIRNode) -> Result<(), CodegenError> {
408 match &node.kind {
409 NodeKind::Module { imports, items, .. } => {
410 if py_module_uses_concurrency(items) {
411 self.buf.push_str(CONCURRENCY_RUNTIME_PY);
412 self.buf.push('\n');
413 }
414 for imp in imports {
415 self.emit_node(imp)?;
416 }
417 if !imports.is_empty() && !items.is_empty() {
418 self.buf.push('\n');
419 }
420 self.impls_by_target.clear();
424 let mut consumed_impls: std::collections::HashSet<bock_air::NodeId> =
425 std::collections::HashSet::new();
426 for item in items.iter() {
427 if let NodeKind::ImplBlock {
428 trait_path: Some(_),
429 target,
430 ..
431 } = &item.kind
432 {
433 if let Some(target_name) = ast_type_name(target) {
434 self.impls_by_target
435 .entry(target_name)
436 .or_default()
437 .push(item.clone());
438 consumed_impls.insert(item.id);
439 }
440 }
441 }
442 for (i, item) in items.iter().enumerate() {
443 if consumed_impls.contains(&item.id) {
444 continue;
445 }
446 if i > 0 && !self.buf.is_empty() && !self.buf.ends_with("\n\n") {
447 self.buf.push('\n');
448 }
449 self.emit_node(item)?;
450 }
451 Ok(())
452 }
453 NodeKind::ImportDecl { path, items } => {
454 let path_str = path
455 .segments
456 .iter()
457 .map(|s| s.name.as_str())
458 .collect::<Vec<_>>()
459 .join(".");
460 match items {
461 ImportItems::Module => {
462 self.writeln(&format!("import {path_str}"));
463 }
464 ImportItems::Named(names) => {
465 let names_str = names
466 .iter()
467 .map(|n| to_snake_case(&n.name.name))
468 .collect::<Vec<_>>()
469 .join(", ");
470 self.writeln(&format!("from {path_str} import {names_str}"));
471 }
472 ImportItems::Glob => {
473 self.writeln(&format!("from {path_str} import *"));
474 }
475 }
476 Ok(())
477 }
478 NodeKind::FnDecl {
479 visibility,
480 is_async,
481 name,
482 params,
483 return_type,
484 effect_clause,
485 body,
486 ..
487 } => self.emit_fn_decl(
488 *visibility,
489 *is_async,
490 &name.name,
491 params,
492 return_type.as_deref(),
493 effect_clause,
494 body,
495 ),
496 NodeKind::RecordDecl { name, fields, .. } => {
497 let impls = self.impls_by_target.remove(&name.name).unwrap_or_default();
502 let bases: Vec<String> = impls
503 .iter()
504 .filter_map(|im| {
505 if let NodeKind::ImplBlock {
506 trait_path: Some(tp),
507 ..
508 } = &im.kind
509 {
510 tp.segments.last().map(|s| s.name.clone())
511 } else {
512 None
513 }
514 })
515 .collect();
516 let base_list = if bases.is_empty() {
517 String::new()
518 } else {
519 format!("({})", bases.join(", "))
520 };
521 if !fields.is_empty() {
526 self.needs_dataclass_import = true;
527 self.writeln("@dataclass");
528 }
529 self.writeln(&format!("class {}{base_list}:", name.name));
530 self.indent += 1;
531 let has_members = !fields.is_empty()
532 || impls
533 .iter()
534 .any(|im| matches!(&im.kind, NodeKind::ImplBlock { methods, .. } if !methods.is_empty()));
535 if !has_members {
536 self.writeln("pass");
537 } else {
538 for f in fields {
539 let type_hint = self.ast_type_to_py(&f.ty);
540 self.writeln(&format!("{}: {type_hint}", to_snake_case(&f.name.name)));
541 }
542 for im in &impls {
543 if let NodeKind::ImplBlock { methods, .. } = &im.kind {
544 for method in methods {
545 self.buf.push('\n');
546 self.emit_class_method(method)?;
547 }
548 }
549 }
550 }
551 self.indent -= 1;
552 Ok(())
553 }
554 NodeKind::EnumDecl { name, variants, .. } => {
555 self.needs_dataclass_import = true;
556 for (i, variant) in variants.iter().enumerate() {
557 if i > 0 {
558 self.buf.push('\n');
559 }
560 self.emit_enum_variant(&name.name, variant)?;
561 }
562 Ok(())
563 }
564 NodeKind::ClassDecl {
565 name,
566 fields,
567 methods,
568 ..
569 } => {
570 self.writeln(&format!("class {}:", name.name));
571 self.indent += 1;
572 if !fields.is_empty() {
574 let params: Vec<String> = fields
575 .iter()
576 .map(|f| {
577 let fname = to_snake_case(&f.name.name);
578 let type_hint = self.ast_type_to_py(&f.ty);
579 format!("{fname}: {type_hint}")
580 })
581 .collect();
582 self.writeln(&format!("def __init__(self, {}):", params.join(", ")));
583 self.indent += 1;
584 for f in fields {
585 let fname = to_snake_case(&f.name.name);
586 self.writeln(&format!("self.{fname} = {fname}"));
587 }
588 self.indent -= 1;
589 }
590 for method in methods {
592 self.buf.push('\n');
593 self.emit_class_method(method)?;
594 }
595 if fields.is_empty() && methods.is_empty() {
596 self.writeln("pass");
597 }
598 self.indent -= 1;
599 Ok(())
600 }
601 NodeKind::TraitDecl { name, methods, .. } => {
602 self.writeln(&format!("# trait {}", name.name));
604 self.writeln(&format!("class {}:", name.name));
605 self.indent += 1;
606 if methods.is_empty() {
607 self.writeln("pass");
608 } else {
609 for (i, method) in methods.iter().enumerate() {
610 if i > 0 {
611 self.buf.push('\n');
612 }
613 self.emit_class_method(method)?;
614 }
615 }
616 self.indent -= 1;
617 Ok(())
618 }
619 NodeKind::ImplBlock {
620 trait_path,
621 target,
622 methods,
623 ..
624 } => {
625 let target_name = self.type_expr_to_string(target);
626 if let Some(tp) = trait_path {
627 let trait_name = tp
628 .segments
629 .iter()
630 .map(|s| s.name.as_str())
631 .collect::<Vec<_>>()
632 .join(".");
633 self.writeln(&format!("# impl {trait_name} for {target_name}"));
634 } else {
635 self.writeln(&format!("# impl {target_name}"));
636 }
637 for method in methods {
638 if let NodeKind::FnDecl {
639 is_async,
640 name,
641 params,
642 return_type,
643 effect_clause,
644 body,
645 ..
646 } = &method.kind
647 {
648 if *is_async {
649 self.needs_asyncio_import = true;
650 }
651 let async_kw = if *is_async { "async " } else { "" };
652 let param_strs = self.collect_param_strs(params);
653 let effects = self.effects_params(effect_clause);
654 let mut all_params = vec!["self".to_string()];
655 all_params.extend(param_strs);
656 all_params.extend(effects);
657 let ret = return_type
658 .as_deref()
659 .map(|t| format!(" -> {}", self.type_to_py(t)))
660 .unwrap_or_default();
661 let fn_name = to_snake_case(&name.name);
662 self.writeln(&format!(
663 "{async_kw}def {fn_name}({}){}:",
664 all_params.join(", "),
665 ret,
666 ));
667 self.indent += 1;
668 let old_handler_vars = self.current_handler_vars.clone();
669 let expanded = self.expand_effect_names(effect_clause);
670 for ename in &expanded {
671 self.current_handler_vars
672 .insert(ename.clone(), to_snake_case(ename));
673 }
674 self.emit_block_body(body)?;
675 self.current_handler_vars = old_handler_vars;
676 self.indent -= 1;
677 }
678 }
679 Ok(())
680 }
681 NodeKind::EffectDecl {
682 name,
683 components,
684 operations,
685 ..
686 } => {
687 if !components.is_empty() {
688 let comp_names: Vec<String> = components
689 .iter()
690 .map(|tp| {
691 tp.segments
692 .last()
693 .map_or("effect".to_string(), |s| s.name.clone())
694 })
695 .collect();
696 self.writeln(&format!(
697 "# composite effect {} = {}",
698 name.name,
699 comp_names.join(" + ")
700 ));
701 self.composite_effects
702 .insert(name.name.clone(), comp_names);
703 return Ok(());
704 }
705 for op in operations {
707 if let NodeKind::FnDecl {
708 name: op_name, ..
709 } = &op.kind
710 {
711 self.effect_ops
712 .insert(op_name.name.clone(), name.name.clone());
713 }
714 }
715 self.needs_abc_import = true;
717 self.writeln(&format!("class {}(ABC):", name.name));
718 self.indent += 1;
719 if operations.is_empty() {
720 self.writeln("pass");
721 } else {
722 for (i, op) in operations.iter().enumerate() {
723 if i > 0 {
724 self.buf.push('\n');
725 }
726 if let NodeKind::FnDecl {
727 name,
728 params,
729 return_type,
730 ..
731 } = &op.kind
732 {
733 self.writeln("@abstractmethod");
734 let param_strs = self.collect_param_strs(params);
735 let mut all_params = vec!["self".to_string()];
736 all_params.extend(param_strs);
737 let ret = return_type
738 .as_deref()
739 .map(|t| format!(" -> {}", self.type_to_py(t)))
740 .unwrap_or_default();
741 let fn_name = to_snake_case(&name.name);
742 self.writeln(&format!(
743 "def {fn_name}({}){}:",
744 all_params.join(", "),
745 ret,
746 ));
747 self.indent += 1;
748 self.writeln("...");
749 self.indent -= 1;
750 }
751 }
752 }
753 self.indent -= 1;
754 Ok(())
755 }
756 NodeKind::TypeAlias { name, .. } => {
757 self.writeln(&format!("# type {} = ...", name.name));
758 Ok(())
759 }
760 NodeKind::ConstDecl {
761 name, value, ty, ..
762 } => {
763 let type_hint = format!(": {}", self.type_to_py(ty));
764 let ind = self.indent_str();
765 let _ = write!(self.buf, "{ind}{}{type_hint} = ", to_snake_case(&name.name));
766 self.emit_expr(value)?;
767 self.buf.push('\n');
768 Ok(())
769 }
770 NodeKind::ModuleHandle { effect, handler } => {
771 let effect_name =
776 effect.segments.last().map_or("effect", |s| s.name.as_str());
777 let var_name = format!("__{}", to_snake_case(effect_name));
778 let ind = self.indent_str();
779 let _ = write!(self.buf, "{ind}{var_name}: {effect_name} = ");
780 self.emit_expr(handler)?;
781 self.buf.push('\n');
782 self.current_handler_vars
783 .insert(effect_name.to_string(), var_name);
784 Ok(())
785 }
786 NodeKind::PropertyTest { name, body, .. } => {
787 self.writeln(&format!("# property test: {name}"));
788 self.writeln("# (property tests are not emitted in Python output)");
789 let _ = body;
790 Ok(())
791 }
792 NodeKind::LetBinding { .. }
794 | NodeKind::If { .. }
795 | NodeKind::For { .. }
796 | NodeKind::While { .. }
797 | NodeKind::Loop { .. }
798 | NodeKind::Return { .. }
799 | NodeKind::Break { .. }
800 | NodeKind::Continue
801 | NodeKind::Guard { .. }
802 | NodeKind::Match { .. }
803 | NodeKind::Block { .. }
804 | NodeKind::HandlingBlock { .. }
805 | NodeKind::Assign { .. } => self.emit_stmt(node),
806 _ => {
808 self.write_indent();
809 self.emit_expr(node)?;
810 self.buf.push('\n');
811 Ok(())
812 }
813 }
814 }
815
816 #[allow(clippy::too_many_arguments)]
819 fn emit_fn_decl(
820 &mut self,
821 _visibility: Visibility,
822 is_async: bool,
823 name: &str,
824 params: &[AIRNode],
825 return_type: Option<&AIRNode>,
826 effect_clause: &[bock_ast::TypePath],
827 body: &AIRNode,
828 ) -> Result<(), CodegenError> {
829 if is_async {
830 self.needs_asyncio_import = true;
831 }
832 let async_kw = if is_async { "async " } else { "" };
833 let param_strs = self.collect_param_strs(params);
834 let effects = self.effects_params(effect_clause);
835 let mut all_params = param_strs;
836 all_params.extend(effects);
837 let ret = return_type
838 .map(|t| format!(" -> {}", self.type_to_py(t)))
839 .unwrap_or_default();
840 if !effect_clause.is_empty() {
841 let effect_names = self.expand_effect_names(effect_clause);
842 self.fn_effects.insert(name.to_string(), effect_names);
843 }
844 let fn_name = to_snake_case(name);
845 self.writeln(&format!(
846 "{async_kw}def {fn_name}({}){}:",
847 all_params.join(", "),
848 ret,
849 ));
850 self.indent += 1;
851 let old_handler_vars = self.current_handler_vars.clone();
852 let expanded = self.expand_effect_names(effect_clause);
853 for ename in &expanded {
854 self.current_handler_vars
855 .insert(ename.clone(), to_snake_case(ename));
856 }
857 self.emit_block_body(body)?;
858 self.current_handler_vars = old_handler_vars;
859 self.indent -= 1;
860 Ok(())
861 }
862
863 fn emit_class_method(&mut self, method: &AIRNode) -> Result<(), CodegenError> {
864 if let NodeKind::FnDecl {
865 is_async,
866 name,
867 params,
868 return_type,
869 effect_clause,
870 body,
871 ..
872 } = &method.kind
873 {
874 if *is_async {
875 self.needs_asyncio_import = true;
876 }
877 let async_kw = if *is_async { "async " } else { "" };
878 let param_strs = self.collect_param_strs(params);
879 let effects = self.effects_params(effect_clause);
880 let mut all_params = vec!["self".to_string()];
881 all_params.extend(param_strs);
882 all_params.extend(effects);
883 let ret = return_type
884 .as_deref()
885 .map(|t| format!(" -> {}", self.type_to_py(t)))
886 .unwrap_or_default();
887 let fn_name = to_snake_case(&name.name);
888 self.writeln(&format!(
889 "{async_kw}def {fn_name}({}){}:",
890 all_params.join(", "),
891 ret,
892 ));
893 self.indent += 1;
894 let old_handler_vars = self.current_handler_vars.clone();
895 let expanded = self.expand_effect_names(effect_clause);
896 for ename in &expanded {
897 self.current_handler_vars
898 .insert(ename.clone(), to_snake_case(ename));
899 }
900 self.emit_block_body(body)?;
901 self.current_handler_vars = old_handler_vars;
902 self.indent -= 1;
903 }
904 Ok(())
905 }
906
907 fn collect_param_strs(&self, params: &[AIRNode]) -> Vec<String> {
908 params
909 .iter()
910 .filter_map(|p| {
911 if let NodeKind::Param {
912 pattern,
913 ty,
914 default,
915 } = &p.kind
916 {
917 let name = to_snake_case(&self.pattern_to_binding_name(pattern));
918 let type_hint = ty
919 .as_ref()
920 .map(|t| format!(": {}", self.type_to_py(t)))
921 .unwrap_or_default();
922 if let Some(def) = default {
923 let mut ctx = PyEmitCtx::new();
924 ctx.indent = self.indent;
925 if ctx.emit_expr(def).is_ok() {
926 let def_str = ctx.buf;
927 return Some(format!("{name}{type_hint} = {def_str}"));
928 }
929 }
930 Some(format!("{name}{type_hint}"))
931 } else {
932 None
933 }
934 })
935 .collect()
936 }
937
938 fn expand_effect_names(&self, effects: &[bock_ast::TypePath]) -> Vec<String> {
940 let mut result = Vec::new();
941 for tp in effects {
942 let name = tp
943 .segments
944 .last()
945 .map_or("effect".to_string(), |s| s.name.clone());
946 if let Some(components) = self.composite_effects.get(&name) {
947 result.extend(components.iter().cloned());
948 } else {
949 result.push(name);
950 }
951 }
952 result
953 }
954
955 fn effects_params(&self, effects: &[bock_ast::TypePath]) -> Vec<String> {
957 if effects.is_empty() {
958 return vec![];
959 }
960 let expanded = self.expand_effect_names(effects);
961 let mut result = vec!["*".to_string()];
962 for name in &expanded {
963 let param_name = to_snake_case(name);
964 result.push(format!("{param_name}: {name}"));
965 }
966 result
967 }
968
969 fn build_effects_call_args_py(&self, fn_name: &str) -> Option<String> {
971 let effects = self.fn_effects.get(fn_name)?;
972 let entries: Vec<String> = effects
973 .iter()
974 .filter_map(|e| {
975 let handler_var = self.current_handler_vars.get(e)?;
976 let param_name = to_snake_case(e);
977 Some(format!("{param_name}={handler_var}"))
978 })
979 .collect();
980 if entries.is_empty() {
981 return None;
982 }
983 Some(entries.join(", "))
984 }
985
986 fn emit_enum_variant(
989 &mut self,
990 enum_name: &str,
991 variant: &AIRNode,
992 ) -> Result<(), CodegenError> {
993 if let NodeKind::EnumVariant { name, payload } = &variant.kind {
994 let vname = &name.name;
995 match payload {
996 EnumVariantPayload::Unit => {
997 self.writeln("@dataclass(frozen=True)");
998 self.writeln(&format!("class {enum_name}_{vname}:"));
999 self.indent += 1;
1000 self.writeln(&format!("_tag: str = \"{vname}\""));
1001 self.indent -= 1;
1002 }
1003 EnumVariantPayload::Struct(fields) => {
1004 self.writeln("@dataclass");
1005 self.writeln(&format!("class {enum_name}_{vname}:"));
1006 self.indent += 1;
1007 for f in fields {
1008 let type_hint = self.ast_type_to_py(&f.ty);
1009 self.writeln(&format!("{}: {type_hint}", to_snake_case(&f.name.name)));
1010 }
1011 self.writeln(&format!("_tag: str = \"{vname}\""));
1012 self.indent -= 1;
1013 }
1014 EnumVariantPayload::Tuple(elems) => {
1015 self.writeln("@dataclass");
1016 self.writeln(&format!("class {enum_name}_{vname}:"));
1017 self.indent += 1;
1018 for (i, elem) in elems.iter().enumerate() {
1019 let type_hint = self.type_to_py(elem);
1020 self.writeln(&format!("_{i}: {type_hint}"));
1021 }
1022 self.writeln(&format!("_tag: str = \"{vname}\""));
1023 self.indent -= 1;
1024 }
1025 }
1026 }
1027 Ok(())
1028 }
1029
1030 fn emit_stmt(&mut self, node: &AIRNode) -> Result<(), CodegenError> {
1033 match &node.kind {
1034 NodeKind::LetBinding {
1035 pattern, value, ty, ..
1036 } => {
1037 let binding = self.pattern_to_py_binding(pattern);
1038 let type_hint = ty
1039 .as_ref()
1040 .map(|t| format!(": {}", self.type_to_py(t)))
1041 .unwrap_or_default();
1042 let ind = self.indent_str();
1043 let _ = write!(self.buf, "{ind}{binding}{type_hint} = ");
1044 let wrap_task = matches!(&value.kind, NodeKind::Call { .. })
1045 && self.task_bound_names.contains(&binding);
1046 if wrap_task {
1047 self.needs_asyncio_import = true;
1048 self.buf.push_str("asyncio.create_task(");
1049 self.emit_expr(value)?;
1050 self.buf.push(')');
1051 } else {
1052 self.emit_expr(value)?;
1053 }
1054 self.buf.push('\n');
1055 Ok(())
1056 }
1057 NodeKind::If {
1058 let_pattern,
1059 condition,
1060 then_block,
1061 else_block,
1062 } => {
1063 if let Some(pat) = let_pattern {
1064 let ind = self.indent_str();
1065 let binding = self.pattern_to_py_binding(pat);
1066 let _ = write!(self.buf, "{ind}{binding} = ");
1067 self.emit_expr(condition)?;
1068 self.buf.push('\n');
1069 self.writeln(&format!("if {binding} is not None:"));
1070 self.indent += 1;
1071 self.emit_block_body(then_block)?;
1072 self.indent -= 1;
1073 } else {
1074 let ind = self.indent_str();
1075 let _ = write!(self.buf, "{ind}if ");
1076 self.emit_expr(condition)?;
1077 self.buf.push_str(":\n");
1078 self.indent += 1;
1079 self.emit_block_body(then_block)?;
1080 self.indent -= 1;
1081 }
1082 if let Some(else_b) = else_block {
1083 if matches!(else_b.kind, NodeKind::If { .. }) {
1084 let ind = self.indent_str();
1085 let _ = write!(self.buf, "{ind}el");
1086 self.emit_stmt(else_b)?;
1087 return Ok(());
1088 }
1089 self.writeln("else:");
1090 self.indent += 1;
1091 self.emit_block_body(else_b)?;
1092 self.indent -= 1;
1093 }
1094 Ok(())
1095 }
1096 NodeKind::For {
1097 pattern,
1098 iterable,
1099 body,
1100 } => {
1101 let binding = self.pattern_to_py_binding(pattern);
1102 let ind = self.indent_str();
1103 let _ = write!(self.buf, "{ind}for {binding} in ");
1104 self.emit_expr(iterable)?;
1105 self.buf.push_str(":\n");
1106 self.indent += 1;
1107 self.emit_block_body(body)?;
1108 self.indent -= 1;
1109 Ok(())
1110 }
1111 NodeKind::While { condition, body } => {
1112 let ind = self.indent_str();
1113 let _ = write!(self.buf, "{ind}while ");
1114 self.emit_expr(condition)?;
1115 self.buf.push_str(":\n");
1116 self.indent += 1;
1117 self.emit_block_body(body)?;
1118 self.indent -= 1;
1119 Ok(())
1120 }
1121 NodeKind::Loop { body } => {
1122 self.writeln("while True:");
1123 self.indent += 1;
1124 self.emit_block_body(body)?;
1125 self.indent -= 1;
1126 Ok(())
1127 }
1128 NodeKind::Return { value } => {
1129 if let Some(val) = value {
1130 let ind = self.indent_str();
1131 let _ = write!(self.buf, "{ind}return ");
1132 self.emit_expr(val)?;
1133 self.buf.push('\n');
1134 } else {
1135 self.writeln("return");
1136 }
1137 Ok(())
1138 }
1139 NodeKind::Break { value } => {
1140 if let Some(val) = value {
1141 let ind = self.indent_str();
1143 let _ = write!(self.buf, "{ind}# break value: ");
1144 self.emit_expr(val)?;
1145 self.buf.push('\n');
1146 self.writeln("break");
1147 } else {
1148 self.writeln("break");
1149 }
1150 Ok(())
1151 }
1152 NodeKind::Continue => {
1153 self.writeln("continue");
1154 Ok(())
1155 }
1156 NodeKind::Guard {
1157 condition,
1158 else_block,
1159 ..
1160 } => {
1161 let ind = self.indent_str();
1162 let _ = write!(self.buf, "{ind}if not (");
1163 self.emit_expr(condition)?;
1164 self.buf.push_str("):\n");
1165 self.indent += 1;
1166 self.emit_block_body(else_block)?;
1167 self.indent -= 1;
1168 Ok(())
1169 }
1170 NodeKind::Match { scrutinee, arms } => self.emit_match(scrutinee, arms),
1171 NodeKind::Block { stmts, tail } => {
1172 for s in stmts {
1173 self.emit_node(s)?;
1174 }
1175 if let Some(t) = tail {
1176 self.write_indent();
1177 self.emit_expr(t)?;
1178 self.buf.push('\n');
1179 }
1180 Ok(())
1181 }
1182 NodeKind::HandlingBlock { handlers, body } => {
1183 let old_handler_vars = self.current_handler_vars.clone();
1190 self.handling_counter += 1;
1191 let suffix = format!("_h{}", self.handling_counter);
1192 for h in handlers {
1193 let effect_name =
1194 h.effect.segments.last().map_or("effect", |s| s.name.as_str());
1195 let var_name = format!("__{}{suffix}", to_snake_case(effect_name));
1196 let ind = self.indent_str();
1197 let _ = write!(self.buf, "{ind}{var_name}: {effect_name} = ");
1198 self.emit_expr(&h.handler)?;
1199 self.buf.push('\n');
1200 self.current_handler_vars
1201 .insert(effect_name.to_string(), var_name);
1202 }
1203 if let NodeKind::Block { stmts, tail } = &body.kind {
1204 for s in stmts {
1205 self.emit_node(s)?;
1206 }
1207 if let Some(t) = tail {
1208 self.write_indent();
1209 self.emit_expr(t)?;
1210 self.buf.push('\n');
1211 }
1212 } else {
1213 self.emit_stmt(body)?;
1214 }
1215 self.current_handler_vars = old_handler_vars;
1216 Ok(())
1217 }
1218 NodeKind::Assign { op, target, value } => {
1219 let ind = self.indent_str();
1220 let _ = write!(self.buf, "{ind}");
1221 self.emit_expr(target)?;
1222 let op_str = match op {
1223 AssignOp::Assign => " = ",
1224 AssignOp::AddAssign => " += ",
1225 AssignOp::SubAssign => " -= ",
1226 AssignOp::MulAssign => " *= ",
1227 AssignOp::DivAssign => " /= ",
1228 AssignOp::RemAssign => " %= ",
1229 };
1230 self.buf.push_str(op_str);
1231 self.emit_expr(value)?;
1232 self.buf.push('\n');
1233 Ok(())
1234 }
1235 _ => {
1236 self.write_indent();
1237 self.emit_expr(node)?;
1238 self.buf.push('\n');
1239 Ok(())
1240 }
1241 }
1242 }
1243
1244 fn emit_expr(&mut self, node: &AIRNode) -> Result<(), CodegenError> {
1247 match &node.kind {
1248 NodeKind::Literal { lit } => {
1249 match lit {
1250 Literal::Int(s) => self.buf.push_str(s),
1251 Literal::Float(s) => self.buf.push_str(s),
1252 Literal::Bool(b) => self.buf.push_str(if *b { "True" } else { "False" }),
1253 Literal::Char(s) => {
1254 self.buf.push('\'');
1255 self.buf.push_str(s);
1256 self.buf.push('\'');
1257 }
1258 Literal::String(s) => {
1259 self.buf.push('"');
1260 self.buf.push_str(&escape_py_string(s));
1261 self.buf.push('"');
1262 }
1263 Literal::Unit => self.buf.push_str("None"),
1264 }
1265 Ok(())
1266 }
1267 NodeKind::Identifier { name } => {
1268 self.buf.push_str(&identifier_to_py(&name.name));
1269 Ok(())
1270 }
1271 NodeKind::BinaryOp { op, left, right } => {
1272 self.buf.push('(');
1273 self.emit_expr(left)?;
1274 let op_str = match op {
1275 BinOp::Add => " + ",
1276 BinOp::Sub => " - ",
1277 BinOp::Mul => " * ",
1278 BinOp::Div => " / ",
1279 BinOp::Rem => " % ",
1280 BinOp::Pow => " ** ",
1281 BinOp::Eq => " == ",
1282 BinOp::Ne => " != ",
1283 BinOp::Lt => " < ",
1284 BinOp::Le => " <= ",
1285 BinOp::Gt => " > ",
1286 BinOp::Ge => " >= ",
1287 BinOp::And => " and ",
1288 BinOp::Or => " or ",
1289 BinOp::BitAnd => " & ",
1290 BinOp::BitOr => " | ",
1291 BinOp::BitXor => " ^ ",
1292 BinOp::Compose => " # compose ",
1293 BinOp::Is => " is ",
1294 };
1295 self.buf.push_str(op_str);
1296 self.emit_expr(right)?;
1297 self.buf.push(')');
1298 Ok(())
1299 }
1300 NodeKind::UnaryOp { op, operand } => {
1301 let op_str = match op {
1302 UnaryOp::Neg => "-",
1303 UnaryOp::Not => "not ",
1304 UnaryOp::BitNot => "~",
1305 };
1306 self.buf.push_str(op_str);
1307 self.emit_expr(operand)?;
1308 Ok(())
1309 }
1310 NodeKind::Call { callee, args, .. } => {
1311 if let Some(code) = self.map_prelude_call(callee, args)? {
1312 self.buf.push_str(&code);
1313 return Ok(());
1314 }
1315 if self.try_emit_time_assoc_call(callee, args)? {
1316 return Ok(());
1317 }
1318 if self.try_emit_time_desugared_method(callee, args)? {
1319 return Ok(());
1320 }
1321 if self.try_emit_concurrency_call(callee, args)? {
1322 return Ok(());
1323 }
1324 if let NodeKind::Identifier { name } = &callee.kind {
1326 if let Some(effect_name) = self.effect_ops.get(&name.name).cloned() {
1327 if let Some(handler_var) =
1328 self.current_handler_vars.get(&effect_name).cloned()
1329 {
1330 let _ = write!(self.buf, "{}.{}", handler_var, to_snake_case(&name.name));
1331 self.buf.push('(');
1332 for (i, arg) in args.iter().enumerate() {
1333 if i > 0 {
1334 self.buf.push_str(", ");
1335 }
1336 self.emit_expr(&arg.value)?;
1337 }
1338 self.buf.push(')');
1339 return Ok(());
1340 }
1341 }
1342 }
1343 let effects_args = if let NodeKind::Identifier { name } = &callee.kind {
1345 self.build_effects_call_args_py(&name.name)
1346 } else {
1347 None
1348 };
1349 self.emit_expr(callee)?;
1350 self.buf.push('(');
1351 for (i, arg) in args.iter().enumerate() {
1352 if i > 0 {
1353 self.buf.push_str(", ");
1354 }
1355 self.emit_expr(&arg.value)?;
1356 }
1357 if let Some(ea) = effects_args {
1358 if !args.is_empty() {
1359 self.buf.push_str(", ");
1360 }
1361 self.buf.push_str(&ea);
1362 }
1363 self.buf.push(')');
1364 Ok(())
1365 }
1366 NodeKind::MethodCall {
1367 receiver,
1368 method,
1369 args,
1370 ..
1371 } => {
1372 if self.try_emit_time_method(receiver, &method.name, args)? {
1373 return Ok(());
1374 }
1375 self.emit_expr(receiver)?;
1376 let _ = write!(self.buf, ".{}", to_snake_case(&method.name));
1377 self.buf.push('(');
1378 for (i, arg) in args.iter().enumerate() {
1379 if i > 0 {
1380 self.buf.push_str(", ");
1381 }
1382 self.emit_expr(&arg.value)?;
1383 }
1384 self.buf.push(')');
1385 Ok(())
1386 }
1387 NodeKind::FieldAccess { object, field } => {
1388 self.emit_expr(object)?;
1389 let _ = write!(self.buf, ".{}", to_snake_case(&field.name));
1390 Ok(())
1391 }
1392 NodeKind::Index { object, index } => {
1393 self.emit_expr(object)?;
1394 self.buf.push('[');
1395 self.emit_expr(index)?;
1396 self.buf.push(']');
1397 Ok(())
1398 }
1399 NodeKind::Lambda { params, body } => {
1400 let param_strs = self.collect_param_strs(params);
1401 let _ = write!(self.buf, "lambda {}: ", param_strs.join(", "));
1402 self.emit_expr(body)?;
1403 Ok(())
1404 }
1405 NodeKind::Pipe { left, right } => self.emit_pipe(left, right),
1406 NodeKind::Compose { left, right } => {
1407 let _ = write!(self.buf, "lambda x: ");
1409 self.emit_expr(right)?;
1410 self.buf.push('(');
1411 self.emit_expr(left)?;
1412 self.buf.push_str("(x))");
1413 Ok(())
1414 }
1415 NodeKind::Await { expr } => {
1416 self.buf.push_str("(await ");
1417 self.emit_expr(expr)?;
1418 self.buf.push(')');
1419 Ok(())
1420 }
1421 NodeKind::Propagate { expr } => {
1422 self.emit_expr(expr)?;
1424 Ok(())
1425 }
1426 NodeKind::Range { lo, hi, inclusive } => {
1427 self.buf.push_str("range(");
1428 self.emit_expr(lo)?;
1429 self.buf.push_str(", ");
1430 self.emit_expr(hi)?;
1431 if *inclusive {
1432 self.buf.push_str(" + 1");
1433 }
1434 self.buf.push(')');
1435 Ok(())
1436 }
1437 NodeKind::RecordConstruct {
1438 path,
1439 fields,
1440 spread,
1441 } => {
1442 let type_name = path
1443 .segments
1444 .iter()
1445 .map(|s| s.name.as_str())
1446 .collect::<Vec<_>>()
1447 .join(".");
1448 if let Some(sp) = spread {
1449 self.buf.push_str(&format!("{type_name}(**{{**vars("));
1451 self.emit_expr(sp)?;
1452 self.buf.push_str("), ");
1453 for (i, f) in fields.iter().enumerate() {
1454 if i > 0 {
1455 self.buf.push_str(", ");
1456 }
1457 let _ = write!(self.buf, "\"{}\": ", to_snake_case(&f.name.name));
1458 if let Some(val) = &f.value {
1459 self.emit_expr(val)?;
1460 } else {
1461 self.buf.push_str(&to_snake_case(&f.name.name));
1462 }
1463 }
1464 self.buf.push_str("})");
1465 } else {
1466 self.buf.push_str(&type_name);
1467 self.buf.push('(');
1468 for (i, f) in fields.iter().enumerate() {
1469 if i > 0 {
1470 self.buf.push_str(", ");
1471 }
1472 let _ = write!(self.buf, "{}=", to_snake_case(&f.name.name));
1473 if let Some(val) = &f.value {
1474 self.emit_expr(val)?;
1475 } else {
1476 self.buf.push_str(&to_snake_case(&f.name.name));
1477 }
1478 }
1479 self.buf.push(')');
1480 }
1481 Ok(())
1482 }
1483 NodeKind::ListLiteral { elems } => {
1484 self.buf.push('[');
1485 for (i, e) in elems.iter().enumerate() {
1486 if i > 0 {
1487 self.buf.push_str(", ");
1488 }
1489 self.emit_expr(e)?;
1490 }
1491 self.buf.push(']');
1492 Ok(())
1493 }
1494 NodeKind::MapLiteral { entries } => {
1495 self.buf.push('{');
1496 for (i, entry) in entries.iter().enumerate() {
1497 if i > 0 {
1498 self.buf.push_str(", ");
1499 }
1500 self.emit_expr(&entry.key)?;
1501 self.buf.push_str(": ");
1502 self.emit_expr(&entry.value)?;
1503 }
1504 self.buf.push('}');
1505 Ok(())
1506 }
1507 NodeKind::SetLiteral { elems } => {
1508 if elems.is_empty() {
1509 self.buf.push_str("set()");
1510 } else {
1511 self.buf.push('{');
1512 for (i, e) in elems.iter().enumerate() {
1513 if i > 0 {
1514 self.buf.push_str(", ");
1515 }
1516 self.emit_expr(e)?;
1517 }
1518 self.buf.push('}');
1519 }
1520 Ok(())
1521 }
1522 NodeKind::TupleLiteral { elems } => {
1523 self.buf.push('(');
1524 for (i, e) in elems.iter().enumerate() {
1525 if i > 0 {
1526 self.buf.push_str(", ");
1527 }
1528 self.emit_expr(e)?;
1529 }
1530 if elems.len() == 1 {
1531 self.buf.push(',');
1532 }
1533 self.buf.push(')');
1534 Ok(())
1535 }
1536 NodeKind::Interpolation { parts } => {
1537 let has_newline = parts.iter().any(|p| matches!(p,
1538 AirInterpolationPart::Literal(s) if s.contains('\n')
1539 ));
1540 if has_newline {
1541 self.buf.push_str("f\"\"\"");
1542 } else {
1543 self.buf.push_str("f\"");
1544 }
1545 for part in parts {
1546 match part {
1547 AirInterpolationPart::Literal(s) => {
1548 if has_newline {
1549 self.buf.push_str(&escape_fstring_triple(s));
1550 } else {
1551 self.buf.push_str(&escape_fstring(s));
1552 }
1553 }
1554 AirInterpolationPart::Expr(expr) => {
1555 self.buf.push('{');
1556 self.emit_expr(expr)?;
1557 self.buf.push('}');
1558 }
1559 }
1560 }
1561 if has_newline {
1562 self.buf.push_str("\"\"\"");
1563 } else {
1564 self.buf.push('"');
1565 }
1566 Ok(())
1567 }
1568 NodeKind::Placeholder => {
1569 self.buf.push('_');
1570 Ok(())
1571 }
1572 NodeKind::Unreachable => {
1573 self.buf.push_str("raise RuntimeError(\"unreachable\")");
1574 Ok(())
1575 }
1576 NodeKind::ResultConstruct { variant, value } => {
1577 match variant {
1578 ResultVariant::Ok => {
1579 self.buf.push_str("{\"_tag\": \"Ok\", \"value\": ");
1580 if let Some(v) = value {
1581 self.emit_expr(v)?;
1582 } else {
1583 self.buf.push_str("None");
1584 }
1585 self.buf.push('}');
1586 }
1587 ResultVariant::Err => {
1588 self.buf.push_str("{\"_tag\": \"Err\", \"error\": ");
1589 if let Some(v) = value {
1590 self.emit_expr(v)?;
1591 } else {
1592 self.buf.push_str("None");
1593 }
1594 self.buf.push('}');
1595 }
1596 }
1597 Ok(())
1598 }
1599 NodeKind::Assign { op, target, value } => {
1600 self.emit_expr(target)?;
1601 let op_str = match op {
1602 AssignOp::Assign => " = ",
1603 AssignOp::AddAssign => " += ",
1604 AssignOp::SubAssign => " -= ",
1605 AssignOp::MulAssign => " *= ",
1606 AssignOp::DivAssign => " /= ",
1607 AssignOp::RemAssign => " %= ",
1608 };
1609 self.buf.push_str(op_str);
1610 self.emit_expr(value)?;
1611 Ok(())
1612 }
1613 NodeKind::If {
1614 condition,
1615 then_block,
1616 else_block,
1617 ..
1618 } => {
1619 self.buf.push('(');
1621 self.emit_block_as_expr(then_block)?;
1622 self.buf.push_str(" if ");
1623 self.emit_expr(condition)?;
1624 self.buf.push_str(" else ");
1625 if let Some(eb) = else_block {
1626 self.emit_block_as_expr(eb)?;
1627 } else {
1628 self.buf.push_str("None");
1629 }
1630 self.buf.push(')');
1631 Ok(())
1632 }
1633 NodeKind::Block { stmts, tail } => {
1634 if stmts.is_empty() {
1638 if let Some(t) = tail {
1639 return self.emit_expr(t);
1640 }
1641 }
1642 self.buf.push_str("(lambda: ");
1644 if let Some(t) = tail {
1645 self.emit_expr(t)?;
1646 } else {
1647 self.buf.push_str("None");
1648 }
1649 self.buf.push_str(")()");
1650 Ok(())
1651 }
1652 NodeKind::Match { scrutinee, arms } => {
1653 self.buf.push_str("(lambda __v: ");
1657 self.emit_match_expr(scrutinee, arms)?;
1658 self.buf.push_str(")(");
1659 self.emit_expr(scrutinee)?;
1660 self.buf.push(')');
1661 Ok(())
1662 }
1663 NodeKind::Move { expr }
1665 | NodeKind::Borrow { expr }
1666 | NodeKind::MutableBorrow { expr } => self.emit_expr(expr),
1667 NodeKind::EffectOp {
1669 effect,
1670 operation,
1671 args,
1672 } => {
1673 let effect_name = effect.segments.last().map_or("effect", |s| s.name.as_str());
1674 let _ = write!(
1675 self.buf,
1676 "{}.{}",
1677 to_snake_case(effect_name),
1678 to_snake_case(&operation.name)
1679 );
1680 self.buf.push('(');
1681 for (i, arg) in args.iter().enumerate() {
1682 if i > 0 {
1683 self.buf.push_str(", ");
1684 }
1685 self.emit_expr(&arg.value)?;
1686 }
1687 self.buf.push(')');
1688 Ok(())
1689 }
1690 NodeKind::TypeNamed { .. }
1692 | NodeKind::TypeTuple { .. }
1693 | NodeKind::TypeFunction { .. }
1694 | NodeKind::TypeOptional { .. }
1695 | NodeKind::TypeSelf => {
1696 self.buf.push_str("# type");
1697 Ok(())
1698 }
1699 NodeKind::EffectRef { path } => {
1700 let name = path
1701 .segments
1702 .iter()
1703 .map(|s| s.name.as_str())
1704 .collect::<Vec<_>>()
1705 .join(".");
1706 self.buf.push_str(&name);
1707 Ok(())
1708 }
1709 NodeKind::Error => {
1710 self.buf.push_str("# error");
1711 Ok(())
1712 }
1713 _ => {
1714 self.buf.push_str("# unsupported");
1715 Ok(())
1716 }
1717 }
1718 }
1719
1720 fn emit_match(&mut self, scrutinee: &AIRNode, arms: &[AIRNode]) -> Result<(), CodegenError> {
1723 let ind = self.indent_str();
1724 let _ = write!(self.buf, "{ind}match ");
1725 self.emit_expr(scrutinee)?;
1726 self.buf.push_str(":\n");
1727 self.indent += 1;
1728 for arm in arms {
1729 self.emit_match_arm(arm)?;
1730 }
1731 self.indent -= 1;
1732 Ok(())
1733 }
1734
1735 fn emit_match_arm(&mut self, arm: &AIRNode) -> Result<(), CodegenError> {
1736 if let NodeKind::MatchArm {
1737 pattern,
1738 guard,
1739 body,
1740 } = &arm.kind
1741 {
1742 let ind = self.indent_str();
1743 let _ = write!(self.buf, "{ind}case ");
1744 self.emit_pattern(pattern)?;
1745 if let Some(g) = guard {
1746 self.buf.push_str(" if ");
1747 self.emit_expr(g)?;
1748 }
1749 self.buf.push_str(":\n");
1750 self.indent += 1;
1751 self.emit_block_body(body)?;
1752 self.indent -= 1;
1753 }
1754 Ok(())
1755 }
1756
1757 fn emit_pattern(&mut self, pat: &AIRNode) -> Result<(), CodegenError> {
1758 match &pat.kind {
1759 NodeKind::WildcardPat => {
1760 self.buf.push('_');
1761 }
1762 NodeKind::BindPat { name, .. } => {
1763 self.buf.push_str(&to_snake_case(&name.name));
1764 }
1765 NodeKind::LiteralPat { lit } => match lit {
1766 Literal::Int(s) => self.buf.push_str(s),
1767 Literal::Float(s) => self.buf.push_str(s),
1768 Literal::Bool(b) => self.buf.push_str(if *b { "True" } else { "False" }),
1769 Literal::Char(s) => {
1770 self.buf.push('\'');
1771 self.buf.push_str(s);
1772 self.buf.push('\'');
1773 }
1774 Literal::String(s) => {
1775 self.buf.push('"');
1776 self.buf.push_str(&escape_py_string(s));
1777 self.buf.push('"');
1778 }
1779 Literal::Unit => self.buf.push_str("None"),
1780 },
1781 NodeKind::ConstructorPat { path, fields } => {
1782 let variant_name = path
1783 .segments
1784 .iter()
1785 .map(|s| s.name.as_str())
1786 .collect::<Vec<_>>()
1787 .join("_");
1788 if fields.is_empty() {
1789 let _ = write!(self.buf, "{variant_name}()");
1790 } else {
1791 let field_pats: Vec<String> = fields
1792 .iter()
1793 .enumerate()
1794 .map(|(i, f)| {
1795 let name = self.pattern_to_binding_name(f);
1796 format!("_{i}={name}")
1797 })
1798 .collect();
1799 let _ = write!(self.buf, "{variant_name}({})", field_pats.join(", "));
1800 }
1801 }
1802 NodeKind::RecordPat { path, fields, .. } => {
1803 let type_name = path
1804 .segments
1805 .iter()
1806 .map(|s| s.name.as_str())
1807 .collect::<Vec<_>>()
1808 .join("_");
1809 let field_pats: Vec<String> = fields
1810 .iter()
1811 .map(|f| {
1812 let field_name = to_snake_case(&f.name.name);
1813 if let Some(pat) = &f.pattern {
1814 let binding = self.pattern_to_binding_name(pat);
1815 format!("{field_name}={binding}")
1816 } else {
1817 field_name
1818 }
1819 })
1820 .collect();
1821 let _ = write!(self.buf, "{type_name}({})", field_pats.join(", "));
1822 }
1823 NodeKind::TuplePat { elems } => {
1824 self.buf.push('(');
1825 for (i, e) in elems.iter().enumerate() {
1826 if i > 0 {
1827 self.buf.push_str(", ");
1828 }
1829 self.emit_pattern(e)?;
1830 }
1831 if elems.len() == 1 {
1832 self.buf.push(',');
1833 }
1834 self.buf.push(')');
1835 }
1836 _ => {
1837 self.buf.push('_');
1838 }
1839 }
1840 Ok(())
1841 }
1842
1843 fn emit_match_expr(
1845 &mut self,
1846 _scrutinee: &AIRNode,
1847 arms: &[AIRNode],
1848 ) -> Result<(), CodegenError> {
1849 for (i, arm) in arms.iter().enumerate() {
1851 if let NodeKind::MatchArm { body, pattern, .. } = &arm.kind {
1852 if i > 0 {
1853 self.buf.push_str(" if False else ");
1854 }
1855 if matches!(pattern.kind, NodeKind::WildcardPat) || i == arms.len() - 1 {
1856 self.emit_block_as_expr(body)?;
1857 return Ok(());
1858 }
1859 self.emit_block_as_expr(body)?;
1860 }
1861 }
1862 self.buf.push_str("None");
1863 Ok(())
1864 }
1865
1866 fn emit_pipe(&mut self, left: &AIRNode, right: &AIRNode) -> Result<(), CodegenError> {
1869 if let NodeKind::Call { callee, args, .. } = &right.kind {
1870 let has_placeholder = args
1871 .iter()
1872 .any(|a| matches!(a.value.kind, NodeKind::Placeholder));
1873 if has_placeholder {
1874 self.emit_expr(callee)?;
1875 self.buf.push('(');
1876 for (i, arg) in args.iter().enumerate() {
1877 if i > 0 {
1878 self.buf.push_str(", ");
1879 }
1880 if matches!(arg.value.kind, NodeKind::Placeholder) {
1881 self.emit_expr(left)?;
1882 } else {
1883 self.emit_expr(&arg.value)?;
1884 }
1885 }
1886 self.buf.push(')');
1887 return Ok(());
1888 }
1889 }
1890 self.emit_expr(right)?;
1891 self.buf.push('(');
1892 self.emit_expr(left)?;
1893 self.buf.push(')');
1894 Ok(())
1895 }
1896
1897 fn type_to_py(&self, node: &AIRNode) -> String {
1900 match &node.kind {
1901 NodeKind::TypeNamed { path, args } => {
1902 let name = path
1903 .segments
1904 .iter()
1905 .map(|s| s.name.as_str())
1906 .collect::<Vec<_>>()
1907 .join(".");
1908 let py_name = self.map_type_name(&name);
1909 if args.is_empty() {
1910 py_name
1911 } else {
1912 let arg_strs: Vec<String> = args.iter().map(|a| self.type_to_py(a)).collect();
1913 format!("{py_name}[{}]", arg_strs.join(", "))
1914 }
1915 }
1916 NodeKind::TypeTuple { elems } => {
1917 let elem_strs: Vec<String> = elems.iter().map(|e| self.type_to_py(e)).collect();
1918 format!("tuple[{}]", elem_strs.join(", "))
1919 }
1920 NodeKind::TypeFunction { params, ret, .. } => {
1921 let param_strs: Vec<String> = params.iter().map(|p| self.type_to_py(p)).collect();
1922 format!(
1923 "Callable[[{}], {}]",
1924 param_strs.join(", "),
1925 self.type_to_py(ret)
1926 )
1927 }
1928 NodeKind::TypeOptional { inner } => {
1929 format!("{} | None", self.type_to_py(inner))
1930 }
1931 NodeKind::TypeSelf => "Self".into(),
1932 _ => "Any".into(),
1933 }
1934 }
1935
1936 fn map_type_name(&self, name: &str) -> String {
1937 match name {
1938 "Int" => "int".into(),
1939 "Float" => "float".into(),
1940 "Bool" => "bool".into(),
1941 "String" => "str".into(),
1942 "Void" | "Unit" => "None".into(),
1943 "List" => "list".into(),
1944 "Map" => "dict".into(),
1945 "Set" => "set".into(),
1946 "Any" => "Any".into(),
1947 "Never" => "Never".into(),
1948 other => other.into(),
1949 }
1950 }
1951
1952 fn ast_type_to_py(&self, ty: &TypeExpr) -> String {
1953 match ty {
1954 TypeExpr::Named { path, args, .. } => {
1955 let name = path
1956 .segments
1957 .iter()
1958 .map(|s| s.name.as_str())
1959 .collect::<Vec<_>>()
1960 .join(".");
1961 let py_name = self.map_type_name(&name);
1962 if args.is_empty() {
1963 py_name
1964 } else {
1965 let arg_strs: Vec<String> =
1966 args.iter().map(|a| self.ast_type_to_py(a)).collect();
1967 format!("{py_name}[{}]", arg_strs.join(", "))
1968 }
1969 }
1970 TypeExpr::Tuple { elems, .. } => {
1971 let elem_strs: Vec<String> = elems.iter().map(|e| self.ast_type_to_py(e)).collect();
1972 format!("tuple[{}]", elem_strs.join(", "))
1973 }
1974 TypeExpr::Function { params, ret, .. } => {
1975 let param_strs: Vec<String> =
1976 params.iter().map(|p| self.ast_type_to_py(p)).collect();
1977 format!(
1978 "Callable[[{}], {}]",
1979 param_strs.join(", "),
1980 self.ast_type_to_py(ret)
1981 )
1982 }
1983 TypeExpr::Optional { inner, .. } => {
1984 format!("{} | None", self.ast_type_to_py(inner))
1985 }
1986 TypeExpr::SelfType { .. } => "Self".into(),
1987 }
1988 }
1989
1990 fn collect_task_bindings(
2001 stmts: &[AIRNode],
2002 ) -> std::collections::HashSet<String> {
2003 let mut awaited: std::collections::HashSet<String> =
2004 std::collections::HashSet::new();
2005 for s in stmts {
2006 Self::collect_awaited_identifiers(s, &mut awaited);
2007 }
2008 let mut out = std::collections::HashSet::new();
2009 for s in stmts {
2010 if let NodeKind::LetBinding { pattern, value, .. } = &s.kind {
2011 if let NodeKind::BindPat { name, .. } = &pattern.kind {
2012 let py_name = to_snake_case(&name.name);
2013 if matches!(&value.kind, NodeKind::Call { .. })
2014 && awaited.contains(&py_name)
2015 {
2016 out.insert(py_name);
2017 }
2018 }
2019 }
2020 }
2021 out
2022 }
2023
2024 fn collect_awaited_identifiers(
2029 node: &AIRNode,
2030 out: &mut std::collections::HashSet<String>,
2031 ) {
2032 match &node.kind {
2033 NodeKind::Await { expr } => {
2034 if let NodeKind::Identifier { name } = &expr.kind {
2035 out.insert(to_snake_case(&name.name));
2036 }
2037 Self::collect_awaited_identifiers(expr, out);
2038 }
2039 NodeKind::Lambda { .. } | NodeKind::FnDecl { .. } => {
2040 }
2042 NodeKind::Block { stmts, tail } => {
2043 for s in stmts {
2044 Self::collect_awaited_identifiers(s, out);
2045 }
2046 if let Some(t) = tail {
2047 Self::collect_awaited_identifiers(t, out);
2048 }
2049 }
2050 NodeKind::LetBinding { value, .. } => {
2051 Self::collect_awaited_identifiers(value, out);
2052 }
2053 NodeKind::Call { callee, args, .. } => {
2054 Self::collect_awaited_identifiers(callee, out);
2055 for a in args {
2056 Self::collect_awaited_identifiers(&a.value, out);
2057 }
2058 }
2059 NodeKind::MethodCall { receiver, args, .. } => {
2060 Self::collect_awaited_identifiers(receiver, out);
2061 for a in args {
2062 Self::collect_awaited_identifiers(&a.value, out);
2063 }
2064 }
2065 NodeKind::BinaryOp { left, right, .. } => {
2066 Self::collect_awaited_identifiers(left, out);
2067 Self::collect_awaited_identifiers(right, out);
2068 }
2069 NodeKind::UnaryOp { operand, .. } => {
2070 Self::collect_awaited_identifiers(operand, out);
2071 }
2072 NodeKind::If {
2073 condition,
2074 then_block,
2075 else_block,
2076 ..
2077 } => {
2078 Self::collect_awaited_identifiers(condition, out);
2079 Self::collect_awaited_identifiers(then_block, out);
2080 if let Some(e) = else_block {
2081 Self::collect_awaited_identifiers(e, out);
2082 }
2083 }
2084 NodeKind::While { condition, body } => {
2085 Self::collect_awaited_identifiers(condition, out);
2086 Self::collect_awaited_identifiers(body, out);
2087 }
2088 NodeKind::For { iterable, body, .. } => {
2089 Self::collect_awaited_identifiers(iterable, out);
2090 Self::collect_awaited_identifiers(body, out);
2091 }
2092 NodeKind::Return { value: Some(v) } | NodeKind::Break { value: Some(v) } => {
2093 Self::collect_awaited_identifiers(v, out);
2094 }
2095 NodeKind::Assign { value, .. } => {
2096 Self::collect_awaited_identifiers(value, out);
2097 }
2098 NodeKind::TupleLiteral { elems }
2099 | NodeKind::ListLiteral { elems } => {
2100 for e in elems {
2101 Self::collect_awaited_identifiers(e, out);
2102 }
2103 }
2104 _ => {}
2105 }
2106 }
2107
2108 fn emit_block_body(&mut self, node: &AIRNode) -> Result<(), CodegenError> {
2109 if let NodeKind::Block { stmts, tail } = &node.kind {
2110 if stmts.is_empty() && tail.is_none() {
2111 self.writeln("pass");
2112 return Ok(());
2113 }
2114 let task_bindings = Self::collect_task_bindings(stmts);
2122 let prev = std::mem::replace(&mut self.task_bound_names, task_bindings);
2123 for s in stmts {
2124 self.emit_node(s)?;
2125 }
2126 self.task_bound_names = prev;
2127 if let Some(t) = tail {
2128 let ind = self.indent_str();
2129 let _ = write!(self.buf, "{ind}return ");
2130 self.emit_expr(t)?;
2131 self.buf.push('\n');
2132 }
2133 } else {
2134 let ind = self.indent_str();
2136 let _ = write!(self.buf, "{ind}return ");
2137 self.emit_expr(node)?;
2138 self.buf.push('\n');
2139 }
2140 Ok(())
2141 }
2142
2143 fn emit_block_as_expr(&mut self, node: &AIRNode) -> Result<(), CodegenError> {
2144 if let NodeKind::Block { stmts, tail } = &node.kind {
2145 if stmts.is_empty() {
2146 if let Some(t) = tail {
2147 return self.emit_expr(t);
2148 }
2149 }
2150 }
2151 self.emit_expr(node)
2152 }
2153
2154 fn pattern_to_binding_name(&self, pat: &AIRNode) -> String {
2155 match &pat.kind {
2156 NodeKind::BindPat { name, .. } => to_snake_case(&name.name),
2157 NodeKind::WildcardPat => "_".into(),
2158 NodeKind::TuplePat { elems } => {
2159 format!(
2160 "({})",
2161 elems
2162 .iter()
2163 .map(|e| self.pattern_to_binding_name(e))
2164 .collect::<Vec<_>>()
2165 .join(", ")
2166 )
2167 }
2168 NodeKind::RecordPat { fields, .. } => {
2169 fields
2171 .first()
2172 .map(|f| to_snake_case(&f.name.name))
2173 .unwrap_or_else(|| "_".into())
2174 }
2175 _ => "_".into(),
2176 }
2177 }
2178
2179 fn pattern_to_py_binding(&self, pat: &AIRNode) -> String {
2180 self.pattern_to_binding_name(pat)
2181 }
2182
2183 fn type_expr_to_string(&self, node: &AIRNode) -> String {
2184 match &node.kind {
2185 NodeKind::TypeNamed { path, .. } => path
2186 .segments
2187 .iter()
2188 .map(|s| s.name.as_str())
2189 .collect::<Vec<_>>()
2190 .join("."),
2191 NodeKind::Identifier { name } => name.name.clone(),
2192 _ => "Unknown".into(),
2193 }
2194 }
2195}
2196
2197fn ast_type_name(node: &AIRNode) -> Option<String> {
2204 if let NodeKind::TypeNamed { path, .. } = &node.kind {
2205 path.segments.last().map(|s| s.name.clone())
2206 } else {
2207 None
2208 }
2209}
2210
2211fn identifier_to_py(s: &str) -> String {
2215 if s.chars().next().is_some_and(char::is_uppercase) {
2216 s.to_string()
2217 } else {
2218 to_snake_case(s)
2219 }
2220}
2221
2222fn is_time_method_name(name: &str) -> bool {
2225 matches!(
2226 name,
2227 "as_nanos"
2228 | "as_millis"
2229 | "as_seconds"
2230 | "is_zero"
2231 | "is_negative"
2232 | "abs"
2233 | "elapsed"
2234 | "duration_since"
2235 )
2236}
2237
2238fn to_snake_case(s: &str) -> String {
2239 if s.is_empty() || s == "_" {
2241 return s.to_string();
2242 }
2243 if s.contains('_') && !s.chars().any(|c| c.is_uppercase()) {
2245 return s.to_string();
2246 }
2247 if !s.chars().any(|c| c.is_uppercase()) {
2249 return s.to_string();
2250 }
2251 if s.len() == 1 {
2253 return s.to_lowercase();
2254 }
2255
2256 let mut result = String::with_capacity(s.len() + 4);
2257 let chars: Vec<char> = s.chars().collect();
2258
2259 for (i, &ch) in chars.iter().enumerate() {
2260 if ch.is_uppercase() {
2261 let prev_is_upper = i > 0 && chars[i - 1].is_uppercase();
2262 let prev_is_underscore = i > 0 && chars[i - 1] == '_';
2263 let next_is_lower = i + 1 < chars.len() && chars[i + 1].is_lowercase();
2264 if i > 0 && !prev_is_underscore && (!prev_is_upper || next_is_lower) {
2265 result.push('_');
2266 }
2267 result.push(
2268 ch.to_lowercase()
2269 .next()
2270 .expect("lowercase yields at least one char"),
2271 );
2272 } else {
2273 result.push(ch);
2274 }
2275 }
2276 result
2277}
2278
2279fn escape_py_string(s: &str) -> String {
2281 let mut out = String::with_capacity(s.len());
2282 for ch in s.chars() {
2283 match ch {
2284 '"' => out.push_str("\\\""),
2285 '\\' => out.push_str("\\\\"),
2286 '\n' => out.push_str("\\n"),
2287 '\r' => out.push_str("\\r"),
2288 '\t' => out.push_str("\\t"),
2289 _ => out.push(ch),
2290 }
2291 }
2292 out
2293}
2294
2295fn escape_fstring(s: &str) -> String {
2297 let mut out = String::with_capacity(s.len());
2298 for ch in s.chars() {
2299 match ch {
2300 '"' => out.push_str("\\\""),
2301 '\\' => out.push_str("\\\\"),
2302 '\n' => out.push_str("\\n"),
2303 '\r' => out.push_str("\\r"),
2304 '\t' => out.push_str("\\t"),
2305 '{' => out.push_str("{{"),
2306 '}' => out.push_str("}}"),
2307 _ => out.push(ch),
2308 }
2309 }
2310 out
2311}
2312
2313fn escape_fstring_triple(s: &str) -> String {
2316 let mut out = String::with_capacity(s.len());
2317 for ch in s.chars() {
2318 match ch {
2319 '\\' => out.push_str("\\\\"),
2320 '{' => out.push_str("{{"),
2321 '}' => out.push_str("}}"),
2322 _ => out.push(ch),
2323 }
2324 }
2325 out
2326}
2327
2328#[cfg(test)]
2331mod tests {
2332 use super::*;
2333 use bock_air::{AirArg, AirRecordField};
2334 use bock_ast::{Ident, TypePath};
2335 use bock_errors::{FileId, Span};
2336
2337 fn span() -> Span {
2338 Span {
2339 file: FileId(0),
2340 start: 0,
2341 end: 0,
2342 }
2343 }
2344
2345 fn ident(name: &str) -> Ident {
2346 Ident {
2347 name: name.to_string(),
2348 span: span(),
2349 }
2350 }
2351
2352 fn type_path(segments: &[&str]) -> TypePath {
2353 TypePath {
2354 segments: segments.iter().map(|s| ident(s)).collect(),
2355 span: span(),
2356 }
2357 }
2358
2359 fn node(id: u32, kind: NodeKind) -> AIRNode {
2360 AIRNode::new(id, span(), kind)
2361 }
2362
2363 fn int_lit(id: u32, val: &str) -> AIRNode {
2364 node(
2365 id,
2366 NodeKind::Literal {
2367 lit: Literal::Int(val.into()),
2368 },
2369 )
2370 }
2371
2372 fn str_lit(id: u32, val: &str) -> AIRNode {
2373 node(
2374 id,
2375 NodeKind::Literal {
2376 lit: Literal::String(val.into()),
2377 },
2378 )
2379 }
2380
2381 fn bool_lit(id: u32, val: bool) -> AIRNode {
2382 node(
2383 id,
2384 NodeKind::Literal {
2385 lit: Literal::Bool(val),
2386 },
2387 )
2388 }
2389
2390 fn id_node(id: u32, name: &str) -> AIRNode {
2391 node(id, NodeKind::Identifier { name: ident(name) })
2392 }
2393
2394 fn bind_pat(id: u32, name: &str) -> AIRNode {
2395 node(
2396 id,
2397 NodeKind::BindPat {
2398 name: ident(name),
2399 is_mut: false,
2400 },
2401 )
2402 }
2403
2404 fn param_node(id: u32, name: &str) -> AIRNode {
2405 node(
2406 id,
2407 NodeKind::Param {
2408 pattern: Box::new(bind_pat(id + 100, name)),
2409 ty: None,
2410 default: None,
2411 },
2412 )
2413 }
2414
2415 fn typed_param_node(id: u32, name: &str, ty_name: &str) -> AIRNode {
2416 node(
2417 id,
2418 NodeKind::Param {
2419 pattern: Box::new(bind_pat(id + 100, name)),
2420 ty: Some(Box::new(node(
2421 id + 200,
2422 NodeKind::TypeNamed {
2423 path: type_path(&[ty_name]),
2424 args: vec![],
2425 },
2426 ))),
2427 default: None,
2428 },
2429 )
2430 }
2431
2432 fn block(id: u32, stmts: Vec<AIRNode>, tail: Option<AIRNode>) -> AIRNode {
2433 node(
2434 id,
2435 NodeKind::Block {
2436 stmts,
2437 tail: tail.map(Box::new),
2438 },
2439 )
2440 }
2441
2442 fn module(imports: Vec<AIRNode>, items: Vec<AIRNode>) -> AIRNode {
2443 node(
2444 0,
2445 NodeKind::Module {
2446 path: None,
2447 annotations: vec![],
2448 imports,
2449 items,
2450 },
2451 )
2452 }
2453
2454 fn gen(module: &AIRNode) -> String {
2455 let gen = PyGenerator::new();
2456 let result = gen.generate_module(module).unwrap();
2457 result.files[0].content.clone()
2458 }
2459
2460 #[test]
2463 fn implements_code_generator_trait() {
2464 let gen = PyGenerator::new();
2465 assert_eq!(gen.target().id, "python");
2466 }
2467
2468 #[test]
2469 fn empty_module() {
2470 let m = module(vec![], vec![]);
2471 let out = gen(&m);
2472 assert_eq!(out, "");
2473 }
2474
2475 #[test]
2476 fn simple_function() {
2477 let body = block(2, vec![], Some(int_lit(3, "42")));
2478 let f = node(
2479 1,
2480 NodeKind::FnDecl {
2481 annotations: vec![],
2482 visibility: Visibility::Private,
2483 is_async: false,
2484 name: ident("answer"),
2485 generic_params: vec![],
2486 params: vec![],
2487 return_type: None,
2488 effect_clause: vec![],
2489 where_clause: vec![],
2490 body: Box::new(body),
2491 },
2492 );
2493 let out = gen(&module(vec![], vec![f]));
2494 assert!(out.contains("def answer():"), "got: {out}");
2495 assert!(out.contains("return 42"), "got: {out}");
2496 }
2497
2498 #[test]
2499 fn function_with_params_and_type_hints() {
2500 let body = block(
2501 5,
2502 vec![],
2503 Some(node(
2504 6,
2505 NodeKind::BinaryOp {
2506 op: BinOp::Add,
2507 left: Box::new(id_node(7, "a")),
2508 right: Box::new(id_node(8, "b")),
2509 },
2510 )),
2511 );
2512 let f = node(
2513 1,
2514 NodeKind::FnDecl {
2515 annotations: vec![],
2516 visibility: Visibility::Public,
2517 is_async: false,
2518 name: ident("add"),
2519 generic_params: vec![],
2520 params: vec![
2521 typed_param_node(2, "a", "Int"),
2522 typed_param_node(3, "b", "Int"),
2523 ],
2524 return_type: Some(Box::new(node(
2525 4,
2526 NodeKind::TypeNamed {
2527 path: type_path(&["Int"]),
2528 args: vec![],
2529 },
2530 ))),
2531 effect_clause: vec![],
2532 where_clause: vec![],
2533 body: Box::new(body),
2534 },
2535 );
2536 let out = gen(&module(vec![], vec![f]));
2537 assert!(
2538 out.contains("def add(a: int, b: int) -> int:"),
2539 "got: {out}"
2540 );
2541 assert!(out.contains("(a + b)"), "got: {out}");
2542 }
2543
2544 #[test]
2545 fn async_function() {
2546 let body = block(
2547 3,
2548 vec![],
2549 Some(node(
2550 4,
2551 NodeKind::Await {
2552 expr: Box::new(node(
2553 5,
2554 NodeKind::Call {
2555 callee: Box::new(id_node(6, "fetch")),
2556 args: vec![AirArg {
2557 label: None,
2558 value: str_lit(7, "https://example.com"),
2559 }],
2560 type_args: vec![],
2561 },
2562 )),
2563 },
2564 )),
2565 );
2566 let f = node(
2567 1,
2568 NodeKind::FnDecl {
2569 annotations: vec![],
2570 visibility: Visibility::Private,
2571 is_async: true,
2572 name: ident("fetchData"),
2573 generic_params: vec![],
2574 params: vec![],
2575 return_type: None,
2576 effect_clause: vec![],
2577 where_clause: vec![],
2578 body: Box::new(body),
2579 },
2580 );
2581 let out = gen(&module(vec![], vec![f]));
2582 assert!(out.contains("async def fetch_data():"), "got: {out}");
2583 assert!(out.contains("await fetch"), "got: {out}");
2584 }
2585
2586 #[test]
2587 fn effects_as_keyword_args() {
2588 let body = block(
2589 3,
2590 vec![node(
2591 4,
2592 NodeKind::LetBinding {
2593 is_mut: false,
2594 pattern: Box::new(bind_pat(5, "msg")),
2595 ty: None,
2596 value: Box::new(str_lit(6, "hello")),
2597 },
2598 )],
2599 Some(node(
2600 7,
2601 NodeKind::EffectOp {
2602 effect: type_path(&["Log"]),
2603 operation: ident("info"),
2604 args: vec![AirArg {
2605 label: None,
2606 value: id_node(8, "msg"),
2607 }],
2608 },
2609 )),
2610 );
2611 let f = node(
2612 1,
2613 NodeKind::FnDecl {
2614 annotations: vec![],
2615 visibility: Visibility::Private,
2616 is_async: false,
2617 name: ident("process"),
2618 generic_params: vec![],
2619 params: vec![param_node(2, "data")],
2620 return_type: None,
2621 effect_clause: vec![type_path(&["Log"]), type_path(&["Clock"])],
2622 where_clause: vec![],
2623 body: Box::new(body),
2624 },
2625 );
2626 let out = gen(&module(vec![], vec![f]));
2627 assert!(
2628 out.contains("def process(data, *, log: Log, clock: Clock):"),
2629 "got: {out}"
2630 );
2631 assert!(out.contains("log.info(msg)"), "got: {out}");
2632 }
2633
2634 #[test]
2635 fn record_to_dataclass() {
2636 let rec = node(
2637 1,
2638 NodeKind::RecordDecl {
2639 annotations: vec![],
2640 visibility: Visibility::Public,
2641 name: ident("Point"),
2642 generic_params: vec![],
2643 fields: vec![
2644 bock_ast::RecordDeclField {
2645 id: 0,
2646 span: span(),
2647 name: ident("x"),
2648 ty: bock_ast::TypeExpr::Named {
2649 id: 0,
2650 span: span(),
2651 path: type_path(&["Float"]),
2652 args: vec![],
2653 },
2654 default: None,
2655 },
2656 bock_ast::RecordDeclField {
2657 id: 0,
2658 span: span(),
2659 name: ident("y"),
2660 ty: bock_ast::TypeExpr::Named {
2661 id: 0,
2662 span: span(),
2663 path: type_path(&["Float"]),
2664 args: vec![],
2665 },
2666 default: None,
2667 },
2668 ],
2669 },
2670 );
2671 let out = gen(&module(vec![], vec![rec]));
2672 assert!(
2673 out.contains("from dataclasses import dataclass"),
2674 "got: {out}"
2675 );
2676 assert!(out.contains("@dataclass"), "got: {out}");
2677 assert!(out.contains("class Point:"), "got: {out}");
2678 assert!(out.contains("x: float"), "got: {out}");
2679 assert!(out.contains("y: float"), "got: {out}");
2680 }
2681
2682 #[test]
2683 fn enum_to_dataclass_variants() {
2684 let enum_decl = node(
2685 1,
2686 NodeKind::EnumDecl {
2687 annotations: vec![],
2688 visibility: Visibility::Public,
2689 name: ident("Shape"),
2690 generic_params: vec![],
2691 variants: vec![
2692 node(
2693 2,
2694 NodeKind::EnumVariant {
2695 name: ident("Circle"),
2696 payload: EnumVariantPayload::Struct(vec![bock_ast::RecordDeclField {
2697 id: 0,
2698 span: span(),
2699 name: ident("radius"),
2700 ty: bock_ast::TypeExpr::Named {
2701 id: 0,
2702 span: span(),
2703 path: type_path(&["Float"]),
2704 args: vec![],
2705 },
2706 default: None,
2707 }]),
2708 },
2709 ),
2710 node(
2711 3,
2712 NodeKind::EnumVariant {
2713 name: ident("None"),
2714 payload: EnumVariantPayload::Unit,
2715 },
2716 ),
2717 ],
2718 },
2719 );
2720 let out = gen(&module(vec![], vec![enum_decl]));
2721 assert!(
2722 out.contains("from dataclasses import dataclass"),
2723 "got: {out}"
2724 );
2725 assert!(out.contains("@dataclass"), "got: {out}");
2726 assert!(out.contains("class Shape_Circle:"), "got: {out}");
2727 assert!(out.contains("radius: float"), "got: {out}");
2728 assert!(out.contains("_tag: str = \"Circle\""), "got: {out}");
2729 assert!(out.contains("class Shape_None:"), "got: {out}");
2730 assert!(out.contains("_tag: str = \"None\""), "got: {out}");
2731 }
2732
2733 #[test]
2734 fn match_to_match_case() {
2735 let scrutinee = id_node(10, "shape");
2736 let arms = vec![
2737 node(
2738 11,
2739 NodeKind::MatchArm {
2740 pattern: Box::new(node(
2741 12,
2742 NodeKind::ConstructorPat {
2743 path: type_path(&["Shape", "Circle"]),
2744 fields: vec![bind_pat(13, "r")],
2745 },
2746 )),
2747 guard: None,
2748 body: Box::new(block(
2749 14,
2750 vec![],
2751 Some(node(
2752 15,
2753 NodeKind::BinaryOp {
2754 op: BinOp::Mul,
2755 left: Box::new(id_node(16, "r")),
2756 right: Box::new(id_node(17, "r")),
2757 },
2758 )),
2759 )),
2760 },
2761 ),
2762 node(
2763 18,
2764 NodeKind::MatchArm {
2765 pattern: Box::new(node(19, NodeKind::WildcardPat)),
2766 guard: None,
2767 body: Box::new(block(20, vec![], Some(int_lit(21, "0")))),
2768 },
2769 ),
2770 ];
2771 let match_stmt = node(
2772 9,
2773 NodeKind::Match {
2774 scrutinee: Box::new(scrutinee),
2775 arms,
2776 },
2777 );
2778 let f = node(
2779 1,
2780 NodeKind::FnDecl {
2781 annotations: vec![],
2782 visibility: Visibility::Private,
2783 is_async: false,
2784 name: ident("area"),
2785 generic_params: vec![],
2786 params: vec![param_node(2, "shape")],
2787 return_type: None,
2788 effect_clause: vec![],
2789 where_clause: vec![],
2790 body: Box::new(block(3, vec![match_stmt], None)),
2791 },
2792 );
2793 let out = gen(&module(vec![], vec![f]));
2794 assert!(out.contains("match shape:"), "got: {out}");
2795 assert!(out.contains("case Shape_Circle(_0=r):"), "got: {out}");
2796 assert!(out.contains("case _:"), "got: {out}");
2797 }
2798
2799 #[test]
2800 fn ownership_erased() {
2801 let move_expr = node(
2802 1,
2803 NodeKind::Move {
2804 expr: Box::new(id_node(2, "x")),
2805 },
2806 );
2807 let borrow_expr = node(
2808 3,
2809 NodeKind::Borrow {
2810 expr: Box::new(id_node(4, "y")),
2811 },
2812 );
2813 let mut_borrow_expr = node(
2814 5,
2815 NodeKind::MutableBorrow {
2816 expr: Box::new(id_node(6, "z")),
2817 },
2818 );
2819 let body = block(
2820 7,
2821 vec![
2822 node(
2823 8,
2824 NodeKind::LetBinding {
2825 is_mut: false,
2826 pattern: Box::new(bind_pat(9, "a")),
2827 ty: None,
2828 value: Box::new(move_expr),
2829 },
2830 ),
2831 node(
2832 10,
2833 NodeKind::LetBinding {
2834 is_mut: false,
2835 pattern: Box::new(bind_pat(11, "b")),
2836 ty: None,
2837 value: Box::new(borrow_expr),
2838 },
2839 ),
2840 node(
2841 12,
2842 NodeKind::LetBinding {
2843 is_mut: false,
2844 pattern: Box::new(bind_pat(13, "c")),
2845 ty: None,
2846 value: Box::new(mut_borrow_expr),
2847 },
2848 ),
2849 ],
2850 None,
2851 );
2852 let f = node(
2853 0,
2854 NodeKind::FnDecl {
2855 annotations: vec![],
2856 visibility: Visibility::Private,
2857 is_async: false,
2858 name: ident("test"),
2859 generic_params: vec![],
2860 params: vec![],
2861 return_type: None,
2862 effect_clause: vec![],
2863 where_clause: vec![],
2864 body: Box::new(body),
2865 },
2866 );
2867 let out = gen(&module(vec![], vec![f]));
2868 assert!(out.contains("a = x"), "got: {out}");
2869 assert!(out.contains("b = y"), "got: {out}");
2870 assert!(out.contains("c = z"), "got: {out}");
2871 }
2872
2873 #[test]
2874 fn string_interpolation_fstring() {
2875 let interp = node(
2876 1,
2877 NodeKind::Interpolation {
2878 parts: vec![
2879 AirInterpolationPart::Literal("Hello, ".into()),
2880 AirInterpolationPart::Expr(Box::new(id_node(2, "name"))),
2881 AirInterpolationPart::Literal("!".into()),
2882 ],
2883 },
2884 );
2885 let binding = node(
2886 3,
2887 NodeKind::LetBinding {
2888 is_mut: false,
2889 pattern: Box::new(bind_pat(4, "msg")),
2890 ty: None,
2891 value: Box::new(interp),
2892 },
2893 );
2894 let f = node(
2895 0,
2896 NodeKind::FnDecl {
2897 annotations: vec![],
2898 visibility: Visibility::Private,
2899 is_async: false,
2900 name: ident("greet"),
2901 generic_params: vec![],
2902 params: vec![param_node(5, "name")],
2903 return_type: None,
2904 effect_clause: vec![],
2905 where_clause: vec![],
2906 body: Box::new(block(6, vec![binding], Some(id_node(7, "msg")))),
2907 },
2908 );
2909 let out = gen(&module(vec![], vec![f]));
2910 assert!(out.contains("f\"Hello, {name}!\""), "got: {out}");
2911 }
2912
2913 #[test]
2914 fn multiline_interpolation_uses_triple_quoted_fstring() {
2915 let interp = node(
2916 1,
2917 NodeKind::Interpolation {
2918 parts: vec![
2919 AirInterpolationPart::Literal("=== ".into()),
2920 AirInterpolationPart::Expr(Box::new(id_node(2, "title"))),
2921 AirInterpolationPart::Literal(" ===\n".into()),
2922 AirInterpolationPart::Expr(Box::new(id_node(3, "msg"))),
2923 AirInterpolationPart::Literal("\n================".into()),
2924 ],
2925 },
2926 );
2927 let binding = node(
2928 4,
2929 NodeKind::LetBinding {
2930 is_mut: false,
2931 pattern: Box::new(bind_pat(5, "result")),
2932 ty: None,
2933 value: Box::new(interp),
2934 },
2935 );
2936 let f = node(
2937 0,
2938 NodeKind::FnDecl {
2939 annotations: vec![],
2940 visibility: Visibility::Private,
2941 is_async: false,
2942 name: ident("banner"),
2943 generic_params: vec![],
2944 params: vec![param_node(6, "title"), param_node(7, "msg")],
2945 return_type: None,
2946 effect_clause: vec![],
2947 where_clause: vec![],
2948 body: Box::new(block(8, vec![binding], Some(id_node(9, "result")))),
2949 },
2950 );
2951 let out = gen(&module(vec![], vec![f]));
2952 assert!(
2953 out.contains("f\"\"\"=== {title} ===\n{msg}\n================\"\"\""),
2954 "got: {out}"
2955 );
2956 assert!(!out.contains("f\"Hello"), "single-line should not appear: {out}");
2958 }
2959
2960 #[test]
2961 fn single_line_interpolation_still_uses_regular_fstring() {
2962 let interp = node(
2963 1,
2964 NodeKind::Interpolation {
2965 parts: vec![
2966 AirInterpolationPart::Literal("Hi ".into()),
2967 AirInterpolationPart::Expr(Box::new(id_node(2, "name"))),
2968 ],
2969 },
2970 );
2971 let binding = node(
2972 3,
2973 NodeKind::LetBinding {
2974 is_mut: false,
2975 pattern: Box::new(bind_pat(4, "greeting")),
2976 ty: None,
2977 value: Box::new(interp),
2978 },
2979 );
2980 let f = node(
2981 0,
2982 NodeKind::FnDecl {
2983 annotations: vec![],
2984 visibility: Visibility::Private,
2985 is_async: false,
2986 name: ident("greet"),
2987 generic_params: vec![],
2988 params: vec![param_node(5, "name")],
2989 return_type: None,
2990 effect_clause: vec![],
2991 where_clause: vec![],
2992 body: Box::new(block(6, vec![binding], Some(id_node(7, "greeting")))),
2993 },
2994 );
2995 let out = gen(&module(vec![], vec![f]));
2996 assert!(out.contains("f\"Hi {name}\""), "got: {out}");
2997 assert!(!out.contains("f\"\"\""), "should not use triple quotes: {out}");
2998 }
2999
3000 #[test]
3001 fn list_map_set_literals() {
3002 let list = node(
3003 1,
3004 NodeKind::ListLiteral {
3005 elems: vec![int_lit(2, "1"), int_lit(3, "2"), int_lit(4, "3")],
3006 },
3007 );
3008 let map = node(
3009 5,
3010 NodeKind::MapLiteral {
3011 entries: vec![bock_air::AirMapEntry {
3012 key: str_lit(6, "a"),
3013 value: int_lit(7, "1"),
3014 }],
3015 },
3016 );
3017 let set = node(
3018 8,
3019 NodeKind::SetLiteral {
3020 elems: vec![int_lit(9, "1"), int_lit(10, "2")],
3021 },
3022 );
3023 let body = block(
3024 11,
3025 vec![
3026 node(
3027 12,
3028 NodeKind::LetBinding {
3029 is_mut: false,
3030 pattern: Box::new(bind_pat(13, "xs")),
3031 ty: None,
3032 value: Box::new(list),
3033 },
3034 ),
3035 node(
3036 14,
3037 NodeKind::LetBinding {
3038 is_mut: false,
3039 pattern: Box::new(bind_pat(15, "m")),
3040 ty: None,
3041 value: Box::new(map),
3042 },
3043 ),
3044 node(
3045 16,
3046 NodeKind::LetBinding {
3047 is_mut: false,
3048 pattern: Box::new(bind_pat(17, "s")),
3049 ty: None,
3050 value: Box::new(set),
3051 },
3052 ),
3053 ],
3054 None,
3055 );
3056 let f = node(
3057 0,
3058 NodeKind::FnDecl {
3059 annotations: vec![],
3060 visibility: Visibility::Private,
3061 is_async: false,
3062 name: ident("collections"),
3063 generic_params: vec![],
3064 params: vec![],
3065 return_type: None,
3066 effect_clause: vec![],
3067 where_clause: vec![],
3068 body: Box::new(body),
3069 },
3070 );
3071 let out = gen(&module(vec![], vec![f]));
3072 assert!(out.contains("[1, 2, 3]"), "got: {out}");
3073 assert!(out.contains("{\"a\": 1}"), "got: {out}");
3074 assert!(out.contains("{1, 2}"), "got: {out}");
3075 }
3076
3077 #[test]
3078 fn record_construction() {
3079 let rec = node(
3080 1,
3081 NodeKind::RecordConstruct {
3082 path: type_path(&["User"]),
3083 fields: vec![
3084 AirRecordField {
3085 name: ident("name"),
3086 value: Some(Box::new(str_lit(2, "Alice"))),
3087 },
3088 AirRecordField {
3089 name: ident("age"),
3090 value: Some(Box::new(int_lit(3, "30"))),
3091 },
3092 ],
3093 spread: None,
3094 },
3095 );
3096 let binding = node(
3097 4,
3098 NodeKind::LetBinding {
3099 is_mut: false,
3100 pattern: Box::new(bind_pat(5, "user")),
3101 ty: None,
3102 value: Box::new(rec),
3103 },
3104 );
3105 let f = node(
3106 0,
3107 NodeKind::FnDecl {
3108 annotations: vec![],
3109 visibility: Visibility::Private,
3110 is_async: false,
3111 name: ident("test"),
3112 generic_params: vec![],
3113 params: vec![],
3114 return_type: None,
3115 effect_clause: vec![],
3116 where_clause: vec![],
3117 body: Box::new(block(6, vec![binding], None)),
3118 },
3119 );
3120 let out = gen(&module(vec![], vec![f]));
3121 assert!(out.contains("User(name=\"Alice\", age=30)"), "got: {out}");
3122 }
3123
3124 #[test]
3125 fn control_flow() {
3126 let if_stmt = node(
3127 1,
3128 NodeKind::If {
3129 let_pattern: None,
3130 condition: Box::new(bool_lit(2, true)),
3131 then_block: Box::new(block(3, vec![], Some(int_lit(4, "1")))),
3132 else_block: Some(Box::new(block(5, vec![], Some(int_lit(6, "2"))))),
3133 },
3134 );
3135 let for_stmt = node(
3136 7,
3137 NodeKind::For {
3138 pattern: Box::new(bind_pat(8, "x")),
3139 iterable: Box::new(id_node(9, "items")),
3140 body: Box::new(block(10, vec![], None)),
3141 },
3142 );
3143 let while_stmt = node(
3144 11,
3145 NodeKind::While {
3146 condition: Box::new(bool_lit(12, true)),
3147 body: Box::new(block(
3148 13,
3149 vec![node(14, NodeKind::Break { value: None })],
3150 None,
3151 )),
3152 },
3153 );
3154 let body = block(15, vec![if_stmt, for_stmt, while_stmt], None);
3155 let f = node(
3156 0,
3157 NodeKind::FnDecl {
3158 annotations: vec![],
3159 visibility: Visibility::Private,
3160 is_async: false,
3161 name: ident("flow"),
3162 generic_params: vec![],
3163 params: vec![param_node(16, "items")],
3164 return_type: None,
3165 effect_clause: vec![],
3166 where_clause: vec![],
3167 body: Box::new(body),
3168 },
3169 );
3170 let out = gen(&module(vec![], vec![f]));
3171 assert!(out.contains("if True:"), "got: {out}");
3172 assert!(out.contains("else:"), "got: {out}");
3173 assert!(out.contains("for x in items:"), "got: {out}");
3174 assert!(out.contains("while True:"), "got: {out}");
3175 assert!(out.contains("break"), "got: {out}");
3176 }
3177
3178 #[test]
3179 fn lambda_and_pipe() {
3180 let lambda = node(
3181 1,
3182 NodeKind::Lambda {
3183 params: vec![param_node(2, "x")],
3184 body: Box::new(node(
3185 3,
3186 NodeKind::BinaryOp {
3187 op: BinOp::Mul,
3188 left: Box::new(id_node(4, "x")),
3189 right: Box::new(int_lit(5, "2")),
3190 },
3191 )),
3192 },
3193 );
3194 let pipe = node(
3195 6,
3196 NodeKind::Pipe {
3197 left: Box::new(int_lit(7, "5")),
3198 right: Box::new(id_node(8, "double")),
3199 },
3200 );
3201 let body = block(
3202 9,
3203 vec![
3204 node(
3205 10,
3206 NodeKind::LetBinding {
3207 is_mut: false,
3208 pattern: Box::new(bind_pat(11, "double")),
3209 ty: None,
3210 value: Box::new(lambda),
3211 },
3212 ),
3213 node(
3214 12,
3215 NodeKind::LetBinding {
3216 is_mut: false,
3217 pattern: Box::new(bind_pat(13, "result")),
3218 ty: None,
3219 value: Box::new(pipe),
3220 },
3221 ),
3222 ],
3223 None,
3224 );
3225 let f = node(
3226 0,
3227 NodeKind::FnDecl {
3228 annotations: vec![],
3229 visibility: Visibility::Private,
3230 is_async: false,
3231 name: ident("test"),
3232 generic_params: vec![],
3233 params: vec![],
3234 return_type: None,
3235 effect_clause: vec![],
3236 where_clause: vec![],
3237 body: Box::new(body),
3238 },
3239 );
3240 let out = gen(&module(vec![], vec![f]));
3241 assert!(out.contains("lambda x: (x * 2)"), "got: {out}");
3242 assert!(out.contains("double(5)"), "got: {out}");
3243 }
3244
3245 #[test]
3246 fn const_declaration() {
3247 let c = node(
3248 1,
3249 NodeKind::ConstDecl {
3250 annotations: vec![],
3251 visibility: Visibility::Public,
3252 name: ident("PI"),
3253 ty: Box::new(node(
3254 2,
3255 NodeKind::TypeNamed {
3256 path: type_path(&["Float"]),
3257 args: vec![],
3258 },
3259 )),
3260 value: Box::new(node(
3261 3,
3262 NodeKind::Literal {
3263 lit: Literal::Float("3.14159".into()),
3264 },
3265 )),
3266 },
3267 );
3268 let out = gen(&module(vec![], vec![c]));
3269 assert!(out.contains("= 3.14159"), "got: {out}");
3271 assert!(out.contains(": float"), "got: {out}");
3272 }
3273
3274 #[test]
3275 fn class_declaration() {
3276 let method_body = block(10, vec![], Some(id_node(11, "undefined")));
3277 let method = node(
3278 5,
3279 NodeKind::FnDecl {
3280 annotations: vec![],
3281 visibility: Visibility::Public,
3282 is_async: false,
3283 name: ident("greet"),
3284 generic_params: vec![],
3285 params: vec![],
3286 return_type: None,
3287 effect_clause: vec![],
3288 where_clause: vec![],
3289 body: Box::new(method_body),
3290 },
3291 );
3292 let cls = node(
3293 1,
3294 NodeKind::ClassDecl {
3295 annotations: vec![],
3296 visibility: Visibility::Public,
3297 name: ident("Person"),
3298 generic_params: vec![],
3299 base: None,
3300 traits: vec![],
3301 fields: vec![bock_ast::RecordDeclField {
3302 id: 0,
3303 span: span(),
3304 name: ident("name"),
3305 ty: bock_ast::TypeExpr::Named {
3306 id: 0,
3307 span: span(),
3308 path: type_path(&["String"]),
3309 args: vec![],
3310 },
3311 default: None,
3312 }],
3313 methods: vec![method],
3314 },
3315 );
3316 let out = gen(&module(vec![], vec![cls]));
3317 assert!(out.contains("class Person:"), "got: {out}");
3318 assert!(out.contains("def __init__(self, name: str):"), "got: {out}");
3319 assert!(out.contains("self.name = name"), "got: {out}");
3320 assert!(out.contains("def greet(self):"), "got: {out}");
3321 }
3322
3323 #[test]
3324 fn boolean_operators() {
3325 let expr = node(
3326 1,
3327 NodeKind::BinaryOp {
3328 op: BinOp::And,
3329 left: Box::new(bool_lit(2, true)),
3330 right: Box::new(bool_lit(3, false)),
3331 },
3332 );
3333 let not_expr = node(
3334 4,
3335 NodeKind::UnaryOp {
3336 op: UnaryOp::Not,
3337 operand: Box::new(bool_lit(5, true)),
3338 },
3339 );
3340 let body = block(
3341 6,
3342 vec![
3343 node(
3344 7,
3345 NodeKind::LetBinding {
3346 is_mut: false,
3347 pattern: Box::new(bind_pat(8, "a")),
3348 ty: None,
3349 value: Box::new(expr),
3350 },
3351 ),
3352 node(
3353 9,
3354 NodeKind::LetBinding {
3355 is_mut: false,
3356 pattern: Box::new(bind_pat(10, "b")),
3357 ty: None,
3358 value: Box::new(not_expr),
3359 },
3360 ),
3361 ],
3362 None,
3363 );
3364 let f = node(
3365 0,
3366 NodeKind::FnDecl {
3367 annotations: vec![],
3368 visibility: Visibility::Private,
3369 is_async: false,
3370 name: ident("test"),
3371 generic_params: vec![],
3372 params: vec![],
3373 return_type: None,
3374 effect_clause: vec![],
3375 where_clause: vec![],
3376 body: Box::new(body),
3377 },
3378 );
3379 let out = gen(&module(vec![], vec![f]));
3380 assert!(out.contains("(True and False)"), "got: {out}");
3381 assert!(out.contains("not True"), "got: {out}");
3382 }
3383
3384 #[test]
3385 fn result_construct() {
3386 let ok = node(
3387 1,
3388 NodeKind::ResultConstruct {
3389 variant: ResultVariant::Ok,
3390 value: Some(Box::new(int_lit(2, "42"))),
3391 },
3392 );
3393 let err = node(
3394 3,
3395 NodeKind::ResultConstruct {
3396 variant: ResultVariant::Err,
3397 value: Some(Box::new(str_lit(4, "failed"))),
3398 },
3399 );
3400 let body = block(
3401 5,
3402 vec![
3403 node(
3404 6,
3405 NodeKind::LetBinding {
3406 is_mut: false,
3407 pattern: Box::new(bind_pat(7, "good")),
3408 ty: None,
3409 value: Box::new(ok),
3410 },
3411 ),
3412 node(
3413 8,
3414 NodeKind::LetBinding {
3415 is_mut: false,
3416 pattern: Box::new(bind_pat(9, "bad")),
3417 ty: None,
3418 value: Box::new(err),
3419 },
3420 ),
3421 ],
3422 None,
3423 );
3424 let f = node(
3425 0,
3426 NodeKind::FnDecl {
3427 annotations: vec![],
3428 visibility: Visibility::Private,
3429 is_async: false,
3430 name: ident("test"),
3431 generic_params: vec![],
3432 params: vec![],
3433 return_type: None,
3434 effect_clause: vec![],
3435 where_clause: vec![],
3436 body: Box::new(body),
3437 },
3438 );
3439 let out = gen(&module(vec![], vec![f]));
3440 assert!(
3441 out.contains("{\"_tag\": \"Ok\", \"value\": 42}"),
3442 "got: {out}"
3443 );
3444 assert!(
3445 out.contains("{\"_tag\": \"Err\", \"error\": \"failed\"}"),
3446 "got: {out}"
3447 );
3448 }
3449
3450 #[test]
3451 fn to_snake_case_conversions() {
3452 assert_eq!(to_snake_case("fetchData"), "fetch_data");
3453 assert_eq!(to_snake_case("MyClass"), "my_class");
3454 assert_eq!(to_snake_case("already_snake"), "already_snake");
3455 assert_eq!(to_snake_case("simple"), "simple");
3456 assert_eq!(to_snake_case("HTMLParser"), "html_parser");
3457 assert_eq!(to_snake_case("x"), "x");
3458 assert_eq!(to_snake_case("_"), "_");
3459 }
3460
3461 fn has_python3() -> bool {
3464 std::process::Command::new("which")
3465 .arg("python3")
3466 .output()
3467 .map(|o| o.status.success())
3468 .unwrap_or(false)
3469 }
3470
3471 fn check_py_syntax(code: &str) -> bool {
3473 let dir = std::env::temp_dir();
3474 let path = dir.join("bock_test_output.py");
3475 std::fs::write(&path, code).expect("failed to write temp file");
3476 let result = std::process::Command::new("python3")
3477 .arg("-m")
3478 .arg("py_compile")
3479 .arg(&path)
3480 .output()
3481 .expect("failed to spawn python3");
3482 let _ = std::fs::remove_file(&path);
3483 result.status.success()
3484 }
3485
3486 fn run_py(code: &str) -> String {
3488 let output = std::process::Command::new("python3")
3489 .arg("-c")
3490 .arg(code)
3491 .output()
3492 .expect("failed to run python3");
3493 String::from_utf8(output.stdout).unwrap().trim().to_string()
3494 }
3495
3496 #[test]
3497 #[ignore]
3498 fn e2e_hello_world() {
3499 if !has_python3() {
3500 return;
3501 }
3502 let body = block(
3503 2,
3504 vec![],
3505 Some(node(
3506 3,
3507 NodeKind::Call {
3508 callee: Box::new(id_node(4, "print")),
3509 args: vec![AirArg {
3510 label: None,
3511 value: str_lit(5, "Hello, World!"),
3512 }],
3513 type_args: vec![],
3514 },
3515 )),
3516 );
3517 let f = node(
3518 1,
3519 NodeKind::FnDecl {
3520 annotations: vec![],
3521 visibility: Visibility::Private,
3522 is_async: false,
3523 name: ident("main"),
3524 generic_params: vec![],
3525 params: vec![],
3526 return_type: None,
3527 effect_clause: vec![],
3528 where_clause: vec![],
3529 body: Box::new(body),
3530 },
3531 );
3532 let code = gen(&module(vec![], vec![f]));
3533 let full = format!("{code}\nmain()\n");
3534 assert!(
3535 check_py_syntax(&full),
3536 "Python syntax check failed:\n{full}"
3537 );
3538 assert_eq!(run_py(&full), "Hello, World!");
3539 }
3540
3541 #[test]
3542 #[ignore]
3543 fn e2e_arithmetic() {
3544 if !has_python3() {
3545 return;
3546 }
3547 let body = block(
3548 2,
3549 vec![],
3550 Some(node(
3551 3,
3552 NodeKind::BinaryOp {
3553 op: BinOp::Add,
3554 left: Box::new(int_lit(4, "10")),
3555 right: Box::new(int_lit(5, "32")),
3556 },
3557 )),
3558 );
3559 let f = node(
3560 1,
3561 NodeKind::FnDecl {
3562 annotations: vec![],
3563 visibility: Visibility::Private,
3564 is_async: false,
3565 name: ident("calc"),
3566 generic_params: vec![],
3567 params: vec![],
3568 return_type: None,
3569 effect_clause: vec![],
3570 where_clause: vec![],
3571 body: Box::new(body),
3572 },
3573 );
3574 let code = gen(&module(vec![], vec![f]));
3575 let full = format!("{code}\nprint(calc())\n");
3576 assert!(
3577 check_py_syntax(&full),
3578 "Python syntax check failed:\n{full}"
3579 );
3580 assert_eq!(run_py(&full), "42");
3581 }
3582
3583 #[test]
3584 #[ignore]
3585 fn e2e_if_else() {
3586 if !has_python3() {
3587 return;
3588 }
3589 let if_stmt = node(
3590 3,
3591 NodeKind::If {
3592 let_pattern: None,
3593 condition: Box::new(node(
3594 4,
3595 NodeKind::BinaryOp {
3596 op: BinOp::Gt,
3597 left: Box::new(id_node(5, "x")),
3598 right: Box::new(int_lit(6, "0")),
3599 },
3600 )),
3601 then_block: Box::new(block(
3602 7,
3603 vec![node(
3604 8,
3605 NodeKind::Return {
3606 value: Some(Box::new(str_lit(9, "positive"))),
3607 },
3608 )],
3609 None,
3610 )),
3611 else_block: Some(Box::new(block(
3612 10,
3613 vec![node(
3614 11,
3615 NodeKind::Return {
3616 value: Some(Box::new(str_lit(12, "non-positive"))),
3617 },
3618 )],
3619 None,
3620 ))),
3621 },
3622 );
3623 let body = block(2, vec![if_stmt], None);
3624 let f = node(
3625 1,
3626 NodeKind::FnDecl {
3627 annotations: vec![],
3628 visibility: Visibility::Private,
3629 is_async: false,
3630 name: ident("classify"),
3631 generic_params: vec![],
3632 params: vec![param_node(13, "x")],
3633 return_type: None,
3634 effect_clause: vec![],
3635 where_clause: vec![],
3636 body: Box::new(body),
3637 },
3638 );
3639 let code = gen(&module(vec![], vec![f]));
3640 let full = format!("{code}\nprint(classify(5))\nprint(classify(-1))\n");
3641 assert!(
3642 check_py_syntax(&full),
3643 "Python syntax check failed:\n{full}"
3644 );
3645 let output = run_py(&full);
3646 assert!(output.contains("positive"), "got: {output}");
3647 assert!(output.contains("non-positive"), "got: {output}");
3648 }
3649
3650 #[test]
3651 #[ignore]
3652 fn e2e_for_loop() {
3653 if !has_python3() {
3654 return;
3655 }
3656 let body = block(
3657 2,
3658 vec![
3659 node(
3660 3,
3661 NodeKind::LetBinding {
3662 is_mut: true,
3663 pattern: Box::new(bind_pat(4, "sum")),
3664 ty: None,
3665 value: Box::new(int_lit(5, "0")),
3666 },
3667 ),
3668 node(
3669 6,
3670 NodeKind::For {
3671 pattern: Box::new(bind_pat(7, "x")),
3672 iterable: Box::new(node(
3673 8,
3674 NodeKind::ListLiteral {
3675 elems: vec![int_lit(9, "1"), int_lit(10, "2"), int_lit(11, "3")],
3676 },
3677 )),
3678 body: Box::new(block(
3679 12,
3680 vec![node(
3681 13,
3682 NodeKind::Assign {
3683 op: AssignOp::AddAssign,
3684 target: Box::new(id_node(14, "sum")),
3685 value: Box::new(id_node(15, "x")),
3686 },
3687 )],
3688 None,
3689 )),
3690 },
3691 ),
3692 ],
3693 Some(id_node(16, "sum")),
3694 );
3695 let f = node(
3696 1,
3697 NodeKind::FnDecl {
3698 annotations: vec![],
3699 visibility: Visibility::Private,
3700 is_async: false,
3701 name: ident("total"),
3702 generic_params: vec![],
3703 params: vec![],
3704 return_type: None,
3705 effect_clause: vec![],
3706 where_clause: vec![],
3707 body: Box::new(body),
3708 },
3709 );
3710 let code = gen(&module(vec![], vec![f]));
3711 let full = format!("{code}\nprint(total())\n");
3712 assert!(
3713 check_py_syntax(&full),
3714 "Python syntax check failed:\n{full}"
3715 );
3716 assert_eq!(run_py(&full), "6");
3717 }
3718
3719 #[test]
3720 #[ignore]
3721 fn e2e_dataclass() {
3722 if !has_python3() {
3723 return;
3724 }
3725 let rec = node(
3726 1,
3727 NodeKind::RecordDecl {
3728 annotations: vec![],
3729 visibility: Visibility::Public,
3730 name: ident("Point"),
3731 generic_params: vec![],
3732 fields: vec![
3733 bock_ast::RecordDeclField {
3734 id: 0,
3735 span: span(),
3736 name: ident("x"),
3737 ty: bock_ast::TypeExpr::Named {
3738 id: 0,
3739 span: span(),
3740 path: type_path(&["Float"]),
3741 args: vec![],
3742 },
3743 default: None,
3744 },
3745 bock_ast::RecordDeclField {
3746 id: 0,
3747 span: span(),
3748 name: ident("y"),
3749 ty: bock_ast::TypeExpr::Named {
3750 id: 0,
3751 span: span(),
3752 path: type_path(&["Float"]),
3753 args: vec![],
3754 },
3755 default: None,
3756 },
3757 ],
3758 },
3759 );
3760 let code = gen(&module(vec![], vec![rec]));
3761 let full = format!("{code}\np = Point(x=1.0, y=2.0)\nprint(f\"{{p.x}}, {{p.y}}\")\n");
3762 assert!(
3763 check_py_syntax(&full),
3764 "Python syntax check failed:\n{full}"
3765 );
3766 let output = run_py(&full);
3767 assert!(output.contains("1.0, 2.0"), "got: {output}");
3768 }
3769
3770 #[test]
3771 #[ignore]
3772 fn e2e_match_statement() {
3773 if !has_python3() {
3774 return;
3775 }
3776 let scrutinee = id_node(10, "x");
3778 let arms = vec![
3779 node(
3780 11,
3781 NodeKind::MatchArm {
3782 pattern: Box::new(node(
3783 12,
3784 NodeKind::LiteralPat {
3785 lit: Literal::Int("1".into()),
3786 },
3787 )),
3788 guard: None,
3789 body: Box::new(block(
3790 13,
3791 vec![node(
3792 14,
3793 NodeKind::Return {
3794 value: Some(Box::new(str_lit(15, "one"))),
3795 },
3796 )],
3797 None,
3798 )),
3799 },
3800 ),
3801 node(
3802 16,
3803 NodeKind::MatchArm {
3804 pattern: Box::new(node(17, NodeKind::WildcardPat)),
3805 guard: None,
3806 body: Box::new(block(
3807 18,
3808 vec![node(
3809 19,
3810 NodeKind::Return {
3811 value: Some(Box::new(str_lit(20, "other"))),
3812 },
3813 )],
3814 None,
3815 )),
3816 },
3817 ),
3818 ];
3819 let match_stmt = node(
3820 9,
3821 NodeKind::Match {
3822 scrutinee: Box::new(scrutinee),
3823 arms,
3824 },
3825 );
3826 let f = node(
3827 1,
3828 NodeKind::FnDecl {
3829 annotations: vec![],
3830 visibility: Visibility::Private,
3831 is_async: false,
3832 name: ident("describe"),
3833 generic_params: vec![],
3834 params: vec![param_node(2, "x")],
3835 return_type: None,
3836 effect_clause: vec![],
3837 where_clause: vec![],
3838 body: Box::new(block(3, vec![match_stmt], None)),
3839 },
3840 );
3841 let code = gen(&module(vec![], vec![f]));
3842 let full = format!("{code}\nprint(describe(1))\nprint(describe(99))\n");
3843 assert!(
3844 check_py_syntax(&full),
3845 "Python syntax check failed:\n{full}"
3846 );
3847 let output = run_py(&full);
3848 assert!(output.contains("one"), "got: {output}");
3849 assert!(output.contains("other"), "got: {output}");
3850 }
3851
3852 fn gen_prelude_call(func_name: &str, arg: AIRNode) -> String {
3856 let call = node(
3857 10,
3858 NodeKind::Call {
3859 callee: Box::new(id_node(11, func_name)),
3860 args: vec![AirArg {
3861 label: None,
3862 value: arg,
3863 }],
3864 type_args: vec![],
3865 },
3866 );
3867 let body = block(2, vec![call], None);
3868 let f = node(
3869 1,
3870 NodeKind::FnDecl {
3871 name: ident("main"),
3872 params: vec![],
3873 return_type: None,
3874 body: Box::new(body),
3875 generic_params: vec![],
3876 visibility: Visibility::Private,
3877 annotations: vec![],
3878 effect_clause: vec![],
3879 where_clause: vec![],
3880 is_async: false,
3881 },
3882 );
3883 gen(&module(vec![], vec![f]))
3884 }
3885
3886 fn gen_prelude_call_no_args(func_name: &str) -> String {
3888 let call = node(
3889 10,
3890 NodeKind::Call {
3891 callee: Box::new(id_node(11, func_name)),
3892 args: vec![],
3893 type_args: vec![],
3894 },
3895 );
3896 let body = block(2, vec![call], None);
3897 let f = node(
3898 1,
3899 NodeKind::FnDecl {
3900 name: ident("main"),
3901 params: vec![],
3902 return_type: None,
3903 body: Box::new(body),
3904 generic_params: vec![],
3905 visibility: Visibility::Private,
3906 annotations: vec![],
3907 effect_clause: vec![],
3908 where_clause: vec![],
3909 is_async: false,
3910 },
3911 );
3912 gen(&module(vec![], vec![f]))
3913 }
3914
3915 #[test]
3916 fn prelude_println_maps_to_print() {
3917 let out = gen_prelude_call("println", str_lit(12, "hello"));
3918 assert!(
3919 out.contains("print("),
3920 "println should map to print, got: {out}"
3921 );
3922 assert!(
3923 !out.contains("println("),
3924 "should not emit bare println(, got: {out}"
3925 );
3926 }
3927
3928 #[test]
3929 fn prelude_print_maps_to_print_no_newline() {
3930 let out = gen_prelude_call("print", str_lit(12, "hello"));
3931 assert!(
3932 out.contains("print(") && out.contains("end=\"\""),
3933 "print should map to print with end=\"\", got: {out}"
3934 );
3935 }
3936
3937 #[test]
3938 fn prelude_debug_maps_to_repr() {
3939 let out = gen_prelude_call("debug", str_lit(12, "val"));
3940 assert!(
3941 out.contains("print(repr("),
3942 "debug should map to print(repr(...)), got: {out}"
3943 );
3944 }
3945
3946 #[test]
3947 fn prelude_assert_maps_to_assert() {
3948 let out = gen_prelude_call("assert", bool_lit(12, true));
3949 assert!(
3950 out.contains("assert "),
3951 "assert should map to Python assert, got: {out}"
3952 );
3953 assert!(
3954 !out.contains("assert("),
3955 "should not emit assert as function call, got: {out}"
3956 );
3957 }
3958
3959 #[test]
3960 fn prelude_todo_maps_to_not_implemented_error() {
3961 let out = gen_prelude_call_no_args("todo");
3962 assert!(
3963 out.contains("raise NotImplementedError()"),
3964 "todo should map to raise NotImplementedError, got: {out}"
3965 );
3966 }
3967
3968 #[test]
3969 fn prelude_unreachable_maps_to_runtime_error() {
3970 let out = gen_prelude_call_no_args("unreachable");
3971 assert!(
3972 out.contains("raise RuntimeError(\"unreachable\")"),
3973 "unreachable should map to raise RuntimeError, got: {out}"
3974 );
3975 }
3976
3977 #[test]
3978 fn non_prelude_call_passes_through() {
3979 let out = gen_prelude_call("my_custom_func", str_lit(12, "arg"));
3980 assert!(
3981 out.contains("my_custom_func("),
3982 "non-prelude call should use snake_case, got: {out}"
3983 );
3984 }
3985
3986 #[test]
3989 fn effect_decl_becomes_abc() {
3990 let effect = node(
3991 1,
3992 NodeKind::EffectDecl {
3993 annotations: vec![],
3994 visibility: Visibility::Public,
3995 name: ident("Logger"),
3996 generic_params: vec![],
3997 components: vec![],
3998 operations: vec![
3999 node(
4000 2,
4001 NodeKind::FnDecl {
4002 annotations: vec![],
4003 visibility: Visibility::Public,
4004 is_async: false,
4005 name: ident("log"),
4006 generic_params: vec![],
4007 params: vec![
4008 typed_param_node(3, "level", "String"),
4009 typed_param_node(4, "msg", "String"),
4010 ],
4011 return_type: Some(Box::new(node(
4012 5,
4013 NodeKind::TypeNamed {
4014 path: type_path(&["Void"]),
4015 args: vec![],
4016 },
4017 ))),
4018 effect_clause: vec![],
4019 where_clause: vec![],
4020 body: Box::new(block(6, vec![], None)),
4021 },
4022 ),
4023 node(
4024 7,
4025 NodeKind::FnDecl {
4026 annotations: vec![],
4027 visibility: Visibility::Public,
4028 is_async: false,
4029 name: ident("flush"),
4030 generic_params: vec![],
4031 params: vec![],
4032 return_type: Some(Box::new(node(
4033 8,
4034 NodeKind::TypeNamed {
4035 path: type_path(&["Void"]),
4036 args: vec![],
4037 },
4038 ))),
4039 effect_clause: vec![],
4040 where_clause: vec![],
4041 body: Box::new(block(9, vec![], None)),
4042 },
4043 ),
4044 ],
4045 },
4046 );
4047 let out = gen(&module(vec![], vec![effect]));
4048 assert!(
4049 out.contains("from abc import ABC, abstractmethod"),
4050 "got: {out}"
4051 );
4052 assert!(out.contains("class Logger(ABC):"), "got: {out}");
4053 assert!(out.contains("@abstractmethod"), "got: {out}");
4054 assert!(
4055 out.contains("def log(self, level: str, msg: str) -> None:"),
4056 "got: {out}"
4057 );
4058 assert!(out.contains("def flush(self) -> None:"), "got: {out}");
4059 assert!(out.contains(" ..."), "got: {out}");
4060 }
4061
4062 #[test]
4063 fn effect_decl_empty_operations() {
4064 let effect = node(
4065 1,
4066 NodeKind::EffectDecl {
4067 annotations: vec![],
4068 visibility: Visibility::Public,
4069 name: ident("Empty"),
4070 generic_params: vec![],
4071 components: vec![],
4072 operations: vec![],
4073 },
4074 );
4075 let out = gen(&module(vec![], vec![effect]));
4076 assert!(out.contains("class Empty(ABC):"), "got: {out}");
4077 assert!(out.contains(" pass"), "got: {out}");
4078 }
4079
4080 #[test]
4081 fn handling_block_passes_handlers_to_effectful_call() {
4082 use bock_air::AirHandlerPair;
4083
4084 let effect_decl = node(
4085 1,
4086 NodeKind::EffectDecl {
4087 annotations: vec![],
4088 visibility: Visibility::Public,
4089 name: ident("Logger"),
4090 generic_params: vec![],
4091 components: vec![],
4092 operations: vec![node(
4093 2,
4094 NodeKind::FnDecl {
4095 annotations: vec![],
4096 visibility: Visibility::Public,
4097 is_async: false,
4098 name: ident("log"),
4099 generic_params: vec![],
4100 params: vec![typed_param_node(3, "msg", "String")],
4101 return_type: None,
4102 effect_clause: vec![],
4103 where_clause: vec![],
4104 body: Box::new(block(4, vec![], None)),
4105 },
4106 )],
4107 },
4108 );
4109
4110 let inner_fn = node(
4111 10,
4112 NodeKind::FnDecl {
4113 annotations: vec![],
4114 visibility: Visibility::Private,
4115 is_async: false,
4116 name: ident("inner"),
4117 generic_params: vec![],
4118 params: vec![],
4119 return_type: None,
4120 effect_clause: vec![type_path(&["Logger"])],
4121 where_clause: vec![],
4122 body: Box::new(block(12, vec![], Some(str_lit(13, "hello")))),
4123 },
4124 );
4125
4126 let call_inner = node(
4127 20,
4128 NodeKind::Call {
4129 callee: Box::new(id_node(21, "inner")),
4130 args: vec![],
4131 type_args: vec![],
4132 },
4133 );
4134 let handling = node(
4135 30,
4136 NodeKind::HandlingBlock {
4137 handlers: vec![AirHandlerPair {
4138 effect: type_path(&["Logger"]),
4139 handler: Box::new(node(
4140 31,
4141 NodeKind::Call {
4142 callee: Box::new(id_node(32, "StdoutLogger")),
4143 args: vec![],
4144 type_args: vec![],
4145 },
4146 )),
4147 }],
4148 body: Box::new(block(33, vec![], Some(call_inner))),
4149 },
4150 );
4151 let main_fn = node(
4152 40,
4153 NodeKind::FnDecl {
4154 annotations: vec![],
4155 visibility: Visibility::Private,
4156 is_async: false,
4157 name: ident("main"),
4158 generic_params: vec![],
4159 params: vec![],
4160 return_type: None,
4161 effect_clause: vec![],
4162 where_clause: vec![],
4163 body: Box::new(block(41, vec![handling], None)),
4164 },
4165 );
4166
4167 let out = gen(&module(vec![], vec![effect_decl, inner_fn, main_fn]));
4168 assert!(
4171 out.contains("inner(logger=__logger_h"),
4172 "handling block should pass handler to effectful call, got: {out}"
4173 );
4174 assert!(
4176 out.contains(": Logger = StdoutLogger()"),
4177 "handling block should instantiate handler, got: {out}"
4178 );
4179 }
4180
4181 #[test]
4184 fn async_function_imports_asyncio() {
4185 let body = block(3, vec![], None);
4186 let f = node(
4187 1,
4188 NodeKind::FnDecl {
4189 annotations: vec![],
4190 visibility: Visibility::Private,
4191 is_async: true,
4192 name: ident("tick"),
4193 generic_params: vec![],
4194 params: vec![],
4195 return_type: None,
4196 effect_clause: vec![],
4197 where_clause: vec![],
4198 body: Box::new(body),
4199 },
4200 );
4201 let out = gen(&module(vec![], vec![f]));
4202 assert!(out.contains("import asyncio"), "got: {out}");
4203 assert!(out.contains("async def tick():"), "got: {out}");
4204 }
4205
4206 #[test]
4207 fn sync_module_has_no_asyncio_import() {
4208 let body = block(3, vec![], Some(int_lit(4, "1")));
4209 let f = node(
4210 1,
4211 NodeKind::FnDecl {
4212 annotations: vec![],
4213 visibility: Visibility::Private,
4214 is_async: false,
4215 name: ident("one"),
4216 generic_params: vec![],
4217 params: vec![],
4218 return_type: None,
4219 effect_clause: vec![],
4220 where_clause: vec![],
4221 body: Box::new(body),
4222 },
4223 );
4224 let out = gen(&module(vec![], vec![f]));
4225 assert!(!out.contains("import asyncio"), "got: {out}");
4226 }
4227
4228 #[test]
4229 fn entry_invocation_async_main_python() {
4230 let inv = PyGenerator::new().entry_invocation(true).unwrap();
4231 assert!(inv.contains("asyncio.run(main())"), "got: {inv}");
4232 }
4233
4234 #[test]
4235 fn entry_invocation_sync_main_python() {
4236 let inv = PyGenerator::new().entry_invocation(false).unwrap();
4237 assert_eq!(inv, "if __name__ == \"__main__\":\n main()\n");
4238 }
4239
4240 #[test]
4241 fn generate_project_async_main_uses_asyncio_run() {
4242 let main_fn = node(
4243 1,
4244 NodeKind::FnDecl {
4245 annotations: vec![],
4246 visibility: Visibility::Private,
4247 is_async: true,
4248 name: ident("main"),
4249 generic_params: vec![],
4250 params: vec![],
4251 return_type: None,
4252 effect_clause: vec![],
4253 where_clause: vec![],
4254 body: Box::new(block(2, vec![], None)),
4255 },
4256 );
4257 let m = module(vec![], vec![main_fn]);
4258 let gen = PyGenerator::new();
4259 let out = gen.generate_project(&[&m]).unwrap();
4260 let src = &out.files[0].content;
4261 assert!(src.contains("import asyncio"), "got: {src}");
4262 assert!(src.contains("async def main():"), "got: {src}");
4263 assert!(src.contains("asyncio.run(main())"), "got: {src}");
4264 }
4265
4266 #[test]
4267 fn concurrent_pattern_wraps_in_create_task() {
4268 let call_task = |id: u32, name: &str| {
4275 node(
4276 id,
4277 NodeKind::Call {
4278 callee: Box::new(id_node(id + 1, name)),
4279 args: vec![],
4280 type_args: vec![],
4281 },
4282 )
4283 };
4284 let let_stmt = |id: u32, name: &str, val: AIRNode| {
4285 node(
4286 id,
4287 NodeKind::LetBinding {
4288 is_mut: false,
4289 pattern: Box::new(bind_pat(id + 1, name)),
4290 ty: None,
4291 value: Box::new(val),
4292 },
4293 )
4294 };
4295 let await_id = |id: u32, name: &str| {
4296 node(
4297 id,
4298 NodeKind::Await {
4299 expr: Box::new(id_node(id + 1, name)),
4300 },
4301 )
4302 };
4303
4304 let body = block(
4305 10,
4306 vec![
4307 let_stmt(20, "a", call_task(21, "task1")),
4308 let_stmt(30, "b", call_task(31, "task2")),
4309 let_stmt(40, "ra", await_id(41, "a")),
4310 let_stmt(50, "rb", await_id(51, "b")),
4311 ],
4312 Some(id_node(60, "ra")),
4313 );
4314 let f = node(
4315 1,
4316 NodeKind::FnDecl {
4317 annotations: vec![],
4318 visibility: Visibility::Private,
4319 is_async: true,
4320 name: ident("run"),
4321 generic_params: vec![],
4322 params: vec![],
4323 return_type: None,
4324 effect_clause: vec![],
4325 where_clause: vec![],
4326 body: Box::new(body),
4327 },
4328 );
4329 let out = gen(&module(vec![], vec![f]));
4330 assert!(
4331 out.contains("a = asyncio.create_task(task1())"),
4332 "task1 should be scheduled as a task, got: {out}"
4333 );
4334 assert!(
4335 out.contains("b = asyncio.create_task(task2())"),
4336 "task2 should be scheduled as a task, got: {out}"
4337 );
4338 assert!(out.contains("ra = (await a)"), "got: {out}");
4339 assert!(out.contains("rb = (await b)"), "got: {out}");
4340 }
4341
4342 #[test]
4343 fn sequential_await_no_task_wrapping() {
4344 let await_call = node(
4346 20,
4347 NodeKind::Await {
4348 expr: Box::new(node(
4349 21,
4350 NodeKind::Call {
4351 callee: Box::new(id_node(22, "task1")),
4352 args: vec![],
4353 type_args: vec![],
4354 },
4355 )),
4356 },
4357 );
4358 let let_stmt = node(
4359 10,
4360 NodeKind::LetBinding {
4361 is_mut: false,
4362 pattern: Box::new(bind_pat(11, "a")),
4363 ty: None,
4364 value: Box::new(await_call),
4365 },
4366 );
4367 let body = block(30, vec![let_stmt], Some(id_node(40, "a")));
4368 let f = node(
4369 1,
4370 NodeKind::FnDecl {
4371 annotations: vec![],
4372 visibility: Visibility::Private,
4373 is_async: true,
4374 name: ident("run"),
4375 generic_params: vec![],
4376 params: vec![],
4377 return_type: None,
4378 effect_clause: vec![],
4379 where_clause: vec![],
4380 body: Box::new(body),
4381 },
4382 );
4383 let out = gen(&module(vec![], vec![f]));
4384 assert!(
4385 !out.contains("create_task"),
4386 "sequential await should not wrap in create_task, got: {out}"
4387 );
4388 assert!(out.contains("a = (await task1())"), "got: {out}");
4389 }
4390
4391 #[test]
4392 fn non_call_rhs_not_wrapped_in_task() {
4393 let let_stmt = node(
4395 10,
4396 NodeKind::LetBinding {
4397 is_mut: false,
4398 pattern: Box::new(bind_pat(11, "a")),
4399 ty: None,
4400 value: Box::new(int_lit(12, "42")),
4401 },
4402 );
4403 let await_a = node(
4404 20,
4405 NodeKind::Await {
4406 expr: Box::new(id_node(21, "a")),
4407 },
4408 );
4409 let body = block(30, vec![let_stmt], Some(await_a));
4410 let f = node(
4411 1,
4412 NodeKind::FnDecl {
4413 annotations: vec![],
4414 visibility: Visibility::Private,
4415 is_async: true,
4416 name: ident("run"),
4417 generic_params: vec![],
4418 params: vec![],
4419 return_type: None,
4420 effect_clause: vec![],
4421 where_clause: vec![],
4422 body: Box::new(body),
4423 },
4424 );
4425 let out = gen(&module(vec![], vec![f]));
4426 assert!(!out.contains("create_task"), "got: {out}");
4427 assert!(out.contains("a = 42"), "got: {out}");
4428 }
4429}