use std::cell::Cell;
use std::collections::HashMap;
use std::fmt::Write;
use std::path::PathBuf;
use bock_air::{AIRNode, AirInterpolationPart, EnumVariantPayload, NodeKind, ResultVariant};
use bock_ast::{AssignOp, BinOp, Literal, TypeExpr, UnaryOp, Visibility};
use bock_types::AIRModule;
use crate::error::CodegenError;
use crate::generator::{CodeGenerator, GeneratedCode, OutputFile, SourceMap};
use crate::profile::TargetProfile;
fn py_module_uses_concurrency(items: &[AIRNode]) -> bool {
items.iter().any(|n| {
let s = format!("{n:?}");
s.contains("\"Channel\"") || s.contains("\"spawn\"")
})
}
fn py_module_uses_optional(items: &[AIRNode]) -> bool {
items.iter().any(|n| {
let s = format!("{n:?}");
s.contains("\"Optional\"")
|| s.contains("TypeOptional")
|| s.contains("\"Some\"")
|| s.contains("\"None\"")
|| s.contains("\"pop\"")
})
}
fn py_module_uses_list_mutators(items: &[AIRNode]) -> bool {
items.iter().any(|n| {
let s = format!("{n:?}");
s.contains("\"remove_at\"") || s.contains("\"insert\"") || s.contains("\"set\"")
})
}
fn py_module_uses_result(items: &[AIRNode]) -> bool {
items.iter().any(|n| {
let s = format!("{n:?}");
s.contains("\"Result\"")
|| s.contains("ResultConstruct")
|| s.contains("\"Ok\"")
|| s.contains("\"Err\"")
})
}
fn py_module_uses_propagate(items: &[AIRNode]) -> bool {
items.iter().any(|n| format!("{n:?}").contains("Propagate"))
}
fn py_module_uses_str(items: &[AIRNode]) -> bool {
items
.iter()
.any(|n| format!("{n:?}").contains("Interpolation"))
}
fn py_module_uses_list_functional(items: &[AIRNode]) -> bool {
items.iter().any(|n| {
let s = format!("{n:?}");
s.contains("\"reduce\"")
|| s.contains("\"fold\"")
|| s.contains("\"find\"")
|| s.contains("\"for_each\"")
})
}
fn py_module_uses_ordering(items: &[AIRNode]) -> bool {
items.iter().any(|n| {
let s = format!("{n:?}");
s.contains("\"Ordering\"")
|| s.contains("\"Less\"")
|| s.contains("\"Equal\"")
|| s.contains("\"Greater\"")
|| s.contains("\"compare\"")
})
}
const OPTIONAL_RUNTIME_PY: &str = "\
# ── Bock Optional runtime ──
class _BockSome:
__match_args__ = ('_0',)
__slots__ = ('_0',)
def __init__(self, _0):
self._0 = _0
def __repr__(self):
return f'Some({self._0!r})'
def __eq__(self, other):
if not isinstance(other, _BockSome):
return NotImplemented
return self._0 == other._0
def __hash__(self):
return hash(('Some', self._0))
class _BockNone:
__slots__ = ()
def __repr__(self):
return 'None'
def __eq__(self, other):
if not isinstance(other, _BockNone):
return NotImplemented
return True
def __hash__(self):
return hash('None')
_bock_none = _BockNone()
";
const RESULT_RUNTIME_PY: &str = "\
# ── Bock Result runtime ──
class _BockOk:
__match_args__ = ('_0',)
__slots__ = ('_0',)
def __init__(self, _0):
self._0 = _0
def __repr__(self):
return f'Ok({self._0!r})'
def __eq__(self, other):
if not isinstance(other, _BockOk):
return NotImplemented
return self._0 == other._0
def __hash__(self):
return hash(('Ok', self._0))
class _BockErr:
__match_args__ = ('_0',)
__slots__ = ('_0',)
def __init__(self, _0):
self._0 = _0
def __repr__(self):
return f'Err({self._0!r})'
def __eq__(self, other):
if not isinstance(other, _BockErr):
return NotImplemented
return self._0 == other._0
def __hash__(self):
return hash(('Err', self._0))
def _bock_parse_int(s, mk_err):
try:
return _BockOk(int(s.strip()))
except (ValueError, TypeError):
return _BockErr(mk_err(f\"cannot parse {s!r} as Int\"))
def _bock_parse_float(s, mk_err):
try:
t = s.strip()
if t == '':
raise ValueError()
return _BockOk(float(t))
except (ValueError, TypeError):
return _BockErr(mk_err(f\"cannot parse {s!r} as Float\"))
";
const PROPAGATE_RUNTIME_PY: &str = "\
# ── Bock `?` propagate runtime ──
class _BockPropagate(Exception):
__slots__ = ('value',)
def __init__(self, value):
super().__init__()
self.value = value
def _bock_try(v):
if type(v).__name__ in ('_BockOk', '_BockSome'):
return v._0
raise _BockPropagate(v)
";
const ORDERING_RUNTIME_PY: &str = "\
# ── Bock Ordering runtime ──
class _BockOrderingLess:
__slots__ = ()
def __repr__(self):
return 'Less'
class _BockOrderingEqual:
__slots__ = ()
def __repr__(self):
return 'Equal'
class _BockOrderingGreater:
__slots__ = ()
def __repr__(self):
return 'Greater'
_bock_less = _BockOrderingLess()
_bock_equal = _BockOrderingEqual()
_bock_greater = _BockOrderingGreater()
def _bock_compare(a, b):
m = getattr(a, 'compare', None)
if callable(m):
return m(b)
return _bock_less if a < b else (_bock_equal if a == b else _bock_greater)
";
const LIST_MUTATOR_RUNTIME_PY: &str = "\
# ── Bock List in-place-mutator runtime ──
def _bock_list_abort(op, i, n):
raise IndexError(f'List.{op}: index {i} out of bounds (len {n})')
";
const STR_RUNTIME_PY: &str = "\
# ── Bock display-string runtime ──
def _bock_str(x):
m = getattr(x, 'to_string', None)
if callable(m):
return m()
return str(x)
";
const LIST_FUNCTIONAL_RUNTIME_PY: &str = "\
# ── Bock List functional-combinator runtime ──
def _bock_reduce(xs, f):
it = iter(xs)
acc = next(it)
for x in it:
acc = f(acc, x)
return acc
def _bock_fold(xs, init, f):
acc = init
for x in xs:
acc = f(acc, x)
return acc
def _bock_find(xs, pred):
for x in xs:
if pred(x):
return _BockSome(x)
return _bock_none
def _bock_for_each(xs, f):
for x in xs:
f(x)
return None
";
const RUNTIME_PRELUDE_NAMES: &[&str] = &[
"Optional", "Some", "None", "Result", "Ok", "Err", "Ordering", "Less", "Equal", "Greater",
];
fn collect_public_symbol_modules(
modules: &[(&AIRModule, &std::path::Path)],
) -> HashMap<String, String> {
let mut map: HashMap<String, String> = HashMap::new();
for (module, _) in modules {
let Some(module_path) = crate::generator::module_path_string(module) else {
continue;
};
let NodeKind::Module { items, .. } = &module.kind else {
continue;
};
for item in items {
let mut record = |name: &str| {
if !RUNTIME_PRELUDE_NAMES.contains(&name) {
map.entry(name.to_string())
.or_insert_with(|| module_path.clone());
}
};
match &item.kind {
NodeKind::FnDecl {
visibility, name, ..
}
| NodeKind::RecordDecl {
visibility, name, ..
}
| NodeKind::TraitDecl {
visibility, name, ..
}
| NodeKind::ClassDecl {
visibility, name, ..
}
| NodeKind::EffectDecl {
visibility, name, ..
}
| NodeKind::TypeAlias {
visibility, name, ..
}
| NodeKind::ConstDecl {
visibility, name, ..
} => {
if matches!(visibility, Visibility::Public) {
record(&name.name);
}
}
NodeKind::EnumDecl {
visibility,
name,
variants,
..
} => {
if matches!(visibility, Visibility::Public) {
record(&name.name);
for v in variants {
if let NodeKind::EnumVariant { name: vname, .. } = &v.kind {
record(&format!("{}_{}", name.name, vname.name));
}
}
}
}
_ => {}
}
}
}
map
}
fn module_path_string_of(module: &AIRModule) -> String {
crate::generator::module_path_string(module).unwrap_or_default()
}
fn locally_declared_names(module: &AIRModule) -> std::collections::HashSet<String> {
let mut names = std::collections::HashSet::new();
let NodeKind::Module { items, .. } = &module.kind else {
return names;
};
for item in items {
match &item.kind {
NodeKind::FnDecl { name, .. }
| NodeKind::RecordDecl { name, .. }
| NodeKind::TraitDecl { name, .. }
| NodeKind::ClassDecl { name, .. }
| NodeKind::EffectDecl { name, .. }
| NodeKind::TypeAlias { name, .. }
| NodeKind::ConstDecl { name, .. } => {
names.insert(name.name.clone());
}
NodeKind::EnumDecl { name, variants, .. } => {
names.insert(name.name.clone());
for v in variants {
if let NodeKind::EnumVariant { name: vname, .. } = &v.kind {
names.insert(format!("{}_{}", name.name, vname.name));
}
}
}
_ => {}
}
}
names
}
fn explicitly_imported_names(module: &AIRModule) -> std::collections::HashSet<String> {
let mut names = std::collections::HashSet::new();
let NodeKind::Module { imports, .. } = &module.kind else {
return names;
};
for import in imports {
if let NodeKind::ImportDecl {
items: bock_ast::ImportItems::Named(named),
..
} = &import.kind
{
for n in named {
names.insert(n.name.name.clone());
if let Some(alias) = &n.alias {
names.insert(alias.name.clone());
}
}
}
}
names
}
fn field_label_occurrences(module: &AIRModule) -> HashMap<String, usize> {
let mut counts: HashMap<String, usize> = HashMap::new();
fn bump(counts: &mut HashMap<String, usize>, name: &str) {
*counts.entry(name.to_string()).or_insert(0) += 1;
}
fn walk_decl_fields(counts: &mut HashMap<String, usize>, fields: &[bock_ast::RecordDeclField]) {
for f in fields {
bump(counts, &f.name.name);
}
}
fn walk(counts: &mut HashMap<String, usize>, node: &AIRNode) {
match &node.kind {
NodeKind::RecordDecl { fields, .. } | NodeKind::ClassDecl { fields, .. } => {
walk_decl_fields(counts, fields);
}
NodeKind::EnumVariant {
payload: bock_air::EnumVariantPayload::Struct(fields),
..
} => {
walk_decl_fields(counts, fields);
}
NodeKind::FieldAccess { field, object } => {
bump(counts, &field.name);
walk(counts, object);
}
NodeKind::RecordConstruct { fields, spread, .. } => {
for f in fields {
bump(counts, &f.name.name);
if let Some(v) = &f.value {
walk(counts, v);
}
}
if let Some(s) = spread {
walk(counts, s);
}
}
NodeKind::RecordPat { fields, .. } => {
for f in fields {
bump(counts, &f.name.name);
if let Some(p) = &f.pattern {
walk(counts, p);
}
}
}
_ => {}
}
for child in child_nodes(node) {
walk(counts, child);
}
}
walk(&mut counts, module);
counts
}
fn child_nodes(node: &AIRNode) -> Vec<&AIRNode> {
let mut out: Vec<&AIRNode> = Vec::new();
macro_rules! p {
($e:expr) => {
out.push($e)
};
}
macro_rules! popt {
($e:expr) => {
if let Some(n) = $e {
out.push(n)
}
};
}
macro_rules! pvec {
($e:expr) => {
for n in $e {
out.push(n)
}
};
}
match &node.kind {
NodeKind::Module { imports, items, .. } => {
pvec!(imports);
pvec!(items);
}
NodeKind::FnDecl {
params,
return_type,
body,
..
} => {
pvec!(params);
popt!(return_type.as_deref());
p!(body);
}
NodeKind::EnumDecl { variants, .. } => {
pvec!(variants);
}
NodeKind::EnumVariant {
payload: EnumVariantPayload::Tuple(types),
..
} => {
pvec!(types);
}
NodeKind::ClassDecl { methods, .. } => {
pvec!(methods);
}
NodeKind::TraitDecl { methods, .. } => {
pvec!(methods);
}
NodeKind::ImplBlock {
target, methods, ..
} => {
p!(target);
pvec!(methods);
}
NodeKind::EffectDecl { operations, .. } => {
pvec!(operations);
}
NodeKind::TypeAlias { ty, .. } => p!(ty),
NodeKind::ConstDecl { ty, value, .. } => {
p!(ty);
p!(value);
}
NodeKind::ModuleHandle { handler, .. } => p!(handler),
NodeKind::PropertyTest { body, .. } => p!(body),
NodeKind::Param {
pattern,
ty,
default,
} => {
p!(pattern);
popt!(ty.as_deref());
popt!(default.as_deref());
}
NodeKind::TypeNamed { args, .. } => {
pvec!(args);
}
NodeKind::TypeTuple { elems } => {
pvec!(elems);
}
NodeKind::TypeFunction { params, ret, .. } => {
pvec!(params);
p!(ret);
}
NodeKind::TypeOptional { inner } => p!(inner),
NodeKind::BinaryOp { left, right, .. } => {
p!(left);
p!(right);
}
NodeKind::UnaryOp { operand, .. } => p!(operand),
NodeKind::Assign { target, value, .. } => {
p!(target);
p!(value);
}
NodeKind::Call {
callee,
args,
type_args,
} => {
p!(callee);
for a in args {
out.push(&a.value);
}
pvec!(type_args);
}
NodeKind::MethodCall {
receiver,
type_args,
args,
..
} => {
p!(receiver);
pvec!(type_args);
for a in args {
out.push(&a.value);
}
}
NodeKind::Index { object, index } => {
p!(object);
p!(index);
}
NodeKind::Propagate { expr }
| NodeKind::Await { expr }
| NodeKind::Move { expr }
| NodeKind::Borrow { expr }
| NodeKind::MutableBorrow { expr } => p!(expr),
NodeKind::Lambda { params, body } => {
pvec!(params);
p!(body);
}
NodeKind::Pipe { left, right } | NodeKind::Compose { left, right } => {
p!(left);
p!(right);
}
NodeKind::Range { lo, hi, .. } | NodeKind::RangePat { lo, hi, .. } => {
p!(lo);
p!(hi);
}
NodeKind::ListLiteral { elems }
| NodeKind::SetLiteral { elems }
| NodeKind::TupleLiteral { elems }
| NodeKind::TuplePat { elems } => {
pvec!(elems);
}
NodeKind::MapLiteral { entries } => {
for e in entries {
out.push(&e.key);
out.push(&e.value);
}
}
NodeKind::Interpolation { parts } => {
for part in parts {
if let bock_air::AirInterpolationPart::Expr(n) = part {
out.push(n.as_ref());
}
}
}
NodeKind::ResultConstruct { value, .. }
| NodeKind::Return { value }
| NodeKind::Break { value } => {
popt!(value.as_deref());
}
NodeKind::If {
let_pattern,
condition,
then_block,
else_block,
} => {
popt!(let_pattern.as_deref());
p!(condition);
p!(then_block);
popt!(else_block.as_deref());
}
NodeKind::Guard {
let_pattern,
condition,
else_block,
} => {
popt!(let_pattern.as_deref());
p!(condition);
p!(else_block);
}
NodeKind::Match { scrutinee, arms } => {
p!(scrutinee);
pvec!(arms);
}
NodeKind::MatchArm {
pattern,
guard,
body,
} => {
p!(pattern);
popt!(guard.as_deref());
p!(body);
}
NodeKind::For {
pattern,
iterable,
body,
} => {
p!(pattern);
p!(iterable);
p!(body);
}
NodeKind::While { condition, body } => {
p!(condition);
p!(body);
}
NodeKind::Loop { body } => p!(body),
NodeKind::Block { stmts, tail } => {
pvec!(stmts);
popt!(tail.as_deref());
}
NodeKind::LetBinding {
pattern, ty, value, ..
} => {
p!(pattern);
popt!(ty.as_deref());
p!(value);
}
NodeKind::EffectOp { args, .. } => {
for a in args {
out.push(&a.value);
}
}
NodeKind::HandlingBlock { handlers, body } => {
for h in handlers {
out.push(&h.handler);
}
p!(body);
}
NodeKind::ConstructorPat { fields, .. } => {
pvec!(fields);
}
NodeKind::ListPat { elems, rest } => {
pvec!(elems);
popt!(rest.as_deref());
}
NodeKind::OrPat { alternatives } => {
pvec!(alternatives);
}
NodeKind::GuardPat { pattern, guard } => {
p!(pattern);
p!(guard);
}
NodeKind::ImportDecl { .. }
| NodeKind::RecordDecl { .. }
| NodeKind::FieldAccess { .. }
| NodeKind::RecordConstruct { .. }
| NodeKind::RecordPat { .. }
| NodeKind::TypeSelf
| NodeKind::Literal { .. }
| NodeKind::Identifier { .. }
| NodeKind::Placeholder
| NodeKind::Unreachable
| NodeKind::Continue
| NodeKind::WildcardPat
| NodeKind::BindPat { .. }
| NodeKind::LiteralPat { .. }
| NodeKind::RestPat
| NodeKind::Error
| NodeKind::EffectRef { .. } => {}
_ => {}
}
out
}
fn quoted_token_count(rendered: &str, name: &str) -> usize {
rendered.matches(&format!("\"{name}\"")).count()
}
fn value_needs_stmt_form(value: &AIRNode) -> bool {
match &value.kind {
NodeKind::Match { arms, .. } => {
crate::generator::match_has_statement_arm(arms)
|| control_flow_has_raise_branch(value)
|| match_value_needs_stmt_form(arms)
}
NodeKind::Loop { .. } | NodeKind::While { .. } => true,
NodeKind::If { .. } => {
crate::generator::node_is_statement(value) || control_flow_has_raise_branch(value)
}
NodeKind::Block { stmts, .. } => !stmts.is_empty(),
_ => false,
}
}
fn is_raise_expr(node: &AIRNode) -> bool {
match &node.kind {
NodeKind::Unreachable => true,
NodeKind::Call { callee, .. } => matches!(
&callee.kind,
NodeKind::Identifier { name }
if matches!(name.name.as_str(), "todo" | "unreachable")
),
_ => false,
}
}
fn body_contains_propagate(node: &AIRNode) -> bool {
if matches!(node.kind, NodeKind::Propagate { .. }) {
return true;
}
if matches!(
node.kind,
NodeKind::FnDecl { .. } | NodeKind::Lambda { .. } | NodeKind::ClassDecl { .. }
) {
return false;
}
child_nodes(node).iter().any(|c| body_contains_propagate(c))
}
fn unwrap_block_tail(node: &AIRNode) -> &AIRNode {
if let NodeKind::Block {
stmts,
tail: Some(t),
} = &node.kind
{
if stmts.is_empty() {
return t;
}
}
node
}
fn control_flow_has_raise_branch(node: &AIRNode) -> bool {
match &node.kind {
NodeKind::If {
then_block,
else_block,
..
} => {
is_raise_expr(unwrap_block_tail(then_block))
|| control_flow_has_raise_branch(then_block)
|| else_block.as_ref().is_some_and(|eb| {
is_raise_expr(unwrap_block_tail(eb)) || control_flow_has_raise_branch(eb)
})
}
NodeKind::Match { arms, .. } => arms.iter().any(|arm| {
if let NodeKind::MatchArm { body, .. } = &arm.kind {
is_raise_expr(unwrap_block_tail(body)) || control_flow_has_raise_branch(body)
} else {
false
}
}),
_ => false,
}
}
fn if_value_needs_stmt_form(node: &AIRNode) -> bool {
let NodeKind::If {
then_block,
else_block,
..
} = &node.kind
else {
return false;
};
block_has_droppable_stmts(then_block)
|| else_block.as_deref().is_some_and(|eb| {
if matches!(eb.kind, NodeKind::If { .. }) {
if_value_needs_stmt_form(eb)
} else {
block_has_droppable_stmts(eb)
}
})
}
fn block_has_droppable_stmts(node: &AIRNode) -> bool {
matches!(&node.kind, NodeKind::Block { stmts, .. } if !stmts.is_empty())
}
fn match_value_needs_stmt_form(arms: &[AIRNode]) -> bool {
crate::generator::match_needs_ifchain(arms)
|| arms.iter().any(|arm| {
matches!(
&arm.kind,
NodeKind::MatchArm { pattern, .. }
if matches!(pattern.kind, NodeKind::RecordPat { .. })
)
})
|| arms
.iter()
.any(|arm| matches!(&arm.kind, NodeKind::MatchArm { pattern, .. } if arm_constructor_binds_payload(pattern)))
|| match_arm_drops_leading_stmts(arms)
}
fn arm_constructor_binds_payload(pattern: &AIRNode) -> bool {
let NodeKind::ConstructorPat { path, fields } = &pattern.kind else {
return false;
};
let leaf = path.segments.last().map_or("", |s| s.name.as_str());
if matches!(leaf, "Some" | "None" | "Ok" | "Err") {
return false;
}
fields
.iter()
.any(|f| !matches!(f.kind, NodeKind::WildcardPat))
}
fn match_arm_drops_leading_stmts(arms: &[AIRNode]) -> bool {
arms.iter().any(|arm| {
let NodeKind::MatchArm { body, .. } = &arm.kind else {
return false;
};
let NodeKind::Block {
stmts,
tail: Some(_),
} = &body.kind
else {
return false;
};
stmts.iter().any(stmt_not_lambda_expressible)
})
}
fn stmt_not_lambda_expressible(stmt: &AIRNode) -> bool {
match &stmt.kind {
NodeKind::LetBinding {
is_mut, pattern, ..
} => *is_mut || PyEmitCtx::simple_bind_name(pattern).is_none(),
NodeKind::Assign { .. }
| NodeKind::While { .. }
| NodeKind::For { .. }
| NodeKind::Loop { .. }
| NodeKind::Return { .. }
| NodeKind::Break { .. }
| NodeKind::Continue
| NodeKind::Block { .. } => true,
_ => false,
}
}
fn implicit_imports_for(
module: &AIRModule,
public_symbols: &HashMap<String, String>,
own_path: &str,
) -> Vec<(String, String)> {
let local = locally_declared_names(module);
let explicit = explicitly_imported_names(module);
let rendered = format!("{module:?}");
let field_labels = field_label_occurrences(module);
let mut out: Vec<(String, String)> = Vec::new();
for (name, declaring_module) in public_symbols {
if declaring_module == own_path || local.contains(name) || explicit.contains(name) {
continue;
}
let total = quoted_token_count(&rendered, name);
let labels = field_labels.get(name).copied().unwrap_or(0);
if total > labels {
out.push((declaring_module.clone(), name.clone()));
}
}
out
}
const RUNTIME_MODULE_PY: &str = "_bock_runtime";
fn ordering_singleton_py(variant: &str) -> &'static str {
match variant {
"Less" => "_bock_less",
"Equal" => "_bock_equal",
_ => "_bock_greater",
}
}
fn ordering_class_py(variant: &str) -> &'static str {
match variant {
"Less" => "_BockOrderingLess",
"Equal" => "_BockOrderingEqual",
_ => "_BockOrderingGreater",
}
}
const CONCURRENCY_RUNTIME_PY: &str = "\
# ── Bock concurrency runtime ──
import asyncio as __bock_asyncio
class __BockChannel:
__slots__ = ('_q',)
def __init__(self):
self._q = __bock_asyncio.Queue()
def send(self, v):
self._q.put_nowait(v)
async def recv(self):
return await self._q.get()
def close(self):
pass
def __bock_channel_new():
ch = __BockChannel()
return (ch, ch)
def __bock_spawn(x):
# If already a coroutine, wrap it in a Task so it starts eagerly.
if __bock_asyncio.iscoroutine(x):
return __bock_asyncio.create_task(x)
return x
";
#[derive(Debug)]
pub struct PyGenerator {
profile: TargetProfile,
}
impl PyGenerator {
#[must_use]
pub fn new() -> Self {
Self {
profile: TargetProfile::python(),
}
}
}
impl Default for PyGenerator {
fn default() -> Self {
Self::new()
}
}
impl CodeGenerator for PyGenerator {
fn target(&self) -> &TargetProfile {
&self.profile
}
fn generate_module(&self, module: &AIRModule) -> Result<GeneratedCode, CodegenError> {
let module =
&crate::generator::hoist_value_cf(crate::generator::lower_blanket_into(module.clone()));
let mut ctx = PyEmitCtx::new();
ctx.enum_variants =
crate::generator::collect_enum_variants(&[(module, std::path::Path::new(""))]);
ctx.trait_decls =
crate::generator::collect_trait_decls(&[(module, std::path::Path::new(""))]);
ctx.const_names =
crate::generator::collect_const_names(&[(module, std::path::Path::new(""))]);
ctx.emit_node(module)?;
let content = ctx.finish();
let source_map = SourceMap {
generated_file: String::new(),
..Default::default()
};
Ok(GeneratedCode {
files: vec![OutputFile {
path: PathBuf::new(),
content,
source_map: Some(source_map),
}],
})
}
fn entry_invocation(&self, main_is_async: bool) -> Option<String> {
if main_is_async {
Some("if __name__ == \"__main__\":\n asyncio.run(main())\n".to_string())
} else {
Some("if __name__ == \"__main__\":\n main()\n".to_string())
}
}
fn generate_project(
&self,
modules: &[(&AIRModule, &std::path::Path)],
) -> Result<GeneratedCode, CodegenError> {
let hoisted: Vec<(AIRModule, &std::path::Path)> = modules
.iter()
.map(|(m, p)| {
(
crate::generator::hoist_value_cf(crate::generator::lower_blanket_into(
(*m).clone(),
)),
*p,
)
})
.collect();
let modules: Vec<(&AIRModule, &std::path::Path)> =
hoisted.iter().map(|(m, p)| (m, *p)).collect();
let modules = modules.as_slice();
let reachable = crate::generator::reachable_modules(modules);
let modules = reachable.as_slice();
if modules.is_empty() {
return Ok(GeneratedCode { files: vec![] });
}
let entry_idx = modules
.iter()
.position(|(m, _)| crate::generator::module_declares_main_fn(m))
.unwrap_or(modules.len() - 1);
let enum_variants = crate::generator::collect_enum_variants(modules);
let trait_decls = crate::generator::collect_trait_decls(modules);
let const_names = crate::generator::collect_const_names(modules);
let public_symbols = collect_public_symbol_modules(modules);
let mut field_method_collisions = std::collections::HashSet::new();
for (module, _) in modules {
field_method_collisions.extend(crate::generator::collect_record_field_names(
module,
to_snake_case,
));
}
let main_is_async = modules
.iter()
.any(|(m, _)| crate::generator::module_main_fn_is_async(m));
let invocation = self.entry_invocation(main_is_async);
let mut files: Vec<OutputFile> = Vec::with_capacity(modules.len() + 1);
let mut runtime_optional = false;
let mut runtime_result = false;
let mut runtime_ordering = false;
let mut runtime_concurrency = false;
let mut runtime_list_functional = false;
let mut runtime_list_mutators = false;
let mut runtime_propagate = false;
let mut runtime_str = false;
for (i, (module, source_path)) in modules.iter().enumerate() {
let mut ctx = PyEmitCtx::new();
ctx.per_module = true;
ctx.is_entry_module =
i == entry_idx && crate::generator::module_declares_main_fn(module);
ctx.enum_variants = enum_variants.clone();
ctx.trait_decls = trait_decls.clone();
ctx.const_names = const_names.clone();
ctx.field_method_collisions = field_method_collisions.clone();
ctx.seed_effect_registries(modules);
ctx.implicit_imports =
implicit_imports_for(module, &public_symbols, &module_path_string_of(module));
ctx.emit_node(module)?;
runtime_optional |= ctx.needs_runtime_optional;
runtime_result |= ctx.needs_runtime_result;
runtime_ordering |= ctx.needs_runtime_ordering;
runtime_concurrency |= ctx.needs_runtime_concurrency;
runtime_list_functional |= ctx.needs_runtime_list_functional;
runtime_list_mutators |= ctx.needs_runtime_list_mutators;
runtime_propagate |= ctx.needs_runtime_propagate;
runtime_str |= ctx.needs_runtime_str;
let mut content = ctx.finish();
if i == entry_idx && crate::generator::module_declares_main_fn(module) {
if let Some(invoc) = invocation.as_ref() {
if !content.is_empty() && !content.ends_with('\n') {
content.push('\n');
}
content.push_str(invoc);
}
}
let out_path = self.module_output_path(module, source_path, i == entry_idx);
let generated_file = out_path
.file_name()
.and_then(|s| s.to_str())
.unwrap_or("")
.to_string();
let source_map = SourceMap {
generated_file,
..Default::default()
};
files.push(OutputFile {
path: out_path,
content,
source_map: Some(source_map),
});
}
if runtime_optional
|| runtime_result
|| runtime_ordering
|| runtime_concurrency
|| runtime_list_functional
|| runtime_list_mutators
|| runtime_propagate
|| runtime_str
{
let mut content = String::new();
let mut all_names: Vec<&str> = Vec::new();
if runtime_optional {
content.push_str(OPTIONAL_RUNTIME_PY);
content.push('\n');
all_names.extend(["_BockSome", "_BockNone", "_bock_none"]);
}
if runtime_result {
content.push_str(RESULT_RUNTIME_PY);
content.push('\n');
all_names.extend([
"_BockOk",
"_BockErr",
"_bock_parse_int",
"_bock_parse_float",
]);
}
if runtime_ordering {
content.push_str(ORDERING_RUNTIME_PY);
content.push('\n');
all_names.extend([
"_BockOrderingLess",
"_BockOrderingEqual",
"_BockOrderingGreater",
"_bock_less",
"_bock_equal",
"_bock_greater",
"_bock_compare",
]);
}
if runtime_concurrency {
content.push_str(CONCURRENCY_RUNTIME_PY);
content.push('\n');
all_names.extend(["__BockChannel", "__bock_channel_new", "__bock_spawn"]);
}
if runtime_list_functional {
content.push_str(LIST_FUNCTIONAL_RUNTIME_PY);
content.push('\n');
all_names.extend(["_bock_reduce", "_bock_fold", "_bock_find", "_bock_for_each"]);
}
if runtime_list_mutators {
content.push_str(LIST_MUTATOR_RUNTIME_PY);
content.push('\n');
all_names.push("_bock_list_abort");
}
if runtime_propagate {
content.push_str(PROPAGATE_RUNTIME_PY);
content.push('\n');
all_names.extend(["_BockPropagate", "_bock_try"]);
}
if runtime_str {
content.push_str(STR_RUNTIME_PY);
content.push('\n');
all_names.push("_bock_str");
}
let all_list = all_names
.iter()
.map(|n| format!("\"{n}\""))
.collect::<Vec<_>>()
.join(", ");
content.push_str(&format!("__all__ = [{all_list}]\n"));
files.push(OutputFile {
path: PathBuf::from(format!("{RUNTIME_MODULE_PY}.py")),
content,
source_map: Some(SourceMap {
generated_file: format!("{RUNTIME_MODULE_PY}.py"),
..Default::default()
}),
});
}
Ok(GeneratedCode { files })
}
fn generate_tests(
&self,
modules: &[(&AIRModule, &std::path::Path)],
framework: &str,
) -> Result<crate::generator::TestArtifacts, CodegenError> {
let reachable = crate::generator::reachable_modules(modules);
let modules = reachable.as_slice();
let tests = crate::generator::collect_test_fns(modules);
if tests.is_empty() {
return Ok(crate::generator::TestArtifacts::default());
}
let entry_idx = modules
.iter()
.position(|(m, _)| crate::generator::module_declares_main_fn(m))
.unwrap_or(modules.len().saturating_sub(1));
let enum_variants = crate::generator::collect_enum_variants(modules);
let trait_decls = crate::generator::collect_trait_decls(modules);
let const_names = crate::generator::collect_const_names(modules);
let mut field_method_collisions = std::collections::HashSet::new();
for (module, _) in modules {
field_method_collisions.extend(crate::generator::collect_record_field_names(
module,
to_snake_case,
));
}
let mut ctx = PyEmitCtx::new();
ctx.per_module = true;
ctx.enum_variants = enum_variants;
ctx.trait_decls = trait_decls;
ctx.const_names = const_names;
ctx.field_method_collisions = field_method_collisions;
ctx.seed_effect_registries(modules);
let mut import_lines: Vec<String> = Vec::new();
for (i, (module, _)) in modules.iter().enumerate() {
let mut import_names: Vec<String> = crate::generator::exportable_value_names(module)
.into_iter()
.filter(|e| e.is_fn)
.map(|e| to_snake_case(&e.name))
.collect();
import_names.extend(crate::generator::enum_variant_value_names(module));
if import_names.is_empty() {
continue;
}
let module_import = if i == entry_idx {
"main".to_string()
} else {
crate::generator::module_path_string(module).unwrap_or_else(|| "main".to_string())
};
import_lines.push(format!(
"from {module_import} import {}",
import_names.join(", ")
));
}
import_lines.sort_unstable();
import_lines.dedup();
let mut runtime_imports: std::collections::BTreeSet<&str> =
std::collections::BTreeSet::new();
for (test_fn, _) in &tests {
if let NodeKind::FnDecl { body, .. } = &test_fn.kind {
collect_runtime_tag_imports(body, &mut runtime_imports);
}
}
let is_unittest = framework == "unittest";
let mut out = String::new();
if is_unittest {
out.push_str("import unittest\n");
}
if !runtime_imports.is_empty() {
let names: Vec<&str> = runtime_imports.iter().copied().collect();
out.push_str(&format!("from _bock_runtime import {}\n", names.join(", ")));
}
for line in &import_lines {
out.push_str(line);
out.push('\n');
}
out.push_str("\n\n");
if is_unittest {
out.push_str("class TestBock(unittest.TestCase):\n");
for (i, (test_fn, _module_path)) in tests.iter().enumerate() {
let NodeKind::FnDecl { name, body, .. } = &test_fn.kind else {
continue;
};
if i > 0 {
out.push('\n');
}
out.push_str(&format!(" def {}(self):\n", to_snake_case(&name.name)));
ctx.emit_py_test_body(body, true, 2, &mut out)?;
}
out.push_str("\n\nif __name__ == \"__main__\":\n unittest.main()\n");
} else {
for (i, (test_fn, _module_path)) in tests.iter().enumerate() {
let NodeKind::FnDecl { name, body, .. } = &test_fn.kind else {
continue;
};
if i > 0 {
out.push_str("\n\n");
}
out.push_str(&format!("def {}():\n", to_snake_case(&name.name)));
ctx.emit_py_test_body(body, false, 1, &mut out)?;
}
}
Ok(crate::generator::TestArtifacts {
files: vec![OutputFile {
path: PathBuf::from("test_bock.py"),
content: out,
source_map: None,
}],
entry_append: None,
})
}
}
impl PyGenerator {
fn module_output_path(
&self,
module: &AIRModule,
source_path: &std::path::Path,
is_entry: bool,
) -> PathBuf {
if is_entry {
return crate::generator::derive_output_path(source_path, self.target());
}
match crate::generator::module_path_string(module) {
Some(path) if !path.is_empty() => {
let rel: PathBuf = path.split('.').collect();
rel.with_extension(&self.target().conventions.file_extension)
}
_ => crate::generator::derive_output_path(source_path, self.target()),
}
}
}
#[derive(Default)]
struct ShadowScope {
bound: std::collections::HashSet<String>,
renames: HashMap<String, String>,
}
struct PyEmitCtx {
buf: String,
indent: usize,
needs_dataclass_import: bool,
needs_abc_import: bool,
needs_asyncio_import: bool,
needs_time_import: bool,
needs_math_import: bool,
task_bound_names: std::collections::HashSet<String>,
effect_ops: HashMap<String, String>,
current_handler_vars: HashMap<String, String>,
fn_effects: HashMap<String, Vec<String>>,
composite_effects: HashMap<String, Vec<String>>,
handling_counter: usize,
impls_by_target: HashMap<String, Vec<AIRNode>>,
optional_runtime_emitted: bool,
result_runtime_emitted: bool,
ordering_runtime_emitted: bool,
concurrency_runtime_emitted: bool,
list_functional_runtime_emitted: bool,
list_mutator_runtime_emitted: bool,
propagate_runtime_emitted: bool,
str_runtime_emitted: bool,
needs_union_import: bool,
needs_typing_callable: Cell<bool>,
needs_typing_any: Cell<bool>,
needs_typing_self: Cell<bool>,
needs_typing_never: Cell<bool>,
needs_typing_typevar: Cell<bool>,
emitted_typevars: std::collections::HashSet<String>,
enum_variants: crate::generator::EnumVariantRegistry,
trait_decls: crate::generator::TraitDeclRegistry,
per_module: bool,
needs_runtime_optional: bool,
needs_runtime_result: bool,
needs_runtime_ordering: bool,
needs_runtime_concurrency: bool,
needs_runtime_list_functional: bool,
needs_runtime_list_mutators: bool,
needs_runtime_propagate: bool,
needs_runtime_str: bool,
implicit_imports: Vec<(String, String)>,
field_method_collisions: std::collections::HashSet<String>,
is_entry_module: bool,
loop_value_targets: Vec<Option<String>>,
const_names: std::collections::HashSet<String>,
shadow_scopes: Vec<ShadowScope>,
shadow_counter: usize,
pending_scope_seed: Vec<String>,
in_loop_body_tail: bool,
in_stmt_construct_arm: bool,
}
impl PyEmitCtx {
fn new() -> Self {
Self {
buf: String::with_capacity(4096),
indent: 0,
needs_dataclass_import: false,
needs_abc_import: false,
needs_asyncio_import: false,
needs_time_import: false,
needs_math_import: false,
task_bound_names: std::collections::HashSet::new(),
effect_ops: HashMap::new(),
current_handler_vars: HashMap::new(),
fn_effects: HashMap::new(),
composite_effects: HashMap::new(),
handling_counter: 0,
impls_by_target: HashMap::new(),
optional_runtime_emitted: false,
result_runtime_emitted: false,
ordering_runtime_emitted: false,
concurrency_runtime_emitted: false,
list_functional_runtime_emitted: false,
list_mutator_runtime_emitted: false,
propagate_runtime_emitted: false,
str_runtime_emitted: false,
needs_union_import: false,
needs_typing_callable: Cell::new(false),
needs_typing_any: Cell::new(false),
needs_typing_self: Cell::new(false),
needs_typing_never: Cell::new(false),
needs_typing_typevar: Cell::new(false),
emitted_typevars: std::collections::HashSet::new(),
enum_variants: crate::generator::EnumVariantRegistry::new(),
trait_decls: crate::generator::TraitDeclRegistry::new(),
per_module: false,
needs_runtime_optional: false,
needs_runtime_result: false,
needs_runtime_ordering: false,
needs_runtime_concurrency: false,
needs_runtime_list_functional: false,
needs_runtime_list_mutators: false,
needs_runtime_propagate: false,
needs_runtime_str: false,
implicit_imports: Vec::new(),
field_method_collisions: std::collections::HashSet::new(),
const_names: std::collections::HashSet::new(),
is_entry_module: false,
loop_value_targets: Vec::new(),
shadow_scopes: Vec::new(),
shadow_counter: 0,
pending_scope_seed: Vec::new(),
in_loop_body_tail: false,
in_stmt_construct_arm: false,
}
}
fn py_method_name(&self, name: &str) -> String {
let snake = to_snake_case(name);
let escaped =
if crate::generator::is_target_keyword(&snake, crate::generator::KeywordTarget::Python)
{
format!("{snake}_")
} else {
snake
};
crate::generator::disambiguate_method_name(
escaped,
&self.field_method_collisions,
"_method",
)
}
fn finish(mut self) -> String {
if self.buf.is_empty() {
return self.buf;
}
let mut preamble = String::new();
preamble.push_str("from __future__ import annotations\n");
if self.is_entry_module {
preamble.push_str(
"import sys as _sys\n\
if hasattr(_sys.stdout, \"reconfigure\"):\n \
_sys.stdout.reconfigure(encoding=\"utf-8\")\n\
if hasattr(_sys.stderr, \"reconfigure\"):\n \
_sys.stderr.reconfigure(encoding=\"utf-8\")\n",
);
}
if self.per_module
&& (self.needs_runtime_optional
|| self.needs_runtime_result
|| self.needs_runtime_ordering
|| self.needs_runtime_concurrency
|| self.needs_runtime_list_functional
|| self.needs_runtime_list_mutators
|| self.needs_runtime_propagate
|| self.needs_runtime_str)
{
let _ = writeln!(preamble, "from {RUNTIME_MODULE_PY} import *");
}
if self.needs_asyncio_import {
preamble.push_str("import asyncio\n");
}
if self.needs_time_import {
preamble.push_str("import time\n");
}
if self.needs_math_import {
preamble.push_str("import math\n");
}
let mut typing_names: Vec<&str> = Vec::new();
if self.needs_union_import {
typing_names.push("Union");
}
if self.needs_typing_callable.get() {
typing_names.push("Callable");
}
if self.needs_typing_any.get() {
typing_names.push("Any");
}
if self.needs_typing_self.get() {
typing_names.push("Self");
}
if self.needs_typing_never.get() {
typing_names.push("Never");
}
if self.needs_typing_typevar.get() {
typing_names.push("TypeVar");
typing_names.push("Generic");
}
if !typing_names.is_empty() {
typing_names.sort_unstable();
typing_names.dedup();
preamble.push_str(&format!("from typing import {}\n", typing_names.join(", ")));
}
if self.needs_abc_import {
preamble.push_str("from abc import ABC, abstractmethod\n");
}
if self.needs_dataclass_import {
preamble.push_str("from dataclasses import dataclass\n");
}
if !preamble.is_empty() {
preamble.push('\n');
self.buf.insert_str(0, &preamble);
}
self.buf
}
fn emit_py_test_body(
&mut self,
body: &AIRNode,
use_self: bool,
indent: usize,
out: &mut String,
) -> Result<(), CodegenError> {
let pad = " ".repeat(indent);
let stmts: Vec<&AIRNode> = match &body.kind {
NodeKind::Block { stmts, tail } => stmts.iter().chain(tail.as_deref()).collect(),
_ => vec![body],
};
let mut emitted_any = false;
for stmt in stmts {
emitted_any = true;
if let Some((assertion, actual, expected)) = crate::generator::classify_assertion(stmt)
{
let a = self.expr_to_string(actual)?;
use crate::generator::TestAssertion as T;
let line = if use_self {
match assertion {
T::Equal => {
let e = match expected {
Some(e) => self.expr_to_string(e)?,
None => "None".to_string(),
};
format!("self.assertEqual({a}, {e})")
}
T::BeTrue => format!("self.assertTrue({a})"),
T::BeFalse => format!("self.assertFalse({a})"),
T::BeSome => format!("self.assertIsInstance({a}, _BockSome)"),
T::BeNone => format!("self.assertIsInstance({a}, _BockNone)"),
T::BeOk => format!("self.assertIsInstance({a}, _BockOk)"),
T::BeErr => format!("self.assertIsInstance({a}, _BockErr)"),
}
} else {
match assertion {
T::Equal => {
let e = match expected {
Some(e) => self.expr_to_string(e)?,
None => "None".to_string(),
};
format!("assert ({a}) == ({e})")
}
T::BeTrue => format!("assert ({a}) is True"),
T::BeFalse => format!("assert ({a}) is False"),
T::BeSome => format!("assert isinstance({a}, _BockSome)"),
T::BeNone => format!("assert isinstance({a}, _BockNone)"),
T::BeOk => format!("assert isinstance({a}, _BockOk)"),
T::BeErr => format!("assert isinstance({a}, _BockErr)"),
}
};
out.push_str(&format!("{pad}{line}\n"));
} else if let NodeKind::LetBinding { pattern, value, .. } = &stmt.kind {
let name = match &pattern.kind {
NodeKind::BindPat { name, .. } => to_snake_case(&name.name),
_ => {
emitted_any = false;
continue;
}
};
let v = self.expr_to_string(value)?;
out.push_str(&format!("{pad}{name} = {v}\n"));
} else {
let s = self.expr_to_string(stmt)?;
out.push_str(&format!("{pad}{s}\n"));
}
}
if !emitted_any {
out.push_str(&format!("{pad}pass\n"));
}
Ok(())
}
fn seed_effect_registries(&mut self, modules: &[(&AIRModule, &std::path::Path)]) {
for (module, _) in modules {
let NodeKind::Module { items, .. } = &module.kind else {
continue;
};
for item in items {
let NodeKind::EffectDecl {
name,
components,
operations,
..
} = &item.kind
else {
continue;
};
if !components.is_empty() {
let comp_names: Vec<String> = components
.iter()
.map(|tp| {
tp.segments
.last()
.map_or("effect".to_string(), |s| s.name.clone())
})
.collect();
self.composite_effects.insert(name.name.clone(), comp_names);
continue;
}
for op in operations {
if let NodeKind::FnDecl { name: op_name, .. } = &op.kind {
self.effect_ops
.insert(op_name.name.clone(), name.name.clone());
}
}
}
}
}
fn user_variant_for_path(
&self,
path: &bock_ast::TypePath,
) -> Option<&crate::generator::EnumVariantInfo> {
let info = crate::generator::registered_variant(&self.enum_variants, path)?;
if matches!(info.enum_name.as_str(), "Optional" | "Result") {
return None;
}
Some(info)
}
fn user_variant_for_name(&self, name: &str) -> Option<&crate::generator::EnumVariantInfo> {
let info = self.enum_variants.get(name)?;
if matches!(info.enum_name.as_str(), "Optional" | "Result") {
return None;
}
Some(info)
}
fn indent_str(&self) -> String {
" ".repeat(self.indent)
}
fn write_indent(&mut self) {
let indent = self.indent_str();
self.buf.push_str(&indent);
}
fn writeln(&mut self, s: &str) {
self.write_indent();
self.buf.push_str(s);
self.buf.push('\n');
}
fn expr_to_string(&mut self, node: &AIRNode) -> Result<String, CodegenError> {
let start = self.buf.len();
self.emit_expr(node)?;
let s = self.buf[start..].to_string();
self.buf.truncate(start);
Ok(s)
}
fn pattern_to_py(&mut self, pat: &AIRNode) -> Result<String, CodegenError> {
let start = self.buf.len();
self.emit_pattern(pat)?;
let s = self.buf[start..].to_string();
self.buf.truncate(start);
Ok(s)
}
fn map_prelude_call(
&mut self,
callee: &AIRNode,
args: &[bock_air::AirArg],
) -> Result<Option<String>, CodegenError> {
let name = match &callee.kind {
NodeKind::Identifier { name } => name.name.as_str(),
_ => return Ok(None),
};
let arg_strs: Vec<String> = args
.iter()
.map(|a| self.expr_to_string(&a.value))
.collect::<Result<_, _>>()?;
let code = match name {
"println" => {
let a = arg_strs.first().map_or(String::new(), |s| s.clone());
format!("print({a})")
}
"print" => {
let a = arg_strs.first().map_or(String::new(), |s| s.clone());
format!("print({a}, end=\"\")")
}
"debug" => {
let a = arg_strs.first().map_or(String::new(), |s| s.clone());
format!("print(repr({a}))")
}
"assert" => {
let a = arg_strs.first().map_or(String::new(), |s| s.clone());
format!("assert {a}")
}
"todo" => "raise NotImplementedError()".to_string(),
"unreachable" => "raise RuntimeError(\"unreachable\")".to_string(),
"Some" => {
let a = arg_strs.first().map_or(String::new(), |s| s.clone());
format!("_BockSome({a})")
}
"Ok" => {
let a = arg_strs.first().map_or(String::new(), |s| s.clone());
format!("_BockOk({a})")
}
"Err" => {
let a = arg_strs.first().map_or(String::new(), |s| s.clone());
format!("_BockErr({a})")
}
"sleep" => {
let a = arg_strs.first().map_or(String::new(), |s| s.clone());
if let Some(handler) = self.clock_handler_var() {
format!("{handler}.{}({a})", to_snake_case("sleep"))
} else {
self.needs_asyncio_import = true;
format!("asyncio.sleep(({a}) / 1_000_000_000)")
}
}
_ => return Ok(None),
};
Ok(Some(code))
}
fn try_emit_container_method(
&mut self,
node: &AIRNode,
callee: &AIRNode,
args: &[bock_air::AirArg],
) -> Result<bool, CodegenError> {
if let Some((recv, method, rest)) =
crate::generator::desugared_optional_method(node, callee, args)
{
self.emit_tagged_container_method(recv, method, rest, "_BockSome", "_BockSome")?;
return Ok(true);
}
if let Some((recv, method, rest)) =
crate::generator::desugared_result_method(node, callee, args)
{
self.emit_tagged_container_method(recv, method, rest, "_BockOk", "_BockErr")?;
return Ok(true);
}
Ok(false)
}
fn emit_tagged_container_method(
&mut self,
recv: &AIRNode,
method: &str,
rest: &[bock_air::AirArg],
present_cls: &str,
err_cls: &str,
) -> Result<(), CodegenError> {
match method {
"is_some" | "is_ok" => {
self.buf.push_str("isinstance(");
self.emit_expr(recv)?;
let _ = write!(self.buf, ", {present_cls})");
return Ok(());
}
"is_none" | "is_err" => {
self.buf.push_str("(not isinstance(");
self.emit_expr(recv)?;
let _ = write!(self.buf, ", {present_cls}))");
return Ok(());
}
_ => {}
}
self.buf.push_str("(lambda __c: ");
match method {
"unwrap" => {
let _ = write!(
self.buf,
"__c._0 if isinstance(__c, {present_cls}) else None"
);
}
"unwrap_or" => {
let _ = write!(self.buf, "__c._0 if isinstance(__c, {present_cls}) else (");
if let Some(d) = rest.first() {
self.emit_expr(&d.value)?;
} else {
self.buf.push_str("None");
}
self.buf.push(')');
}
"map" => {
let _ = write!(self.buf, "{present_cls}((");
if let Some(f) = rest.first() {
self.emit_expr(&f.value)?;
} else {
self.buf.push_str("lambda x: x");
}
let _ = write!(
self.buf,
")(__c._0)) if isinstance(__c, {present_cls}) else __c"
);
}
"flat_map" => {
let _ = write!(self.buf, "(");
if let Some(f) = rest.first() {
self.emit_expr(&f.value)?;
} else {
self.buf.push_str("lambda x: x");
}
let _ = write!(
self.buf,
")(__c._0) if isinstance(__c, {present_cls}) else __c"
);
}
"map_err" => {
let _ = write!(self.buf, "{err_cls}((");
if let Some(f) = rest.first() {
self.emit_expr(&f.value)?;
} else {
self.buf.push_str("lambda x: x");
}
let _ = write!(
self.buf,
")(__c._0)) if isinstance(__c, {err_cls}) else __c"
);
}
_ => self.buf.push_str("None"),
}
self.buf.push_str(")(");
self.emit_expr(recv)?;
self.buf.push(')');
Ok(())
}
fn try_emit_list_method(
&mut self,
node: &AIRNode,
callee: &AIRNode,
args: &[bock_air::AirArg],
) -> Result<bool, CodegenError> {
let Some((recv, method, rest)) =
crate::generator::desugared_list_method(node, callee, args)
else {
return Ok(false);
};
match method {
"len" | "length" | "count" => {
self.buf.push_str("len(");
self.emit_expr(recv)?;
self.buf.push(')');
}
"is_empty" => {
self.buf.push_str("(len(");
self.emit_expr(recv)?;
self.buf.push_str(") == 0)");
}
"get" => {
let Some(idx) = rest.first() else {
return Ok(false);
};
self.buf
.push_str("(lambda __r, __i: _BockSome(__r[__i]) if 0 <= __i < len(__r) else _bock_none)(");
self.emit_expr(recv)?;
self.buf.push_str(", ");
self.emit_expr(&idx.value)?;
self.buf.push(')');
}
"first" => {
self.buf
.push_str("(lambda __r: _BockSome(__r[0]) if len(__r) > 0 else _bock_none)(");
self.emit_expr(recv)?;
self.buf.push(')');
}
"last" => {
self.buf
.push_str("(lambda __r: _BockSome(__r[-1]) if len(__r) > 0 else _bock_none)(");
self.emit_expr(recv)?;
self.buf.push(')');
}
"contains" => {
let Some(x) = rest.first() else {
return Ok(false);
};
self.buf.push('(');
self.emit_expr(&x.value)?;
self.buf.push_str(" in ");
self.emit_expr(recv)?;
self.buf.push(')');
}
"index_of" => {
let Some(x) = rest.first() else {
return Ok(false);
};
self.buf.push_str(
"(lambda __r, __x: _BockSome(__r.index(__x)) if __x in __r else _bock_none)(",
);
self.emit_expr(recv)?;
self.buf.push_str(", ");
self.emit_expr(&x.value)?;
self.buf.push(')');
}
"concat" => {
let Some(o) = rest.first() else {
return Ok(false);
};
self.buf.push('(');
self.emit_expr(recv)?;
self.buf.push_str(" + ");
self.emit_expr(&o.value)?;
self.buf.push(')');
}
"join" => {
let Some(sep) = rest.first() else {
return Ok(false);
};
self.buf.push('(');
self.emit_expr(&sep.value)?;
self.buf.push_str(").join(");
self.emit_expr(recv)?;
self.buf.push(')');
}
_ => return Ok(false),
}
Ok(true)
}
fn try_emit_list_mutating_method(
&mut self,
node: &AIRNode,
callee: &AIRNode,
args: &[bock_air::AirArg],
) -> Result<bool, CodegenError> {
let Some((recv, _method, rest)) =
crate::generator::desugared_list_mutating_method(node, callee, args)
else {
return Ok(false);
};
let Some(x) = rest.first() else {
return Ok(false);
};
self.buf.push('(');
self.emit_expr(recv)?;
self.buf.push_str(").append(");
self.emit_expr(&x.value)?;
self.buf.push(')');
Ok(true)
}
fn try_emit_list_inplace_mutator(
&mut self,
node: &AIRNode,
callee: &AIRNode,
args: &[bock_air::AirArg],
) -> Result<bool, CodegenError> {
let Some((recv, method, rest)) =
crate::generator::desugared_list_inplace_mutator(node, callee, args)
else {
return Ok(false);
};
match method {
"pop" => {
self.buf.push_str(
"(lambda __r: _BockSome(__r.pop()) if len(__r) > 0 else _bock_none)(",
);
self.emit_expr(recv)?;
self.buf.push(')');
}
"remove_at" => {
let Some(idx) = rest.first() else {
return Ok(false);
};
self.buf.push_str(
"(lambda __r, __i: __r.pop(__i) if 0 <= __i < len(__r) \
else _bock_list_abort('remove_at', __i, len(__r)))(",
);
self.emit_expr(recv)?;
self.buf.push_str(", ");
self.emit_expr(&idx.value)?;
self.buf.push(')');
}
"insert" => {
let (Some(idx), Some(x)) = (rest.first(), rest.get(1)) else {
return Ok(false);
};
self.buf.push_str(
"(lambda __r, __i, __x: __r.insert(__i, __x) if 0 <= __i <= len(__r) \
else _bock_list_abort('insert', __i, len(__r)))(",
);
self.emit_expr(recv)?;
self.buf.push_str(", ");
self.emit_expr(&idx.value)?;
self.buf.push_str(", ");
self.emit_expr(&x.value)?;
self.buf.push(')');
}
"reverse" => {
self.buf.push('(');
self.emit_expr(recv)?;
self.buf.push_str(").reverse()");
}
"set" => {
let (Some(idx), Some(x)) = (rest.first(), rest.get(1)) else {
return Ok(false);
};
self.buf.push_str(
"(lambda __r, __i, __x: __r.__setitem__(__i, __x) if 0 <= __i < len(__r) \
else _bock_list_abort('set', __i, len(__r)))(",
);
self.emit_expr(recv)?;
self.buf.push_str(", ");
self.emit_expr(&idx.value)?;
self.buf.push_str(", ");
self.emit_expr(&x.value)?;
self.buf.push(')');
}
_ => return Ok(false),
}
Ok(true)
}
fn try_emit_list_functional_method(
&mut self,
node: &AIRNode,
callee: &AIRNode,
args: &[bock_air::AirArg],
) -> Result<bool, CodegenError> {
let Some((recv, method, rest)) =
crate::generator::desugared_list_functional_method(node, callee, args)
else {
return Ok(false);
};
match method {
"map" | "filter" => {
let Some(cb) = rest.first() else {
return Ok(false);
};
let _ = write!(self.buf, "list({method}(");
self.emit_expr(&cb.value)?;
self.buf.push_str(", ");
self.emit_expr(recv)?;
self.buf.push_str("))");
}
"any" | "all" => {
let Some(cb) = rest.first() else {
return Ok(false);
};
let _ = write!(self.buf, "{method}(map(");
self.emit_expr(&cb.value)?;
self.buf.push_str(", ");
self.emit_expr(recv)?;
self.buf.push_str("))");
}
"flat_map" => {
let Some(cb) = rest.first() else {
return Ok(false);
};
self.buf.push_str("[__y for __x in ");
self.emit_expr(recv)?;
self.buf.push_str(" for __y in (");
self.emit_expr(&cb.value)?;
self.buf.push_str(")(__x)]");
}
"reduce" => {
let Some(cb) = rest.first() else {
return Ok(false);
};
self.buf.push_str("_bock_reduce(");
self.emit_expr(recv)?;
self.buf.push_str(", ");
self.emit_expr(&cb.value)?;
self.buf.push(')');
}
"fold" => {
let (Some(init), Some(cb)) = (rest.first(), rest.get(1)) else {
return Ok(false);
};
self.buf.push_str("_bock_fold(");
self.emit_expr(recv)?;
self.buf.push_str(", ");
self.emit_expr(&init.value)?;
self.buf.push_str(", ");
self.emit_expr(&cb.value)?;
self.buf.push(')');
}
"find" => {
let Some(cb) = rest.first() else {
return Ok(false);
};
self.buf.push_str("_bock_find(");
self.emit_expr(recv)?;
self.buf.push_str(", ");
self.emit_expr(&cb.value)?;
self.buf.push(')');
}
"for_each" => {
let Some(cb) = rest.first() else {
return Ok(false);
};
self.buf.push_str("_bock_for_each(");
self.emit_expr(recv)?;
self.buf.push_str(", ");
self.emit_expr(&cb.value)?;
self.buf.push(')');
}
_ => return Ok(false),
}
Ok(true)
}
fn try_emit_map_method(
&mut self,
node: &AIRNode,
callee: &AIRNode,
args: &[bock_air::AirArg],
) -> Result<bool, CodegenError> {
let Some((recv, method, rest)) = crate::generator::desugared_map_method(node, callee, args)
else {
return Ok(false);
};
match method {
"len" | "length" | "count" => {
self.buf.push_str("len(");
self.emit_expr(recv)?;
self.buf.push(')');
}
"is_empty" => {
self.buf.push_str("(len(");
self.emit_expr(recv)?;
self.buf.push_str(") == 0)");
}
"contains_key" => {
let Some(k) = rest.first() else {
return Ok(false);
};
self.buf.push('(');
self.emit_expr(&k.value)?;
self.buf.push_str(" in ");
self.emit_expr(recv)?;
self.buf.push(')');
}
"get" => {
let Some(k) = rest.first() else {
return Ok(false);
};
self.buf.push_str(
"(lambda __m, __k: _BockSome(__m[__k]) if __k in __m else _bock_none)(",
);
self.emit_expr(recv)?;
self.buf.push_str(", ");
self.emit_expr(&k.value)?;
self.buf.push(')');
}
"set" => {
let (Some(k), Some(v)) = (rest.first(), rest.get(1)) else {
return Ok(false);
};
self.buf
.push_str("(lambda __m, __k, __v: (__m.__setitem__(__k, __v), __m)[1])(");
self.emit_expr(recv)?;
self.buf.push_str(", ");
self.emit_expr(&k.value)?;
self.buf.push_str(", ");
self.emit_expr(&v.value)?;
self.buf.push(')');
}
"delete" => {
let Some(k) = rest.first() else {
return Ok(false);
};
self.buf
.push_str("(lambda __m, __k: (__m.pop(__k, None), __m)[1])(");
self.emit_expr(recv)?;
self.buf.push_str(", ");
self.emit_expr(&k.value)?;
self.buf.push(')');
}
"merge" => {
let Some(o) = rest.first() else {
return Ok(false);
};
self.buf
.push_str("(lambda __m, __o: (__m.update(__o), __m)[1])(");
self.emit_expr(recv)?;
self.buf.push_str(", ");
self.emit_expr(&o.value)?;
self.buf.push(')');
}
"filter" => {
let Some(f) = rest.first() else {
return Ok(false);
};
self.buf.push_str(
"(lambda __m, __f: {__k: __v for __k, __v in __m.items() if __f(__k, __v)})(",
);
self.emit_expr(recv)?;
self.buf.push_str(", ");
self.emit_expr(&f.value)?;
self.buf.push(')');
}
"keys" => {
self.buf.push_str("list(");
self.emit_expr(recv)?;
self.buf.push_str(".keys())");
}
"values" => {
self.buf.push_str("list(");
self.emit_expr(recv)?;
self.buf.push_str(".values())");
}
"entries" | "to_list" => {
self.buf.push_str("list(");
self.emit_expr(recv)?;
self.buf.push_str(".items())");
}
"for_each" => {
let Some(f) = rest.first() else {
return Ok(false);
};
self.buf.push_str("[(");
self.emit_expr(&f.value)?;
self.buf.push_str(")(__k, __v) for __k, __v in (");
self.emit_expr(recv)?;
self.buf.push_str(").items()]");
}
_ => return Ok(false),
}
Ok(true)
}
fn try_emit_set_method(
&mut self,
node: &AIRNode,
callee: &AIRNode,
args: &[bock_air::AirArg],
) -> Result<bool, CodegenError> {
let Some((recv, method, rest)) = crate::generator::desugared_set_method(node, callee, args)
else {
return Ok(false);
};
match method {
"len" | "length" | "count" => {
self.buf.push_str("len(");
self.emit_expr(recv)?;
self.buf.push(')');
}
"is_empty" => {
self.buf.push_str("(len(");
self.emit_expr(recv)?;
self.buf.push_str(") == 0)");
}
"contains" => {
let Some(x) = rest.first() else {
return Ok(false);
};
self.buf.push('(');
self.emit_expr(&x.value)?;
self.buf.push_str(" in ");
self.emit_expr(recv)?;
self.buf.push(')');
}
"add" => {
let Some(x) = rest.first() else {
return Ok(false);
};
self.buf
.push_str("(lambda __s, __x: (__s.add(__x), __s)[1])(");
self.emit_expr(recv)?;
self.buf.push_str(", ");
self.emit_expr(&x.value)?;
self.buf.push(')');
}
"remove" => {
let Some(x) = rest.first() else {
return Ok(false);
};
self.buf
.push_str("(lambda __s, __x: (__s.discard(__x), __s)[1])(");
self.emit_expr(recv)?;
self.buf.push_str(", ");
self.emit_expr(&x.value)?;
self.buf.push(')');
}
"union" => {
let Some(o) = rest.first() else {
return Ok(false);
};
self.buf.push('(');
self.emit_expr(recv)?;
self.buf.push_str(" | ");
self.emit_expr(&o.value)?;
self.buf.push(')');
}
"intersection" => {
let Some(o) = rest.first() else {
return Ok(false);
};
self.buf.push('(');
self.emit_expr(recv)?;
self.buf.push_str(" & ");
self.emit_expr(&o.value)?;
self.buf.push(')');
}
"difference" => {
let Some(o) = rest.first() else {
return Ok(false);
};
self.buf.push('(');
self.emit_expr(recv)?;
self.buf.push_str(" - ");
self.emit_expr(&o.value)?;
self.buf.push(')');
}
"is_subset" => {
let Some(o) = rest.first() else {
return Ok(false);
};
self.buf.push('(');
self.emit_expr(recv)?;
self.buf.push_str(" <= ");
self.emit_expr(&o.value)?;
self.buf.push(')');
}
"is_superset" => {
let Some(o) = rest.first() else {
return Ok(false);
};
self.buf.push('(');
self.emit_expr(recv)?;
self.buf.push_str(" >= ");
self.emit_expr(&o.value)?;
self.buf.push(')');
}
"filter" => {
let Some(f) = rest.first() else {
return Ok(false);
};
self.buf.push_str("{__x for __x in (");
self.emit_expr(recv)?;
self.buf.push_str(") if (");
self.emit_expr(&f.value)?;
self.buf.push_str(")(__x)}");
}
"map" => {
let Some(f) = rest.first() else {
return Ok(false);
};
self.buf.push_str("{(");
self.emit_expr(&f.value)?;
self.buf.push_str(")(__x) for __x in (");
self.emit_expr(recv)?;
self.buf.push_str(")}");
}
"to_list" => {
self.buf.push_str("list(");
self.emit_expr(recv)?;
self.buf.push(')');
}
"for_each" => {
let Some(f) = rest.first() else {
return Ok(false);
};
self.buf.push_str("[(");
self.emit_expr(&f.value)?;
self.buf.push_str(")(__x) for __x in (");
self.emit_expr(recv)?;
self.buf.push_str(")]");
}
_ => return Ok(false),
}
Ok(true)
}
fn try_emit_string_method(
&mut self,
node: &AIRNode,
callee: &AIRNode,
args: &[bock_air::AirArg],
) -> Result<bool, CodegenError> {
if crate::generator::primitive_recv_kind(node) != Some("String") {
return Ok(false);
}
let Some((recv, field, rest)) = crate::generator::desugared_self_call(callee, args) else {
return Ok(false);
};
let method = field.name.as_str();
let recv_str = self.expr_to_string(recv)?;
let arg0 = |this: &mut Self| -> Result<Option<String>, CodegenError> {
rest.first()
.map(|a| this.expr_to_string(&a.value))
.transpose()
};
let code = match method {
"len" | "length" | "count" => format!("len({recv_str})"),
"byte_len" => format!("len(({recv_str}).encode())"),
"is_empty" => format!("(len({recv_str}) == 0)"),
"to_upper" => format!("({recv_str}).upper()"),
"to_lower" => format!("({recv_str}).lower()"),
"trim" => format!("({recv_str}).strip()"),
"trim_start" => format!("({recv_str}).lstrip()"),
"trim_end" => format!("({recv_str}).rstrip()"),
"reverse" => format!("({recv_str})[::-1]"),
"to_string" | "display" => format!("str({recv_str})"),
"repeat" => {
let Some(n) = arg0(self)? else {
return Ok(false);
};
format!("(({recv_str}) * ({n}))")
}
"contains" => {
let Some(p) = arg0(self)? else {
return Ok(false);
};
format!("(({p}) in ({recv_str}))")
}
"starts_with" => {
let Some(p) = arg0(self)? else {
return Ok(false);
};
format!("({recv_str}).startswith({p})")
}
"ends_with" => {
let Some(p) = arg0(self)? else {
return Ok(false);
};
format!("({recv_str}).endswith({p})")
}
"replace" => {
let Some(from) = arg0(self)? else {
return Ok(false);
};
let Some(to) = rest
.get(1)
.map(|a| self.expr_to_string(&a.value))
.transpose()?
else {
return Ok(false);
};
format!("({recv_str}).replace({from}, {to})")
}
"split" => {
let Some(sep) = arg0(self)? else {
return Ok(false);
};
format!("({recv_str}).split({sep})")
}
"slice" | "substring" => {
let Some(start) = arg0(self)? else {
return Ok(false);
};
let Some(end) = rest
.get(1)
.map(|a| self.expr_to_string(&a.value))
.transpose()?
else {
return Ok(false);
};
format!("({recv_str})[{start}:{end}]")
}
"char_at" => {
let Some(i) = arg0(self)? else {
return Ok(false);
};
format!(
"(lambda __s, __i: _BockSome(__s[__i]) if 0 <= __i < len(__s) else _bock_none)({recv_str}, {i})"
)
}
"index_of" => {
let Some(p) = arg0(self)? else {
return Ok(false);
};
format!(
"(lambda __s, __p: (lambda __b: _BockSome(__b) if __b >= 0 else _bock_none)(__s.find(__p)))({recv_str}, {p})"
)
}
_ => return Ok(false),
};
self.buf.push_str(&code);
Ok(true)
}
fn try_emit_primitive_conversion(
&mut self,
node: &AIRNode,
callee: &AIRNode,
args: &[bock_air::AirArg],
) -> Result<bool, CodegenError> {
let Some((target, method, arg)) =
crate::generator::primitive_conversion_call(node, callee, args)
else {
return Ok(false);
};
let arg_str = self.expr_to_string(arg)?;
let code = match (target, method) {
("Float", "from") => format!("float({arg_str})"),
("Int", "from") => format!("int({arg_str})"),
("String", "from") => format!("str({arg_str})"),
("Int", "try_from") => {
self.needs_runtime_result = true;
format!("_bock_parse_int({arg_str}, lambda __m: ConvertError(message=__m))")
}
("Float", "try_from") => {
self.needs_runtime_result = true;
format!("_bock_parse_float({arg_str}, lambda __m: ConvertError(message=__m))")
}
_ => return Ok(false),
};
self.buf.push_str(&code);
Ok(true)
}
fn try_emit_numeric_method(
&mut self,
node: &AIRNode,
callee: &AIRNode,
args: &[bock_air::AirArg],
) -> Result<bool, CodegenError> {
let prim = match crate::generator::primitive_recv_kind(node) {
Some(p @ ("Int" | "Float" | "Char" | "Bool")) => p,
_ => return Ok(false),
};
let Some((recv, field, rest)) = crate::generator::desugared_self_call(callee, args) else {
return Ok(false);
};
let method = field.name.as_str();
let recv_str = self.expr_to_string(recv)?;
let arg = |this: &mut Self, i: usize| -> Result<Option<String>, CodegenError> {
rest.get(i)
.map(|a| this.expr_to_string(&a.value))
.transpose()
};
let code = match (prim, method) {
("Int", "to_float") => format!("float({recv_str})"),
("Float", "to_int") => format!("int({recv_str})"),
("Char", "to_int") => format!("ord({recv_str})"),
("Bool", "to_int") => format!("(1 if ({recv_str}) else 0)"),
("Int", "abs") => format!("abs({recv_str})"),
("Int" | "Float", "min") => {
let Some(o) = arg(self, 0)? else {
return Ok(false);
};
format!("min({recv_str}, {o})")
}
("Int" | "Float", "max") => {
let Some(o) = arg(self, 0)? else {
return Ok(false);
};
format!("max({recv_str}, {o})")
}
("Int" | "Float", "clamp") => {
let (Some(lo), Some(hi)) = (arg(self, 0)?, arg(self, 1)?) else {
return Ok(false);
};
format!("min(max({recv_str}, {lo}), {hi})")
}
("Int", "shift_left") => {
let Some(o) = arg(self, 0)? else {
return Ok(false);
};
format!("(({recv_str}) << ({o}))")
}
("Int", "shift_right") => {
let Some(o) = arg(self, 0)? else {
return Ok(false);
};
format!("(({recv_str}) >> ({o}))")
}
("Float", "abs") => format!("abs({recv_str})"),
("Float", "floor") => {
self.needs_math_import = true;
format!("float(math.floor({recv_str}))")
}
("Float", "ceil") => {
self.needs_math_import = true;
format!("float(math.ceil({recv_str}))")
}
("Float", "round") => format!("float(round({recv_str}))"),
("Float", "sqrt") => {
self.needs_math_import = true;
format!("math.sqrt({recv_str})")
}
("Float", "is_nan") => {
self.needs_math_import = true;
format!("math.isnan({recv_str})")
}
("Float", "is_infinite") => {
self.needs_math_import = true;
format!("math.isinf({recv_str})")
}
("Bool", "negate") => format!("(not ({recv_str}))"),
("Bool", "to_string" | "display") => {
format!("('true' if ({recv_str}) else 'false')")
}
("Char", "to_upper") => format!("({recv_str}).upper()"),
("Char", "to_lower") => format!("({recv_str}).lower()"),
("Char", "is_alpha") => format!("({recv_str}).isalpha()"),
("Char", "is_digit") => format!("({recv_str}).isdigit()"),
("Char", "is_whitespace") => format!("({recv_str}).isspace()"),
_ => return Ok(false),
};
self.buf.push_str(&code);
Ok(true)
}
fn try_emit_primitive_bridge(
&mut self,
node: &AIRNode,
callee: &AIRNode,
args: &[bock_air::AirArg],
) -> Result<bool, CodegenError> {
let Some((recv, method, rest, _prim)) =
crate::generator::primitive_bridge_call(node, callee, args)
else {
return Ok(false);
};
self.emit_bridge_method(recv, method, rest)
}
fn try_emit_trait_bound_bridge(
&mut self,
node: &AIRNode,
callee: &AIRNode,
args: &[bock_air::AirArg],
) -> Result<bool, CodegenError> {
let Some((recv, method, rest, _tr)) =
crate::generator::trait_bound_bridge_call(node, callee, args, &self.trait_decls)
else {
return Ok(false);
};
if method == "compare" {
let Some(other) = rest.first() else {
return Ok(false);
};
let recv_str = self.expr_to_string(recv)?;
let other = self.expr_to_string(&other.value)?;
self.needs_runtime_ordering = true;
let _ = write!(self.buf, "_bock_compare({recv_str}, {other})");
return Ok(true);
}
self.emit_bridge_method(recv, method, rest)
}
fn emit_bridge_method(
&mut self,
recv: &AIRNode,
method: &str,
rest: &[bock_air::AirArg],
) -> Result<bool, CodegenError> {
let recv_str = self.expr_to_string(recv)?;
match method {
"compare" => {
let Some(other) = rest.first() else {
return Ok(false);
};
let other = self.expr_to_string(&other.value)?;
let _ = write!(
self.buf,
"(_bock_less if ({recv_str}) < ({other}) else \
(_bock_equal if ({recv_str}) == ({other}) else _bock_greater))"
);
}
"eq" => {
let Some(other) = rest.first() else {
return Ok(false);
};
let other = self.expr_to_string(&other.value)?;
let _ = write!(self.buf, "(({recv_str}) == ({other}))");
}
"to_string" | "display" => {
let _ = write!(self.buf, "str({recv_str})");
}
_ => return Ok(false),
}
Ok(true)
}
fn try_emit_time_assoc_call(
&mut self,
callee: &AIRNode,
args: &[bock_air::AirArg],
) -> Result<bool, CodegenError> {
let NodeKind::FieldAccess { object, field } = &callee.kind else {
return Ok(false);
};
let NodeKind::Identifier { name: type_name } = &object.kind else {
return Ok(false);
};
let arg_strs: Vec<String> = args
.iter()
.map(|a| self.expr_to_string(&a.value))
.collect::<Result<_, _>>()?;
let arg0 = || arg_strs.first().cloned().unwrap_or_default();
let code = match (type_name.name.as_str(), field.name.as_str()) {
("Duration", "zero") => "0".to_string(),
("Duration", "nanos") => arg0(),
("Duration", "micros") => format!("(({}) * 1_000)", arg0()),
("Duration", "millis") => format!("(({}) * 1_000_000)", arg0()),
("Duration", "seconds") => format!("(({}) * 1_000_000_000)", arg0()),
("Duration", "minutes") => format!("(({}) * 60_000_000_000)", arg0()),
("Duration", "hours") => format!("(({}) * 3_600_000_000_000)", arg0()),
("Instant", "now") => {
if let Some(handler) = self.clock_handler_var() {
format!("{handler}.{}()", to_snake_case("now_monotonic"))
} else {
self.needs_time_import = true;
"time.monotonic_ns()".to_string()
}
}
_ => return Ok(false),
};
self.buf.push_str(&code);
Ok(true)
}
fn try_emit_concurrency_call(
&mut self,
callee: &AIRNode,
args: &[bock_air::AirArg],
) -> Result<bool, CodegenError> {
if let NodeKind::Identifier { name } = &callee.kind {
if name.name == "spawn" {
self.buf.push_str("__bock_spawn(");
for (i, arg) in args.iter().enumerate() {
if i > 0 {
self.buf.push_str(", ");
}
self.emit_expr(&arg.value)?;
}
self.buf.push(')');
return Ok(true);
}
}
let NodeKind::FieldAccess { object, field } = &callee.kind else {
return Ok(false);
};
if let NodeKind::Identifier { name: type_name } = &object.kind {
if type_name.name == "Channel" && field.name == "new" {
self.buf.push_str("__bock_channel_new()");
return Ok(true);
}
}
if matches!(field.name.as_str(), "send" | "recv" | "close") {
self.emit_expr(object)?;
let _ = write!(self.buf, ".{}", field.name);
self.buf.push('(');
for (i, arg) in args.iter().skip(1).enumerate() {
if i > 0 {
self.buf.push_str(", ");
}
self.emit_expr(&arg.value)?;
}
self.buf.push(')');
return Ok(true);
}
Ok(false)
}
fn try_emit_time_desugared_method(
&mut self,
node: &AIRNode,
callee: &AIRNode,
args: &[bock_air::AirArg],
) -> Result<bool, CodegenError> {
if crate::generator::primitive_recv_kind(node).is_some() {
return Ok(false);
}
let NodeKind::FieldAccess { object, field } = &callee.kind else {
return Ok(false);
};
if let NodeKind::Identifier { name } = &object.kind {
if matches!(name.name.as_str(), "Duration" | "Instant") {
return Ok(false);
}
}
if !is_time_method_name(&field.name) {
return Ok(false);
}
let remaining: Vec<bock_air::AirArg> = args.iter().skip(1).cloned().collect();
self.try_emit_time_method(object, &field.name, &remaining)
}
fn try_emit_time_method(
&mut self,
receiver: &AIRNode,
method: &str,
args: &[bock_air::AirArg],
) -> Result<bool, CodegenError> {
let recv_str = self.expr_to_string(receiver)?;
let arg_strs: Vec<String> = args
.iter()
.map(|a| self.expr_to_string(&a.value))
.collect::<Result<_, _>>()?;
let code = match method {
"as_nanos" => format!("({recv_str})"),
"as_millis" => format!("(({recv_str}) // 1_000_000)"),
"as_seconds" => format!("(({recv_str}) // 1_000_000_000)"),
"is_zero" => format!("(({recv_str}) == 0)"),
"is_negative" => format!("(({recv_str}) < 0)"),
"abs" => format!("abs({recv_str})"),
"elapsed" => {
if let Some(handler) = self.clock_handler_var() {
format!(
"({handler}.{}() - ({recv_str}))",
to_snake_case("now_monotonic")
)
} else {
self.needs_time_import = true;
format!("(time.monotonic_ns() - ({recv_str}))")
}
}
"duration_since" => {
let other = arg_strs.first().cloned().unwrap_or_default();
format!("(({recv_str}) - ({other}))")
}
_ => return Ok(false),
};
self.buf.push_str(&code);
Ok(true)
}
fn emit_node(&mut self, node: &AIRNode) -> Result<(), CodegenError> {
match &node.kind {
NodeKind::Module { items, imports, .. } => {
self.field_method_collisions
.extend(crate::generator::collect_record_field_names(
node,
to_snake_case,
));
if self.per_module {
if py_module_uses_optional(items) {
self.needs_runtime_optional = true;
}
if py_module_uses_result(items) {
self.needs_runtime_result = true;
}
if py_module_uses_ordering(items) {
self.needs_runtime_ordering = true;
}
if py_module_uses_concurrency(items) {
self.needs_runtime_concurrency = true;
}
if py_module_uses_list_functional(items) {
self.needs_runtime_list_functional = true;
self.needs_runtime_optional = true;
}
if py_module_uses_list_mutators(items) {
self.needs_runtime_list_mutators = true;
}
if py_module_uses_propagate(items) {
self.needs_runtime_propagate = true;
}
if py_module_uses_str(items) {
self.needs_runtime_str = true;
}
} else {
if !self.optional_runtime_emitted && py_module_uses_optional(items) {
self.buf.push_str(OPTIONAL_RUNTIME_PY);
self.buf.push('\n');
self.optional_runtime_emitted = true;
}
if !self.result_runtime_emitted && py_module_uses_result(items) {
self.buf.push_str(RESULT_RUNTIME_PY);
self.buf.push('\n');
self.result_runtime_emitted = true;
}
if !self.ordering_runtime_emitted && py_module_uses_ordering(items) {
self.buf.push_str(ORDERING_RUNTIME_PY);
self.buf.push('\n');
self.ordering_runtime_emitted = true;
}
if !self.concurrency_runtime_emitted && py_module_uses_concurrency(items) {
self.buf.push_str(CONCURRENCY_RUNTIME_PY);
self.buf.push('\n');
self.concurrency_runtime_emitted = true;
}
if !self.list_functional_runtime_emitted
&& py_module_uses_list_functional(items)
{
if !self.optional_runtime_emitted {
self.buf.push_str(OPTIONAL_RUNTIME_PY);
self.buf.push('\n');
self.optional_runtime_emitted = true;
}
self.buf.push_str(LIST_FUNCTIONAL_RUNTIME_PY);
self.buf.push('\n');
self.list_functional_runtime_emitted = true;
}
if !self.list_mutator_runtime_emitted && py_module_uses_list_mutators(items) {
self.buf.push_str(LIST_MUTATOR_RUNTIME_PY);
self.buf.push('\n');
self.list_mutator_runtime_emitted = true;
}
if !self.propagate_runtime_emitted && py_module_uses_propagate(items) {
self.buf.push_str(PROPAGATE_RUNTIME_PY);
self.buf.push('\n');
self.propagate_runtime_emitted = true;
}
if !self.str_runtime_emitted && py_module_uses_str(items) {
self.buf.push_str(STR_RUNTIME_PY);
self.buf.push('\n');
self.str_runtime_emitted = true;
}
}
if self.per_module {
for import in imports {
self.emit_node(import)?;
}
let mut by_module: std::collections::BTreeMap<String, Vec<String>> =
std::collections::BTreeMap::new();
for (module_path, name) in &self.implicit_imports {
by_module
.entry(module_path.clone())
.or_default()
.push(name.clone());
}
let import_lines: Vec<String> = by_module
.into_iter()
.map(|(module_path, mut names)| {
names.sort_unstable();
names.dedup();
format!("from {module_path} import {}", names.join(", "))
})
.collect();
for line in import_lines {
self.writeln(&line);
}
}
self.impls_by_target.clear();
let class_targets: std::collections::HashSet<String> = items
.iter()
.filter_map(|it| match &it.kind {
NodeKind::RecordDecl { name, .. } | NodeKind::ClassDecl { name, .. } => {
Some(name.name.clone())
}
_ => None,
})
.collect();
let mut consumed_impls: std::collections::HashSet<bock_air::NodeId> =
std::collections::HashSet::new();
for item in items.iter() {
if let NodeKind::ImplBlock { target, .. } = &item.kind {
if let Some(target_name) = ast_type_name(target) {
if class_targets.contains(&target_name) {
self.impls_by_target
.entry(target_name)
.or_default()
.push(item.clone());
consumed_impls.insert(item.id);
}
}
}
}
let order = type_decl_emission_order(items, &self.impls_by_target);
for (idx, &i) in order.iter().enumerate() {
let item = &items[i];
if consumed_impls.contains(&item.id) {
continue;
}
if crate::generator::fn_is_test(item) {
continue;
}
if idx > 0 && !self.buf.is_empty() && !self.buf.ends_with("\n\n") {
self.buf.push('\n');
}
self.emit_node(item)?;
}
Ok(())
}
NodeKind::ImportDecl { path, items } => {
if !self.per_module {
return Ok(());
}
let module_path = path
.segments
.iter()
.map(|s| s.name.as_str())
.collect::<Vec<_>>()
.join(".");
if module_path.is_empty() {
return Ok(());
}
match items {
bock_ast::ImportItems::Named(names) => {
let rendered: Vec<String> = names
.iter()
.filter(|n| {
n.alias.is_some()
|| self.user_variant_for_name(&n.name.name).is_none()
})
.map(|n| match &n.alias {
Some(alias) => format!("{} as {}", n.name.name, alias.name),
None => n.name.name.clone(),
})
.collect();
if rendered.is_empty() {
if names.is_empty() {
self.writeln(&format!("import {module_path}"));
}
} else {
self.writeln(&format!(
"from {module_path} import {}",
rendered.join(", ")
));
}
}
bock_ast::ImportItems::Glob => {
self.writeln(&format!("from {module_path} import *"));
}
bock_ast::ImportItems::Module => {
self.writeln(&format!("from {module_path} import *"));
}
}
Ok(())
}
NodeKind::FnDecl {
visibility,
is_async,
name,
generic_params,
params,
return_type,
effect_clause,
body,
..
} => self.emit_fn_decl(
*visibility,
*is_async,
&name.name,
generic_params,
params,
return_type.as_deref(),
effect_clause,
body,
),
NodeKind::RecordDecl {
name,
fields,
generic_params,
..
} => {
self.emit_typevars(generic_params);
let impls = self.impls_by_target.remove(&name.name).unwrap_or_default();
let mut bases: Vec<String> = impls
.iter()
.filter_map(|im| {
if let NodeKind::ImplBlock {
trait_path: Some(tp),
..
} = &im.kind
{
let trait_name = tp.segments.last().map(|s| s.name.clone())?;
if crate::generator::is_unimplemented_sealed_core_trait(
&trait_name,
&self.trait_decls,
) {
return None;
}
if crate::generator::impl_has_instance_method(im, &self.effect_ops) {
Some(trait_name)
} else {
None
}
} else {
None
}
})
.collect();
bases.extend(self.generic_base(generic_params));
let base_list = if bases.is_empty() {
String::new()
} else {
format!("({})", bases.join(", "))
};
let custom_eq = matches!(
node.metadata.get(bock_types::checker::CUSTOM_EQ_META_KEY),
Some(bock_air::Value::Bool(true))
);
if !fields.is_empty() {
self.needs_dataclass_import = true;
if custom_eq {
self.writeln("@dataclass(eq=False)");
} else {
self.writeln("@dataclass");
}
}
self.writeln(&format!("class {}{base_list}:", name.name));
self.indent += 1;
let has_members = !fields.is_empty()
|| impls
.iter()
.any(|im| matches!(&im.kind, NodeKind::ImplBlock { methods, .. } if !methods.is_empty()));
if !has_members {
self.writeln("pass");
} else {
for f in fields {
let type_hint = self.ast_type_to_py(&f.ty);
self.writeln(&format!("{}: {type_hint}", py_field_ident(&f.name.name)));
}
for method in Self::dedup_impl_methods(&impls) {
self.buf.push('\n');
self.emit_class_method(method)?;
}
if custom_eq {
self.buf.push('\n');
self.writeln("def __eq__(self, other: object) -> bool:");
self.indent += 1;
self.writeln(&format!("if not isinstance(other, {}):", name.name));
self.indent += 1;
self.writeln("return NotImplemented");
self.indent -= 1;
self.writeln("return self.eq(other)");
self.indent -= 1;
}
}
self.indent -= 1;
Ok(())
}
NodeKind::EnumDecl {
name,
variants,
generic_params,
..
} => {
self.needs_dataclass_import = true;
self.emit_typevars(generic_params);
for (i, variant) in variants.iter().enumerate() {
if i > 0 {
self.buf.push('\n');
}
self.emit_enum_variant(&name.name, variant)?;
}
let variant_types: Vec<String> = variants
.iter()
.filter_map(|v| {
if let NodeKind::EnumVariant { name: vname, .. } = &v.kind {
Some(format!("{}_{}", name.name, vname.name))
} else {
None
}
})
.collect();
if !variant_types.is_empty() {
self.needs_union_import = true;
self.writeln(&format!(
"{} = Union[{}]",
name.name,
variant_types.join(", ")
));
}
Ok(())
}
NodeKind::ClassDecl {
name,
fields,
methods,
generic_params,
base,
traits,
..
} => {
self.emit_typevars(generic_params);
let impls = self.impls_by_target.remove(&name.name).unwrap_or_default();
let mut bases: Vec<String> = Vec::new();
if let Some(b) = base {
bases.push(
b.segments
.last()
.map(|s| s.name.clone())
.unwrap_or_default(),
);
}
for tp in traits {
if let Some(seg) = tp.segments.last() {
if crate::generator::is_unimplemented_sealed_core_trait(
&seg.name,
&self.trait_decls,
) {
continue;
}
bases.push(seg.name.clone());
}
}
for im in &impls {
if let NodeKind::ImplBlock {
trait_path: Some(tp),
..
} = &im.kind
{
if let Some(seg) = tp.segments.last() {
if crate::generator::is_unimplemented_sealed_core_trait(
&seg.name,
&self.trait_decls,
) {
continue;
}
bases.push(seg.name.clone());
}
}
}
let mut seen_bases: std::collections::HashSet<String> =
std::collections::HashSet::new();
bases.retain(|b| seen_bases.insert(b.clone()));
bases.extend(self.generic_base(generic_params));
let base_list = if bases.is_empty() {
String::new()
} else {
format!("({})", bases.join(", "))
};
self.writeln(&format!("class {}{base_list}:", name.name));
self.indent += 1;
if !fields.is_empty() {
let params: Vec<String> = fields
.iter()
.map(|f| {
let fname = py_field_ident(&f.name.name);
let type_hint = self.ast_type_to_py(&f.ty);
format!("{fname}: {type_hint}")
})
.collect();
self.writeln(&format!("def __init__(self, {}):", params.join(", ")));
self.indent += 1;
for f in fields {
let fname = py_field_ident(&f.name.name);
self.writeln(&format!("self.{fname} = {fname}"));
}
self.indent -= 1;
}
let mut inline_names: std::collections::HashSet<String> =
std::collections::HashSet::new();
for method in methods {
if let NodeKind::FnDecl { name, .. } = &method.kind {
inline_names.insert(name.name.clone());
}
self.buf.push('\n');
self.emit_class_method(method)?;
}
let impl_methods: Vec<&AIRNode> = Self::dedup_impl_methods(&impls)
.into_iter()
.filter(|m| {
!matches!(&m.kind, NodeKind::FnDecl { name, .. } if inline_names.contains(&name.name))
})
.collect();
let has_impl_methods = !impl_methods.is_empty();
for method in impl_methods {
self.buf.push('\n');
self.emit_class_method(method)?;
}
if fields.is_empty() && methods.is_empty() && !has_impl_methods {
self.writeln("pass");
}
self.indent -= 1;
Ok(())
}
NodeKind::TraitDecl { name, methods, .. } => {
self.writeln(&format!("# trait {}", name.name));
self.writeln(&format!("class {}:", name.name));
self.indent += 1;
if methods.is_empty() {
self.writeln("pass");
} else {
for (i, method) in methods.iter().enumerate() {
if i > 0 {
self.buf.push('\n');
}
self.emit_class_method(method)?;
}
}
self.indent -= 1;
Ok(())
}
NodeKind::ImplBlock {
trait_path,
target,
methods,
..
} => {
let target_name = self.type_expr_to_string(target);
if let Some(tp) = trait_path {
let trait_name = tp
.segments
.iter()
.map(|s| s.name.as_str())
.collect::<Vec<_>>()
.join(".");
self.writeln(&format!("# impl {trait_name} for {target_name}"));
} else {
self.writeln(&format!("# impl {target_name}"));
}
for method in methods {
if let NodeKind::FnDecl {
is_async,
name,
params,
return_type,
effect_clause,
body,
..
} = &method.kind
{
if *is_async {
self.needs_asyncio_import = true;
}
let async_kw = if *is_async { "async " } else { "" };
let rest = match params.first().map(crate::generator::param_binds_self) {
Some(Some(_)) => ¶ms[1..],
_ => ¶ms[..],
};
let param_strs = self.collect_param_strs(rest);
let effects = self.effects_params(effect_clause);
let mut all_params = vec!["self".to_string()];
all_params.extend(param_strs);
all_params.extend(effects);
let ret = return_type
.as_deref()
.map(|t| format!(" -> {}", self.type_to_py(t)))
.unwrap_or_default();
let fn_name = self.py_method_name(&name.name);
self.writeln(&format!(
"{async_kw}def {fn_name}({}){}:",
all_params.join(", "),
ret,
));
self.indent += 1;
let old_handler_vars = self.current_handler_vars.clone();
let expanded = self.expand_effect_names(effect_clause);
for ename in &expanded {
self.current_handler_vars
.insert(ename.clone(), to_snake_case(ename));
}
self.emit_block_body(body)?;
self.current_handler_vars = old_handler_vars;
self.indent -= 1;
}
}
Ok(())
}
NodeKind::EffectDecl {
name,
components,
operations,
..
} => {
if !components.is_empty() {
let comp_names: Vec<String> = components
.iter()
.map(|tp| {
tp.segments
.last()
.map_or("effect".to_string(), |s| s.name.clone())
})
.collect();
self.writeln(&format!(
"# composite effect {} = {}",
name.name,
comp_names.join(" + ")
));
self.composite_effects.insert(name.name.clone(), comp_names);
return Ok(());
}
for op in operations {
if let NodeKind::FnDecl { name: op_name, .. } = &op.kind {
self.effect_ops
.insert(op_name.name.clone(), name.name.clone());
}
}
self.needs_abc_import = true;
self.writeln(&format!("class {}(ABC):", name.name));
self.indent += 1;
if operations.is_empty() {
self.writeln("pass");
} else {
for (i, op) in operations.iter().enumerate() {
if i > 0 {
self.buf.push('\n');
}
if let NodeKind::FnDecl {
name,
params,
return_type,
..
} = &op.kind
{
self.writeln("@abstractmethod");
let rest = match params.first().map(crate::generator::param_binds_self)
{
Some(Some(_)) => ¶ms[1..],
_ => ¶ms[..],
};
let param_strs = self.collect_param_strs(rest);
let mut all_params = vec!["self".to_string()];
all_params.extend(param_strs);
let ret = return_type
.as_deref()
.map(|t| format!(" -> {}", self.type_to_py(t)))
.unwrap_or_default();
let fn_name = to_snake_case(&name.name);
self.writeln(&format!(
"def {fn_name}({}){}:",
all_params.join(", "),
ret,
));
self.indent += 1;
self.writeln("...");
self.indent -= 1;
}
}
}
self.indent -= 1;
Ok(())
}
NodeKind::TypeAlias { name, .. } => {
self.writeln(&format!("# type {} = ...", name.name));
Ok(())
}
NodeKind::ConstDecl {
name, value, ty, ..
} => {
let type_hint = format!(": {}", self.type_to_py(ty));
let ind = self.indent_str();
let _ = write!(self.buf, "{ind}{}{type_hint} = ", name.name);
self.emit_expr(value)?;
self.buf.push('\n');
Ok(())
}
NodeKind::ModuleHandle { effect, handler } => {
let effect_name = effect.segments.last().map_or("effect", |s| s.name.as_str());
let var_name = format!("__{}", to_snake_case(effect_name));
let ind = self.indent_str();
let _ = write!(self.buf, "{ind}{var_name}: {effect_name} = ");
self.emit_expr(handler)?;
self.buf.push('\n');
self.current_handler_vars
.insert(effect_name.to_string(), var_name);
Ok(())
}
NodeKind::PropertyTest { name, body, .. } => {
self.writeln(&format!("# property test: {name}"));
self.writeln("# (property tests are not emitted in Python output)");
let _ = body;
Ok(())
}
NodeKind::LetBinding { .. }
| NodeKind::If { .. }
| NodeKind::For { .. }
| NodeKind::While { .. }
| NodeKind::Loop { .. }
| NodeKind::Return { .. }
| NodeKind::Break { .. }
| NodeKind::Continue
| NodeKind::Guard { .. }
| NodeKind::Match { .. }
| NodeKind::Block { .. }
| NodeKind::HandlingBlock { .. }
| NodeKind::Assign { .. } => self.emit_stmt(node),
_ => {
self.write_indent();
self.emit_expr(node)?;
self.buf.push('\n');
Ok(())
}
}
}
fn emit_typevars(&mut self, generic_params: &[bock_ast::GenericParam]) {
for gp in generic_params {
let name = gp.name.name.clone();
if !self.emitted_typevars.insert(name.clone()) {
continue;
}
self.needs_typing_typevar.set(true);
let bound = gp
.bounds
.first()
.and_then(|tp| tp.segments.last())
.filter(|seg| {
!crate::generator::is_unimplemented_sealed_core_trait(
&seg.name,
&self.trait_decls,
)
})
.map(|seg| format!(", bound={}", self.map_type_name(&seg.name)))
.unwrap_or_default();
self.writeln(&format!("{name} = TypeVar(\"{name}\"{bound})"));
}
}
fn generic_base(&self, generic_params: &[bock_ast::GenericParam]) -> Vec<String> {
if generic_params.is_empty() {
return Vec::new();
}
self.needs_typing_typevar.set(true);
let names: Vec<String> = generic_params
.iter()
.map(|gp| gp.name.name.clone())
.collect();
vec![format!("Generic[{}]", names.join(", "))]
}
#[allow(clippy::too_many_arguments)]
fn emit_fn_decl(
&mut self,
_visibility: Visibility,
is_async: bool,
name: &str,
generic_params: &[bock_ast::GenericParam],
params: &[AIRNode],
return_type: Option<&AIRNode>,
effect_clause: &[bock_ast::TypePath],
body: &AIRNode,
) -> Result<(), CodegenError> {
if is_async {
self.needs_asyncio_import = true;
}
self.emit_typevars(generic_params);
let async_kw = if is_async { "async " } else { "" };
let param_strs = self.collect_param_strs(params);
let effects = self.effects_params(effect_clause);
let mut all_params = param_strs;
all_params.extend(effects);
let ret = return_type
.map(|t| format!(" -> {}", self.type_to_py(t)))
.unwrap_or_default();
if !effect_clause.is_empty() {
let effect_names = self.expand_effect_names(effect_clause);
self.fn_effects.insert(name.to_string(), effect_names);
}
let fn_name = py_value_ident(name);
self.writeln(&format!(
"{async_kw}def {fn_name}({}){}:",
all_params.join(", "),
ret,
));
self.indent += 1;
let old_handler_vars = self.current_handler_vars.clone();
let expanded = self.expand_effect_names(effect_clause);
for ename in &expanded {
self.current_handler_vars
.insert(ename.clone(), to_snake_case(ename));
}
self.pending_scope_seed = Self::param_value_names(params);
let prev_discard = std::mem::replace(&mut self.in_loop_body_tail, false);
let prev_match = std::mem::replace(&mut self.in_stmt_construct_arm, false);
let body_res = self.emit_fn_body(body);
self.in_loop_body_tail = prev_discard;
self.in_stmt_construct_arm = prev_match;
body_res?;
self.current_handler_vars = old_handler_vars;
self.indent -= 1;
Ok(())
}
fn emit_class_method(&mut self, method: &AIRNode) -> Result<(), CodegenError> {
if let NodeKind::FnDecl {
is_async,
name,
params,
return_type,
effect_clause,
body,
..
} = &method.kind
{
if *is_async {
self.needs_asyncio_import = true;
}
let async_kw = if *is_async { "async " } else { "" };
let is_assoc = crate::generator::is_associated_impl_method(method, &self.effect_ops);
let rest = match params.first().map(crate::generator::param_binds_self) {
Some(Some(_)) => ¶ms[1..],
_ => ¶ms[..],
};
let param_strs = self.collect_param_strs(rest);
let effects = self.effects_params(effect_clause);
let mut all_params = if is_assoc {
Vec::new()
} else {
vec!["self".to_string()]
};
all_params.extend(param_strs);
all_params.extend(effects);
let ret = return_type
.as_deref()
.map(|t| format!(" -> {}", self.type_to_py(t)))
.unwrap_or_default();
let fn_name = self.py_method_name(&name.name);
if is_assoc {
self.writeln("@staticmethod");
}
self.writeln(&format!(
"{async_kw}def {fn_name}({}){}:",
all_params.join(", "),
ret,
));
self.indent += 1;
let old_handler_vars = self.current_handler_vars.clone();
let expanded = self.expand_effect_names(effect_clause);
for ename in &expanded {
self.current_handler_vars
.insert(ename.clone(), to_snake_case(ename));
}
let mut seed = if is_assoc {
Vec::new()
} else {
vec!["self".to_string()]
};
seed.extend(Self::param_value_names(rest));
self.pending_scope_seed = seed;
let prev_discard = std::mem::replace(&mut self.in_loop_body_tail, false);
let prev_match = std::mem::replace(&mut self.in_stmt_construct_arm, false);
let body_res = self.emit_fn_body(body);
self.in_loop_body_tail = prev_discard;
self.in_stmt_construct_arm = prev_match;
body_res?;
self.current_handler_vars = old_handler_vars;
self.indent -= 1;
}
Ok(())
}
fn dedup_impl_methods<'a>(impls: &'a [AIRNode]) -> Vec<&'a AIRNode> {
let mut out: Vec<&'a AIRNode> = Vec::new();
let mut seen: std::collections::HashSet<String> = std::collections::HashSet::new();
let inherent_first = impls.iter().filter(|im| {
matches!(
&im.kind,
NodeKind::ImplBlock {
trait_path: None,
..
}
)
});
let trait_after = impls.iter().filter(|im| {
matches!(
&im.kind,
NodeKind::ImplBlock {
trait_path: Some(_),
..
}
)
});
for im in inherent_first.chain(trait_after) {
if let NodeKind::ImplBlock { methods, .. } = &im.kind {
for method in methods {
if let NodeKind::FnDecl { name, .. } = &method.kind {
if seen.insert(name.name.clone()) {
out.push(method);
}
}
}
}
}
out
}
fn param_value_names(params: &[AIRNode]) -> Vec<String> {
params
.iter()
.filter_map(|p| {
if let NodeKind::Param { pattern, .. } = &p.kind {
Self::simple_bind_name(pattern)
} else {
None
}
})
.collect()
}
fn collect_param_strs(&self, params: &[AIRNode]) -> Vec<String> {
self.collect_param_strs_inner(params, true)
}
fn collect_param_strs_bare(&self, params: &[AIRNode]) -> Vec<String> {
self.collect_param_strs_inner(params, false)
}
fn collect_param_strs_inner(&self, params: &[AIRNode], hints: bool) -> Vec<String> {
params
.iter()
.filter_map(|p| {
if let NodeKind::Param {
pattern,
ty,
default,
} = &p.kind
{
let name = to_snake_case(&self.pattern_to_binding_name(pattern));
let type_hint = if hints {
ty.as_ref()
.map(|t| format!(": {}", self.type_to_py(t)))
.unwrap_or_default()
} else {
String::new()
};
if let Some(def) = default {
let mut ctx = PyEmitCtx::new();
ctx.indent = self.indent;
ctx.enum_variants = self.enum_variants.clone();
if ctx.emit_expr(def).is_ok() {
let def_str = ctx.buf;
return Some(format!("{name}{type_hint} = {def_str}"));
}
}
Some(format!("{name}{type_hint}"))
} else {
None
}
})
.collect()
}
fn expand_effect_names(&self, effects: &[bock_ast::TypePath]) -> Vec<String> {
let mut result = Vec::new();
for tp in effects {
let name = tp
.segments
.last()
.map_or("effect".to_string(), |s| s.name.clone());
if let Some(components) = self.composite_effects.get(&name) {
result.extend(components.iter().cloned());
} else {
result.push(name);
}
}
result
}
fn effects_params(&self, effects: &[bock_ast::TypePath]) -> Vec<String> {
if effects.is_empty() {
return vec![];
}
let expanded = self.expand_effect_names(effects);
let mut result = vec!["*".to_string()];
for name in &expanded {
let param_name = to_snake_case(name);
result.push(format!("{param_name}: {name}"));
}
result
}
fn clock_handler_var(&self) -> Option<&str> {
self.current_handler_vars.get("Clock").map(String::as_str)
}
fn build_effects_call_args_py(&self, fn_name: &str) -> Option<String> {
let effects = self.fn_effects.get(fn_name)?;
let entries: Vec<String> = effects
.iter()
.filter_map(|e| {
let handler_var = self.current_handler_vars.get(e)?;
let param_name = to_snake_case(e);
Some(format!("{param_name}={handler_var}"))
})
.collect();
if entries.is_empty() {
return None;
}
Some(entries.join(", "))
}
fn emit_enum_variant(
&mut self,
enum_name: &str,
variant: &AIRNode,
) -> Result<(), CodegenError> {
if let NodeKind::EnumVariant { name, payload } = &variant.kind {
let vname = &name.name;
match payload {
EnumVariantPayload::Unit => {
self.writeln("@dataclass(frozen=True)");
self.writeln(&format!("class {enum_name}_{vname}:"));
self.indent += 1;
self.writeln(&format!("_tag: str = \"{vname}\""));
self.indent -= 1;
}
EnumVariantPayload::Struct(fields) => {
self.writeln("@dataclass");
self.writeln(&format!("class {enum_name}_{vname}:"));
self.indent += 1;
for f in fields {
let type_hint = self.ast_type_to_py(&f.ty);
self.writeln(&format!("{}: {type_hint}", py_field_ident(&f.name.name)));
}
self.writeln(&format!("_tag: str = \"{vname}\""));
self.indent -= 1;
}
EnumVariantPayload::Tuple(elems) => {
self.writeln("@dataclass");
self.writeln(&format!("class {enum_name}_{vname}:"));
self.indent += 1;
for (i, elem) in elems.iter().enumerate() {
let type_hint = self.type_to_py(elem);
self.writeln(&format!("_{i}: {type_hint}"));
}
self.writeln(&format!("_tag: str = \"{vname}\""));
self.indent -= 1;
}
}
}
Ok(())
}
fn emit_stmt(&mut self, node: &AIRNode) -> Result<(), CodegenError> {
match &node.kind {
NodeKind::LetBinding {
pattern, value, ty, ..
} => {
let raw_name = Self::simple_bind_name(pattern);
let (binding, pending) = match &raw_name {
Some(n) => self.plan_shadow_let(n),
None => (self.pattern_to_py_binding(pattern), None),
};
let type_hint = ty
.as_ref()
.map(|t| format!(": {}", self.type_to_py(t)))
.unwrap_or_default();
if node.metadata.contains_key(crate::generator::DECL_ONLY_META) {
let ind = self.indent_str();
let _ = writeln!(self.buf, "{ind}{binding}{type_hint} = None");
if let Some(n) = &raw_name {
self.commit_shadow_let(n, pending);
}
return Ok(());
}
if value_needs_stmt_form(value) {
let ind = self.indent_str();
let _ = writeln!(self.buf, "{ind}{binding}{type_hint} = None");
let r = self.emit_value_binding(&binding, value);
if let Some(n) = &raw_name {
self.commit_shadow_let(n, pending);
}
return r;
}
if is_raise_expr(value) {
self.write_indent();
self.emit_expr(value)?;
self.buf.push('\n');
return Ok(());
}
let ind = self.indent_str();
let _ = write!(self.buf, "{ind}{binding}{type_hint} = ");
let wrap_task = matches!(&value.kind, NodeKind::Call { .. })
&& self.task_bound_names.contains(&binding);
if wrap_task {
self.needs_asyncio_import = true;
self.buf.push_str("asyncio.create_task(");
self.emit_expr(value)?;
self.buf.push(')');
} else {
self.emit_expr(value)?;
}
self.buf.push('\n');
if let Some(n) = &raw_name {
self.commit_shadow_let(n, pending);
}
Ok(())
}
NodeKind::If { .. } => {
let prev = std::mem::replace(&mut self.in_stmt_construct_arm, true);
let r = self.emit_stmt_if(node, false);
self.in_stmt_construct_arm = prev;
r
}
NodeKind::For {
pattern,
iterable,
body,
} => {
let binding = self.pattern_to_py_binding(pattern);
let ind = self.indent_str();
let _ = write!(self.buf, "{ind}for {binding} in ");
self.emit_expr(iterable)?;
self.buf.push_str(":\n");
self.indent += 1;
self.loop_value_targets.push(None);
self.emit_loop_body(body)?;
self.loop_value_targets.pop();
self.indent -= 1;
Ok(())
}
NodeKind::While { condition, body } => {
let ind = self.indent_str();
let _ = write!(self.buf, "{ind}while ");
self.emit_expr(condition)?;
self.buf.push_str(":\n");
self.indent += 1;
self.loop_value_targets.push(None);
self.emit_loop_body(body)?;
self.loop_value_targets.pop();
self.indent -= 1;
Ok(())
}
NodeKind::Loop { body } => {
self.writeln("while True:");
self.indent += 1;
self.loop_value_targets.push(None);
self.emit_loop_body(body)?;
self.loop_value_targets.pop();
self.indent -= 1;
Ok(())
}
NodeKind::Return { value } => {
if let Some(val) = value {
let ind = self.indent_str();
let _ = write!(self.buf, "{ind}return ");
self.emit_expr(val)?;
self.buf.push('\n');
} else {
self.writeln("return");
}
Ok(())
}
NodeKind::Break { value } => {
if let Some(val) = value {
if let Some(Some(target)) = self.loop_value_targets.last() {
let target = target.clone();
let ind = self.indent_str();
let _ = write!(self.buf, "{ind}{target} = ");
self.emit_expr(val)?;
self.buf.push('\n');
self.writeln("break");
} else {
let ind = self.indent_str();
let _ = write!(self.buf, "{ind}# break value: ");
self.emit_expr(val)?;
self.buf.push('\n');
self.writeln("break");
}
} else {
self.writeln("break");
}
Ok(())
}
NodeKind::Continue => {
self.writeln("continue");
Ok(())
}
NodeKind::Guard {
let_pattern,
condition,
else_block,
} => {
let prev = std::mem::replace(&mut self.in_stmt_construct_arm, true);
let r = self.emit_stmt_guard(let_pattern.as_deref(), condition, else_block);
self.in_stmt_construct_arm = prev;
r
}
NodeKind::Match { scrutinee, arms } => {
let prev = std::mem::replace(&mut self.in_stmt_construct_arm, true);
let r = self.emit_match(scrutinee, arms);
self.in_stmt_construct_arm = prev;
r
}
NodeKind::Block { stmts, tail } => {
for s in stmts {
self.emit_node(s)?;
}
if let Some(t) = tail {
self.write_indent();
self.emit_expr(t)?;
self.buf.push('\n');
}
Ok(())
}
NodeKind::HandlingBlock { handlers, body } => {
let old_handler_vars = self.current_handler_vars.clone();
self.handling_counter += 1;
let suffix = format!("_h{}", self.handling_counter);
for h in handlers {
let effect_name = h
.effect
.segments
.last()
.map_or("effect", |s| s.name.as_str());
let var_name = format!("__{}{suffix}", to_snake_case(effect_name));
let ind = self.indent_str();
let _ = write!(self.buf, "{ind}{var_name}: {effect_name} = ");
self.emit_expr(&h.handler)?;
self.buf.push('\n');
self.current_handler_vars
.insert(effect_name.to_string(), var_name);
}
if let NodeKind::Block { stmts, tail } = &body.kind {
for s in stmts {
self.emit_node(s)?;
}
if let Some(t) = tail {
self.write_indent();
self.emit_expr(t)?;
self.buf.push('\n');
}
} else {
self.emit_stmt(body)?;
}
self.current_handler_vars = old_handler_vars;
Ok(())
}
NodeKind::Assign { op, target, value } => {
let ind = self.indent_str();
let _ = write!(self.buf, "{ind}");
self.emit_expr(target)?;
let op_str = match op {
AssignOp::Assign => " = ",
AssignOp::AddAssign => " += ",
AssignOp::SubAssign => " -= ",
AssignOp::MulAssign => " *= ",
AssignOp::DivAssign => " /= ",
AssignOp::RemAssign => " %= ",
};
self.buf.push_str(op_str);
self.emit_expr(value)?;
self.buf.push('\n');
Ok(())
}
_ => {
self.write_indent();
self.emit_expr(node)?;
self.buf.push('\n');
Ok(())
}
}
}
fn emit_stmt_if(&mut self, node: &AIRNode, inline: bool) -> Result<(), CodegenError> {
let NodeKind::If {
let_pattern,
condition,
then_block,
else_block,
} = &node.kind
else {
return self.emit_stmt(node);
};
if let Some(pat) = let_pattern {
let ind = self.indent_str();
let binding = self.pattern_to_py_binding(pat);
let _ = write!(self.buf, "{ind}{binding} = ");
self.emit_expr(condition)?;
self.buf.push('\n');
self.writeln(&format!("if {binding} is not None:"));
} else {
if inline {
self.buf.push_str("if ");
} else {
let ind = self.indent_str();
let _ = write!(self.buf, "{ind}if ");
}
self.emit_expr(condition)?;
self.buf.push_str(":\n");
}
self.indent += 1;
self.emit_block_body(then_block)?;
self.indent -= 1;
if let Some(else_b) = else_block {
if let NodeKind::If {
let_pattern: nested_let,
..
} = &else_b.kind
{
if nested_let.is_none() {
let ind = self.indent_str();
let _ = write!(self.buf, "{ind}el");
return self.emit_stmt_if(else_b, true);
}
self.writeln("else:");
self.indent += 1;
let r = self.emit_stmt_if(else_b, false);
self.indent -= 1;
return r;
}
self.writeln("else:");
self.indent += 1;
self.emit_block_body(else_b)?;
self.indent -= 1;
}
Ok(())
}
fn emit_stmt_guard(
&mut self,
let_pattern: Option<&AIRNode>,
condition: &AIRNode,
else_block: &AIRNode,
) -> Result<(), CodegenError> {
if let Some(pat) = let_pattern {
let ind = self.indent_str();
let _ = write!(self.buf, "{ind}match ");
self.emit_expr(condition)?;
self.buf.push_str(":\n");
self.indent += 1;
let ind = self.indent_str();
let _ = write!(self.buf, "{ind}case ");
self.emit_pattern(pat)?;
self.buf.push_str(":\n");
self.indent += 1;
self.writeln("pass");
self.indent -= 1;
self.writeln("case _:");
self.indent += 1;
self.emit_block_body(else_block)?;
self.indent -= 1;
self.indent -= 1;
return Ok(());
}
let ind = self.indent_str();
let _ = write!(self.buf, "{ind}if not (");
self.emit_expr(condition)?;
self.buf.push_str("):\n");
self.indent += 1;
self.emit_block_body(else_block)?;
self.indent -= 1;
Ok(())
}
fn emit_expr(&mut self, node: &AIRNode) -> Result<(), CodegenError> {
match &node.kind {
NodeKind::Literal { lit } => {
match lit {
Literal::Int(s) => self.buf.push_str(s),
Literal::Float(s) => self.buf.push_str(s),
Literal::Bool(b) => self.buf.push_str(if *b { "True" } else { "False" }),
Literal::Char(s) => {
self.buf.push('\'');
self.buf.push_str(s);
self.buf.push('\'');
}
Literal::String(s) => {
self.buf.push('"');
self.buf.push_str(&escape_py_string(s));
self.buf.push('"');
}
Literal::Unit => self.buf.push_str("None"),
}
Ok(())
}
NodeKind::Identifier { name } => {
if name.name == "None" {
self.buf.push_str("_bock_none");
} else if let Some(variant) = crate::generator::ordering_variant(&name.name) {
self.buf.push_str(ordering_singleton_py(variant));
} else if let Some(enum_name) = self
.user_variant_for_name(&name.name)
.map(|i| i.enum_name.clone())
{
let _ = write!(self.buf, "{enum_name}_{}()", name.name);
} else if self.const_names.contains(&name.name) {
self.buf.push_str(&name.name);
} else {
let py = identifier_to_py(&name.name);
self.buf.push_str(&self.resolve_shadow_name(&py));
}
Ok(())
}
NodeKind::BinaryOp { op, left, right } => {
if matches!(op, BinOp::Div | BinOp::Rem) && crate::generator::is_int_arith(node) {
let lam = if matches!(op, BinOp::Div) {
"(lambda __a, __b: (abs(__a) // abs(__b)) * (1 if (__a < 0) == (__b < 0) else -1))("
} else {
"(lambda __a, __b: (abs(__a) % abs(__b)) * (1 if __a >= 0 else -1))("
};
self.buf.push_str(lam);
self.emit_expr(left)?;
self.buf.push_str(", ");
self.emit_expr(right)?;
self.buf.push(')');
return Ok(());
}
if crate::generator::is_user_compare(node) {
if let Some((tag, is_eq)) = crate::generator::user_compare_variant(*op) {
let recv = self.expr_to_string(left)?;
let other = self.expr_to_string(right)?;
let class = ordering_class_py(tag);
let neg = if is_eq { "" } else { "not " };
let _ = write!(
self.buf,
"({neg}isinstance(({recv}).compare({other}), {class}))"
);
return Ok(());
}
}
if matches!(op, BinOp::Eq | BinOp::Ne)
&& crate::generator::user_eq_kind(node) == Some("impl")
{
let recv = self.expr_to_string(left)?;
let other = self.expr_to_string(right)?;
let neg = if *op == BinOp::Ne { "not " } else { "" };
let _ = write!(self.buf, "({neg}({recv}).eq({other}))");
return Ok(());
}
self.buf.push('(');
self.emit_expr(left)?;
let op_str = match op {
BinOp::Add => " + ",
BinOp::Sub => " - ",
BinOp::Mul => " * ",
BinOp::Div => " / ",
BinOp::Rem => " % ",
BinOp::Pow => " ** ",
BinOp::Eq => " == ",
BinOp::Ne => " != ",
BinOp::Lt => " < ",
BinOp::Le => " <= ",
BinOp::Gt => " > ",
BinOp::Ge => " >= ",
BinOp::And => " and ",
BinOp::Or => " or ",
BinOp::BitAnd => " & ",
BinOp::BitOr => " | ",
BinOp::BitXor => " ^ ",
BinOp::Compose => " # compose ",
BinOp::Is => " is ",
};
self.buf.push_str(op_str);
self.emit_expr(right)?;
self.buf.push(')');
Ok(())
}
NodeKind::UnaryOp { op, operand } => {
let op_str = match op {
UnaryOp::Neg => "-",
UnaryOp::Not => "not ",
UnaryOp::BitNot => "~",
};
self.buf.push_str(op_str);
self.emit_expr(operand)?;
Ok(())
}
NodeKind::Call { callee, args, .. } => {
if let Some(code) = self.map_prelude_call(callee, args)? {
self.buf.push_str(&code);
return Ok(());
}
if let NodeKind::Identifier { name } = &callee.kind {
if let Some(enum_name) = self
.user_variant_for_name(&name.name)
.map(|i| i.enum_name.clone())
{
let _ = write!(self.buf, "{enum_name}_{}(", name.name);
for (i, arg) in args.iter().enumerate() {
if i > 0 {
self.buf.push_str(", ");
}
self.emit_expr(&arg.value)?;
}
self.buf.push(')');
return Ok(());
}
}
if self.try_emit_time_assoc_call(callee, args)? {
return Ok(());
}
if self.try_emit_time_desugared_method(node, callee, args)? {
return Ok(());
}
if self.try_emit_concurrency_call(callee, args)? {
return Ok(());
}
if self.try_emit_map_method(node, callee, args)? {
return Ok(());
}
if self.try_emit_set_method(node, callee, args)? {
return Ok(());
}
if self.try_emit_string_method(node, callee, args)? {
return Ok(());
}
if self.try_emit_numeric_method(node, callee, args)? {
return Ok(());
}
if self.try_emit_list_mutating_method(node, callee, args)? {
return Ok(());
}
if self.try_emit_list_inplace_mutator(node, callee, args)? {
return Ok(());
}
if self.try_emit_list_method(node, callee, args)? {
return Ok(());
}
if self.try_emit_list_functional_method(node, callee, args)? {
return Ok(());
}
if self.try_emit_primitive_bridge(node, callee, args)? {
return Ok(());
}
if self.try_emit_trait_bound_bridge(node, callee, args)? {
return Ok(());
}
if self.try_emit_container_method(node, callee, args)? {
return Ok(());
}
if self.try_emit_primitive_conversion(node, callee, args)? {
return Ok(());
}
if crate::generator::is_associated_call(node) {
if let NodeKind::FieldAccess { object, field } = &callee.kind {
if let NodeKind::Identifier { name: type_name } = &object.kind {
let _ = write!(
self.buf,
"{}.{}",
type_name.name,
self.py_method_name(&field.name)
);
self.buf.push('(');
for (i, arg) in args.iter().enumerate() {
if i > 0 {
self.buf.push_str(", ");
}
self.emit_expr(&arg.value)?;
}
self.buf.push(')');
return Ok(());
}
}
}
if let Some((recv, method, rest)) =
crate::generator::desugared_self_call(callee, args)
{
self.emit_expr(recv)?;
let _ = write!(self.buf, ".{}", self.py_method_name(&method.name));
self.buf.push('(');
for (i, arg) in rest.iter().enumerate() {
if i > 0 {
self.buf.push_str(", ");
}
self.emit_expr(&arg.value)?;
}
self.buf.push(')');
return Ok(());
}
if let NodeKind::Identifier { name } = &callee.kind {
if let Some(effect_name) = self.effect_ops.get(&name.name).cloned() {
if let Some(handler_var) =
self.current_handler_vars.get(&effect_name).cloned()
{
let _ =
write!(self.buf, "{}.{}", handler_var, to_snake_case(&name.name));
self.buf.push('(');
for (i, arg) in args.iter().enumerate() {
if i > 0 {
self.buf.push_str(", ");
}
self.emit_expr(&arg.value)?;
}
self.buf.push(')');
return Ok(());
}
}
}
let effects_args = if let NodeKind::Identifier { name } = &callee.kind {
self.build_effects_call_args_py(&name.name)
} else {
None
};
self.emit_callee(callee)?;
self.buf.push('(');
for (i, arg) in args.iter().enumerate() {
if i > 0 {
self.buf.push_str(", ");
}
self.emit_expr(&arg.value)?;
}
if let Some(ea) = effects_args {
if !args.is_empty() {
self.buf.push_str(", ");
}
self.buf.push_str(&ea);
}
self.buf.push(')');
Ok(())
}
NodeKind::MethodCall {
receiver,
method,
args,
..
} => {
if self.try_emit_time_method(receiver, &method.name, args)? {
return Ok(());
}
self.emit_expr(receiver)?;
let _ = write!(self.buf, ".{}", self.py_method_name(&method.name));
self.buf.push('(');
for (i, arg) in args.iter().enumerate() {
if i > 0 {
self.buf.push_str(", ");
}
self.emit_expr(&arg.value)?;
}
self.buf.push(')');
Ok(())
}
NodeKind::FieldAccess { object, field } => {
self.emit_expr(object)?;
let _ = write!(self.buf, ".{}", py_field_ident(&field.name));
Ok(())
}
NodeKind::Index { object, index } => {
self.emit_expr(object)?;
self.buf.push('[');
self.emit_expr(index)?;
self.buf.push(']');
Ok(())
}
NodeKind::Lambda { params, body } => {
let param_strs = self.collect_param_strs_bare(params);
let _ = write!(self.buf, "lambda {}: ", param_strs.join(", "));
self.emit_expr(body)?;
Ok(())
}
NodeKind::Pipe { left, right } => self.emit_pipe(left, right),
NodeKind::Compose { left, right } => {
self.buf.push_str("(lambda x: ");
self.emit_callee(right)?;
self.buf.push('(');
self.emit_callee(left)?;
self.buf.push_str("(x)))");
Ok(())
}
NodeKind::Await { expr } => {
self.buf.push_str("(await ");
self.emit_expr(expr)?;
self.buf.push(')');
Ok(())
}
NodeKind::Propagate { expr } => {
self.buf.push_str("_bock_try(");
self.emit_expr(expr)?;
self.buf.push(')');
Ok(())
}
NodeKind::Range { lo, hi, inclusive } => {
self.buf.push_str("range(");
self.emit_expr(lo)?;
self.buf.push_str(", ");
self.emit_expr(hi)?;
if *inclusive {
self.buf.push_str(" + 1");
}
self.buf.push(')');
Ok(())
}
NodeKind::RecordConstruct {
path,
fields,
spread,
} => {
let type_name = if let Some(info) = self.user_variant_for_path(path) {
let variant = path.segments.last().map_or("", |s| s.name.as_str());
format!("{}_{variant}", info.enum_name)
} else {
path.segments
.iter()
.map(|s| s.name.as_str())
.collect::<Vec<_>>()
.join(".")
};
if let Some(sp) = spread {
self.buf.push_str(&format!("{type_name}(**{{**vars("));
self.emit_expr(sp)?;
self.buf.push_str("), ");
for (i, f) in fields.iter().enumerate() {
if i > 0 {
self.buf.push_str(", ");
}
let _ = write!(self.buf, "\"{}\": ", py_field_ident(&f.name.name));
if let Some(val) = &f.value {
self.emit_expr(val)?;
} else {
self.buf.push_str(&py_value_ident(&f.name.name));
}
}
self.buf.push_str("})");
} else {
self.buf.push_str(&type_name);
self.buf.push('(');
for (i, f) in fields.iter().enumerate() {
if i > 0 {
self.buf.push_str(", ");
}
let _ = write!(self.buf, "{}=", py_field_ident(&f.name.name));
if let Some(val) = &f.value {
self.emit_expr(val)?;
} else {
self.buf.push_str(&py_value_ident(&f.name.name));
}
}
self.buf.push(')');
}
Ok(())
}
NodeKind::ListLiteral { elems } => {
self.buf.push('[');
for (i, e) in elems.iter().enumerate() {
if i > 0 {
self.buf.push_str(", ");
}
self.emit_expr(e)?;
}
self.buf.push(']');
Ok(())
}
NodeKind::MapLiteral { entries } => {
self.buf.push('{');
for (i, entry) in entries.iter().enumerate() {
if i > 0 {
self.buf.push_str(", ");
}
self.emit_expr(&entry.key)?;
self.buf.push_str(": ");
self.emit_expr(&entry.value)?;
}
self.buf.push('}');
Ok(())
}
NodeKind::SetLiteral { elems } => {
if elems.is_empty() {
self.buf.push_str("set()");
} else {
self.buf.push('{');
for (i, e) in elems.iter().enumerate() {
if i > 0 {
self.buf.push_str(", ");
}
self.emit_expr(e)?;
}
self.buf.push('}');
}
Ok(())
}
NodeKind::TupleLiteral { elems } => {
self.buf.push('(');
for (i, e) in elems.iter().enumerate() {
if i > 0 {
self.buf.push_str(", ");
}
self.emit_expr(e)?;
}
if elems.len() == 1 {
self.buf.push(',');
}
self.buf.push(')');
Ok(())
}
NodeKind::Interpolation { parts } => {
let has_newline = parts.iter().any(|p| {
matches!(p,
AirInterpolationPart::Literal(s) if s.contains('\n')
)
});
if has_newline {
self.buf.push_str("f\"\"\"");
} else {
self.buf.push_str("f\"");
}
for part in parts {
match part {
AirInterpolationPart::Literal(s) => {
if has_newline {
self.buf.push_str(&escape_fstring_triple(s));
} else {
self.buf.push_str(&escape_fstring(s));
}
}
AirInterpolationPart::Expr(expr) => {
self.buf.push('{');
if crate::generator::is_bool_stringify(expr) {
self.buf.push_str("'true' if (");
self.emit_expr(expr)?;
self.buf.push_str(") else 'false'");
} else {
self.needs_runtime_str = true;
self.buf.push_str("_bock_str(");
self.emit_expr(expr)?;
self.buf.push(')');
}
self.buf.push('}');
}
}
}
if has_newline {
self.buf.push_str("\"\"\"");
} else {
self.buf.push('"');
}
Ok(())
}
NodeKind::Placeholder => {
self.buf.push('_');
Ok(())
}
NodeKind::Unreachable => {
self.buf.push_str("raise RuntimeError(\"unreachable\")");
Ok(())
}
NodeKind::ResultConstruct { variant, value } => {
let cls = match variant {
ResultVariant::Ok => "_BockOk",
ResultVariant::Err => "_BockErr",
};
let _ = write!(self.buf, "{cls}(");
if let Some(v) = value {
self.emit_expr(v)?;
} else {
self.buf.push_str("None");
}
self.buf.push(')');
Ok(())
}
NodeKind::Assign { op, target, value } => {
self.emit_expr(target)?;
let op_str = match op {
AssignOp::Assign => " = ",
AssignOp::AddAssign => " += ",
AssignOp::SubAssign => " -= ",
AssignOp::MulAssign => " *= ",
AssignOp::DivAssign => " /= ",
AssignOp::RemAssign => " %= ",
};
self.buf.push_str(op_str);
self.emit_expr(value)?;
Ok(())
}
NodeKind::If {
condition,
then_block,
else_block,
..
} => {
self.buf.push('(');
self.emit_block_as_expr(then_block)?;
self.buf.push_str(" if ");
self.emit_expr(condition)?;
self.buf.push_str(" else ");
if let Some(eb) = else_block {
self.emit_block_as_expr(eb)?;
} else {
self.buf.push_str("None");
}
self.buf.push(')');
Ok(())
}
NodeKind::Block { stmts, tail } => {
if stmts.is_empty() {
if let Some(t) = tail {
return self.emit_expr(t);
}
} else if self.try_emit_block_stmts_as_expr(stmts, tail.as_deref())? {
return Ok(());
}
self.buf.push_str("(lambda: ");
if let Some(t) = tail {
self.emit_expr(t)?;
} else {
self.buf.push_str("None");
}
self.buf.push_str(")()");
Ok(())
}
NodeKind::Match { scrutinee, arms } => {
self.buf.push_str("(lambda __v: ");
self.emit_match_expr(scrutinee, arms)?;
self.buf.push_str(")(");
self.emit_expr(scrutinee)?;
self.buf.push(')');
Ok(())
}
NodeKind::Move { expr }
| NodeKind::Borrow { expr }
| NodeKind::MutableBorrow { expr } => self.emit_expr(expr),
NodeKind::EffectOp {
effect,
operation,
args,
} => {
let effect_name = effect.segments.last().map_or("effect", |s| s.name.as_str());
let _ = write!(
self.buf,
"{}.{}",
to_snake_case(effect_name),
to_snake_case(&operation.name)
);
self.buf.push('(');
for (i, arg) in args.iter().enumerate() {
if i > 0 {
self.buf.push_str(", ");
}
self.emit_expr(&arg.value)?;
}
self.buf.push(')');
Ok(())
}
NodeKind::TypeNamed { .. }
| NodeKind::TypeTuple { .. }
| NodeKind::TypeFunction { .. }
| NodeKind::TypeOptional { .. }
| NodeKind::TypeSelf => {
self.buf.push_str("# type");
Ok(())
}
NodeKind::EffectRef { path } => {
let name = path
.segments
.iter()
.map(|s| s.name.as_str())
.collect::<Vec<_>>()
.join(".");
self.buf.push_str(&name);
Ok(())
}
NodeKind::Error => {
self.buf.push_str("# error");
Ok(())
}
_ => {
self.buf.push_str("# unsupported");
Ok(())
}
}
}
fn emit_match(&mut self, scrutinee: &AIRNode, arms: &[AIRNode]) -> Result<(), CodegenError> {
let ind = self.indent_str();
let _ = write!(self.buf, "{ind}match ");
self.emit_expr(scrutinee)?;
self.buf.push_str(":\n");
self.indent += 1;
for arm in arms {
self.emit_match_arm(arm)?;
}
self.indent -= 1;
Ok(())
}
fn emit_match_arm(&mut self, arm: &AIRNode) -> Result<(), CodegenError> {
if let NodeKind::MatchArm {
pattern,
guard,
body,
} = &arm.kind
{
let ind = self.indent_str();
let _ = write!(self.buf, "{ind}case ");
if let NodeKind::RangePat { lo, hi, inclusive } = &pattern.kind {
let lo_s = range_bound_to_py(lo);
let hi_s = range_bound_to_py(hi);
let upper = if *inclusive { "<=" } else { "<" };
let _ = write!(self.buf, "__rv if {lo_s} <= __rv {upper} {hi_s}");
if let Some(g) = guard {
self.buf.push_str(" and (");
self.emit_expr(g)?;
self.buf.push(')');
}
} else {
self.emit_pattern(pattern)?;
if let Some(g) = guard {
self.buf.push_str(" if ");
self.emit_expr(g)?;
}
}
self.buf.push_str(":\n");
self.indent += 1;
self.emit_block_body(body)?;
self.indent -= 1;
}
Ok(())
}
fn emit_pattern(&mut self, pat: &AIRNode) -> Result<(), CodegenError> {
match &pat.kind {
NodeKind::WildcardPat => {
self.buf.push('_');
}
NodeKind::BindPat { name, .. } => {
self.buf.push_str(&py_value_ident(&name.name));
}
NodeKind::LiteralPat { lit } => match lit {
Literal::Int(s) => self.buf.push_str(s),
Literal::Float(s) => self.buf.push_str(s),
Literal::Bool(b) => self.buf.push_str(if *b { "True" } else { "False" }),
Literal::Char(s) => {
self.buf.push('\'');
self.buf.push_str(s);
self.buf.push('\'');
}
Literal::String(s) => {
self.buf.push('"');
self.buf.push_str(&escape_py_string(s));
self.buf.push('"');
}
Literal::Unit => self.buf.push_str("None"),
},
NodeKind::ConstructorPat { path, fields } => {
let leaf = path.segments.last().map_or("", |s| s.name.as_str());
match leaf {
"Some" => {
if let Some(f) = fields.first() {
let sub = self.pattern_to_py(f)?;
let _ = write!(self.buf, "_BockSome({sub})");
} else {
self.buf.push_str("_BockSome(_)");
}
return Ok(());
}
"None" => {
self.buf.push_str("_BockNone()");
return Ok(());
}
"Ok" | "Err" => {
let cls = if leaf == "Ok" { "_BockOk" } else { "_BockErr" };
if let Some(f) = fields.first() {
let sub = self.pattern_to_py(f)?;
let _ = write!(self.buf, "{cls}({sub})");
} else {
let _ = write!(self.buf, "{cls}(_)");
}
return Ok(());
}
_ => {}
}
if let Some(variant) = crate::generator::ordering_variant(leaf) {
let _ = write!(self.buf, "{}()", ordering_class_py(variant));
return Ok(());
}
let variant_name = if let Some(info) = self.user_variant_for_path(path) {
let variant = path.segments.last().map_or("", |s| s.name.as_str());
format!("{}_{variant}", info.enum_name)
} else {
path.segments
.iter()
.map(|s| s.name.as_str())
.collect::<Vec<_>>()
.join("_")
};
if fields.is_empty() {
let _ = write!(self.buf, "{variant_name}()");
} else {
let mut field_pats: Vec<String> = Vec::with_capacity(fields.len());
for (i, f) in fields.iter().enumerate() {
let sub = self.pattern_to_py(f)?;
field_pats.push(format!("_{i}={sub}"));
}
let _ = write!(self.buf, "{variant_name}({})", field_pats.join(", "));
}
}
NodeKind::RecordPat { path, fields, .. } => {
let type_name = if let Some(info) = self.user_variant_for_path(path) {
let variant = path.segments.last().map_or("", |s| s.name.as_str());
format!("{}_{variant}", info.enum_name)
} else {
path.segments
.iter()
.map(|s| s.name.as_str())
.collect::<Vec<_>>()
.join("_")
};
let mut field_pats: Vec<String> = Vec::with_capacity(fields.len());
for f in fields {
let field_name = py_field_ident(&f.name.name);
if let Some(pat) = &f.pattern {
let sub = self.pattern_to_py(pat)?;
field_pats.push(format!("{field_name}={sub}"));
} else {
field_pats.push(format!("{field_name}={field_name}"));
}
}
let _ = write!(self.buf, "{type_name}({})", field_pats.join(", "));
}
NodeKind::TuplePat { elems } => {
self.buf.push('(');
for (i, e) in elems.iter().enumerate() {
if i > 0 {
self.buf.push_str(", ");
}
self.emit_pattern(e)?;
}
if elems.len() == 1 {
self.buf.push(',');
}
self.buf.push(')');
}
NodeKind::OrPat { alternatives } => {
for (i, alt) in alternatives.iter().enumerate() {
if i > 0 {
self.buf.push_str(" | ");
}
self.emit_pattern(alt)?;
}
}
NodeKind::ListPat { elems, rest } => {
self.buf.push('[');
for (i, e) in elems.iter().enumerate() {
if i > 0 {
self.buf.push_str(", ");
}
self.emit_pattern(e)?;
}
if let Some(r) = rest {
if !elems.is_empty() {
self.buf.push_str(", ");
}
match &r.kind {
NodeKind::BindPat { name, .. } => {
let _ = write!(self.buf, "*{}", py_value_ident(&name.name));
}
_ => self.buf.push_str("*_"),
}
}
self.buf.push(']');
}
_ => {
self.buf.push('_');
}
}
Ok(())
}
fn emit_match_expr(
&mut self,
_scrutinee: &AIRNode,
arms: &[AIRNode],
) -> Result<(), CodegenError> {
self.emit_match_expr_from(arms, 0)
}
fn emit_match_expr_from(&mut self, arms: &[AIRNode], idx: usize) -> Result<(), CodegenError> {
let Some(NodeKind::MatchArm { pattern, body, .. }) = arms.get(idx).map(|a| &a.kind) else {
self.buf.push_str("None");
return Ok(());
};
let is_last = idx + 1 >= arms.len();
let is_catch_all = matches!(
pattern.kind,
NodeKind::WildcardPat | NodeKind::BindPat { .. }
);
if is_last || is_catch_all {
return self.emit_arm_value(pattern, body, true);
}
self.emit_arm_value(pattern, body, false)?;
self.buf.push_str(" if ");
self.emit_match_expr_test(pattern)?;
self.buf.push_str(" else (");
self.emit_match_expr_from(arms, idx + 1)?;
self.buf.push(')');
Ok(())
}
fn emit_match_expr_test(&mut self, pattern: &AIRNode) -> Result<(), CodegenError> {
match &pattern.kind {
NodeKind::ConstructorPat { path, .. } => {
let leaf = path.segments.last().map_or("", |s| s.name.as_str());
let cls: String = match leaf {
"Some" => "_BockSome".to_string(),
"None" => "_BockNone".to_string(),
"Ok" => "_BockOk".to_string(),
"Err" => "_BockErr".to_string(),
other => {
if let Some(v) = crate::generator::ordering_variant(other) {
ordering_class_py(v).to_string()
} else if let Some(info) = self.user_variant_for_path(path) {
format!("{}_{other}", info.enum_name)
} else {
other.to_string()
}
}
};
let _ = write!(self.buf, "isinstance(__v, {cls})");
}
NodeKind::LiteralPat { .. } => {
self.buf.push_str("__v == ");
self.emit_pattern(pattern)?;
}
NodeKind::ListPat { elems, rest } => {
let n = elems.len();
let len_test = if rest.is_some() {
format!("isinstance(__v, list) and len(__v) >= {n}")
} else {
format!("isinstance(__v, list) and len(__v) == {n}")
};
self.buf.push_str(&len_test);
for (i, e) in elems.iter().enumerate() {
if let NodeKind::LiteralPat { .. } = &e.kind {
self.buf.push_str(&format!(" and __v[{i}] == "));
self.emit_pattern(e)?;
}
}
}
NodeKind::RangePat { lo, hi, inclusive } => {
let lo_s = range_bound_to_py(lo);
let hi_s = range_bound_to_py(hi);
let upper = if *inclusive { "<=" } else { "<" };
let _ = write!(self.buf, "{lo_s} <= __v {upper} {hi_s}");
}
_ => self.buf.push_str("True"),
}
Ok(())
}
fn emit_arm_value(
&mut self,
pattern: &AIRNode,
body: &AIRNode,
whole_scrutinee_bind: bool,
) -> Result<(), CodegenError> {
match &pattern.kind {
NodeKind::ConstructorPat { path, fields }
if path
.segments
.last()
.is_some_and(|s| matches!(s.name.as_str(), "Some" | "Ok" | "Err")) =>
{
if let Some(f) = fields.first() {
let name = self.pattern_to_binding_name(f);
if name != "_" {
let _ = write!(self.buf, "(lambda {name}: ");
self.emit_block_as_expr(body)?;
self.buf.push_str(")(__v._0)");
return Ok(());
}
}
self.emit_block_as_expr(body)
}
NodeKind::BindPat { name, .. } if whole_scrutinee_bind => {
let bind = py_value_ident(&name.name);
let _ = write!(self.buf, "(lambda {bind}: ");
self.emit_block_as_expr(body)?;
self.buf.push_str(")(__v)");
Ok(())
}
NodeKind::ListPat { elems, rest } => {
let mut params: Vec<String> = Vec::new();
let mut argvals: Vec<String> = Vec::new();
for (i, e) in elems.iter().enumerate() {
if let NodeKind::BindPat { name, .. } = &e.kind {
params.push(py_value_ident(&name.name));
argvals.push(format!("__v[{i}]"));
}
}
if let Some(r) = rest {
if let NodeKind::BindPat { name, .. } = &r.kind {
params.push(py_value_ident(&name.name));
argvals.push(format!("__v[{}:]", elems.len()));
}
}
if params.is_empty() {
return self.emit_block_as_expr(body);
}
let _ = write!(self.buf, "(lambda {}: ", params.join(", "));
self.emit_block_as_expr(body)?;
let _ = write!(self.buf, ")({})", argvals.join(", "));
Ok(())
}
_ => self.emit_block_as_expr(body),
}
}
fn emit_callee(&mut self, callee: &AIRNode) -> Result<(), CodegenError> {
if matches!(callee.kind, NodeKind::Lambda { .. }) {
self.buf.push('(');
self.emit_expr(callee)?;
self.buf.push(')');
Ok(())
} else {
self.emit_expr(callee)
}
}
fn emit_pipe(&mut self, left: &AIRNode, right: &AIRNode) -> Result<(), CodegenError> {
if let NodeKind::Call { callee, args, .. } = &right.kind {
let has_placeholder = args
.iter()
.any(|a| matches!(a.value.kind, NodeKind::Placeholder));
if has_placeholder {
self.emit_callee(callee)?;
self.buf.push('(');
for (i, arg) in args.iter().enumerate() {
if i > 0 {
self.buf.push_str(", ");
}
if matches!(arg.value.kind, NodeKind::Placeholder) {
self.emit_expr(left)?;
} else {
self.emit_expr(&arg.value)?;
}
}
self.buf.push(')');
return Ok(());
}
}
self.emit_callee(right)?;
self.buf.push('(');
self.emit_expr(left)?;
self.buf.push(')');
Ok(())
}
fn type_to_py(&self, node: &AIRNode) -> String {
match &node.kind {
NodeKind::TypeNamed { path, args } => {
let name = path
.segments
.iter()
.map(|s| s.name.as_str())
.collect::<Vec<_>>()
.join(".");
if name == "Result" {
return "_BockOk | _BockErr".to_string();
}
let py_name = self.map_type_name(&name);
if args.is_empty() {
py_name
} else {
let arg_strs: Vec<String> = args.iter().map(|a| self.type_to_py(a)).collect();
format!("{py_name}[{}]", arg_strs.join(", "))
}
}
NodeKind::TypeTuple { elems } => {
let elem_strs: Vec<String> = elems.iter().map(|e| self.type_to_py(e)).collect();
format!("tuple[{}]", elem_strs.join(", "))
}
NodeKind::TypeFunction { params, ret, .. } => {
self.needs_typing_callable.set(true);
let param_strs: Vec<String> = params.iter().map(|p| self.type_to_py(p)).collect();
format!(
"Callable[[{}], {}]",
param_strs.join(", "),
self.type_to_py(ret)
)
}
NodeKind::TypeOptional { inner } => {
let _ = inner;
"_BockSome | _BockNone".to_string()
}
NodeKind::TypeSelf => {
self.needs_typing_self.set(true);
"Self".into()
}
_ => {
self.needs_typing_any.set(true);
"Any".into()
}
}
}
fn map_type_name(&self, name: &str) -> String {
match name {
"Int" => "int".into(),
"Float" => "float".into(),
"Bool" => "bool".into(),
"String" => "str".into(),
"Void" | "Unit" => "None".into(),
"List" => "list".into(),
"Map" => "dict".into(),
"Set" => "set".into(),
"Any" => {
self.needs_typing_any.set(true);
"Any".into()
}
"Never" => {
self.needs_typing_never.set(true);
"Never".into()
}
other => other.into(),
}
}
fn ast_type_to_py(&self, ty: &TypeExpr) -> String {
match ty {
TypeExpr::Named { path, args, .. } => {
let name = path
.segments
.iter()
.map(|s| s.name.as_str())
.collect::<Vec<_>>()
.join(".");
if name == "Result" {
return "_BockOk | _BockErr".to_string();
}
let py_name = self.map_type_name(&name);
if args.is_empty() {
py_name
} else {
let arg_strs: Vec<String> =
args.iter().map(|a| self.ast_type_to_py(a)).collect();
format!("{py_name}[{}]", arg_strs.join(", "))
}
}
TypeExpr::Tuple { elems, .. } => {
let elem_strs: Vec<String> = elems.iter().map(|e| self.ast_type_to_py(e)).collect();
format!("tuple[{}]", elem_strs.join(", "))
}
TypeExpr::Function { params, ret, .. } => {
self.needs_typing_callable.set(true);
let param_strs: Vec<String> =
params.iter().map(|p| self.ast_type_to_py(p)).collect();
format!(
"Callable[[{}], {}]",
param_strs.join(", "),
self.ast_type_to_py(ret)
)
}
TypeExpr::Optional { inner, .. } => {
let _ = inner;
"_BockSome | _BockNone".to_string()
}
TypeExpr::SelfType { .. } => {
self.needs_typing_self.set(true);
"Self".into()
}
}
}
fn collect_task_bindings(stmts: &[AIRNode]) -> std::collections::HashSet<String> {
let mut awaited: std::collections::HashSet<String> = std::collections::HashSet::new();
for s in stmts {
Self::collect_awaited_identifiers(s, &mut awaited);
}
let mut out = std::collections::HashSet::new();
for s in stmts {
if let NodeKind::LetBinding { pattern, value, .. } = &s.kind {
if let NodeKind::BindPat { name, .. } = &pattern.kind {
let py_name = py_value_ident(&name.name);
if matches!(&value.kind, NodeKind::Call { .. }) && awaited.contains(&py_name) {
out.insert(py_name);
}
}
}
}
out
}
fn collect_awaited_identifiers(node: &AIRNode, out: &mut std::collections::HashSet<String>) {
match &node.kind {
NodeKind::Await { expr } => {
if let NodeKind::Identifier { name } = &expr.kind {
out.insert(py_value_ident(&name.name));
}
Self::collect_awaited_identifiers(expr, out);
}
NodeKind::Lambda { .. } | NodeKind::FnDecl { .. } => {
}
NodeKind::Block { stmts, tail } => {
for s in stmts {
Self::collect_awaited_identifiers(s, out);
}
if let Some(t) = tail {
Self::collect_awaited_identifiers(t, out);
}
}
NodeKind::LetBinding { value, .. } => {
Self::collect_awaited_identifiers(value, out);
}
NodeKind::Call { callee, args, .. } => {
Self::collect_awaited_identifiers(callee, out);
for a in args {
Self::collect_awaited_identifiers(&a.value, out);
}
}
NodeKind::MethodCall { receiver, args, .. } => {
Self::collect_awaited_identifiers(receiver, out);
for a in args {
Self::collect_awaited_identifiers(&a.value, out);
}
}
NodeKind::BinaryOp { left, right, .. } => {
Self::collect_awaited_identifiers(left, out);
Self::collect_awaited_identifiers(right, out);
}
NodeKind::UnaryOp { operand, .. } => {
Self::collect_awaited_identifiers(operand, out);
}
NodeKind::If {
condition,
then_block,
else_block,
..
} => {
Self::collect_awaited_identifiers(condition, out);
Self::collect_awaited_identifiers(then_block, out);
if let Some(e) = else_block {
Self::collect_awaited_identifiers(e, out);
}
}
NodeKind::While { condition, body } => {
Self::collect_awaited_identifiers(condition, out);
Self::collect_awaited_identifiers(body, out);
}
NodeKind::For { iterable, body, .. } => {
Self::collect_awaited_identifiers(iterable, out);
Self::collect_awaited_identifiers(body, out);
}
NodeKind::Return { value: Some(v) } | NodeKind::Break { value: Some(v) } => {
Self::collect_awaited_identifiers(v, out);
}
NodeKind::Assign { value, .. } => {
Self::collect_awaited_identifiers(value, out);
}
NodeKind::TupleLiteral { elems } | NodeKind::ListLiteral { elems } => {
for e in elems {
Self::collect_awaited_identifiers(e, out);
}
}
_ => {}
}
}
fn enter_shadow_scope(&mut self) {
let mut frame = ShadowScope::default();
for n in self.pending_scope_seed.drain(..) {
frame.bound.insert(n);
}
self.shadow_scopes.push(frame);
}
fn leave_shadow_scope(&mut self) {
self.shadow_scopes.pop();
}
fn shadowed_in_outer_scope(&self, py_name: &str) -> bool {
let n = self.shadow_scopes.len();
if n < 2 {
return false;
}
self.shadow_scopes[..n - 1]
.iter()
.any(|s| s.bound.contains(py_name) || s.renames.contains_key(py_name))
}
fn resolve_shadow_name(&self, py_name: &str) -> String {
for s in self.shadow_scopes.iter().rev() {
if let Some(alias) = s.renames.get(py_name) {
return alias.clone();
}
if s.bound.contains(py_name) {
return py_name.to_string();
}
}
py_name.to_string()
}
fn plan_shadow_let(&mut self, py_name: &str) -> (String, Option<(String, String)>) {
if self.shadow_scopes.is_empty() {
return (py_name.to_string(), None);
}
if let Some(cur) = self.shadow_scopes.last() {
if let Some(alias) = cur.renames.get(py_name) {
return (alias.clone(), None);
}
if cur.bound.contains(py_name) {
return (py_name.to_string(), None);
}
}
if self.shadowed_in_outer_scope(py_name) {
self.shadow_counter += 1;
let alias = format!("{py_name}__s{}", self.shadow_counter);
return (alias.clone(), Some((py_name.to_string(), alias)));
}
if is_shadow_sensitive_py_builtin(py_name) {
self.shadow_counter += 1;
let alias = format!("{py_name}__b{}", self.shadow_counter);
return (alias.clone(), Some((py_name.to_string(), alias)));
}
(py_name.to_string(), None)
}
fn commit_shadow_let(&mut self, py_name: &str, pending: Option<(String, String)>) {
let Some(cur) = self.shadow_scopes.last_mut() else {
return;
};
if let Some((orig, alias)) = pending {
cur.renames.insert(orig, alias.clone());
cur.bound.insert(alias);
} else {
cur.bound.insert(py_name.to_string());
}
}
fn emit_fn_body(&mut self, body: &AIRNode) -> Result<(), CodegenError> {
if body_contains_propagate(body) {
self.writeln("try:");
self.indent += 1;
self.emit_block_body(body)?;
self.indent -= 1;
self.writeln("except _BockPropagate as __bock_p:");
self.indent += 1;
self.writeln("return __bock_p.value");
self.indent -= 1;
Ok(())
} else {
self.emit_block_body(body)
}
}
fn emit_block_body(&mut self, node: &AIRNode) -> Result<(), CodegenError> {
self.enter_shadow_scope();
let r = self.emit_block_body_inner(node);
self.leave_shadow_scope();
r
}
fn emit_loop_body(&mut self, node: &AIRNode) -> Result<(), CodegenError> {
let prev = std::mem::replace(&mut self.in_loop_body_tail, true);
let r = self.emit_block_body(node);
self.in_loop_body_tail = prev;
r
}
fn emit_block_body_inner(&mut self, node: &AIRNode) -> Result<(), CodegenError> {
if let NodeKind::Block { stmts, tail } = &node.kind {
if stmts.is_empty() && tail.is_none() {
self.writeln("pass");
return Ok(());
}
let task_bindings = Self::collect_task_bindings(stmts);
let prev = std::mem::replace(&mut self.task_bound_names, task_bindings);
for s in stmts {
self.emit_node(s)?;
}
self.task_bound_names = prev;
if let Some(t) = tail {
if crate::generator::node_is_statement(t) {
self.emit_node(t)?;
return Ok(());
}
if matches!(
t.kind,
NodeKind::Loop { .. } | NodeKind::While { .. } | NodeKind::For { .. }
) {
self.emit_node(t)?;
return Ok(());
}
if is_raise_expr(t) {
self.write_indent();
self.emit_expr(t)?;
self.buf.push('\n');
return Ok(());
}
if control_flow_has_raise_branch(t) {
return self.emit_tail_control_flow(t);
}
if let NodeKind::Match { scrutinee, arms } = &t.kind {
if crate::generator::match_has_statement_arm(arms)
|| match_value_needs_stmt_form(arms)
{
self.emit_match(scrutinee, arms)?;
return Ok(());
}
}
if if_value_needs_stmt_form(t) {
return self.emit_tail_control_flow(t);
}
self.emit_tail_value_or_discard(t)?;
}
} else if crate::generator::node_is_statement(node) {
self.emit_node(node)?;
} else if matches!(
node.kind,
NodeKind::Loop { .. } | NodeKind::While { .. } | NodeKind::For { .. }
) {
self.emit_node(node)?;
} else if is_raise_expr(node) {
self.write_indent();
self.emit_expr(node)?;
self.buf.push('\n');
} else if control_flow_has_raise_branch(node) {
return self.emit_tail_control_flow(node);
} else if let NodeKind::Match { scrutinee, arms } = &node.kind {
if crate::generator::match_has_statement_arm(arms) || match_value_needs_stmt_form(arms)
{
self.emit_match(scrutinee, arms)?;
} else {
self.emit_tail_value_or_discard(node)?;
}
} else if if_value_needs_stmt_form(node) {
return self.emit_tail_control_flow(node);
} else {
self.emit_tail_value_or_discard(node)?;
}
Ok(())
}
fn emit_tail_value_or_discard(&mut self, node: &AIRNode) -> Result<(), CodegenError> {
let ind = self.indent_str();
if self.in_loop_body_tail || self.in_stmt_construct_arm {
self.buf.push_str(&ind);
} else {
let _ = write!(self.buf, "{ind}return ");
}
self.emit_expr(node)?;
self.buf.push('\n');
Ok(())
}
fn emit_tail_control_flow(&mut self, node: &AIRNode) -> Result<(), CodegenError> {
match &node.kind {
NodeKind::If {
condition,
then_block,
else_block,
..
} => {
let ind = self.indent_str();
let _ = write!(self.buf, "{ind}if ");
self.emit_expr(condition)?;
self.buf.push_str(":\n");
self.indent += 1;
self.emit_block_body(then_block)?;
self.indent -= 1;
if let Some(eb) = else_block {
if matches!(eb.kind, NodeKind::If { .. }) {
let ind = self.indent_str();
let _ = write!(self.buf, "{ind}el");
return self.emit_tail_control_flow_inline(eb);
}
self.writeln("else:");
self.indent += 1;
self.emit_block_body(eb)?;
self.indent -= 1;
}
Ok(())
}
NodeKind::Match { scrutinee, arms } => self.emit_match(scrutinee, arms),
_ => self.emit_tail_value_or_discard(node),
}
}
fn emit_tail_control_flow_inline(&mut self, node: &AIRNode) -> Result<(), CodegenError> {
let NodeKind::If {
condition,
then_block,
else_block,
..
} = &node.kind
else {
return self.emit_tail_control_flow(node);
};
self.buf.push_str("if ");
self.emit_expr(condition)?;
self.buf.push_str(":\n");
self.indent += 1;
self.emit_block_body(then_block)?;
self.indent -= 1;
if let Some(eb) = else_block {
if matches!(eb.kind, NodeKind::If { .. }) {
let ind = self.indent_str();
let _ = write!(self.buf, "{ind}el");
return self.emit_tail_control_flow_inline(eb);
}
self.writeln("else:");
self.indent += 1;
self.emit_block_body(eb)?;
self.indent -= 1;
}
Ok(())
}
fn emit_block_body_assigning(
&mut self,
target: &str,
node: &AIRNode,
) -> Result<(), CodegenError> {
let prev_discard = std::mem::replace(&mut self.in_loop_body_tail, false);
let prev_match = std::mem::replace(&mut self.in_stmt_construct_arm, false);
self.enter_shadow_scope();
let r = self.emit_block_body_assigning_inner(target, node);
self.leave_shadow_scope();
self.in_loop_body_tail = prev_discard;
self.in_stmt_construct_arm = prev_match;
r
}
fn emit_block_body_assigning_inner(
&mut self,
target: &str,
node: &AIRNode,
) -> Result<(), CodegenError> {
if let NodeKind::Block { stmts, tail } = &node.kind {
let task_bindings = Self::collect_task_bindings(stmts);
let prev = std::mem::replace(&mut self.task_bound_names, task_bindings);
for s in stmts {
self.emit_node(s)?;
}
self.task_bound_names = prev;
match tail {
None => {
if stmts.is_empty() {
self.writeln("pass");
}
}
Some(t) if crate::generator::node_is_statement(t) => {
self.emit_node(t)?;
}
Some(t) => self.emit_value_assign(target, t)?,
}
} else if crate::generator::node_is_statement(node) {
self.emit_node(node)?;
} else {
self.emit_value_assign(target, node)?;
}
Ok(())
}
fn emit_value_assign(&mut self, target: &str, expr: &AIRNode) -> Result<(), CodegenError> {
if value_needs_stmt_form(expr) {
return self.emit_value_binding(target, expr);
}
if is_raise_expr(expr) {
self.write_indent();
self.emit_expr(expr)?;
self.buf.push('\n');
return Ok(());
}
let ind = self.indent_str();
let _ = write!(self.buf, "{ind}{target} = ");
self.emit_expr(expr)?;
self.buf.push('\n');
Ok(())
}
fn emit_value_binding(&mut self, target: &str, value: &AIRNode) -> Result<(), CodegenError> {
match &value.kind {
NodeKind::Block { .. } => self.emit_block_body_assigning(target, value),
NodeKind::Match { scrutinee, arms } => {
self.emit_match_assigning(target, scrutinee, arms)
}
NodeKind::If {
condition,
then_block,
else_block,
..
} => {
let ind = self.indent_str();
let _ = write!(self.buf, "{ind}if ");
self.emit_expr(condition)?;
self.buf.push_str(":\n");
self.indent += 1;
self.emit_block_body_assigning(target, then_block)?;
self.indent -= 1;
if let Some(eb) = else_block {
if matches!(eb.kind, NodeKind::If { .. }) {
let ind = self.indent_str();
let _ = write!(self.buf, "{ind}el");
self.emit_value_binding_if_chain(target, eb)?;
} else {
self.writeln("else:");
self.indent += 1;
self.emit_block_body_assigning(target, eb)?;
self.indent -= 1;
}
}
Ok(())
}
NodeKind::Loop { body } => {
self.writeln("while True:");
self.indent += 1;
self.loop_value_targets.push(Some(target.to_string()));
self.emit_loop_body(body)?;
self.loop_value_targets.pop();
self.indent -= 1;
Ok(())
}
NodeKind::While { condition, body } => {
let ind = self.indent_str();
let _ = write!(self.buf, "{ind}while ");
self.emit_expr(condition)?;
self.buf.push_str(":\n");
self.indent += 1;
self.loop_value_targets.push(Some(target.to_string()));
self.emit_loop_body(body)?;
self.loop_value_targets.pop();
self.indent -= 1;
Ok(())
}
_ => self.emit_value_assign(target, value),
}
}
fn emit_value_binding_if_chain(
&mut self,
target: &str,
node: &AIRNode,
) -> Result<(), CodegenError> {
let NodeKind::If {
condition,
then_block,
else_block,
..
} = &node.kind
else {
return self.emit_value_binding(target, node);
};
self.buf.push_str("if ");
self.emit_expr(condition)?;
self.buf.push_str(":\n");
self.indent += 1;
self.emit_block_body_assigning(target, then_block)?;
self.indent -= 1;
if let Some(eb) = else_block {
if matches!(eb.kind, NodeKind::If { .. }) {
let ind = self.indent_str();
let _ = write!(self.buf, "{ind}el");
self.emit_value_binding_if_chain(target, eb)?;
} else {
self.writeln("else:");
self.indent += 1;
self.emit_block_body_assigning(target, eb)?;
self.indent -= 1;
}
}
Ok(())
}
fn emit_match_assigning(
&mut self,
target: &str,
scrutinee: &AIRNode,
arms: &[AIRNode],
) -> Result<(), CodegenError> {
let ind = self.indent_str();
let _ = write!(self.buf, "{ind}match ");
self.emit_expr(scrutinee)?;
self.buf.push_str(":\n");
self.indent += 1;
for arm in arms {
if let NodeKind::MatchArm {
pattern,
guard,
body,
} = &arm.kind
{
let ind = self.indent_str();
let _ = write!(self.buf, "{ind}case ");
self.emit_pattern(pattern)?;
if let Some(g) = guard {
self.buf.push_str(" if ");
self.emit_expr(g)?;
}
self.buf.push_str(":\n");
self.indent += 1;
self.emit_block_body_assigning(target, body)?;
self.indent -= 1;
}
}
self.indent -= 1;
Ok(())
}
fn emit_block_as_expr(&mut self, node: &AIRNode) -> Result<(), CodegenError> {
if let NodeKind::Block { stmts, tail } = &node.kind {
if stmts.is_empty() {
if let Some(t) = tail {
return self.emit_expr(t);
}
} else if self.try_emit_block_stmts_as_expr(stmts, tail.as_deref())? {
return Ok(());
}
}
self.emit_expr(node)
}
fn try_emit_block_stmts_as_expr(
&mut self,
stmts: &[AIRNode],
tail: Option<&AIRNode>,
) -> Result<bool, CodegenError> {
for s in stmts {
match &s.kind {
NodeKind::LetBinding {
is_mut, pattern, ..
} if *is_mut || Self::simple_bind_name(pattern).is_none() => {
return Ok(false);
}
NodeKind::LetBinding { .. } => {}
NodeKind::Assign { .. }
| NodeKind::While { .. }
| NodeKind::For { .. }
| NodeKind::Loop { .. }
| NodeKind::Return { .. }
| NodeKind::Break { .. }
| NodeKind::Continue
| NodeKind::Block { .. } => return Ok(false),
_ => {}
}
}
self.emit_block_stmt_chain(stmts, tail)?;
Ok(true)
}
fn emit_block_stmt_chain(
&mut self,
stmts: &[AIRNode],
tail: Option<&AIRNode>,
) -> Result<(), CodegenError> {
let Some((first, rest)) = stmts.split_first() else {
return match tail {
Some(t) => self.emit_expr(t),
None => {
self.buf.push_str("None");
Ok(())
}
};
};
match &first.kind {
NodeKind::LetBinding { pattern, value, .. } => {
let name = Self::simple_bind_name(pattern).unwrap_or_else(|| "_".to_string());
let _ = write!(self.buf, "(lambda {name}: ");
self.emit_block_stmt_chain(rest, tail)?;
self.buf.push_str(")(");
self.emit_expr(value)?;
self.buf.push(')');
Ok(())
}
_ => {
self.buf.push_str("(lambda _: ");
self.emit_block_stmt_chain(rest, tail)?;
self.buf.push_str(")(");
self.emit_expr(first)?;
self.buf.push(')');
Ok(())
}
}
}
fn simple_bind_name(pat: &AIRNode) -> Option<String> {
match &pat.kind {
NodeKind::BindPat { name, .. } => Some(py_value_ident(&name.name)),
_ => None,
}
}
fn pattern_to_binding_name(&self, pat: &AIRNode) -> String {
match &pat.kind {
NodeKind::BindPat { name, .. } => py_value_ident(&name.name),
NodeKind::WildcardPat => "_".into(),
NodeKind::TuplePat { elems } => {
format!(
"({})",
elems
.iter()
.map(|e| self.pattern_to_binding_name(e))
.collect::<Vec<_>>()
.join(", ")
)
}
NodeKind::RecordPat { fields, .. } => {
fields
.first()
.map(|f| py_value_ident(&f.name.name))
.unwrap_or_else(|| "_".into())
}
_ => "_".into(),
}
}
fn pattern_to_py_binding(&self, pat: &AIRNode) -> String {
self.pattern_to_binding_name(pat)
}
fn type_expr_to_string(&self, node: &AIRNode) -> String {
match &node.kind {
NodeKind::TypeNamed { path, .. } => path
.segments
.iter()
.map(|s| s.name.as_str())
.collect::<Vec<_>>()
.join("."),
NodeKind::Identifier { name } => name.name.clone(),
_ => "Unknown".into(),
}
}
}
fn ast_type_name(node: &AIRNode) -> Option<String> {
if let NodeKind::TypeNamed { path, .. } = &node.kind {
path.segments.last().map(|s| s.name.clone())
} else {
None
}
}
fn type_decl_emission_order(
items: &[AIRNode],
impls_by_target: &HashMap<String, Vec<AIRNode>>,
) -> Vec<usize> {
use std::collections::HashMap as Map;
let mut decl_index: Map<String, usize> = Map::new();
for (i, item) in items.iter().enumerate() {
match &item.kind {
NodeKind::TraitDecl { name, .. }
| NodeKind::RecordDecl { name, .. }
| NodeKind::ClassDecl { name, .. }
| NodeKind::EffectDecl { name, .. } => {
decl_index.entry(name.name.clone()).or_insert(i);
}
_ => {}
}
}
let mut deps: Vec<Vec<usize>> = vec![Vec::new(); items.len()];
let add_dep = |deps: &mut Vec<Vec<usize>>, i: usize, dep_name: &str| {
if let Some(&j) = decl_index.get(dep_name) {
if j != i && !deps[i].contains(&j) {
deps[i].push(j);
}
}
};
for (i, item) in items.iter().enumerate() {
let (name, declared_base, declared_traits) = match &item.kind {
NodeKind::ClassDecl {
name, base, traits, ..
} => (Some(name), base.as_ref(), traits.as_slice()),
NodeKind::RecordDecl { name, .. } | NodeKind::TraitDecl { name, .. } => {
(Some(name), None, [].as_slice())
}
_ => (None, None, [].as_slice()),
};
let Some(name) = name else { continue };
if let Some(b) = declared_base {
if let Some(seg) = b.segments.last() {
add_dep(&mut deps, i, &seg.name);
}
}
for tp in declared_traits {
if let Some(seg) = tp.segments.last() {
add_dep(&mut deps, i, &seg.name);
}
}
if let Some(impls) = impls_by_target.get(&name.name) {
for im in impls {
if let NodeKind::ImplBlock {
trait_path: Some(tp),
..
} = &im.kind
{
if let Some(seg) = tp.segments.last() {
add_dep(&mut deps, i, &seg.name);
}
}
}
}
}
let n = items.len();
let mut emitted = vec![false; n];
let mut order = Vec::with_capacity(n);
for _ in 0..n {
let mut pick = None;
for i in 0..n {
if emitted[i] {
continue;
}
if deps[i].iter().all(|&d| emitted[d]) {
pick = Some(i);
break;
}
}
let i = pick.unwrap_or_else(|| (0..n).find(|&i| !emitted[i]).unwrap_or(0));
emitted[i] = true;
order.push(i);
}
order
}
fn identifier_to_py(s: &str) -> String {
if s.chars().next().is_some_and(char::is_uppercase) {
s.to_string()
} else {
py_value_ident(s)
}
}
fn py_value_ident(name: &str) -> String {
crate::generator::escape_target_keyword(
&to_snake_case(name),
crate::generator::KeywordTarget::Python,
)
}
fn py_field_ident(name: &str) -> String {
py_value_ident(name)
}
fn is_shadow_sensitive_py_builtin(name: &str) -> bool {
matches!(
name,
"list"
| "set"
| "dict"
| "map"
| "filter"
| "sorted"
| "enumerate"
| "range"
| "len"
| "tuple"
| "frozenset"
| "zip"
| "iter"
| "next"
| "print"
| "str"
| "int"
| "float"
| "bool"
| "abs"
| "min"
| "max"
| "sum"
| "round"
)
}
fn range_bound_to_py(node: &AIRNode) -> String {
let lit = match &node.kind {
NodeKind::LiteralPat { lit } | NodeKind::Literal { lit } => Some(lit),
NodeKind::Identifier { name } => return py_value_ident(&name.name),
_ => None,
};
match lit {
Some(Literal::Int(s)) | Some(Literal::Float(s)) => s.clone(),
Some(Literal::Bool(b)) => if *b { "True" } else { "False" }.to_string(),
Some(Literal::Char(s)) => format!("'{s}'"),
Some(Literal::String(s)) => format!("\"{}\"", escape_py_string(s)),
Some(Literal::Unit) | None => "0".to_string(),
}
}
fn is_time_method_name(name: &str) -> bool {
matches!(
name,
"as_nanos"
| "as_millis"
| "as_seconds"
| "is_zero"
| "is_negative"
| "abs"
| "elapsed"
| "duration_since"
)
}
fn collect_runtime_tag_imports(node: &AIRNode, out: &mut std::collections::BTreeSet<&'static str>) {
if let Some((assertion, _actual, _expected)) = crate::generator::classify_assertion(node) {
use crate::generator::TestAssertion as T;
match assertion {
T::BeSome => {
out.insert("_BockSome");
}
T::BeNone => {
out.insert("_BockNone");
}
T::BeOk => {
out.insert("_BockOk");
}
T::BeErr => {
out.insert("_BockErr");
}
_ => {}
}
}
if let NodeKind::Block { stmts, tail } = &node.kind {
for s in stmts {
collect_runtime_tag_imports(s, out);
}
if let Some(t) = tail {
collect_runtime_tag_imports(t, out);
}
}
}
fn to_snake_case(s: &str) -> String {
if s.is_empty() || s == "_" {
return s.to_string();
}
if s.contains('_') && !s.chars().any(|c| c.is_uppercase()) {
return s.to_string();
}
if !s.chars().any(|c| c.is_uppercase()) {
return s.to_string();
}
if s.len() == 1 {
return s.to_lowercase();
}
let mut result = String::with_capacity(s.len() + 4);
let chars: Vec<char> = s.chars().collect();
for (i, &ch) in chars.iter().enumerate() {
if ch.is_uppercase() {
let prev_is_upper = i > 0 && chars[i - 1].is_uppercase();
let prev_is_underscore = i > 0 && chars[i - 1] == '_';
let next_is_lower = i + 1 < chars.len() && chars[i + 1].is_lowercase();
if i > 0 && !prev_is_underscore && (!prev_is_upper || next_is_lower) {
result.push('_');
}
result.push(
ch.to_lowercase()
.next()
.expect("lowercase yields at least one char"),
);
} else {
result.push(ch);
}
}
result
}
fn escape_py_string(s: &str) -> String {
let mut out = String::with_capacity(s.len());
for ch in s.chars() {
match ch {
'"' => out.push_str("\\\""),
'\\' => out.push_str("\\\\"),
'\n' => out.push_str("\\n"),
'\r' => out.push_str("\\r"),
'\t' => out.push_str("\\t"),
_ => out.push(ch),
}
}
out
}
fn escape_fstring(s: &str) -> String {
let mut out = String::with_capacity(s.len());
for ch in s.chars() {
match ch {
'"' => out.push_str("\\\""),
'\\' => out.push_str("\\\\"),
'\n' => out.push_str("\\n"),
'\r' => out.push_str("\\r"),
'\t' => out.push_str("\\t"),
'{' => out.push_str("{{"),
'}' => out.push_str("}}"),
_ => out.push(ch),
}
}
out
}
fn escape_fstring_triple(s: &str) -> String {
let mut out = String::with_capacity(s.len());
for ch in s.chars() {
match ch {
'\\' => out.push_str("\\\\"),
'{' => out.push_str("{{"),
'}' => out.push_str("}}"),
_ => out.push(ch),
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
use bock_air::{AirArg, AirRecordField, AirRecordPatternField};
use bock_ast::{Ident, TypePath};
use bock_errors::{FileId, Span};
fn span() -> Span {
Span {
file: FileId(0),
start: 0,
end: 0,
}
}
fn ident(name: &str) -> Ident {
Ident {
name: name.to_string(),
span: span(),
}
}
fn type_path(segments: &[&str]) -> TypePath {
TypePath {
segments: segments.iter().map(|s| ident(s)).collect(),
span: span(),
}
}
fn node(id: u32, kind: NodeKind) -> AIRNode {
AIRNode::new(id, span(), kind)
}
fn int_lit(id: u32, val: &str) -> AIRNode {
node(
id,
NodeKind::Literal {
lit: Literal::Int(val.into()),
},
)
}
fn str_lit(id: u32, val: &str) -> AIRNode {
node(
id,
NodeKind::Literal {
lit: Literal::String(val.into()),
},
)
}
fn bool_lit(id: u32, val: bool) -> AIRNode {
node(
id,
NodeKind::Literal {
lit: Literal::Bool(val),
},
)
}
fn id_node(id: u32, name: &str) -> AIRNode {
node(id, NodeKind::Identifier { name: ident(name) })
}
fn bind_pat(id: u32, name: &str) -> AIRNode {
node(
id,
NodeKind::BindPat {
name: ident(name),
is_mut: false,
},
)
}
fn param_node(id: u32, name: &str) -> AIRNode {
node(
id,
NodeKind::Param {
pattern: Box::new(bind_pat(id + 100, name)),
ty: None,
default: None,
},
)
}
fn typed_param_node(id: u32, name: &str, ty_name: &str) -> AIRNode {
node(
id,
NodeKind::Param {
pattern: Box::new(bind_pat(id + 100, name)),
ty: Some(Box::new(node(
id + 200,
NodeKind::TypeNamed {
path: type_path(&[ty_name]),
args: vec![],
},
))),
default: None,
},
)
}
fn block(id: u32, stmts: Vec<AIRNode>, tail: Option<AIRNode>) -> AIRNode {
node(
id,
NodeKind::Block {
stmts,
tail: tail.map(Box::new),
},
)
}
fn module(imports: Vec<AIRNode>, items: Vec<AIRNode>) -> AIRNode {
node(
0,
NodeKind::Module {
path: None,
annotations: vec![],
imports,
items,
},
)
}
fn gen(module: &AIRNode) -> String {
let gen = PyGenerator::new();
let result = gen.generate_module(module).unwrap();
result.files[0].content.clone()
}
#[test]
fn implements_code_generator_trait() {
let gen = PyGenerator::new();
assert_eq!(gen.target().id, "python");
}
#[test]
fn empty_module() {
let m = module(vec![], vec![]);
let out = gen(&m);
assert_eq!(out, "");
}
fn module_with_path(path: &[&str], imports: Vec<AIRNode>, items: Vec<AIRNode>) -> AIRNode {
node(
0,
NodeKind::Module {
path: Some(bock_ast::ModulePath {
segments: path.iter().map(|s| ident(s)).collect(),
span: span(),
}),
annotations: vec![],
imports,
items,
},
)
}
fn import_named(id: u32, path: &[&str], name: &str) -> AIRNode {
node(
id,
NodeKind::ImportDecl {
path: bock_ast::ModulePath {
segments: path.iter().map(|s| ident(s)).collect(),
span: span(),
},
items: bock_ast::ImportItems::Named(vec![bock_ast::ImportedName {
span: span(),
name: ident(name),
alias: None,
}]),
},
)
}
fn fn_decl_tail(id: u32, vis: Visibility, name: &str, tail: AIRNode) -> AIRNode {
node(
id,
NodeKind::FnDecl {
annotations: vec![],
visibility: vis,
is_async: false,
name: ident(name),
generic_params: vec![],
params: vec![],
return_type: None,
effect_clause: vec![],
where_clause: vec![],
body: Box::new(block(id + 1, vec![], Some(tail))),
},
)
}
#[test]
fn per_module_emits_native_import_tree() {
let call = node(
10,
NodeKind::Call {
callee: Box::new(id_node(11, "add_one")),
args: vec![AirArg {
label: None,
value: int_lit(12, "6"),
}],
type_args: vec![],
},
);
let main_mod = module_with_path(
&["main"],
vec![import_named(5, &["mathutil"], "add_one")],
vec![fn_decl_tail(1, Visibility::Private, "main", call)],
);
let mathutil_mod = module_with_path(
&["mathutil"],
vec![],
vec![fn_decl_tail(
20,
Visibility::Public,
"add_one",
int_lit(22, "7"),
)],
);
let gen = PyGenerator::new();
let main_path = std::path::Path::new("src/main.bock");
let util_path = std::path::Path::new("src/mathutil.bock");
let out = gen
.generate_project(&[(&main_mod, main_path), (&mathutil_mod, util_path)])
.unwrap();
let by_name = |p: &str| out.files.iter().find(|f| f.path == std::path::Path::new(p));
let main_file = by_name("main.py").expect("main.py emitted");
let util_file = by_name("mathutil.py").expect("mathutil.py emitted");
assert!(
main_file.content.contains("from mathutil import add_one"),
"main.py must import from the sibling module; got:\n{}",
main_file.content
);
assert!(
main_file.content.contains("if __name__ == \"__main__\":"),
"main.py must carry the entry invocation; got:\n{}",
main_file.content
);
assert!(
util_file.content.contains("def add_one():"),
"mathutil.py must carry the exported fn; got:\n{}",
util_file.content
);
assert!(
!main_file.content.contains("# import"),
"per-module path emits a real import, not a comment"
);
}
#[test]
fn per_module_shares_optional_runtime() {
let main_mod = module_with_path(
&["main"],
vec![import_named(5, &["other"], "thing")],
vec![fn_decl_tail(
1,
Visibility::Private,
"main",
id_node(12, "None"),
)],
);
let other_mod = module_with_path(
&["other"],
vec![],
vec![fn_decl_tail(
20,
Visibility::Public,
"thing",
id_node(22, "None"),
)],
);
let gen = PyGenerator::new();
let out = gen
.generate_project(&[
(&main_mod, std::path::Path::new("src/main.bock")),
(&other_mod, std::path::Path::new("src/other.bock")),
])
.unwrap();
let runtime = out
.files
.iter()
.find(|f| f.path == std::path::Path::new("_bock_runtime.py"))
.expect("_bock_runtime.py emitted once");
assert!(runtime.content.contains("class _BockNone:"), "got runtime");
assert!(
runtime.content.contains("__all__") && runtime.content.contains("\"_bock_none\""),
"runtime must export underscore names via __all__; got:\n{}",
runtime.content
);
let importers = out
.files
.iter()
.filter(|f| f.content.contains("from _bock_runtime import *"))
.count();
assert_eq!(importers, 2, "both modules import the shared runtime");
}
#[test]
fn simple_function() {
let body = block(2, vec![], Some(int_lit(3, "42")));
let f = node(
1,
NodeKind::FnDecl {
annotations: vec![],
visibility: Visibility::Private,
is_async: false,
name: ident("answer"),
generic_params: vec![],
params: vec![],
return_type: None,
effect_clause: vec![],
where_clause: vec![],
body: Box::new(body),
},
);
let out = gen(&module(vec![], vec![f]));
assert!(out.contains("def answer():"), "got: {out}");
assert!(out.contains("return 42"), "got: {out}");
}
#[test]
fn function_with_params_and_type_hints() {
let body = block(
5,
vec![],
Some(node(
6,
NodeKind::BinaryOp {
op: BinOp::Add,
left: Box::new(id_node(7, "a")),
right: Box::new(id_node(8, "b")),
},
)),
);
let f = node(
1,
NodeKind::FnDecl {
annotations: vec![],
visibility: Visibility::Public,
is_async: false,
name: ident("add"),
generic_params: vec![],
params: vec![
typed_param_node(2, "a", "Int"),
typed_param_node(3, "b", "Int"),
],
return_type: Some(Box::new(node(
4,
NodeKind::TypeNamed {
path: type_path(&["Int"]),
args: vec![],
},
))),
effect_clause: vec![],
where_clause: vec![],
body: Box::new(body),
},
);
let out = gen(&module(vec![], vec![f]));
assert!(
out.contains("def add(a: int, b: int) -> int:"),
"got: {out}"
);
assert!(out.contains("(a + b)"), "got: {out}");
}
#[test]
fn async_function() {
let body = block(
3,
vec![],
Some(node(
4,
NodeKind::Await {
expr: Box::new(node(
5,
NodeKind::Call {
callee: Box::new(id_node(6, "fetch")),
args: vec![AirArg {
label: None,
value: str_lit(7, "https://example.com"),
}],
type_args: vec![],
},
)),
},
)),
);
let f = node(
1,
NodeKind::FnDecl {
annotations: vec![],
visibility: Visibility::Private,
is_async: true,
name: ident("fetchData"),
generic_params: vec![],
params: vec![],
return_type: None,
effect_clause: vec![],
where_clause: vec![],
body: Box::new(body),
},
);
let out = gen(&module(vec![], vec![f]));
assert!(out.contains("async def fetch_data():"), "got: {out}");
assert!(out.contains("await fetch"), "got: {out}");
}
#[test]
fn effects_as_keyword_args() {
let body = block(
3,
vec![node(
4,
NodeKind::LetBinding {
is_mut: false,
pattern: Box::new(bind_pat(5, "msg")),
ty: None,
value: Box::new(str_lit(6, "hello")),
},
)],
Some(node(
7,
NodeKind::EffectOp {
effect: type_path(&["Log"]),
operation: ident("info"),
args: vec![AirArg {
label: None,
value: id_node(8, "msg"),
}],
},
)),
);
let f = node(
1,
NodeKind::FnDecl {
annotations: vec![],
visibility: Visibility::Private,
is_async: false,
name: ident("process"),
generic_params: vec![],
params: vec![param_node(2, "data")],
return_type: None,
effect_clause: vec![type_path(&["Log"]), type_path(&["Clock"])],
where_clause: vec![],
body: Box::new(body),
},
);
let out = gen(&module(vec![], vec![f]));
assert!(
out.contains("def process(data, *, log: Log, clock: Clock):"),
"got: {out}"
);
assert!(out.contains("log.info(msg)"), "got: {out}");
}
#[test]
fn clock_time_ops_route_through_handler() {
let out = gen(&module(vec![], vec![clock_timed_fn()]));
assert!(out.contains("clock.now_monotonic()"), "got: {out}");
assert!(out.contains("clock.sleep("), "got: {out}");
assert!(
!out.contains("time.monotonic_ns()"),
"host clock primitive leaked past the handler: {out}"
);
assert!(
!out.contains("asyncio.sleep("),
"host sleep primitive leaked past the handler: {out}"
);
}
fn clock_timed_fn() -> AIRNode {
let instant_now = node(
40,
NodeKind::Call {
callee: Box::new(node(
41,
NodeKind::FieldAccess {
object: Box::new(id_node(42, "Instant")),
field: ident("now"),
},
)),
args: vec![],
type_args: vec![],
},
);
let duration_millis = node(
50,
NodeKind::Call {
callee: Box::new(node(
51,
NodeKind::FieldAccess {
object: Box::new(id_node(52, "Duration")),
field: ident("millis"),
},
)),
args: vec![AirArg {
label: None,
value: int_lit(53, "1"),
}],
type_args: vec![],
},
);
let sleep_call = node(
60,
NodeKind::Call {
callee: Box::new(id_node(61, "sleep")),
args: vec![AirArg {
label: None,
value: duration_millis,
}],
type_args: vec![],
},
);
let elapsed_call = node(
70,
NodeKind::MethodCall {
receiver: Box::new(id_node(71, "start")),
method: ident("elapsed"),
type_args: vec![],
args: vec![],
},
);
let body = block(
30,
vec![
node(
31,
NodeKind::LetBinding {
is_mut: false,
pattern: Box::new(bind_pat(32, "start")),
ty: None,
value: Box::new(instant_now),
},
),
sleep_call,
node(
33,
NodeKind::LetBinding {
is_mut: false,
pattern: Box::new(bind_pat(34, "d")),
ty: None,
value: Box::new(elapsed_call),
},
),
],
None,
);
node(
1,
NodeKind::FnDecl {
annotations: vec![],
visibility: Visibility::Private,
is_async: false,
name: ident("timed"),
generic_params: vec![],
params: vec![],
return_type: None,
effect_clause: vec![type_path(&["Clock"])],
where_clause: vec![],
body: Box::new(body),
},
)
}
#[test]
fn record_to_dataclass() {
let rec = node(
1,
NodeKind::RecordDecl {
annotations: vec![],
visibility: Visibility::Public,
name: ident("Point"),
generic_params: vec![],
fields: vec![
bock_ast::RecordDeclField {
id: 0,
span: span(),
name: ident("x"),
ty: bock_ast::TypeExpr::Named {
id: 0,
span: span(),
path: type_path(&["Float"]),
args: vec![],
},
default: None,
},
bock_ast::RecordDeclField {
id: 0,
span: span(),
name: ident("y"),
ty: bock_ast::TypeExpr::Named {
id: 0,
span: span(),
path: type_path(&["Float"]),
args: vec![],
},
default: None,
},
],
},
);
let out = gen(&module(vec![], vec![rec]));
assert!(
out.contains("from dataclasses import dataclass"),
"got: {out}"
);
assert!(out.contains("@dataclass"), "got: {out}");
assert!(out.contains("class Point:"), "got: {out}");
assert!(out.contains("x: float"), "got: {out}");
assert!(out.contains("y: float"), "got: {out}");
}
fn print_call(id: u32, s: &str) -> AIRNode {
node(
id,
NodeKind::Call {
callee: Box::new(id_node(id + 1, "print")),
args: vec![AirArg {
label: None,
value: str_lit(id + 2, s),
}],
type_args: vec![],
},
)
}
#[test]
fn stmt_position_if_else_discards_branch_tails_and_chains_elif() {
let chain = node(
10,
NodeKind::If {
let_pattern: None,
condition: Box::new(id_node(11, "c1")),
then_block: Box::new(block(12, vec![], Some(print_call(13, "a")))),
else_block: Some(Box::new(node(
20,
NodeKind::If {
let_pattern: None,
condition: Box::new(id_node(21, "c2")),
then_block: Box::new(block(22, vec![], Some(print_call(23, "b")))),
else_block: Some(Box::new(block(30, vec![], Some(print_call(31, "c"))))),
},
))),
},
);
let f = node(
1,
NodeKind::FnDecl {
annotations: vec![],
visibility: Visibility::Private,
is_async: false,
name: ident("report"),
generic_params: vec![],
params: vec![param_node(2, "c1"), param_node(3, "c2")],
return_type: None,
effect_clause: vec![],
where_clause: vec![],
body: Box::new(block(4, vec![chain, print_call(40, "after")], None)),
},
);
let out = gen(&module(vec![], vec![f]));
assert!(
!out.contains("return print("),
"statement-if branch tails must be discarded, not returned: {out}"
);
assert!(
out.contains("elif c2:"),
"`else if` must chain as a single `elif`: {out}"
);
assert!(
!out.contains("el "),
"the `el` prefix must not be followed by an indented statement: {out}"
);
assert!(
out.contains("print(\"after\""),
"the statement after the chain must still be emitted: {out}"
);
}
#[test]
fn stmt_guard_nondiverging_else_discards_tail() {
let guard = node(
10,
NodeKind::Guard {
let_pattern: None,
condition: Box::new(id_node(11, "ok")),
else_block: Box::new(block(12, vec![], Some(print_call(13, "warn")))),
},
);
let f = node(
1,
NodeKind::FnDecl {
annotations: vec![],
visibility: Visibility::Private,
is_async: false,
name: ident("report"),
generic_params: vec![],
params: vec![param_node(2, "ok")],
return_type: None,
effect_clause: vec![],
where_clause: vec![],
body: Box::new(block(4, vec![guard, print_call(40, "after")], None)),
},
);
let out = gen(&module(vec![], vec![f]));
assert!(
!out.contains("return print("),
"a non-diverging guard else must fall through, not return: {out}"
);
assert!(
out.contains("print(\"after\""),
"the statement after the guard must still be emitted: {out}"
);
}
#[test]
fn keyword_record_fields_escaped_at_every_site() {
let int_ty = || bock_ast::TypeExpr::Named {
id: 0,
span: span(),
path: type_path(&["Int"]),
args: vec![],
};
let rec = node(
1,
NodeKind::RecordDecl {
annotations: vec![],
visibility: Visibility::Public,
name: ident("Tally"),
generic_params: vec![],
fields: vec![bock_ast::RecordDeclField {
id: 0,
span: span(),
name: ident("pass"),
ty: int_ty(),
default: None,
}],
},
);
let enum_decl = node(
2,
NodeKind::EnumDecl {
annotations: vec![],
visibility: Visibility::Public,
name: ident("Gate"),
generic_params: vec![],
variants: vec![node(
3,
NodeKind::EnumVariant {
name: ident("Open"),
payload: EnumVariantPayload::Struct(vec![bock_ast::RecordDeclField {
id: 0,
span: span(),
name: ident("lambda"),
ty: int_ty(),
default: None,
}]),
},
)],
},
);
let let_t = node(
10,
NodeKind::LetBinding {
is_mut: false,
pattern: Box::new(bind_pat(11, "t")),
ty: None,
value: Box::new(node(
12,
NodeKind::RecordConstruct {
path: type_path(&["Tally"]),
fields: vec![AirRecordField {
name: ident("pass"),
value: Some(Box::new(int_lit(13, "7"))),
}],
spread: None,
},
)),
},
);
let access = node(
20,
NodeKind::Call {
callee: Box::new(id_node(21, "print")),
args: vec![AirArg {
label: None,
value: node(
22,
NodeKind::FieldAccess {
object: Box::new(id_node(23, "t")),
field: ident("pass"),
},
),
}],
type_args: vec![],
},
);
let m = node(
30,
NodeKind::Match {
scrutinee: Box::new(id_node(31, "t")),
arms: vec![node(
32,
NodeKind::MatchArm {
pattern: Box::new(node(
33,
NodeKind::RecordPat {
path: type_path(&["Tally"]),
fields: vec![AirRecordPatternField {
name: ident("pass"),
pattern: Some(Box::new(bind_pat(34, "p"))),
}],
rest: false,
},
)),
guard: None,
body: Box::new(block(35, vec![], Some(print_call(36, "x")))),
},
)],
},
);
let let_g = node(
40,
NodeKind::LetBinding {
is_mut: false,
pattern: Box::new(bind_pat(41, "g")),
ty: None,
value: Box::new(node(
42,
NodeKind::RecordConstruct {
path: type_path(&["Open"]),
fields: vec![AirRecordField {
name: ident("lambda"),
value: Some(Box::new(int_lit(43, "3"))),
}],
spread: None,
},
)),
},
);
let f = node(
5,
NodeKind::FnDecl {
annotations: vec![],
visibility: Visibility::Private,
is_async: false,
name: ident("main"),
generic_params: vec![],
params: vec![],
return_type: None,
effect_clause: vec![],
where_clause: vec![],
body: Box::new(block(6, vec![let_t, access, m, let_g], None)),
},
);
let out = gen(&module(vec![], vec![rec, enum_decl, f]));
assert!(
out.contains("pass_: int"),
"dataclass field must be keyword-escaped: {out}"
);
assert!(
out.contains("Tally(pass_=7)"),
"constructor kwargs must be keyword-escaped: {out}"
);
assert!(
out.contains("t.pass_"),
"field access must be keyword-escaped: {out}"
);
assert!(
out.contains("Tally(pass_=p)"),
"record-pattern destructuring must be keyword-escaped: {out}"
);
assert!(
out.contains("lambda_: int"),
"enum struct-variant field must be keyword-escaped: {out}"
);
assert!(
out.contains("Gate_Open(lambda_=3)"),
"variant constructor kwargs must be keyword-escaped: {out}"
);
assert!(
!out.contains("pass:") && !out.contains("pass=") && !out.contains("lambda:"),
"no field position may emit the unescaped keyword: {out}"
);
}
#[test]
fn enum_to_dataclass_variants() {
let enum_decl = node(
1,
NodeKind::EnumDecl {
annotations: vec![],
visibility: Visibility::Public,
name: ident("Shape"),
generic_params: vec![],
variants: vec![
node(
2,
NodeKind::EnumVariant {
name: ident("Circle"),
payload: EnumVariantPayload::Struct(vec![bock_ast::RecordDeclField {
id: 0,
span: span(),
name: ident("radius"),
ty: bock_ast::TypeExpr::Named {
id: 0,
span: span(),
path: type_path(&["Float"]),
args: vec![],
},
default: None,
}]),
},
),
node(
3,
NodeKind::EnumVariant {
name: ident("None"),
payload: EnumVariantPayload::Unit,
},
),
],
},
);
let out = gen(&module(vec![], vec![enum_decl]));
assert!(
out.contains("from dataclasses import dataclass"),
"got: {out}"
);
assert!(out.contains("@dataclass"), "got: {out}");
assert!(out.contains("class Shape_Circle:"), "got: {out}");
assert!(out.contains("radius: float"), "got: {out}");
assert!(out.contains("_tag: str = \"Circle\""), "got: {out}");
assert!(out.contains("class Shape_None:"), "got: {out}");
assert!(out.contains("_tag: str = \"None\""), "got: {out}");
}
#[test]
fn match_to_match_case() {
let scrutinee = id_node(10, "shape");
let arms = vec![
node(
11,
NodeKind::MatchArm {
pattern: Box::new(node(
12,
NodeKind::ConstructorPat {
path: type_path(&["Shape", "Circle"]),
fields: vec![bind_pat(13, "r")],
},
)),
guard: None,
body: Box::new(block(
14,
vec![],
Some(node(
15,
NodeKind::BinaryOp {
op: BinOp::Mul,
left: Box::new(id_node(16, "r")),
right: Box::new(id_node(17, "r")),
},
)),
)),
},
),
node(
18,
NodeKind::MatchArm {
pattern: Box::new(node(19, NodeKind::WildcardPat)),
guard: None,
body: Box::new(block(20, vec![], Some(int_lit(21, "0")))),
},
),
];
let match_stmt = node(
9,
NodeKind::Match {
scrutinee: Box::new(scrutinee),
arms,
},
);
let f = node(
1,
NodeKind::FnDecl {
annotations: vec![],
visibility: Visibility::Private,
is_async: false,
name: ident("area"),
generic_params: vec![],
params: vec![param_node(2, "shape")],
return_type: None,
effect_clause: vec![],
where_clause: vec![],
body: Box::new(block(3, vec![match_stmt], None)),
},
);
let out = gen(&module(vec![], vec![f]));
assert!(out.contains("match shape:"), "got: {out}");
assert!(out.contains("case Shape_Circle(_0=r):"), "got: {out}");
assert!(out.contains("case _:"), "got: {out}");
}
#[test]
fn expr_position_user_enum_match_test_resolves_variant_class() {
let enum_decl = node(
1,
NodeKind::EnumDecl {
annotations: vec![],
visibility: Visibility::Public,
name: ident("Light"),
generic_params: vec![],
variants: vec![
node(
2,
NodeKind::EnumVariant {
name: ident("Red"),
payload: EnumVariantPayload::Unit,
},
),
node(
3,
NodeKind::EnumVariant {
name: ident("Green"),
payload: EnumVariantPayload::Unit,
},
),
],
},
);
let red_arm = node(
20,
NodeKind::MatchArm {
pattern: Box::new(node(
21,
NodeKind::ConstructorPat {
path: type_path(&["Red"]),
fields: vec![],
},
)),
guard: None,
body: Box::new(block(22, vec![], Some(int_lit(23, "1")))),
},
);
let default_arm = node(
30,
NodeKind::MatchArm {
pattern: Box::new(node(31, NodeKind::WildcardPat)),
guard: None,
body: Box::new(block(32, vec![], Some(int_lit(33, "0")))),
},
);
let m = node(
40,
NodeKind::Match {
scrutinee: Box::new(id_node(41, "l")),
arms: vec![red_arm, default_arm],
},
);
let let_n = node(
50,
NodeKind::LetBinding {
is_mut: false,
pattern: Box::new(bind_pat(51, "n")),
ty: Some(Box::new(node(
52,
NodeKind::TypeNamed {
path: type_path(&["Int"]),
args: vec![],
},
))),
value: Box::new(m),
},
);
let f = node(
5,
NodeKind::FnDecl {
annotations: vec![],
visibility: Visibility::Private,
is_async: false,
name: ident("rank"),
generic_params: vec![],
params: vec![param_node(6, "l")],
return_type: None,
effect_clause: vec![],
where_clause: vec![],
body: Box::new(block(7, vec![let_n], None)),
},
);
let out = gen(&module(vec![], vec![enum_decl, f]));
assert!(
out.contains("isinstance(__v, Light_Red)"),
"expr-position variant test must resolve to the dataclass Light_Red, got: {out}"
);
assert!(
!out.contains("isinstance(__v, Red)"),
"must not test against the bare variant leaf name (undefined), got: {out}"
);
}
#[test]
fn ownership_erased() {
let move_expr = node(
1,
NodeKind::Move {
expr: Box::new(id_node(2, "x")),
},
);
let borrow_expr = node(
3,
NodeKind::Borrow {
expr: Box::new(id_node(4, "y")),
},
);
let mut_borrow_expr = node(
5,
NodeKind::MutableBorrow {
expr: Box::new(id_node(6, "z")),
},
);
let body = block(
7,
vec![
node(
8,
NodeKind::LetBinding {
is_mut: false,
pattern: Box::new(bind_pat(9, "a")),
ty: None,
value: Box::new(move_expr),
},
),
node(
10,
NodeKind::LetBinding {
is_mut: false,
pattern: Box::new(bind_pat(11, "b")),
ty: None,
value: Box::new(borrow_expr),
},
),
node(
12,
NodeKind::LetBinding {
is_mut: false,
pattern: Box::new(bind_pat(13, "c")),
ty: None,
value: Box::new(mut_borrow_expr),
},
),
],
None,
);
let f = node(
0,
NodeKind::FnDecl {
annotations: vec![],
visibility: Visibility::Private,
is_async: false,
name: ident("test"),
generic_params: vec![],
params: vec![],
return_type: None,
effect_clause: vec![],
where_clause: vec![],
body: Box::new(body),
},
);
let out = gen(&module(vec![], vec![f]));
assert!(out.contains("a = x"), "got: {out}");
assert!(out.contains("b = y"), "got: {out}");
assert!(out.contains("c = z"), "got: {out}");
}
#[test]
fn string_interpolation_fstring() {
let interp = node(
1,
NodeKind::Interpolation {
parts: vec![
AirInterpolationPart::Literal("Hello, ".into()),
AirInterpolationPart::Expr(Box::new(id_node(2, "name"))),
AirInterpolationPart::Literal("!".into()),
],
},
);
let binding = node(
3,
NodeKind::LetBinding {
is_mut: false,
pattern: Box::new(bind_pat(4, "msg")),
ty: None,
value: Box::new(interp),
},
);
let f = node(
0,
NodeKind::FnDecl {
annotations: vec![],
visibility: Visibility::Private,
is_async: false,
name: ident("greet"),
generic_params: vec![],
params: vec![param_node(5, "name")],
return_type: None,
effect_clause: vec![],
where_clause: vec![],
body: Box::new(block(6, vec![binding], Some(id_node(7, "msg")))),
},
);
let out = gen(&module(vec![], vec![f]));
assert!(out.contains("f\"Hello, {_bock_str(name)}!\""), "got: {out}");
}
#[test]
fn multiline_interpolation_uses_triple_quoted_fstring() {
let interp = node(
1,
NodeKind::Interpolation {
parts: vec![
AirInterpolationPart::Literal("=== ".into()),
AirInterpolationPart::Expr(Box::new(id_node(2, "title"))),
AirInterpolationPart::Literal(" ===\n".into()),
AirInterpolationPart::Expr(Box::new(id_node(3, "msg"))),
AirInterpolationPart::Literal("\n================".into()),
],
},
);
let binding = node(
4,
NodeKind::LetBinding {
is_mut: false,
pattern: Box::new(bind_pat(5, "result")),
ty: None,
value: Box::new(interp),
},
);
let f = node(
0,
NodeKind::FnDecl {
annotations: vec![],
visibility: Visibility::Private,
is_async: false,
name: ident("banner"),
generic_params: vec![],
params: vec![param_node(6, "title"), param_node(7, "msg")],
return_type: None,
effect_clause: vec![],
where_clause: vec![],
body: Box::new(block(8, vec![binding], Some(id_node(9, "result")))),
},
);
let out = gen(&module(vec![], vec![f]));
assert!(
out.contains(
"f\"\"\"=== {_bock_str(title)} ===\n{_bock_str(msg)}\n================\"\"\""
),
"got: {out}"
);
assert!(
!out.contains("f\"Hello"),
"single-line should not appear: {out}"
);
}
#[test]
fn single_line_interpolation_still_uses_regular_fstring() {
let interp = node(
1,
NodeKind::Interpolation {
parts: vec![
AirInterpolationPart::Literal("Hi ".into()),
AirInterpolationPart::Expr(Box::new(id_node(2, "name"))),
],
},
);
let binding = node(
3,
NodeKind::LetBinding {
is_mut: false,
pattern: Box::new(bind_pat(4, "greeting")),
ty: None,
value: Box::new(interp),
},
);
let f = node(
0,
NodeKind::FnDecl {
annotations: vec![],
visibility: Visibility::Private,
is_async: false,
name: ident("greet"),
generic_params: vec![],
params: vec![param_node(5, "name")],
return_type: None,
effect_clause: vec![],
where_clause: vec![],
body: Box::new(block(6, vec![binding], Some(id_node(7, "greeting")))),
},
);
let out = gen(&module(vec![], vec![f]));
assert!(out.contains("f\"Hi {_bock_str(name)}\""), "got: {out}");
assert!(
!out.contains("f\"\"\""),
"should not use triple quotes: {out}"
);
}
#[test]
fn list_map_set_literals() {
let list = node(
1,
NodeKind::ListLiteral {
elems: vec![int_lit(2, "1"), int_lit(3, "2"), int_lit(4, "3")],
},
);
let map = node(
5,
NodeKind::MapLiteral {
entries: vec![bock_air::AirMapEntry {
key: str_lit(6, "a"),
value: int_lit(7, "1"),
}],
},
);
let set = node(
8,
NodeKind::SetLiteral {
elems: vec![int_lit(9, "1"), int_lit(10, "2")],
},
);
let body = block(
11,
vec![
node(
12,
NodeKind::LetBinding {
is_mut: false,
pattern: Box::new(bind_pat(13, "xs")),
ty: None,
value: Box::new(list),
},
),
node(
14,
NodeKind::LetBinding {
is_mut: false,
pattern: Box::new(bind_pat(15, "m")),
ty: None,
value: Box::new(map),
},
),
node(
16,
NodeKind::LetBinding {
is_mut: false,
pattern: Box::new(bind_pat(17, "s")),
ty: None,
value: Box::new(set),
},
),
],
None,
);
let f = node(
0,
NodeKind::FnDecl {
annotations: vec![],
visibility: Visibility::Private,
is_async: false,
name: ident("collections"),
generic_params: vec![],
params: vec![],
return_type: None,
effect_clause: vec![],
where_clause: vec![],
body: Box::new(body),
},
);
let out = gen(&module(vec![], vec![f]));
assert!(out.contains("[1, 2, 3]"), "got: {out}");
assert!(out.contains("{\"a\": 1}"), "got: {out}");
assert!(out.contains("{1, 2}"), "got: {out}");
}
#[test]
fn record_construction() {
let rec = node(
1,
NodeKind::RecordConstruct {
path: type_path(&["User"]),
fields: vec![
AirRecordField {
name: ident("name"),
value: Some(Box::new(str_lit(2, "Alice"))),
},
AirRecordField {
name: ident("age"),
value: Some(Box::new(int_lit(3, "30"))),
},
],
spread: None,
},
);
let binding = node(
4,
NodeKind::LetBinding {
is_mut: false,
pattern: Box::new(bind_pat(5, "user")),
ty: None,
value: Box::new(rec),
},
);
let f = node(
0,
NodeKind::FnDecl {
annotations: vec![],
visibility: Visibility::Private,
is_async: false,
name: ident("test"),
generic_params: vec![],
params: vec![],
return_type: None,
effect_clause: vec![],
where_clause: vec![],
body: Box::new(block(6, vec![binding], None)),
},
);
let out = gen(&module(vec![], vec![f]));
assert!(out.contains("User(name=\"Alice\", age=30)"), "got: {out}");
}
#[test]
fn control_flow() {
let if_stmt = node(
1,
NodeKind::If {
let_pattern: None,
condition: Box::new(bool_lit(2, true)),
then_block: Box::new(block(3, vec![], Some(int_lit(4, "1")))),
else_block: Some(Box::new(block(5, vec![], Some(int_lit(6, "2"))))),
},
);
let for_stmt = node(
7,
NodeKind::For {
pattern: Box::new(bind_pat(8, "x")),
iterable: Box::new(id_node(9, "items")),
body: Box::new(block(10, vec![], None)),
},
);
let while_stmt = node(
11,
NodeKind::While {
condition: Box::new(bool_lit(12, true)),
body: Box::new(block(
13,
vec![node(14, NodeKind::Break { value: None })],
None,
)),
},
);
let body = block(15, vec![if_stmt, for_stmt, while_stmt], None);
let f = node(
0,
NodeKind::FnDecl {
annotations: vec![],
visibility: Visibility::Private,
is_async: false,
name: ident("flow"),
generic_params: vec![],
params: vec![param_node(16, "items")],
return_type: None,
effect_clause: vec![],
where_clause: vec![],
body: Box::new(body),
},
);
let out = gen(&module(vec![], vec![f]));
assert!(out.contains("if True:"), "got: {out}");
assert!(out.contains("else:"), "got: {out}");
assert!(out.contains("for x in items:"), "got: {out}");
assert!(out.contains("while True:"), "got: {out}");
assert!(out.contains("break"), "got: {out}");
}
fn py_println_call(id: u32, arg: AIRNode) -> AIRNode {
node(
id,
NodeKind::Call {
callee: Box::new(id_node(id + 1, "println")),
args: vec![AirArg {
label: None,
value: arg,
}],
type_args: vec![],
},
)
}
#[test]
fn py_for_loop_body_tail_call_is_statement_not_returned() {
let range = node(
20,
NodeKind::Range {
lo: Box::new(int_lit(21, "1")),
hi: Box::new(int_lit(22, "3")),
inclusive: true,
},
);
let loop_body = block(30, vec![], Some(py_println_call(31, id_node(33, "i"))));
let for_loop = node(
10,
NodeKind::For {
pattern: Box::new(bind_pat(11, "i")),
iterable: Box::new(range),
body: Box::new(loop_body),
},
);
let f = fn_decl_body(0, "main", block(2, vec![for_loop], None));
let out = gen(&module(vec![], vec![f]));
assert!(
!out.contains("return print(i)"),
"for-loop body tail must be a bare statement, not `return` (would abort \
main after one iteration); got:\n{out}"
);
assert!(
out.contains("print(i)"),
"for-loop body tail call must still be emitted; got:\n{out}"
);
}
#[test]
fn py_while_loop_body_tail_call_is_statement_not_returned() {
let loop_body = block(30, vec![], Some(py_println_call(31, id_node(33, "i"))));
let while_loop = node(
10,
NodeKind::While {
condition: Box::new(bool_lit(12, true)),
body: Box::new(loop_body),
},
);
let f = fn_decl_body(0, "main", block(2, vec![while_loop], None));
let out = gen(&module(vec![], vec![f]));
assert!(
!out.contains("return print(i)"),
"while-loop body tail must be a bare statement, not `return`; got:\n{out}"
);
assert!(
out.contains("print(i)"),
"while-loop body tail call must still be emitted; got:\n{out}"
);
}
#[test]
fn py_infinite_loop_body_tail_call_is_statement_not_returned() {
let loop_body = block(30, vec![], Some(py_println_call(31, id_node(33, "i"))));
let inf_loop = node(
10,
NodeKind::Loop {
body: Box::new(loop_body),
},
);
let f = fn_decl_body(0, "main", block(2, vec![inf_loop], None));
let out = gen(&module(vec![], vec![f]));
assert!(
!out.contains("return print(i)"),
"loop body tail must be a bare statement, not `return`; got:\n{out}"
);
assert!(
out.contains("print(i)"),
"loop body tail call must still be emitted; got:\n{out}"
);
}
#[test]
fn py_function_body_tail_call_still_returned() {
let f = fn_decl_body(0, "answer", block(2, vec![], Some(int_lit(3, "42"))));
let out = gen(&module(vec![], vec![f]));
assert!(
out.contains("return 42"),
"function-body tail must still be returned; got:\n{out}"
);
}
#[test]
fn lambda_and_pipe() {
let lambda = node(
1,
NodeKind::Lambda {
params: vec![param_node(2, "x")],
body: Box::new(node(
3,
NodeKind::BinaryOp {
op: BinOp::Mul,
left: Box::new(id_node(4, "x")),
right: Box::new(int_lit(5, "2")),
},
)),
},
);
let pipe = node(
6,
NodeKind::Pipe {
left: Box::new(int_lit(7, "5")),
right: Box::new(id_node(8, "double")),
},
);
let body = block(
9,
vec![
node(
10,
NodeKind::LetBinding {
is_mut: false,
pattern: Box::new(bind_pat(11, "double")),
ty: None,
value: Box::new(lambda),
},
),
node(
12,
NodeKind::LetBinding {
is_mut: false,
pattern: Box::new(bind_pat(13, "result")),
ty: None,
value: Box::new(pipe),
},
),
],
None,
);
let f = node(
0,
NodeKind::FnDecl {
annotations: vec![],
visibility: Visibility::Private,
is_async: false,
name: ident("test"),
generic_params: vec![],
params: vec![],
return_type: None,
effect_clause: vec![],
where_clause: vec![],
body: Box::new(body),
},
);
let out = gen(&module(vec![], vec![f]));
assert!(out.contains("lambda x: (x * 2)"), "got: {out}");
assert!(out.contains("double(5)"), "got: {out}");
}
#[test]
fn py_call_with_lambda_callee_parenthesizes() {
let lambda = node(
1,
NodeKind::Lambda {
params: vec![param_node(2, "x")],
body: Box::new(id_node(3, "x")),
},
);
let call = node(
4,
NodeKind::Call {
callee: Box::new(lambda),
args: vec![AirArg {
label: None,
value: int_lit(5, "42"),
}],
type_args: vec![],
},
);
let f = fn_decl_tail(0, Visibility::Private, "test", call);
let out = gen(&module(vec![], vec![f]));
assert!(
out.contains("(lambda x: x)(42)"),
"lambda callee must be parenthesized so it is invoked; got: {out}"
);
}
#[test]
fn const_declaration() {
let c = node(
1,
NodeKind::ConstDecl {
annotations: vec![],
visibility: Visibility::Public,
name: ident("PI"),
ty: Box::new(node(
2,
NodeKind::TypeNamed {
path: type_path(&["Float"]),
args: vec![],
},
)),
value: Box::new(node(
3,
NodeKind::Literal {
lit: Literal::Float("3.14159".into()),
},
)),
},
);
let out = gen(&module(vec![], vec![c]));
assert!(out.contains("= 3.14159"), "got: {out}");
assert!(out.contains(": float"), "got: {out}");
}
#[test]
fn class_declaration() {
let method_body = block(10, vec![], Some(id_node(11, "undefined")));
let method = node(
5,
NodeKind::FnDecl {
annotations: vec![],
visibility: Visibility::Public,
is_async: false,
name: ident("greet"),
generic_params: vec![],
params: vec![param_node(6, "self")],
return_type: None,
effect_clause: vec![],
where_clause: vec![],
body: Box::new(method_body),
},
);
let cls = node(
1,
NodeKind::ClassDecl {
annotations: vec![],
visibility: Visibility::Public,
name: ident("Person"),
generic_params: vec![],
base: None,
traits: vec![],
fields: vec![bock_ast::RecordDeclField {
id: 0,
span: span(),
name: ident("name"),
ty: bock_ast::TypeExpr::Named {
id: 0,
span: span(),
path: type_path(&["String"]),
args: vec![],
},
default: None,
}],
methods: vec![method],
},
);
let out = gen(&module(vec![], vec![cls]));
assert!(out.contains("class Person:"), "got: {out}");
assert!(out.contains("def __init__(self, name: str):"), "got: {out}");
assert!(out.contains("self.name = name"), "got: {out}");
assert!(out.contains("def greet(self):"), "got: {out}");
}
fn self_method_returning(id: u32, name: &str, lit: &str) -> AIRNode {
node(
id,
NodeKind::FnDecl {
annotations: vec![],
visibility: Visibility::Public,
is_async: false,
name: ident(name),
generic_params: vec![],
params: vec![param_node(id + 1, "self")],
return_type: Some(Box::new(node(
id + 2,
NodeKind::TypeNamed {
path: type_path(&["String"]),
args: vec![],
},
))),
effect_clause: vec![],
where_clause: vec![],
body: Box::new(block(id + 3, vec![], Some(str_lit(id + 4, lit)))),
},
)
}
fn impl_block_node(
id: u32,
target: &str,
trait_name: Option<&str>,
methods: Vec<AIRNode>,
) -> AIRNode {
node(
id,
NodeKind::ImplBlock {
annotations: vec![],
target: Box::new(node(
id + 1,
NodeKind::TypeNamed {
path: type_path(&[target]),
args: vec![],
},
)),
trait_path: trait_name.map(|t| type_path(&[t])),
trait_args: vec![],
generic_params: vec![],
where_clause: vec![],
methods,
},
)
}
fn trait_node(id: u32, name: &str, method: &str) -> AIRNode {
node(
id,
NodeKind::TraitDecl {
annotations: vec![],
visibility: Visibility::Public,
is_platform: false,
name: ident(name),
generic_params: vec![],
associated_types: vec![],
methods: vec![self_method_returning(id + 50, method, "")],
},
)
}
fn class_with_field(id: u32, name: &str, field: &str) -> AIRNode {
node(
id,
NodeKind::ClassDecl {
annotations: vec![],
visibility: Visibility::Public,
name: ident(name),
generic_params: vec![],
base: None,
traits: vec![],
fields: vec![named_field(field, "String")],
methods: vec![],
},
)
}
#[test]
fn py_class_attaches_inherent_and_trait_impl_methods() {
let cls = class_with_field(1, "Widget", "name");
let inherent = impl_block_node(
10,
"Widget",
None,
vec![self_method_returning(11, "describe", "a widget")],
);
let trait_decl = trait_node(20, "Render", "render");
let trait_impl = impl_block_node(
30,
"Widget",
Some("Render"),
vec![self_method_returning(31, "render", "<widget/>")],
);
let out = gen(&module(vec![], vec![trait_decl, cls, inherent, trait_impl]));
assert!(
out.contains("def describe(self)"),
"inherent-impl method must be attached to the class, got:\n{out}"
);
assert!(
out.contains("def render(self)"),
"trait-impl method must be attached to the class, got:\n{out}"
);
assert!(
out.contains("class Widget(Render):"),
"class must subclass the implemented trait, got:\n{out}"
);
assert!(
!out.contains("\ndef describe("),
"impl method must not leak as a module-level function, got:\n{out}"
);
}
#[test]
fn py_class_methods_dispatch_at_runtime() {
if !has_python3() {
return;
}
let cls = class_with_field(1, "Widget", "name");
let inherent = impl_block_node(
10,
"Widget",
None,
vec![self_method_returning(11, "describe", "a widget")],
);
let trait_decl = trait_node(20, "Render", "render");
let trait_impl = impl_block_node(
30,
"Widget",
Some("Render"),
vec![self_method_returning(31, "render", "<widget/>")],
);
let out = gen(&module(vec![], vec![trait_decl, cls, inherent, trait_impl]));
let program =
format!("{out}\nw = Widget(name=\"x\")\nprint(w.describe())\nprint(w.render())\n");
let got = run_py(&program);
assert_eq!(got, "a widget\n<widget/>", "got:\n{got}\nfrom:\n{out}");
}
#[test]
fn py_trait_emitted_before_subclassing_record() {
let rec = node(
1,
NodeKind::RecordDecl {
annotations: vec![],
visibility: Visibility::Public,
name: ident("Widget"),
generic_params: vec![],
fields: vec![named_field("name", "String")],
},
);
let trait_impl = impl_block_node(
10,
"Widget",
Some("Render"),
vec![self_method_returning(11, "render", "<w/>")],
);
let trait_decl = trait_node(20, "Render", "render");
let out = gen(&module(vec![], vec![rec, trait_impl, trait_decl]));
let widget_pos = out
.find("class Widget(Render):")
.unwrap_or_else(|| panic!("expected `class Widget(Render):`, got:\n{out}"));
let render_pos = out
.find("class Render:")
.unwrap_or_else(|| panic!("expected `class Render:`, got:\n{out}"));
assert!(
render_pos < widget_pos,
"trait ABC `Render` must be emitted before subclass `Widget`, got:\n{out}"
);
if has_python3() {
assert!(
check_py_syntax(&out),
"ordered output must parse, got:\n{out}"
);
let program = format!("{out}\nw = Widget(name=\"x\")\nprint(w.render())\n");
assert_eq!(run_py(&program), "<w/>", "got from:\n{out}");
}
}
#[test]
fn py_inherent_method_wins_over_colliding_trait_method() {
let cls = class_with_field(1, "Button", "label");
let inherent = impl_block_node(
10,
"Button",
None,
vec![self_method_returning(11, "render", "<button/>")],
);
let trait_decl = trait_node(20, "Component", "render");
let trait_render = node(
31,
NodeKind::FnDecl {
annotations: vec![],
visibility: Visibility::Public,
is_async: false,
name: ident("render"),
generic_params: vec![],
params: vec![param_node(32, "self")],
return_type: Some(Box::new(node(
33,
NodeKind::TypeNamed {
path: type_path(&["String"]),
args: vec![],
},
))),
effect_clause: vec![],
where_clause: vec![],
body: Box::new(block(
34,
vec![],
Some(node(
35,
NodeKind::Call {
callee: Box::new(node(
36,
NodeKind::FieldAccess {
object: Box::new(id_node(37, "self")),
field: ident("render"),
},
)),
type_args: vec![],
args: vec![],
},
)),
)),
},
);
let trait_impl = impl_block_node(30, "Button", Some("Component"), vec![trait_render]);
let out = gen(&module(vec![], vec![trait_decl, cls, inherent, trait_impl]));
let count = out.matches("def render(self)").count();
assert_eq!(
count,
2, "expected one `render` in Button + one in the ABC, got {count}:\n{out}"
);
assert!(
out.contains("return \"<button/>\""),
"Button.render must be the concrete inherent body, got:\n{out}"
);
if has_python3() {
let program = format!("{out}\nb = Button(label=\"x\")\nprint(b.render())\n");
assert_eq!(run_py(&program), "<button/>", "got from:\n{out}");
}
}
#[test]
fn py_base_class_emitted_before_subclass() {
let sub = node(
1,
NodeKind::ClassDecl {
annotations: vec![],
visibility: Visibility::Public,
name: ident("Sub"),
generic_params: vec![],
base: Some(type_path(&["Base"])),
traits: vec![],
fields: vec![named_field("name", "String")],
methods: vec![],
},
);
let base = class_with_field(10, "Base", "id");
let out = gen(&module(vec![], vec![sub, base]));
let base_pos = out
.find("class Base:")
.unwrap_or_else(|| panic!("expected `class Base:`, got:\n{out}"));
let sub_pos = out
.find("class Sub(Base):")
.unwrap_or_else(|| panic!("expected `class Sub(Base):`, got:\n{out}"));
assert!(
base_pos < sub_pos,
"base class must be emitted before subclass, got:\n{out}"
);
}
#[test]
fn py_effect_emitted_before_subclassing_record() {
let rec = node(
1,
NodeKind::RecordDecl {
annotations: vec![],
visibility: Visibility::Public,
name: ident("StubChannel"),
generic_params: vec![],
fields: vec![named_field("tag", "String")],
},
);
let chan_impl = impl_block_node(
10,
"StubChannel",
Some("Channel"),
vec![self_method_returning(11, "send", "sent")],
);
let effect = node(
20,
NodeKind::EffectDecl {
annotations: vec![],
visibility: Visibility::Public,
name: ident("Channel"),
generic_params: vec![],
components: vec![],
operations: vec![self_method_returning(21, "send", "")],
},
);
let out = gen(&module(vec![], vec![rec, chan_impl, effect]));
let chan_pos = out
.find("class Channel(ABC):")
.unwrap_or_else(|| panic!("expected `class Channel(ABC):`, got:\n{out}"));
let stub_pos = out
.find("class StubChannel(Channel):")
.unwrap_or_else(|| panic!("expected `class StubChannel(Channel):`, got:\n{out}"));
assert!(
chan_pos < stub_pos,
"effect ABC `Channel` must precede the record that impls it, got:\n{out}"
);
}
#[test]
fn boolean_operators() {
let expr = node(
1,
NodeKind::BinaryOp {
op: BinOp::And,
left: Box::new(bool_lit(2, true)),
right: Box::new(bool_lit(3, false)),
},
);
let not_expr = node(
4,
NodeKind::UnaryOp {
op: UnaryOp::Not,
operand: Box::new(bool_lit(5, true)),
},
);
let body = block(
6,
vec![
node(
7,
NodeKind::LetBinding {
is_mut: false,
pattern: Box::new(bind_pat(8, "a")),
ty: None,
value: Box::new(expr),
},
),
node(
9,
NodeKind::LetBinding {
is_mut: false,
pattern: Box::new(bind_pat(10, "b")),
ty: None,
value: Box::new(not_expr),
},
),
],
None,
);
let f = node(
0,
NodeKind::FnDecl {
annotations: vec![],
visibility: Visibility::Private,
is_async: false,
name: ident("test"),
generic_params: vec![],
params: vec![],
return_type: None,
effect_clause: vec![],
where_clause: vec![],
body: Box::new(body),
},
);
let out = gen(&module(vec![], vec![f]));
assert!(out.contains("(True and False)"), "got: {out}");
assert!(out.contains("not True"), "got: {out}");
}
#[test]
fn result_construct() {
let ok = node(
1,
NodeKind::ResultConstruct {
variant: ResultVariant::Ok,
value: Some(Box::new(int_lit(2, "42"))),
},
);
let err = node(
3,
NodeKind::ResultConstruct {
variant: ResultVariant::Err,
value: Some(Box::new(str_lit(4, "failed"))),
},
);
let body = block(
5,
vec![
node(
6,
NodeKind::LetBinding {
is_mut: false,
pattern: Box::new(bind_pat(7, "good")),
ty: None,
value: Box::new(ok),
},
),
node(
8,
NodeKind::LetBinding {
is_mut: false,
pattern: Box::new(bind_pat(9, "bad")),
ty: None,
value: Box::new(err),
},
),
],
None,
);
let f = node(
0,
NodeKind::FnDecl {
annotations: vec![],
visibility: Visibility::Private,
is_async: false,
name: ident("test"),
generic_params: vec![],
params: vec![],
return_type: None,
effect_clause: vec![],
where_clause: vec![],
body: Box::new(body),
},
);
let out = gen(&module(vec![], vec![f]));
assert!(out.contains("_BockOk(42)"), "got: {out}");
assert!(out.contains("_BockErr(\"failed\")"), "got: {out}");
}
fn let_node(id: u32, name: &str, value: AIRNode) -> AIRNode {
node(
id,
NodeKind::LetBinding {
is_mut: false,
pattern: Box::new(bind_pat(id + 1, name)),
ty: None,
value: Box::new(value),
},
)
}
fn propagate(id: u32, expr: AIRNode) -> AIRNode {
node(
id,
NodeKind::Propagate {
expr: Box::new(expr),
},
)
}
#[test]
fn propagate_unwraps_and_wraps_body() {
let inner_call = node(
10,
NodeKind::Call {
callee: Box::new(id_node(11, "fallible")),
args: vec![],
type_args: vec![],
},
);
let body = block(
2,
vec![let_node(3, "v", propagate(4, inner_call))],
Some(node(
6,
NodeKind::ResultConstruct {
variant: ResultVariant::Ok,
value: Some(Box::new(id_node(7, "v"))),
},
)),
);
let f = fn_decl_body(1, "do_it", body);
let out = gen(&module(vec![], vec![f]));
assert!(out.contains("_bock_try(fallible())"), "got: {out}");
assert!(out.contains("try:"), "got: {out}");
assert!(
out.contains("except _BockPropagate as __bock_p:"),
"got: {out}"
);
assert!(out.contains("return __bock_p.value"), "got: {out}");
assert!(out.contains("def _bock_try(v):"), "got: {out}");
}
#[test]
fn no_propagate_no_envelope() {
let body = block(2, vec![], Some(int_lit(3, "1")));
let f = fn_decl_body(1, "plain", body);
let out = gen(&module(vec![], vec![f]));
assert!(!out.contains("_bock_try"), "got: {out}");
assert!(!out.contains("_BockPropagate"), "got: {out}");
}
#[test]
fn nested_block_let_shadow_is_renamed() {
let add = |id, l: AIRNode, r: AIRNode| {
node(
id,
NodeKind::BinaryOp {
op: BinOp::Add,
left: Box::new(l),
right: Box::new(r),
},
)
};
let mul = |id, l: AIRNode, r: AIRNode| {
node(
id,
NodeKind::BinaryOp {
op: BinOp::Mul,
left: Box::new(l),
right: Box::new(r),
},
)
};
let inner_block = block(
20,
vec![let_node(
21,
"y",
add(22, id_node(23, "y"), int_lit(24, "10")),
)],
Some(mul(25, id_node(26, "y"), int_lit(27, "2"))),
);
let body = block(
2,
vec![
let_node(3, "y", int_lit(4, "1")),
let_node(5, "z", inner_block),
],
Some(add(8, id_node(9, "y"), id_node(10, "z"))),
);
let f = fn_decl_body(1, "nested", body);
let out = gen(&module(vec![], vec![f]));
assert!(out.contains("y__s"), "expected a shadow alias, got: {out}");
assert!(out.contains("y = 1"), "got: {out}");
assert!(out.contains("return (y + z)"), "got: {out}");
}
#[test]
fn same_block_rebind_is_not_renamed() {
let add = |id, l: AIRNode, r: AIRNode| {
node(
id,
NodeKind::BinaryOp {
op: BinOp::Add,
left: Box::new(l),
right: Box::new(r),
},
)
};
let body = block(
2,
vec![
let_node(3, "acc", int_lit(4, "1")),
let_node(5, "acc", add(6, id_node(7, "acc"), int_lit(8, "2"))),
],
Some(id_node(9, "acc")),
);
let f = fn_decl_body(1, "rebind", body);
let out = gen(&module(vec![], vec![f]));
assert!(!out.contains("acc__s"), "must not rename, got: {out}");
assert!(out.contains("acc = 1"), "got: {out}");
assert!(out.contains("acc = (acc + 2)"), "got: {out}");
}
#[test]
fn tail_loop_emits_while_not_unsupported() {
let loop_body = block(10, vec![node(11, NodeKind::Break { value: None })], None);
let tail_loop = node(
5,
NodeKind::Loop {
body: Box::new(loop_body),
},
);
let body = block(2, vec![], Some(tail_loop));
let f = fn_decl_body(1, "spin", body);
let out = gen(&module(vec![], vec![f]));
assert!(out.contains("while True:"), "got: {out}");
assert!(!out.contains("# unsupported"), "got: {out}");
assert!(!out.contains("return # unsupported"), "got: {out}");
}
fn fn_decl_body(id: u32, name: &str, body: AIRNode) -> AIRNode {
node(
id,
NodeKind::FnDecl {
annotations: vec![],
visibility: Visibility::Private,
is_async: false,
name: ident(name),
generic_params: vec![],
params: vec![],
return_type: None,
effect_clause: vec![],
where_clause: vec![],
body: Box::new(body),
},
)
}
#[test]
fn to_snake_case_conversions() {
assert_eq!(to_snake_case("fetchData"), "fetch_data");
assert_eq!(to_snake_case("MyClass"), "my_class");
assert_eq!(to_snake_case("already_snake"), "already_snake");
assert_eq!(to_snake_case("simple"), "simple");
assert_eq!(to_snake_case("HTMLParser"), "html_parser");
assert_eq!(to_snake_case("x"), "x");
assert_eq!(to_snake_case("_"), "_");
}
fn has_python3() -> bool {
std::process::Command::new("which")
.arg("python3")
.output()
.map(|o| o.status.success())
.unwrap_or(false)
}
fn check_py_syntax(code: &str) -> bool {
use std::sync::atomic::{AtomicU64, Ordering};
static SEQ: AtomicU64 = AtomicU64::new(0);
let dir = std::env::temp_dir();
let path = dir.join(format!(
"bock_test_output_{}_{}.py",
std::process::id(),
SEQ.fetch_add(1, Ordering::Relaxed)
));
std::fs::write(&path, code).expect("failed to write temp file");
let result = std::process::Command::new("python3")
.arg("-m")
.arg("py_compile")
.arg(&path)
.output()
.expect("failed to spawn python3");
let _ = std::fs::remove_file(&path);
result.status.success()
}
fn run_py(code: &str) -> String {
let output = std::process::Command::new("python3")
.arg("-c")
.arg(code)
.output()
.expect("failed to run python3");
String::from_utf8(output.stdout)
.unwrap()
.replace("\r\n", "\n")
.trim()
.to_string()
}
#[test]
#[ignore]
fn e2e_hello_world() {
if !has_python3() {
return;
}
let body = block(
2,
vec![],
Some(node(
3,
NodeKind::Call {
callee: Box::new(id_node(4, "print")),
args: vec![AirArg {
label: None,
value: str_lit(5, "Hello, World!"),
}],
type_args: vec![],
},
)),
);
let f = node(
1,
NodeKind::FnDecl {
annotations: vec![],
visibility: Visibility::Private,
is_async: false,
name: ident("main"),
generic_params: vec![],
params: vec![],
return_type: None,
effect_clause: vec![],
where_clause: vec![],
body: Box::new(body),
},
);
let code = gen(&module(vec![], vec![f]));
let full = format!("{code}\nmain()\n");
assert!(
check_py_syntax(&full),
"Python syntax check failed:\n{full}"
);
assert_eq!(run_py(&full), "Hello, World!");
}
#[test]
#[ignore]
fn e2e_arithmetic() {
if !has_python3() {
return;
}
let body = block(
2,
vec![],
Some(node(
3,
NodeKind::BinaryOp {
op: BinOp::Add,
left: Box::new(int_lit(4, "10")),
right: Box::new(int_lit(5, "32")),
},
)),
);
let f = node(
1,
NodeKind::FnDecl {
annotations: vec![],
visibility: Visibility::Private,
is_async: false,
name: ident("calc"),
generic_params: vec![],
params: vec![],
return_type: None,
effect_clause: vec![],
where_clause: vec![],
body: Box::new(body),
},
);
let code = gen(&module(vec![], vec![f]));
let full = format!("{code}\nprint(calc())\n");
assert!(
check_py_syntax(&full),
"Python syntax check failed:\n{full}"
);
assert_eq!(run_py(&full), "42");
}
#[test]
#[ignore]
fn e2e_if_else() {
if !has_python3() {
return;
}
let if_stmt = node(
3,
NodeKind::If {
let_pattern: None,
condition: Box::new(node(
4,
NodeKind::BinaryOp {
op: BinOp::Gt,
left: Box::new(id_node(5, "x")),
right: Box::new(int_lit(6, "0")),
},
)),
then_block: Box::new(block(
7,
vec![node(
8,
NodeKind::Return {
value: Some(Box::new(str_lit(9, "positive"))),
},
)],
None,
)),
else_block: Some(Box::new(block(
10,
vec![node(
11,
NodeKind::Return {
value: Some(Box::new(str_lit(12, "non-positive"))),
},
)],
None,
))),
},
);
let body = block(2, vec![if_stmt], None);
let f = node(
1,
NodeKind::FnDecl {
annotations: vec![],
visibility: Visibility::Private,
is_async: false,
name: ident("classify"),
generic_params: vec![],
params: vec![param_node(13, "x")],
return_type: None,
effect_clause: vec![],
where_clause: vec![],
body: Box::new(body),
},
);
let code = gen(&module(vec![], vec![f]));
let full = format!("{code}\nprint(classify(5))\nprint(classify(-1))\n");
assert!(
check_py_syntax(&full),
"Python syntax check failed:\n{full}"
);
let output = run_py(&full);
assert!(output.contains("positive"), "got: {output}");
assert!(output.contains("non-positive"), "got: {output}");
}
#[test]
#[ignore]
fn e2e_for_loop() {
if !has_python3() {
return;
}
let body = block(
2,
vec![
node(
3,
NodeKind::LetBinding {
is_mut: true,
pattern: Box::new(bind_pat(4, "sum")),
ty: None,
value: Box::new(int_lit(5, "0")),
},
),
node(
6,
NodeKind::For {
pattern: Box::new(bind_pat(7, "x")),
iterable: Box::new(node(
8,
NodeKind::ListLiteral {
elems: vec![int_lit(9, "1"), int_lit(10, "2"), int_lit(11, "3")],
},
)),
body: Box::new(block(
12,
vec![node(
13,
NodeKind::Assign {
op: AssignOp::AddAssign,
target: Box::new(id_node(14, "sum")),
value: Box::new(id_node(15, "x")),
},
)],
None,
)),
},
),
],
Some(id_node(16, "sum")),
);
let f = node(
1,
NodeKind::FnDecl {
annotations: vec![],
visibility: Visibility::Private,
is_async: false,
name: ident("total"),
generic_params: vec![],
params: vec![],
return_type: None,
effect_clause: vec![],
where_clause: vec![],
body: Box::new(body),
},
);
let code = gen(&module(vec![], vec![f]));
let full = format!("{code}\nprint(total())\n");
assert!(
check_py_syntax(&full),
"Python syntax check failed:\n{full}"
);
assert_eq!(run_py(&full), "6");
}
#[test]
#[ignore]
fn e2e_dataclass() {
if !has_python3() {
return;
}
let rec = node(
1,
NodeKind::RecordDecl {
annotations: vec![],
visibility: Visibility::Public,
name: ident("Point"),
generic_params: vec![],
fields: vec![
bock_ast::RecordDeclField {
id: 0,
span: span(),
name: ident("x"),
ty: bock_ast::TypeExpr::Named {
id: 0,
span: span(),
path: type_path(&["Float"]),
args: vec![],
},
default: None,
},
bock_ast::RecordDeclField {
id: 0,
span: span(),
name: ident("y"),
ty: bock_ast::TypeExpr::Named {
id: 0,
span: span(),
path: type_path(&["Float"]),
args: vec![],
},
default: None,
},
],
},
);
let code = gen(&module(vec![], vec![rec]));
let full = format!("{code}\np = Point(x=1.0, y=2.0)\nprint(f\"{{p.x}}, {{p.y}}\")\n");
assert!(
check_py_syntax(&full),
"Python syntax check failed:\n{full}"
);
let output = run_py(&full);
assert!(output.contains("1.0, 2.0"), "got: {output}");
}
#[test]
#[ignore]
fn e2e_match_statement() {
if !has_python3() {
return;
}
let scrutinee = id_node(10, "x");
let arms = vec![
node(
11,
NodeKind::MatchArm {
pattern: Box::new(node(
12,
NodeKind::LiteralPat {
lit: Literal::Int("1".into()),
},
)),
guard: None,
body: Box::new(block(
13,
vec![node(
14,
NodeKind::Return {
value: Some(Box::new(str_lit(15, "one"))),
},
)],
None,
)),
},
),
node(
16,
NodeKind::MatchArm {
pattern: Box::new(node(17, NodeKind::WildcardPat)),
guard: None,
body: Box::new(block(
18,
vec![node(
19,
NodeKind::Return {
value: Some(Box::new(str_lit(20, "other"))),
},
)],
None,
)),
},
),
];
let match_stmt = node(
9,
NodeKind::Match {
scrutinee: Box::new(scrutinee),
arms,
},
);
let f = node(
1,
NodeKind::FnDecl {
annotations: vec![],
visibility: Visibility::Private,
is_async: false,
name: ident("describe"),
generic_params: vec![],
params: vec![param_node(2, "x")],
return_type: None,
effect_clause: vec![],
where_clause: vec![],
body: Box::new(block(3, vec![match_stmt], None)),
},
);
let code = gen(&module(vec![], vec![f]));
let full = format!("{code}\nprint(describe(1))\nprint(describe(99))\n");
assert!(
check_py_syntax(&full),
"Python syntax check failed:\n{full}"
);
let output = run_py(&full);
assert!(output.contains("one"), "got: {output}");
assert!(output.contains("other"), "got: {output}");
}
fn gen_prelude_call(func_name: &str, arg: AIRNode) -> String {
let call = node(
10,
NodeKind::Call {
callee: Box::new(id_node(11, func_name)),
args: vec![AirArg {
label: None,
value: arg,
}],
type_args: vec![],
},
);
let body = block(2, vec![call], None);
let f = node(
1,
NodeKind::FnDecl {
name: ident("main"),
params: vec![],
return_type: None,
body: Box::new(body),
generic_params: vec![],
visibility: Visibility::Private,
annotations: vec![],
effect_clause: vec![],
where_clause: vec![],
is_async: false,
},
);
gen(&module(vec![], vec![f]))
}
fn gen_prelude_call_no_args(func_name: &str) -> String {
let call = node(
10,
NodeKind::Call {
callee: Box::new(id_node(11, func_name)),
args: vec![],
type_args: vec![],
},
);
let body = block(2, vec![call], None);
let f = node(
1,
NodeKind::FnDecl {
name: ident("main"),
params: vec![],
return_type: None,
body: Box::new(body),
generic_params: vec![],
visibility: Visibility::Private,
annotations: vec![],
effect_clause: vec![],
where_clause: vec![],
is_async: false,
},
);
gen(&module(vec![], vec![f]))
}
#[test]
fn prelude_println_maps_to_print() {
let out = gen_prelude_call("println", str_lit(12, "hello"));
assert!(
out.contains("print("),
"println should map to print, got: {out}"
);
assert!(
!out.contains("println("),
"should not emit bare println(, got: {out}"
);
}
#[test]
fn prelude_print_maps_to_print_no_newline() {
let out = gen_prelude_call("print", str_lit(12, "hello"));
assert!(
out.contains("print(") && out.contains("end=\"\""),
"print should map to print with end=\"\", got: {out}"
);
}
#[test]
fn prelude_debug_maps_to_repr() {
let out = gen_prelude_call("debug", str_lit(12, "val"));
assert!(
out.contains("print(repr("),
"debug should map to print(repr(...)), got: {out}"
);
}
#[test]
fn prelude_assert_maps_to_assert() {
let out = gen_prelude_call("assert", bool_lit(12, true));
assert!(
out.contains("assert "),
"assert should map to Python assert, got: {out}"
);
assert!(
!out.contains("assert("),
"should not emit assert as function call, got: {out}"
);
}
#[test]
fn prelude_todo_maps_to_not_implemented_error() {
let out = gen_prelude_call_no_args("todo");
assert!(
out.contains("raise NotImplementedError()"),
"todo should map to raise NotImplementedError, got: {out}"
);
}
#[test]
fn prelude_unreachable_maps_to_runtime_error() {
let out = gen_prelude_call_no_args("unreachable");
assert!(
out.contains("raise RuntimeError(\"unreachable\")"),
"unreachable should map to raise RuntimeError, got: {out}"
);
}
#[test]
fn non_prelude_call_passes_through() {
let out = gen_prelude_call("my_custom_func", str_lit(12, "arg"));
assert!(
out.contains("my_custom_func("),
"non-prelude call should use snake_case, got: {out}"
);
}
fn wildcard_arm(id: u32, body: AIRNode) -> AIRNode {
node(
id,
NodeKind::MatchArm {
pattern: Box::new(node(id + 100, NodeKind::WildcardPat)),
guard: None,
body: Box::new(body),
},
)
}
fn block_with_tail(id: u32, stmts: Vec<AIRNode>, tail: &str) -> AIRNode {
node(
id,
NodeKind::Block {
stmts,
tail: Some(Box::new(str_lit(id + 1, tail))),
},
)
}
#[test]
fn valpos_arm_with_loop_leading_stmt_needs_stmt_form() {
let loop_stmt = node(
10,
NodeKind::For {
pattern: Box::new(node(11, NodeKind::WildcardPat)),
iterable: Box::new(id_node(12, "xs")),
body: Box::new(node(
13,
NodeKind::Block {
stmts: vec![],
tail: None,
},
)),
},
);
let arms = vec![wildcard_arm(1, block_with_tail(20, vec![loop_stmt], "v"))];
assert!(
match_arm_drops_leading_stmts(&arms),
"a value-tail arm with a leading `for` loop must route to statement form"
);
assert!(match_value_needs_stmt_form(&arms));
assert!(value_needs_stmt_form(&node(
30,
NodeKind::Match {
scrutinee: Box::new(id_node(31, "j")),
arms,
}
)));
}
#[test]
fn valpos_arm_with_assign_leading_stmt_needs_stmt_form() {
let assign = node(
10,
NodeKind::Assign {
target: Box::new(id_node(11, "x")),
op: AssignOp::Assign,
value: Box::new(int_lit(12, "1")),
},
);
let arms = vec![wildcard_arm(1, block_with_tail(20, vec![assign], "v"))];
assert!(match_arm_drops_leading_stmts(&arms));
}
#[test]
fn valpos_arm_with_mut_let_leading_stmt_needs_stmt_form() {
let mut_let = node(
10,
NodeKind::LetBinding {
is_mut: true,
pattern: Box::new(node(
11,
NodeKind::BindPat {
name: ident("x"),
is_mut: true,
},
)),
ty: None,
value: Box::new(int_lit(12, "1")),
},
);
let arms = vec![wildcard_arm(1, block_with_tail(20, vec![mut_let], "v"))];
assert!(match_arm_drops_leading_stmts(&arms));
}
#[test]
fn valpos_arm_with_simple_let_stays_on_lambda_chain() {
let simple_let = node(
10,
NodeKind::LetBinding {
is_mut: false,
pattern: Box::new(node(
11,
NodeKind::BindPat {
name: ident("x"),
is_mut: false,
},
)),
ty: None,
value: Box::new(int_lit(12, "1")),
},
);
let arms = vec![wildcard_arm(1, block_with_tail(20, vec![simple_let], "v"))];
assert!(
!match_arm_drops_leading_stmts(&arms),
"a simple immutable `let` is lambda-expressible — keep it on the chain"
);
}
#[test]
fn valpos_arm_with_bare_call_stays_on_lambda_chain() {
let call = node(
10,
NodeKind::Call {
callee: Box::new(id_node(11, "f")),
args: vec![],
type_args: vec![],
},
);
let arms = vec![wildcard_arm(1, block_with_tail(20, vec![call], "v"))];
assert!(!match_arm_drops_leading_stmts(&arms));
}
#[test]
fn valpos_arm_tail_only_block_stays_on_lambda_chain() {
let arms = vec![wildcard_arm(1, block_with_tail(20, vec![], "v"))];
assert!(!match_arm_drops_leading_stmts(&arms));
}
#[test]
fn effect_decl_becomes_abc() {
let effect = node(
1,
NodeKind::EffectDecl {
annotations: vec![],
visibility: Visibility::Public,
name: ident("Logger"),
generic_params: vec![],
components: vec![],
operations: vec![
node(
2,
NodeKind::FnDecl {
annotations: vec![],
visibility: Visibility::Public,
is_async: false,
name: ident("log"),
generic_params: vec![],
params: vec![
typed_param_node(3, "level", "String"),
typed_param_node(4, "msg", "String"),
],
return_type: Some(Box::new(node(
5,
NodeKind::TypeNamed {
path: type_path(&["Void"]),
args: vec![],
},
))),
effect_clause: vec![],
where_clause: vec![],
body: Box::new(block(6, vec![], None)),
},
),
node(
7,
NodeKind::FnDecl {
annotations: vec![],
visibility: Visibility::Public,
is_async: false,
name: ident("flush"),
generic_params: vec![],
params: vec![],
return_type: Some(Box::new(node(
8,
NodeKind::TypeNamed {
path: type_path(&["Void"]),
args: vec![],
},
))),
effect_clause: vec![],
where_clause: vec![],
body: Box::new(block(9, vec![], None)),
},
),
],
},
);
let out = gen(&module(vec![], vec![effect]));
assert!(
out.contains("from abc import ABC, abstractmethod"),
"got: {out}"
);
assert!(out.contains("class Logger(ABC):"), "got: {out}");
assert!(out.contains("@abstractmethod"), "got: {out}");
assert!(
out.contains("def log(self, level: str, msg: str) -> None:"),
"got: {out}"
);
assert!(out.contains("def flush(self) -> None:"), "got: {out}");
assert!(out.contains(" ..."), "got: {out}");
}
#[test]
fn effect_decl_empty_operations() {
let effect = node(
1,
NodeKind::EffectDecl {
annotations: vec![],
visibility: Visibility::Public,
name: ident("Empty"),
generic_params: vec![],
components: vec![],
operations: vec![],
},
);
let out = gen(&module(vec![], vec![effect]));
assert!(out.contains("class Empty(ABC):"), "got: {out}");
assert!(out.contains(" pass"), "got: {out}");
}
#[test]
fn handling_block_passes_handlers_to_effectful_call() {
use bock_air::AirHandlerPair;
let effect_decl = node(
1,
NodeKind::EffectDecl {
annotations: vec![],
visibility: Visibility::Public,
name: ident("Logger"),
generic_params: vec![],
components: vec![],
operations: vec![node(
2,
NodeKind::FnDecl {
annotations: vec![],
visibility: Visibility::Public,
is_async: false,
name: ident("log"),
generic_params: vec![],
params: vec![typed_param_node(3, "msg", "String")],
return_type: None,
effect_clause: vec![],
where_clause: vec![],
body: Box::new(block(4, vec![], None)),
},
)],
},
);
let inner_fn = node(
10,
NodeKind::FnDecl {
annotations: vec![],
visibility: Visibility::Private,
is_async: false,
name: ident("inner"),
generic_params: vec![],
params: vec![],
return_type: None,
effect_clause: vec![type_path(&["Logger"])],
where_clause: vec![],
body: Box::new(block(12, vec![], Some(str_lit(13, "hello")))),
},
);
let call_inner = node(
20,
NodeKind::Call {
callee: Box::new(id_node(21, "inner")),
args: vec![],
type_args: vec![],
},
);
let handling = node(
30,
NodeKind::HandlingBlock {
handlers: vec![AirHandlerPair {
effect: type_path(&["Logger"]),
handler: Box::new(node(
31,
NodeKind::Call {
callee: Box::new(id_node(32, "StdoutLogger")),
args: vec![],
type_args: vec![],
},
)),
}],
body: Box::new(block(33, vec![], Some(call_inner))),
},
);
let main_fn = node(
40,
NodeKind::FnDecl {
annotations: vec![],
visibility: Visibility::Private,
is_async: false,
name: ident("main"),
generic_params: vec![],
params: vec![],
return_type: None,
effect_clause: vec![],
where_clause: vec![],
body: Box::new(block(41, vec![handling], None)),
},
);
let out = gen(&module(vec![], vec![effect_decl, inner_fn, main_fn]));
assert!(
out.contains("inner(logger=__logger_h"),
"handling block should pass handler to effectful call, got: {out}"
);
assert!(
out.contains(": Logger = StdoutLogger()"),
"handling block should instantiate handler, got: {out}"
);
}
#[test]
fn async_function_imports_asyncio() {
let body = block(3, vec![], None);
let f = node(
1,
NodeKind::FnDecl {
annotations: vec![],
visibility: Visibility::Private,
is_async: true,
name: ident("tick"),
generic_params: vec![],
params: vec![],
return_type: None,
effect_clause: vec![],
where_clause: vec![],
body: Box::new(body),
},
);
let out = gen(&module(vec![], vec![f]));
assert!(out.contains("import asyncio"), "got: {out}");
assert!(out.contains("async def tick():"), "got: {out}");
}
#[test]
fn sync_module_has_no_asyncio_import() {
let body = block(3, vec![], Some(int_lit(4, "1")));
let f = node(
1,
NodeKind::FnDecl {
annotations: vec![],
visibility: Visibility::Private,
is_async: false,
name: ident("one"),
generic_params: vec![],
params: vec![],
return_type: None,
effect_clause: vec![],
where_clause: vec![],
body: Box::new(body),
},
);
let out = gen(&module(vec![], vec![f]));
assert!(!out.contains("import asyncio"), "got: {out}");
}
#[test]
fn entry_invocation_async_main_python() {
let inv = PyGenerator::new().entry_invocation(true).unwrap();
assert!(inv.contains("asyncio.run(main())"), "got: {inv}");
}
#[test]
fn entry_invocation_sync_main_python() {
let inv = PyGenerator::new().entry_invocation(false).unwrap();
assert_eq!(inv, "if __name__ == \"__main__\":\n main()\n");
}
#[test]
fn generate_project_async_main_uses_asyncio_run() {
let main_fn = node(
1,
NodeKind::FnDecl {
annotations: vec![],
visibility: Visibility::Private,
is_async: true,
name: ident("main"),
generic_params: vec![],
params: vec![],
return_type: None,
effect_clause: vec![],
where_clause: vec![],
body: Box::new(block(2, vec![], None)),
},
);
let m = module(vec![], vec![main_fn]);
let gen = PyGenerator::new();
let src_path = std::path::Path::new("src/main.bock");
let out = gen.generate_project(&[(&m, src_path)]).unwrap();
let src = &out.files[0].content;
assert_eq!(out.files[0].path, std::path::PathBuf::from("main.py"));
assert!(src.contains("import asyncio"), "got: {src}");
assert!(src.contains("async def main():"), "got: {src}");
assert!(src.contains("asyncio.run(main())"), "got: {src}");
}
#[test]
fn concurrent_pattern_wraps_in_create_task() {
let call_task = |id: u32, name: &str| {
node(
id,
NodeKind::Call {
callee: Box::new(id_node(id + 1, name)),
args: vec![],
type_args: vec![],
},
)
};
let let_stmt = |id: u32, name: &str, val: AIRNode| {
node(
id,
NodeKind::LetBinding {
is_mut: false,
pattern: Box::new(bind_pat(id + 1, name)),
ty: None,
value: Box::new(val),
},
)
};
let await_id = |id: u32, name: &str| {
node(
id,
NodeKind::Await {
expr: Box::new(id_node(id + 1, name)),
},
)
};
let body = block(
10,
vec![
let_stmt(20, "a", call_task(21, "task1")),
let_stmt(30, "b", call_task(31, "task2")),
let_stmt(40, "ra", await_id(41, "a")),
let_stmt(50, "rb", await_id(51, "b")),
],
Some(id_node(60, "ra")),
);
let f = node(
1,
NodeKind::FnDecl {
annotations: vec![],
visibility: Visibility::Private,
is_async: true,
name: ident("run"),
generic_params: vec![],
params: vec![],
return_type: None,
effect_clause: vec![],
where_clause: vec![],
body: Box::new(body),
},
);
let out = gen(&module(vec![], vec![f]));
assert!(
out.contains("a = asyncio.create_task(task1())"),
"task1 should be scheduled as a task, got: {out}"
);
assert!(
out.contains("b = asyncio.create_task(task2())"),
"task2 should be scheduled as a task, got: {out}"
);
assert!(out.contains("ra = (await a)"), "got: {out}");
assert!(out.contains("rb = (await b)"), "got: {out}");
}
#[test]
fn sequential_await_no_task_wrapping() {
let await_call = node(
20,
NodeKind::Await {
expr: Box::new(node(
21,
NodeKind::Call {
callee: Box::new(id_node(22, "task1")),
args: vec![],
type_args: vec![],
},
)),
},
);
let let_stmt = node(
10,
NodeKind::LetBinding {
is_mut: false,
pattern: Box::new(bind_pat(11, "a")),
ty: None,
value: Box::new(await_call),
},
);
let body = block(30, vec![let_stmt], Some(id_node(40, "a")));
let f = node(
1,
NodeKind::FnDecl {
annotations: vec![],
visibility: Visibility::Private,
is_async: true,
name: ident("run"),
generic_params: vec![],
params: vec![],
return_type: None,
effect_clause: vec![],
where_clause: vec![],
body: Box::new(body),
},
);
let out = gen(&module(vec![], vec![f]));
assert!(
!out.contains("create_task"),
"sequential await should not wrap in create_task, got: {out}"
);
assert!(out.contains("a = (await task1())"), "got: {out}");
}
#[test]
fn non_call_rhs_not_wrapped_in_task() {
let let_stmt = node(
10,
NodeKind::LetBinding {
is_mut: false,
pattern: Box::new(bind_pat(11, "a")),
ty: None,
value: Box::new(int_lit(12, "42")),
},
);
let await_a = node(
20,
NodeKind::Await {
expr: Box::new(id_node(21, "a")),
},
);
let body = block(30, vec![let_stmt], Some(await_a));
let f = node(
1,
NodeKind::FnDecl {
annotations: vec![],
visibility: Visibility::Private,
is_async: true,
name: ident("run"),
generic_params: vec![],
params: vec![],
return_type: None,
effect_clause: vec![],
where_clause: vec![],
body: Box::new(body),
},
);
let out = gen(&module(vec![], vec![f]));
assert!(!out.contains("create_task"), "got: {out}");
assert!(out.contains("a = 42"), "got: {out}");
}
#[test]
fn optional_runtime_construct_and_match() {
let opt_int_ty = node(
200,
NodeKind::TypeOptional {
inner: Box::new(node(
201,
NodeKind::TypeNamed {
path: type_path(&["Int"]),
args: vec![],
},
)),
},
);
let o_param = node(
30,
NodeKind::Param {
pattern: Box::new(bind_pat(31, "o")),
ty: Some(Box::new(opt_int_ty)),
default: None,
},
);
let some_call = node(
70,
NodeKind::Call {
callee: Box::new(id_node(71, "Some")),
args: vec![AirArg {
label: None,
value: int_lit(72, "1"),
}],
type_args: vec![],
},
);
let none_ref = id_node(73, "None");
let some_arm = node(
40,
NodeKind::MatchArm {
pattern: Box::new(node(
41,
NodeKind::ConstructorPat {
path: type_path(&["Some"]),
fields: vec![bind_pat(42, "x")],
},
)),
guard: None,
body: Box::new(block(
43,
vec![node(
44,
NodeKind::Return {
value: Some(Box::new(id_node(45, "x"))),
},
)],
None,
)),
},
);
let none_arm = node(
50,
NodeKind::MatchArm {
pattern: Box::new(node(
51,
NodeKind::ConstructorPat {
path: type_path(&["None"]),
fields: vec![],
},
)),
guard: None,
body: Box::new(block(
52,
vec![node(
53,
NodeKind::Return {
value: Some(Box::new(int_lit(54, "0"))),
},
)],
None,
)),
},
);
let match_stmt = node(
60,
NodeKind::Match {
scrutinee: Box::new(id_node(61, "o")),
arms: vec![some_arm, none_arm],
},
);
let f = node(
1,
NodeKind::FnDecl {
annotations: vec![],
visibility: Visibility::Private,
is_async: false,
name: ident("describe"),
generic_params: vec![],
params: vec![o_param],
return_type: Some(Box::new(node(
2,
NodeKind::TypeNamed {
path: type_path(&["Int"]),
args: vec![],
},
))),
effect_clause: vec![],
where_clause: vec![],
body: Box::new(block(
3,
vec![
node(
80,
NodeKind::LetBinding {
is_mut: false,
pattern: Box::new(bind_pat(81, "a")),
ty: None,
value: Box::new(some_call),
},
),
node(
82,
NodeKind::LetBinding {
is_mut: false,
pattern: Box::new(bind_pat(83, "b")),
ty: None,
value: Box::new(none_ref),
},
),
match_stmt,
],
None,
)),
},
);
let out = gen(&module(vec![], vec![f]));
assert!(out.contains("class _BockSome:"), "got: {out}");
assert!(out.contains("class _BockNone:"), "got: {out}");
assert!(out.contains("_bock_none = _BockNone()"), "got: {out}");
assert!(out.contains("_BockSome(1)"), "got: {out}");
assert!(out.contains("b = _bock_none"), "got: {out}");
assert!(out.contains("case _BockSome(x):"), "got: {out}");
assert!(out.contains("case _BockNone():"), "got: {out}");
assert!(!out.contains("case None()"), "got: {out}");
}
#[test]
fn optional_match_in_expression_position_binds_payload() {
let opt_int_ty = node(
200,
NodeKind::TypeOptional {
inner: Box::new(node(
201,
NodeKind::TypeNamed {
path: type_path(&["Int"]),
args: vec![],
},
)),
},
);
let o_param = node(
30,
NodeKind::Param {
pattern: Box::new(bind_pat(31, "o")),
ty: Some(Box::new(opt_int_ty)),
default: None,
},
);
let some_arm = node(
40,
NodeKind::MatchArm {
pattern: Box::new(node(
41,
NodeKind::ConstructorPat {
path: type_path(&["Some"]),
fields: vec![bind_pat(42, "x")],
},
)),
guard: None,
body: Box::new(block(
43,
vec![],
Some(node(
44,
NodeKind::BinaryOp {
op: BinOp::Add,
left: Box::new(id_node(45, "x")),
right: Box::new(int_lit(46, "1")),
},
)),
)),
},
);
let none_arm = node(
50,
NodeKind::MatchArm {
pattern: Box::new(node(
51,
NodeKind::ConstructorPat {
path: type_path(&["None"]),
fields: vec![],
},
)),
guard: None,
body: Box::new(block(52, vec![], Some(int_lit(53, "0")))),
},
);
let match_expr = node(
60,
NodeKind::Match {
scrutinee: Box::new(id_node(61, "o")),
arms: vec![some_arm, none_arm],
},
);
let let_r = node(
70,
NodeKind::LetBinding {
is_mut: false,
pattern: Box::new(bind_pat(71, "r")),
ty: None,
value: Box::new(match_expr),
},
);
let f = node(
1,
NodeKind::FnDecl {
annotations: vec![],
visibility: Visibility::Private,
is_async: false,
name: ident("pick"),
generic_params: vec![],
params: vec![o_param],
return_type: Some(Box::new(node(
2,
NodeKind::TypeNamed {
path: type_path(&["Int"]),
args: vec![],
},
))),
effect_clause: vec![],
where_clause: vec![],
body: Box::new(block(
3,
vec![
let_r,
node(
80,
NodeKind::Return {
value: Some(Box::new(id_node(81, "r"))),
},
),
],
None,
)),
},
);
let out = gen(&module(vec![], vec![f]));
assert!(
!out.contains("if False"),
"expression-position match must not emit the `if False` stub, got: {out}"
);
assert!(
out.contains("isinstance(__v, _BockSome)"),
"expected a tag test, got: {out}"
);
assert!(
out.contains("(lambda x:") && out.contains(")(__v._0)"),
"expected the Some payload bound from __v._0, got: {out}"
);
}
fn generic_param(name: &str, bound: Option<&str>) -> bock_ast::GenericParam {
bock_ast::GenericParam {
id: 0,
span: span(),
name: ident(name),
bounds: bound.map(|b| vec![type_path(&[b])]).unwrap_or_default(),
}
}
fn named_field(field: &str, ty_name: &str) -> bock_ast::RecordDeclField {
bock_ast::RecordDeclField {
id: 0,
span: span(),
name: ident(field),
ty: bock_ast::TypeExpr::Named {
id: 0,
span: span(),
path: type_path(&[ty_name]),
args: vec![],
},
default: None,
}
}
#[test]
fn lambda_params_have_no_type_hints() {
let lambda = node(
1,
NodeKind::Lambda {
params: vec![typed_param_node(2, "x", "Int")],
body: Box::new(node(
3,
NodeKind::BinaryOp {
op: BinOp::Add,
left: Box::new(id_node(4, "x")),
right: Box::new(int_lit(5, "1")),
},
)),
},
);
let body = block(
6,
vec![node(
7,
NodeKind::LetBinding {
is_mut: false,
pattern: Box::new(bind_pat(8, "inc")),
ty: None,
value: Box::new(lambda),
},
)],
None,
);
let f = node(
9,
NodeKind::FnDecl {
annotations: vec![],
visibility: Visibility::Private,
is_async: false,
name: ident("run"),
generic_params: vec![],
params: vec![],
return_type: None,
effect_clause: vec![],
where_clause: vec![],
body: Box::new(body),
},
);
let out = gen(&module(vec![], vec![f]));
assert!(
out.contains("lambda x: "),
"lambda must emit a bare param list, got: {out}"
);
assert!(
!out.contains("lambda x: int"),
"lambda param must NOT carry a type hint (SyntaxError), got: {out}"
);
}
#[test]
fn fn_type_param_emits_callable_import() {
let f_param = node(
2,
NodeKind::Param {
pattern: Box::new(bind_pat(3, "f")),
ty: Some(Box::new(node(
4,
NodeKind::TypeFunction {
params: vec![node(
5,
NodeKind::TypeNamed {
path: type_path(&["Int"]),
args: vec![],
},
)],
ret: Box::new(node(
6,
NodeKind::TypeNamed {
path: type_path(&["Int"]),
args: vec![],
},
)),
effects: vec![],
},
))),
default: None,
},
);
let body = block(7, vec![], Some(int_lit(8, "0")));
let f = node(
1,
NodeKind::FnDecl {
annotations: vec![],
visibility: Visibility::Private,
is_async: false,
name: ident("apply"),
generic_params: vec![],
params: vec![f_param],
return_type: None,
effect_clause: vec![],
where_clause: vec![],
body: Box::new(body),
},
);
let out = gen(&module(vec![], vec![f]));
assert!(
out.contains("from typing import Callable"),
"Callable annotation needs its typing import, got: {out}"
);
assert!(
out.contains("f: Callable[[int], int]"),
"expected the Callable annotation, got: {out}"
);
}
#[test]
fn generic_record_emits_typevar_and_generic() {
let rec = node(
1,
NodeKind::RecordDecl {
annotations: vec![],
visibility: Visibility::Public,
name: ident("Box"),
generic_params: vec![generic_param("T", None)],
fields: vec![named_field("value", "T")],
},
);
let out = gen(&module(vec![], vec![rec]));
assert!(
out.contains("from typing import Generic, TypeVar"),
"expected merged typing import, got: {out}"
);
assert!(
out.contains("T = TypeVar(\"T\")"),
"expected a TypeVar declaration, got: {out}"
);
assert!(
out.contains("class Box(Generic[T]):"),
"expected Generic[T] in the class bases, got: {out}"
);
assert!(out.contains("value: T"), "got: {out}");
}
#[test]
fn bounded_type_param_emits_typevar_bound() {
let body = block(3, vec![], Some(int_lit(4, "0")));
let f = node(
1,
NodeKind::FnDecl {
annotations: vec![],
visibility: Visibility::Private,
is_async: false,
name: ident("describe"),
generic_params: vec![generic_param("T", Some("Named"))],
params: vec![typed_param_node(2, "x", "T")],
return_type: None,
effect_clause: vec![],
where_clause: vec![],
body: Box::new(body),
},
);
let out = gen(&module(vec![], vec![f]));
assert!(
out.contains("T = TypeVar(\"T\", bound=Named)"),
"expected a bounded TypeVar, got: {out}"
);
}
#[test]
fn shared_type_param_typevar_is_deduped() {
let box_a = node(
1,
NodeKind::RecordDecl {
annotations: vec![],
visibility: Visibility::Public,
name: ident("Boxed"),
generic_params: vec![generic_param("T", None)],
fields: vec![named_field("value", "T")],
},
);
let box_b = node(
2,
NodeKind::RecordDecl {
annotations: vec![],
visibility: Visibility::Public,
name: ident("Wrapped"),
generic_params: vec![generic_param("T", None)],
fields: vec![named_field("inner", "T")],
},
);
let out = gen(&module(vec![], vec![box_a, box_b]));
let typevar_count = out.matches("T = TypeVar(\"T\")").count();
assert_eq!(
typevar_count, 1,
"shared type param T must be declared exactly once, got {typevar_count} in: {out}"
);
}
#[test]
fn method_colliding_with_field_is_disambiguated() {
let record_decl = node(
1,
NodeKind::RecordDecl {
annotations: vec![],
visibility: Visibility::Public,
name: ident("SimpleError"),
generic_params: vec![],
fields: vec![named_field("message", "String")],
},
);
let method = node(
10,
NodeKind::FnDecl {
annotations: vec![],
visibility: Visibility::Public,
is_async: false,
name: ident("message"),
generic_params: vec![],
params: vec![param_node(11, "self")],
return_type: None,
effect_clause: vec![],
where_clause: vec![],
body: Box::new(block(
12,
vec![],
Some(node(
13,
NodeKind::FieldAccess {
object: Box::new(id_node(14, "self")),
field: ident("message"),
},
)),
)),
},
);
let impl_block = node(
20,
NodeKind::ImplBlock {
annotations: vec![],
target: Box::new(node(
21,
NodeKind::TypeNamed {
path: type_path(&["SimpleError"]),
args: vec![],
},
)),
trait_path: Some(type_path(&["Error"])),
trait_args: vec![],
generic_params: vec![],
where_clause: vec![],
methods: vec![method],
},
);
let read_fn = node(
30,
NodeKind::FnDecl {
annotations: vec![],
visibility: Visibility::Public,
is_async: false,
name: ident("read"),
generic_params: vec![],
params: vec![typed_param_node(31, "e", "SimpleError")],
return_type: None,
effect_clause: vec![],
where_clause: vec![],
body: Box::new(block(
32,
vec![],
Some(node(
33,
NodeKind::Call {
callee: Box::new(node(
34,
NodeKind::FieldAccess {
object: Box::new(id_node(35, "e")),
field: ident("message"),
},
)),
type_args: vec![],
args: vec![AirArg {
label: None,
value: id_node(35, "e"),
}],
},
)),
)),
},
);
let out = gen(&module(vec![], vec![record_decl, impl_block, read_fn]));
assert!(
out.contains("message: str"),
"dataclass field should remain `message: str`, got: {out}"
);
assert!(
out.contains("def message_method(self)"),
"method should be `def message_method`, got: {out}"
);
assert!(
out.contains(".message_method()"),
"call site should be `.message_method()`, got: {out}"
);
assert!(
out.contains("return self.message"),
"method body should read the field `self.message`, got: {out}"
);
assert!(
!out.contains("def message(self)"),
"must NOT emit a `def message(self)` clobbered by the field, got: {out}"
);
}
fn call_no_args(id: u32, name: &str) -> AIRNode {
node(
id,
NodeKind::Call {
callee: Box::new(id_node(id + 1, name)),
args: vec![],
type_args: vec![],
},
)
}
#[test]
fn is_raise_expr_recognises_todo_and_unreachable() {
assert!(is_raise_expr(&call_no_args(1, "todo")));
assert!(is_raise_expr(&call_no_args(3, "unreachable")));
assert!(is_raise_expr(&node(5, NodeKind::Unreachable)));
assert!(!is_raise_expr(&call_no_args(6, "compute")));
assert!(!is_raise_expr(&int_lit(8, "1")));
}
#[test]
fn todo_in_return_and_let_position_emits_bare_raise() {
let f = fn_decl_tail(1, Visibility::Private, "f", call_no_args(10, "todo"));
let out = gen(&module(vec![], vec![f]));
assert!(
out.contains("raise NotImplementedError()"),
"expected a `raise`, got: {out}"
);
assert!(
!out.contains("return raise"),
"must NOT emit `return raise …`, got: {out}"
);
}
#[test]
fn value_loop_break_hoists_to_while_assign() {
let break_node = node(
30,
NodeKind::Break {
value: Some(Box::new(int_lit(31, "5"))),
},
);
let loop_node = node(
20,
NodeKind::Loop {
body: Box::new(block(21, vec![], Some(break_node))),
},
);
let let_r = node(
10,
NodeKind::LetBinding {
is_mut: false,
pattern: Box::new(bind_pat(11, "r")),
ty: None,
value: Box::new(loop_node),
},
);
let body = block(2, vec![let_r], Some(id_node(40, "r")));
let f = node(
1,
NodeKind::FnDecl {
annotations: vec![],
visibility: Visibility::Private,
is_async: false,
name: ident("f"),
generic_params: vec![],
params: vec![],
return_type: None,
effect_clause: vec![],
where_clause: vec![],
body: Box::new(body),
},
);
let out = gen(&module(vec![], vec![f]));
assert!(
out.contains("while True:"),
"value-loop should hoist to `while True:`, got: {out}"
);
assert!(
out.contains("__bock_cf_0 = 5"),
"break value should assign the hoisted temp `__bock_cf_0 = 5`, got: {out}"
);
assert!(
out.contains("r = __bock_cf_0"),
"the let should read the hoisted temp `r = __bock_cf_0`, got: {out}"
);
assert!(
out.contains("break"),
"the loop should still `break`, got: {out}"
);
assert!(
!out.contains("# unsupported"),
"must NOT emit `# unsupported`, got: {out}"
);
}
#[test]
fn field_label_does_not_trigger_implicit_import() {
let summary = node(
5,
NodeKind::RecordDecl {
annotations: vec![],
visibility: Visibility::Public,
name: ident("Summary"),
generic_params: vec![],
fields: vec![bock_ast::RecordDeclField {
id: 6,
span: span(),
name: ident("total"),
ty: bock_ast::TypeExpr::Named {
id: 7,
span: span(),
path: type_path(&["Int"]),
args: vec![],
},
default: None,
}],
},
);
let models = module_with_path(&["models"], vec![], vec![summary]);
let mut public_symbols = HashMap::new();
public_symbols.insert("total".to_string(), "service".to_string());
let imports = implicit_imports_for(&models, &public_symbols, "models");
assert!(
imports.is_empty(),
"a field named `total` must not implicit-import `service.total`, got: {imports:?}"
);
}
fn diverging_value_if_fn() -> AIRNode {
let then_b = block(2, vec![], Some(int_lit(3, "1")));
let ret = node(
5,
NodeKind::Return {
value: Some(Box::new(int_lit(6, "0"))),
},
);
let else_b = block(4, vec![], Some(ret));
let if_node = node(
1,
NodeKind::If {
let_pattern: None,
condition: Box::new(id_node(7, "c")),
then_block: Box::new(then_b),
else_block: Some(Box::new(else_b)),
},
);
let let_x = node(
10,
NodeKind::LetBinding {
is_mut: false,
pattern: Box::new(bind_pat(11, "x")),
ty: None,
value: Box::new(if_node),
},
);
let body = block(20, vec![let_x], Some(id_node(21, "x")));
let f = node(
30,
NodeKind::FnDecl {
annotations: vec![],
visibility: Visibility::Private,
is_async: false,
name: ident("f"),
generic_params: vec![],
params: vec![],
return_type: None,
effect_clause: vec![],
where_clause: vec![],
body: Box::new(body),
},
);
module(vec![], vec![f])
}
#[test]
fn diverging_value_if_hoists_to_stmt_form_no_unsupported() {
let out = gen(&diverging_value_if_fn());
assert!(
!out.contains("# unsupported"),
"diverging value-if must not emit `# unsupported`, got: {out}"
);
assert!(
out.contains("__bock_cf_0 = 1"),
"value arm must assign the temp, got: {out}"
);
assert!(
out.contains("return 0"),
"diverging arm must keep its return, got: {out}"
);
}
fn match_fn(name: &str, arms: Vec<AIRNode>) -> AIRNode {
let match_node = node(
900,
NodeKind::Match {
scrutinee: Box::new(id_node(901, "p")),
arms,
},
);
let f = node(
910,
NodeKind::FnDecl {
annotations: vec![],
visibility: Visibility::Public,
is_async: false,
name: ident(name),
generic_params: vec![],
params: vec![param_node(911, "p")],
return_type: None,
effect_clause: vec![],
where_clause: vec![],
body: Box::new(block(912, vec![], Some(match_node))),
},
);
module(vec![], vec![f])
}
fn record_pat_field(_id: u32, name: &str, pat: Option<AIRNode>) -> AirRecordPatternField {
AirRecordPatternField {
name: ident(name),
pattern: pat.map(Box::new),
}
}
#[test]
fn py_plainrecord_match_binds_field_in_value_position() {
let arm = node(
100,
NodeKind::MatchArm {
pattern: Box::new(node(
101,
NodeKind::RecordPat {
path: type_path(&["Point"]),
fields: vec![record_pat_field(102, "x", None)],
rest: true,
},
)),
guard: None,
body: Box::new(block(103, vec![], Some(id_node(104, "x")))),
},
);
let out = gen(&match_fn("get_x", vec![arm]));
assert!(
out.contains("match p:") && out.contains("case Point(x=x):"),
"plain-record value match must bind the field via case Point(x=x), got:\n{out}"
);
assert!(
!out.contains("(lambda __v: x)"),
"must not emit the field name free inside a value lambda, got:\n{out}"
);
let stubbed = out.replacen(
"from __future__ import annotations\n",
"from __future__ import annotations\nfrom dataclasses import dataclass as _dc\n@_dc\nclass Point:\n x: int = 0\n",
1,
);
assert!(
!has_python3() || check_py_syntax(&stubbed),
"generated python must parse, got:\n{stubbed}"
);
}
#[test]
fn arm_constructor_binds_payload_distinguishes_user_from_runtime() {
let user_bind = node(
1,
NodeKind::ConstructorPat {
path: type_path(&["Circle"]),
fields: vec![bind_pat(2, "r")],
},
);
assert!(
arm_constructor_binds_payload(&user_bind),
"user-enum constructor binding a payload must route to statement form"
);
let some_bind = node(
3,
NodeKind::ConstructorPat {
path: type_path(&["Some"]),
fields: vec![bind_pat(4, "x")],
},
);
assert!(
!arm_constructor_binds_payload(&some_bind),
"Some(x) is bound by the value chain, stays off statement form"
);
let user_unit = node(
5,
NodeKind::ConstructorPat {
path: type_path(&["Red"]),
fields: vec![],
},
);
assert!(
!arm_constructor_binds_payload(&user_unit),
"a payload-less user variant binds nothing, stays off statement form"
);
}
#[test]
fn py_letexpr_constructor_payload_routes_to_statement_form() {
let circle = node(
100,
NodeKind::MatchArm {
pattern: Box::new(node(
101,
NodeKind::ConstructorPat {
path: type_path(&["Circle"]),
fields: vec![bind_pat(102, "r")],
},
)),
guard: None,
body: Box::new(block(103, vec![], Some(id_node(104, "r")))),
},
);
let other = node(
110,
NodeKind::MatchArm {
pattern: Box::new(node(111, NodeKind::WildcardPat)),
guard: None,
body: Box::new(block(112, vec![], Some(int_lit(113, "0")))),
},
);
let match_node = node(
120,
NodeKind::Match {
scrutinee: Box::new(id_node(121, "s")),
arms: vec![circle, other],
},
);
let let_a = node(
130,
NodeKind::LetBinding {
is_mut: false,
pattern: Box::new(bind_pat(131, "a")),
ty: None,
value: Box::new(match_node),
},
);
let f = node(
140,
NodeKind::FnDecl {
annotations: vec![],
visibility: Visibility::Public,
is_async: false,
name: ident("size"),
generic_params: vec![],
params: vec![param_node(141, "s")],
return_type: None,
effect_clause: vec![],
where_clause: vec![],
body: Box::new(block(142, vec![let_a], Some(id_node(143, "a")))),
},
);
let out = gen(&module(vec![], vec![f]));
assert!(
out.contains("match s:") && out.contains("case Circle(_0=r):"),
"let-expr constructor-payload match must bind r via statement form, got:\n{out}"
);
assert!(
!out.contains("lambda __v"),
"must not lower the payload-binding match to a value lambda, got:\n{out}"
);
}
#[test]
fn py_matcharm_guard_value_position_keeps_guard() {
let guarded = node(
200,
NodeKind::MatchArm {
pattern: Box::new(bind_pat(201, "n")),
guard: Some(Box::new(node(
202,
NodeKind::BinaryOp {
op: BinOp::Lt,
left: Box::new(id_node(203, "n")),
right: Box::new(int_lit(204, "0")),
},
))),
body: Box::new(block(205, vec![], Some(str_lit(206, "neg")))),
},
);
let default = node(
210,
NodeKind::MatchArm {
pattern: Box::new(node(211, NodeKind::WildcardPat)),
guard: None,
body: Box::new(block(212, vec![], Some(str_lit(213, "nonneg")))),
},
);
let out = gen(&match_fn("classify", vec![guarded, default]));
assert!(
out.contains("match p:") && out.contains("case n if (n < 0):"),
"guard arm in value position must keep its guard test, got:\n{out}"
);
assert!(
!has_python3() || check_py_syntax(&out),
"generated python must parse, got:\n{out}"
);
}
#[test]
fn py_tuple_match_value_position_binds_and_tests() {
let zero_arm = node(
300,
NodeKind::MatchArm {
pattern: Box::new(node(
301,
NodeKind::TuplePat {
elems: vec![
node(
302,
NodeKind::LiteralPat {
lit: Literal::Int("0".into()),
},
),
node(303, NodeKind::WildcardPat),
],
},
)),
guard: None,
body: Box::new(block(304, vec![], Some(str_lit(305, "zero")))),
},
);
let bind_arm = node(
310,
NodeKind::MatchArm {
pattern: Box::new(node(
311,
NodeKind::TuplePat {
elems: vec![bind_pat(312, "n"), bind_pat(313, "s")],
},
)),
guard: None,
body: Box::new(block(314, vec![], Some(id_node(315, "n")))),
},
);
let out = gen(&match_fn("describe", vec![zero_arm, bind_arm]));
assert!(
out.contains("match p:")
&& out.contains("case (0, _):")
&& out.contains("case (n, s):"),
"tuple value match must test the literal and bind elements, got:\n{out}"
);
assert!(
!has_python3() || check_py_syntax(&out),
"generated python must parse, got:\n{out}"
);
}
#[test]
fn py_nested_constructor_match_value_position_binds_inner() {
let some_ok = node(
400,
NodeKind::MatchArm {
pattern: Box::new(node(
401,
NodeKind::ConstructorPat {
path: type_path(&["Some"]),
fields: vec![node(
402,
NodeKind::ConstructorPat {
path: type_path(&["Ok"]),
fields: vec![bind_pat(403, "n")],
},
)],
},
)),
guard: None,
body: Box::new(block(404, vec![], Some(id_node(405, "n")))),
},
);
let none_arm = node(
410,
NodeKind::MatchArm {
pattern: Box::new(node(
411,
NodeKind::ConstructorPat {
path: type_path(&["None"]),
fields: vec![],
},
)),
guard: None,
body: Box::new(block(412, vec![], Some(str_lit(413, "none")))),
},
);
let out = gen(&match_fn("nested", vec![some_ok, none_arm]));
assert!(
out.contains("match p:") && out.contains("case _BockSome(_BockOk(n)):"),
"nested constructor value match must test+bind the inner Ok, got:\n{out}"
);
assert!(
!has_python3() || check_py_syntax(&out),
"generated python must parse, got:\n{out}"
);
}
#[test]
fn py_valpos_match_arm_block_keeps_leading_let() {
let let_stmt = node(
500,
NodeKind::LetBinding {
is_mut: false,
pattern: Box::new(bind_pat(501, "doubled")),
ty: None,
value: Box::new(node(
502,
NodeKind::BinaryOp {
op: BinOp::Add,
left: Box::new(id_node(503, "n")),
right: Box::new(id_node(504, "n")),
},
)),
},
);
let ok_arm = node(
510,
NodeKind::MatchArm {
pattern: Box::new(node(
511,
NodeKind::ConstructorPat {
path: type_path(&["Ok"]),
fields: vec![bind_pat(512, "n")],
},
)),
guard: None,
body: Box::new(block(513, vec![let_stmt], Some(id_node(514, "doubled")))),
},
);
let err_arm = node(
520,
NodeKind::MatchArm {
pattern: Box::new(node(
521,
NodeKind::ConstructorPat {
path: type_path(&["Err"]),
fields: vec![bind_pat(522, "e")],
},
)),
guard: None,
body: Box::new(block(523, vec![], Some(id_node(524, "e")))),
},
);
let out = gen(&match_fn("keep_let", vec![ok_arm, err_arm]));
assert!(
out.contains("lambda doubled:"),
"value-position match-arm block must keep its `let doubled` binding, got:\n{out}"
);
assert!(
!out.contains("(lambda: "),
"must not emit the statement-dropping `(lambda: <tail>)()` form, got:\n{out}"
);
assert!(
!has_python3() || check_py_syntax(&out),
"generated python must parse, got:\n{out}"
);
}
}