bock_codegen/py.rs
1//! Python code generator — rule-based (Tier 2) transpilation from AIR to Python.
2//!
3//! Handles all capability gaps:
4//! - Records → `@dataclass` classes
5//! - Algebraic types → dataclasses with `_tag` discriminant
6//! - Pattern matching → native `match`/`case` (Python 3.10+)
7//! - Effects → keyword arguments
8//! - Ownership → erased (Python is GC)
9//! - Generics → `TypeVar` + `Generic[T]` (so `T` resolves at class-eval time)
10//! - Type hints on all declarations
11
12use std::cell::Cell;
13use std::collections::HashMap;
14use std::fmt::Write;
15use std::path::PathBuf;
16
17use bock_air::{AIRNode, AirInterpolationPart, EnumVariantPayload, NodeKind, ResultVariant};
18use bock_ast::{AssignOp, BinOp, Literal, TypeExpr, UnaryOp, Visibility};
19use bock_types::AIRModule;
20
21use crate::error::CodegenError;
22use crate::generator::{CodeGenerator, GeneratedCode, OutputFile, SourceMap};
23use crate::profile::TargetProfile;
24
25/// Runtime helpers for Bock concurrency in Python. Backed by
26/// `asyncio.Queue`, which supports multiple producers and a single
27/// consumer awaiting `.get()` at a time. Injected at the top of any
28/// module that references `Channel` or `spawn`.
29/// Conservative module scan for `Channel` / `spawn` references.
30fn py_module_uses_concurrency(items: &[AIRNode]) -> bool {
31 items.iter().any(|n| {
32 let s = format!("{n:?}");
33 s.contains("\"Channel\"") || s.contains("\"spawn\"")
34 })
35}
36
37/// True if the module references `Optional`, `Some`, or `None` anywhere — or
38/// calls `pop`, whose DQ30 lowering builds the tagged Optional values
39/// (`_BockSome(...)` / `_bock_none`) — so the Optional runtime prelude must be
40/// emitted. A cheap structural scan over the debug rendering, mirroring
41/// [`py_module_uses_concurrency`] and the Go/TS backends'
42/// `*_module_uses_optional`. Over-matching only emits unused runtime classes,
43/// which is harmless.
44fn py_module_uses_optional(items: &[AIRNode]) -> bool {
45 items.iter().any(|n| {
46 let s = format!("{n:?}");
47 s.contains("\"Optional\"")
48 || s.contains("TypeOptional")
49 || s.contains("\"Some\"")
50 || s.contains("\"None\"")
51 || s.contains("\"pop\"")
52 })
53}
54
55/// True if the module uses a DQ30 in-place `List` mutator whose Python
56/// lowering pre-checks bounds and aborts through the
57/// [`LIST_MUTATOR_RUNTIME_PY`] helper (`remove_at`/`insert`/`set`). Gates
58/// emission of that prelude, mirroring [`py_module_uses_optional`]. `pop`
59/// (no abort path) and `reverse` (native `lst.reverse()`) need no helper, so
60/// they don't gate here. `set` over-matches `Map.set`, which only emits an
61/// unused helper function — harmless.
62fn py_module_uses_list_mutators(items: &[AIRNode]) -> bool {
63 items.iter().any(|n| {
64 let s = format!("{n:?}");
65 s.contains("\"remove_at\"") || s.contains("\"insert\"") || s.contains("\"set\"")
66 })
67}
68
69/// True if the module references `Result`, `Ok`, or `Err` anywhere, so the
70/// `Result` runtime prelude must be emitted. Mirrors [`py_module_uses_optional`].
71fn py_module_uses_result(items: &[AIRNode]) -> bool {
72 items.iter().any(|n| {
73 let s = format!("{n:?}");
74 s.contains("\"Result\"")
75 || s.contains("ResultConstruct")
76 || s.contains("\"Ok\"")
77 || s.contains("\"Err\"")
78 })
79}
80
81/// True if the module uses the `?` propagate operator anywhere, so the
82/// [`PROPAGATE_RUNTIME_PY`] helper (`_bock_try` / `_BockPropagate`) must be
83/// emitted. Mirrors [`py_module_uses_optional`]: a structural scan over the debug
84/// rendering for the `Propagate` AIR node.
85fn py_module_uses_propagate(items: &[AIRNode]) -> bool {
86 items.iter().any(|n| format!("{n:?}").contains("Propagate"))
87}
88
89/// True if the module contains a string interpolation (`${expr}`), so the
90/// [`STR_RUNTIME_PY`] `_bock_str` helper must be emitted. Gates that prelude,
91/// mirroring [`py_module_uses_propagate`]. (Q-displayable-interpolation-dispatch.)
92fn py_module_uses_str(items: &[AIRNode]) -> bool {
93 items
94 .iter()
95 .any(|n| format!("{n:?}").contains("Interpolation"))
96}
97
98/// True if the module uses a `List` functional combinator that lowers to one of
99/// the [`LIST_FUNCTIONAL_RUNTIME_PY`] helpers (`reduce`/`fold`/`find`/`for_each`).
100/// Gates emission of that prelude, mirroring [`py_module_uses_optional`]. `map`/
101/// `filter`/`any`/`all`/`flat_map` lower to Python builtins (`list(map(..))` /
102/// `any(..)` / a comprehension) and need no helper, so they don't gate here.
103fn py_module_uses_list_functional(items: &[AIRNode]) -> bool {
104 items.iter().any(|n| {
105 let s = format!("{n:?}");
106 s.contains("\"reduce\"")
107 || s.contains("\"fold\"")
108 || s.contains("\"find\"")
109 || s.contains("\"for_each\"")
110 })
111}
112
113/// True if the module references the prelude `Ordering` enum, any of its
114/// variants, or a `compare` method call (which the primitive bridge lowers to an
115/// `Ordering` runtime value). Gates emission of [`ORDERING_RUNTIME_PY`], mirroring
116/// [`py_module_uses_optional`].
117fn py_module_uses_ordering(items: &[AIRNode]) -> bool {
118 items.iter().any(|n| {
119 let s = format!("{n:?}");
120 s.contains("\"Ordering\"")
121 || s.contains("\"Less\"")
122 || s.contains("\"Equal\"")
123 || s.contains("\"Greater\"")
124 || s.contains("\"compare\"")
125 })
126}
127
128/// Runtime for Bock `Optional[T]` in Python. The *value* representation mirrors
129/// JS/TS/Go: a tagged value with a `Some` payload or a `None` marker. Python's
130/// `None` is a keyword (and a distinct concept), so Bock's `None` must NOT
131/// collide with it — it lowers to the singleton `_bock_none`, and `Some(x)` to
132/// `_BockSome(x)`. `__match_args__` lets `case _BockSome(v):` bind the payload
133/// positionally; `case _BockNone():` matches the marker. This keeps type and
134/// value in agreement and makes `match o { Some(x) => ...; None => ... }` lower
135/// to valid structural pattern matching (the old codegen emitted bare
136/// `Some`/`None` with no definitions and `case None():`, a `SyntaxError`).
137const OPTIONAL_RUNTIME_PY: &str = "\
138# ── Bock Optional runtime ──
139class _BockSome:
140 __match_args__ = ('_0',)
141 __slots__ = ('_0',)
142 def __init__(self, _0):
143 self._0 = _0
144 def __repr__(self):
145 return f'Some({self._0!r})'
146 def __eq__(self, other):
147 if not isinstance(other, _BockSome):
148 return NotImplemented
149 return self._0 == other._0
150 def __hash__(self):
151 return hash(('Some', self._0))
152
153class _BockNone:
154 __slots__ = ()
155 def __repr__(self):
156 return 'None'
157 def __eq__(self, other):
158 if not isinstance(other, _BockNone):
159 return NotImplemented
160 return True
161 def __hash__(self):
162 return hash('None')
163
164_bock_none = _BockNone()
165";
166
167/// Runtime for Bock `Result[T, E]` in Python. Mirrors `OPTIONAL_RUNTIME_PY`: the
168/// `Ok` payload and the `Err` payload each live in a distinct class with
169/// `__match_args__` so `case _BockOk(v):` / `case _BockErr(e):` bind the payload
170/// positionally — the same shape the surface `Ok(..)`/`Err(..)` construction
171/// emits (`_BockOk(..)` / `_BockErr(..)`). The old codegen emitted bare
172/// `Ok(..)`/`case Ok(_0=n):` against undefined names; this keeps construction and
173/// match in agreement on the same runtime classes.
174const RESULT_RUNTIME_PY: &str = "\
175# ── Bock Result runtime ──
176class _BockOk:
177 __match_args__ = ('_0',)
178 __slots__ = ('_0',)
179 def __init__(self, _0):
180 self._0 = _0
181 def __repr__(self):
182 return f'Ok({self._0!r})'
183 def __eq__(self, other):
184 if not isinstance(other, _BockOk):
185 return NotImplemented
186 return self._0 == other._0
187 def __hash__(self):
188 return hash(('Ok', self._0))
189
190class _BockErr:
191 __match_args__ = ('_0',)
192 __slots__ = ('_0',)
193 def __init__(self, _0):
194 self._0 = _0
195 def __repr__(self):
196 return f'Err({self._0!r})'
197 def __eq__(self, other):
198 if not isinstance(other, _BockErr):
199 return NotImplemented
200 return self._0 == other._0
201 def __hash__(self):
202 return hash(('Err', self._0))
203
204def _bock_parse_int(s, mk_err):
205 try:
206 return _BockOk(int(s.strip()))
207 except (ValueError, TypeError):
208 return _BockErr(mk_err(f\"cannot parse {s!r} as Int\"))
209
210def _bock_parse_float(s, mk_err):
211 try:
212 t = s.strip()
213 if t == '':
214 raise ValueError()
215 return _BockOk(float(t))
216 except (ValueError, TypeError):
217 return _BockErr(mk_err(f\"cannot parse {s!r} as Float\"))
218";
219
220/// Runtime for the `?` propagate operator in Python. `expr?` lowers to
221/// `_bock_try(expr)`: an `Ok`/`Some` value yields its payload; an `Err`/`None`
222/// value raises the `_BockPropagate` sentinel carrying the original tagged value.
223/// The enclosing function (the one containing the `?`) has its body wrapped in
224/// `try: … except _BockPropagate as __p: return __p.value`, so the `Err`/`None`
225/// is re-returned unchanged — Rust-`?` semantics.
226///
227/// The unwrap test is by **class name** (`type(v).__name__`) rather than
228/// `isinstance`, so the helper is self-contained: it does not hard-reference
229/// `_BockOk`/`_BockSome`, which lets it live in `_bock_runtime` (or be inlined)
230/// even when only one of the Optional/Result preludes is present. Anything that
231/// is not a recognised success tag (including the `_BockNone` singleton) is
232/// treated as the failing case and propagated.
233const PROPAGATE_RUNTIME_PY: &str = "\
234# ── Bock `?` propagate runtime ──
235class _BockPropagate(Exception):
236 __slots__ = ('value',)
237 def __init__(self, value):
238 super().__init__()
239 self.value = value
240
241def _bock_try(v):
242 if type(v).__name__ in ('_BockOk', '_BockSome'):
243 return v._0
244 raise _BockPropagate(v)
245";
246
247/// The prelude `Ordering` runtime: the three variants of `core.compare.Ordering`
248/// as singleton instances of distinct classes, matchable by `case` and emitted
249/// for construction. Mirrors `OPTIONAL_RUNTIME_PY` — when the `core.compare`
250/// enum declaration is not among the reached modules, the primitive bridge
251/// (`(x).compare(y)`) and any bare `Less`/`Equal`/`Greater` need this
252/// self-contained representation. Each class is empty (no payload), so
253/// `case _BockOrderingLess():` matches the corresponding singleton.
254const ORDERING_RUNTIME_PY: &str = "\
255# ── Bock Ordering runtime ──
256class _BockOrderingLess:
257 __slots__ = ()
258 def __repr__(self):
259 return 'Less'
260
261class _BockOrderingEqual:
262 __slots__ = ()
263 def __repr__(self):
264 return 'Equal'
265
266class _BockOrderingGreater:
267 __slots__ = ()
268 def __repr__(self):
269 return 'Greater'
270
271_bock_less = _BockOrderingLess()
272_bock_equal = _BockOrderingEqual()
273_bock_greater = _BockOrderingGreater()
274
275def _bock_compare(a, b):
276 m = getattr(a, 'compare', None)
277 if callable(m):
278 return m(b)
279 return _bock_less if a < b else (_bock_equal if a == b else _bock_greater)
280";
281
282/// Runtime helper for the DQ30 in-place `List` mutators whose Python lowering
283/// pre-checks bounds (`remove_at`/`insert`/`set`): Python cannot `raise` in an
284/// expression (the lowerings are single-evaluation `lambda` IIFEs), so the
285/// violated-contract branch calls this raising helper instead. The message is
286/// the normalized cross-target abort form `List.<op>: index <i> out of bounds
287/// (len <n>)` (op, index, and length — the DQ30 contract), raised as an
288/// `IndexError` so the abort kind matches Python's native indexing aborts (the
289/// DQ23 convention of using the target's idiomatic abort channel). The
290/// pre-checks themselves are load-bearing on Python: native `lst.insert`
291/// CLAMPS out-of-range indices and native `lst.pop(i)` / `lst[i] = x` accept
292/// negative indices — all excluded by the `0 <= i` checks at the call sites.
293/// Gated by [`py_module_uses_list_mutators`].
294const LIST_MUTATOR_RUNTIME_PY: &str = "\
295# ── Bock List in-place-mutator runtime ──
296def _bock_list_abort(op, i, n):
297 raise IndexError(f'List.{op}: index {i} out of bounds (len {n})')
298";
299
300/// Runtime for a `${expr}` interpolation part: render a value with a
301/// `Displayable` impl through its `to_string` method rather than the structural
302/// `repr`/`str` (a dataclass `Point(x=3, y=7)`). The user `Displayable.to_string`
303/// is emitted as a `to_string(self)` method (distinct from Python's `__str__`),
304/// so the helper detects it by `callable(getattr(x, 'to_string', None))` and
305/// calls `x.to_string()`. Primitives and other values fall back to `str(x)`.
306/// Gated by [`py_module_uses_str`]. (Q-displayable-interpolation-dispatch.)
307const STR_RUNTIME_PY: &str = "\
308# ── Bock display-string runtime ──
309def _bock_str(x):
310 m = getattr(x, 'to_string', None)
311 if callable(m):
312 return m()
313 return str(x)
314";
315
316/// Runtime helpers for the closure-taking `List` combinators whose Python form
317/// is not a single builtin expression: `reduce`/`fold` (left folds, no
318/// statement-level loop is expressible in a lambda), `find` (returns the tagged
319/// `Optional`), and `for_each` (a side-effecting drive returning `None`). `map`/
320/// `filter`/`any`/`all`/`flat_map` lower inline to Python builtins and need no
321/// helper. `_bock_find` builds the same tagged `Optional` runtime values
322/// (`_BockSome`/`_bock_none`) that `OPTIONAL_RUNTIME_PY` defines, so that prelude
323/// is co-emitted whenever this one is. Gated by [`py_module_uses_list_functional`].
324const LIST_FUNCTIONAL_RUNTIME_PY: &str = "\
325# ── Bock List functional-combinator runtime ──
326def _bock_reduce(xs, f):
327 it = iter(xs)
328 acc = next(it)
329 for x in it:
330 acc = f(acc, x)
331 return acc
332
333def _bock_fold(xs, init, f):
334 acc = init
335 for x in xs:
336 acc = f(acc, x)
337 return acc
338
339def _bock_find(xs, pred):
340 for x in xs:
341 if pred(x):
342 return _BockSome(x)
343 return _bock_none
344
345def _bock_for_each(xs, f):
346 for x in xs:
347 f(x)
348 return None
349";
350
351/// Runtime-prelude names that resolve through the shared `_bock_runtime`
352/// module (or built-in lowering), NOT through a cross-module import — so the
353/// implicit-import pass must never try to import them from a declaring module.
354/// These are the §18.2-prelude container/ordering symbols whose Python form is
355/// the bespoke tagged runtime (`_BockSome`, …), not the `core.*` declaration.
356const RUNTIME_PRELUDE_NAMES: &[&str] = &[
357 "Optional", "Some", "None", "Result", "Ok", "Err", "Ordering", "Less", "Equal", "Greater",
358];
359
360/// Build a map from every **public top-level symbol name** declared across
361/// `modules` to the dotted module-path that declares it (e.g. `Iterable` →
362/// `core.iter`). Covers functions, records, enums (and each variant's emitted
363/// `Enum_Variant` dataclass name), traits, classes, effects, type aliases, and
364/// consts.
365///
366/// The per-module emission path needs this for **implicit imports**: a prelude
367/// trait used as a base class (`impl Iterable for Bag`, with `Iterable`
368/// auto-imported per §18.2) is referenced without an explicit `use`. Emitting
369/// one file per module means `main.py` must `import` `Iterable` from `core.iter`
370/// even though it never appears in an explicit `use`. This map lets
371/// `generate_project` add exactly those imports for names a module references
372/// but neither declares locally nor imports explicitly.
373///
374/// Runtime-prelude names (`RUNTIME_PRELUDE_NAMES`) are excluded — they resolve
375/// through `_bock_runtime`, not a `core.*` import. The first declarer wins for a
376/// name declared in several modules (deterministic via the dependency order
377/// `modules` arrives in).
378fn collect_public_symbol_modules(
379 modules: &[(&AIRModule, &std::path::Path)],
380) -> HashMap<String, String> {
381 let mut map: HashMap<String, String> = HashMap::new();
382 for (module, _) in modules {
383 let Some(module_path) = crate::generator::module_path_string(module) else {
384 continue;
385 };
386 let NodeKind::Module { items, .. } = &module.kind else {
387 continue;
388 };
389 for item in items {
390 let mut record = |name: &str| {
391 if !RUNTIME_PRELUDE_NAMES.contains(&name) {
392 map.entry(name.to_string())
393 .or_insert_with(|| module_path.clone());
394 }
395 };
396 match &item.kind {
397 NodeKind::FnDecl {
398 visibility, name, ..
399 }
400 | NodeKind::RecordDecl {
401 visibility, name, ..
402 }
403 | NodeKind::TraitDecl {
404 visibility, name, ..
405 }
406 | NodeKind::ClassDecl {
407 visibility, name, ..
408 }
409 | NodeKind::EffectDecl {
410 visibility, name, ..
411 }
412 | NodeKind::TypeAlias {
413 visibility, name, ..
414 }
415 | NodeKind::ConstDecl {
416 visibility, name, ..
417 } => {
418 if matches!(visibility, Visibility::Public) {
419 record(&name.name);
420 }
421 }
422 NodeKind::EnumDecl {
423 visibility,
424 name,
425 variants,
426 ..
427 } => {
428 if matches!(visibility, Visibility::Public) {
429 record(&name.name);
430 for v in variants {
431 if let NodeKind::EnumVariant { name: vname, .. } = &v.kind {
432 record(&format!("{}_{}", name.name, vname.name));
433 }
434 }
435 }
436 }
437 _ => {}
438 }
439 }
440 }
441 map
442}
443
444/// Declared module-path of `module`, or empty if it declares none.
445fn module_path_string_of(module: &AIRModule) -> String {
446 crate::generator::module_path_string(module).unwrap_or_default()
447}
448
449/// Top-level symbol names declared **locally** in `module` (item names plus
450/// each enum variant's emitted `Enum_Variant` name) — the names a per-module
451/// implicit import must never shadow with a cross-module import.
452fn locally_declared_names(module: &AIRModule) -> std::collections::HashSet<String> {
453 let mut names = std::collections::HashSet::new();
454 let NodeKind::Module { items, .. } = &module.kind else {
455 return names;
456 };
457 for item in items {
458 match &item.kind {
459 NodeKind::FnDecl { name, .. }
460 | NodeKind::RecordDecl { name, .. }
461 | NodeKind::TraitDecl { name, .. }
462 | NodeKind::ClassDecl { name, .. }
463 | NodeKind::EffectDecl { name, .. }
464 | NodeKind::TypeAlias { name, .. }
465 | NodeKind::ConstDecl { name, .. } => {
466 names.insert(name.name.clone());
467 }
468 NodeKind::EnumDecl { name, variants, .. } => {
469 names.insert(name.name.clone());
470 for v in variants {
471 if let NodeKind::EnumVariant { name: vname, .. } = &v.kind {
472 names.insert(format!("{}_{}", name.name, vname.name));
473 }
474 }
475 }
476 _ => {}
477 }
478 }
479 names
480}
481
482/// Names brought into scope by `module`'s explicit `use` declarations (the
483/// imported leaf names and their aliases) — already emitted as real imports,
484/// so the implicit-import pass must skip them.
485fn explicitly_imported_names(module: &AIRModule) -> std::collections::HashSet<String> {
486 let mut names = std::collections::HashSet::new();
487 let NodeKind::Module { imports, .. } = &module.kind else {
488 return names;
489 };
490 for import in imports {
491 if let NodeKind::ImportDecl {
492 items: bock_ast::ImportItems::Named(named),
493 ..
494 } = &import.kind
495 {
496 for n in named {
497 names.insert(n.name.name.clone());
498 if let Some(alias) = &n.alias {
499 names.insert(alias.name.clone());
500 }
501 }
502 }
503 }
504 names
505}
506
507/// Tally, per identifier name, how many times that name appears purely as a
508/// **record/enum/class field label** anywhere in `module` — i.e. in a position
509/// that names a *field*, never a cross-module symbol. Covers the four label
510/// positions:
511///
512/// - record / class / enum-struct-variant field **declarations**
513/// (`record R { total_value: Float }`),
514/// - record-construction labels (`R { total_value: v }`),
515/// - record-pattern field labels (`R { total_value }`),
516/// - field **access** (`r.total_value`).
517///
518/// The implicit-import scan ([`implicit_imports_for`]) matches a public symbol
519/// name against the module's debug rendering. A field label produces the same
520/// quoted-identifier token as a genuine reference, so a record field whose name
521/// collides with a sibling module's public function (e.g. `InventorySummary`'s
522/// `total_value` field vs. `service.total_value`) was spuriously "referenced",
523/// pulling in `from service import total_value`. Because `service` already
524/// imports `models`, that creates a Python import **cycle**
525/// (`ImportError: cannot import name … (circular import)`).
526///
527/// Subtracting these label occurrences from the total lets the scan keep its
528/// "over-import is harmless" property for true references while never importing
529/// a name that appears *only* as a field label.
530fn field_label_occurrences(module: &AIRModule) -> HashMap<String, usize> {
531 let mut counts: HashMap<String, usize> = HashMap::new();
532 fn bump(counts: &mut HashMap<String, usize>, name: &str) {
533 *counts.entry(name.to_string()).or_insert(0) += 1;
534 }
535 fn walk_decl_fields(counts: &mut HashMap<String, usize>, fields: &[bock_ast::RecordDeclField]) {
536 for f in fields {
537 bump(counts, &f.name.name);
538 }
539 }
540 fn walk(counts: &mut HashMap<String, usize>, node: &AIRNode) {
541 match &node.kind {
542 NodeKind::RecordDecl { fields, .. } | NodeKind::ClassDecl { fields, .. } => {
543 walk_decl_fields(counts, fields);
544 }
545 NodeKind::EnumVariant {
546 payload: bock_air::EnumVariantPayload::Struct(fields),
547 ..
548 } => {
549 walk_decl_fields(counts, fields);
550 }
551 NodeKind::FieldAccess { field, object } => {
552 bump(counts, &field.name);
553 walk(counts, object);
554 }
555 NodeKind::RecordConstruct { fields, spread, .. } => {
556 for f in fields {
557 bump(counts, &f.name.name);
558 if let Some(v) = &f.value {
559 walk(counts, v);
560 }
561 }
562 if let Some(s) = spread {
563 walk(counts, s);
564 }
565 }
566 NodeKind::RecordPat { fields, .. } => {
567 for f in fields {
568 bump(counts, &f.name.name);
569 if let Some(p) = &f.pattern {
570 walk(counts, p);
571 }
572 }
573 }
574 _ => {}
575 }
576 // Recurse into every child node. The match above handles the
577 // label-bearing kinds; this generic descent reaches the rest. We render
578 // children via the structural API rather than enumerating every
579 // `NodeKind`, so new expression kinds are covered automatically.
580 for child in child_nodes(node) {
581 walk(counts, child);
582 }
583 }
584 walk(&mut counts, module);
585 counts
586}
587
588/// The directly-owned child `AIRNode`s of `node`, for the field-label walk in
589/// [`field_label_occurrences`]. Returns the kind-specific children; the
590/// label-bearing kinds (record/field-access/construct/pattern) are descended by
591/// the caller's `match`, so here they are skipped to avoid double-counting their
592/// label idents.
593fn child_nodes(node: &AIRNode) -> Vec<&AIRNode> {
594 let mut out: Vec<&AIRNode> = Vec::new();
595 macro_rules! p {
596 ($e:expr) => {
597 out.push($e)
598 };
599 }
600 macro_rules! popt {
601 ($e:expr) => {
602 if let Some(n) = $e {
603 out.push(n)
604 }
605 };
606 }
607 macro_rules! pvec {
608 ($e:expr) => {
609 for n in $e {
610 out.push(n)
611 }
612 };
613 }
614 match &node.kind {
615 NodeKind::Module { imports, items, .. } => {
616 pvec!(imports);
617 pvec!(items);
618 }
619 NodeKind::FnDecl {
620 params,
621 return_type,
622 body,
623 ..
624 } => {
625 pvec!(params);
626 popt!(return_type.as_deref());
627 p!(body);
628 }
629 NodeKind::EnumDecl { variants, .. } => {
630 pvec!(variants);
631 }
632 // Tuple-payload element types are descended; struct-payload field labels
633 // are handled by the caller's match (so they are counted, not skipped).
634 NodeKind::EnumVariant {
635 payload: EnumVariantPayload::Tuple(types),
636 ..
637 } => {
638 pvec!(types);
639 }
640 NodeKind::ClassDecl { methods, .. } => {
641 // Field labels handled by caller; descend into methods only.
642 pvec!(methods);
643 }
644 NodeKind::TraitDecl { methods, .. } => {
645 pvec!(methods);
646 }
647 NodeKind::ImplBlock {
648 target, methods, ..
649 } => {
650 p!(target);
651 pvec!(methods);
652 }
653 NodeKind::EffectDecl { operations, .. } => {
654 pvec!(operations);
655 }
656 NodeKind::TypeAlias { ty, .. } => p!(ty),
657 NodeKind::ConstDecl { ty, value, .. } => {
658 p!(ty);
659 p!(value);
660 }
661 NodeKind::ModuleHandle { handler, .. } => p!(handler),
662 NodeKind::PropertyTest { body, .. } => p!(body),
663 NodeKind::Param {
664 pattern,
665 ty,
666 default,
667 } => {
668 p!(pattern);
669 popt!(ty.as_deref());
670 popt!(default.as_deref());
671 }
672 NodeKind::TypeNamed { args, .. } => {
673 pvec!(args);
674 }
675 NodeKind::TypeTuple { elems } => {
676 pvec!(elems);
677 }
678 NodeKind::TypeFunction { params, ret, .. } => {
679 pvec!(params);
680 p!(ret);
681 }
682 NodeKind::TypeOptional { inner } => p!(inner),
683 NodeKind::BinaryOp { left, right, .. } => {
684 p!(left);
685 p!(right);
686 }
687 NodeKind::UnaryOp { operand, .. } => p!(operand),
688 NodeKind::Assign { target, value, .. } => {
689 p!(target);
690 p!(value);
691 }
692 NodeKind::Call {
693 callee,
694 args,
695 type_args,
696 } => {
697 p!(callee);
698 for a in args {
699 out.push(&a.value);
700 }
701 pvec!(type_args);
702 }
703 NodeKind::MethodCall {
704 receiver,
705 type_args,
706 args,
707 ..
708 } => {
709 p!(receiver);
710 pvec!(type_args);
711 for a in args {
712 out.push(&a.value);
713 }
714 }
715 NodeKind::Index { object, index } => {
716 p!(object);
717 p!(index);
718 }
719 NodeKind::Propagate { expr }
720 | NodeKind::Await { expr }
721 | NodeKind::Move { expr }
722 | NodeKind::Borrow { expr }
723 | NodeKind::MutableBorrow { expr } => p!(expr),
724 NodeKind::Lambda { params, body } => {
725 pvec!(params);
726 p!(body);
727 }
728 NodeKind::Pipe { left, right } | NodeKind::Compose { left, right } => {
729 p!(left);
730 p!(right);
731 }
732 NodeKind::Range { lo, hi, .. } | NodeKind::RangePat { lo, hi, .. } => {
733 p!(lo);
734 p!(hi);
735 }
736 NodeKind::ListLiteral { elems }
737 | NodeKind::SetLiteral { elems }
738 | NodeKind::TupleLiteral { elems }
739 | NodeKind::TuplePat { elems } => {
740 pvec!(elems);
741 }
742 NodeKind::MapLiteral { entries } => {
743 for e in entries {
744 out.push(&e.key);
745 out.push(&e.value);
746 }
747 }
748 NodeKind::Interpolation { parts } => {
749 for part in parts {
750 if let bock_air::AirInterpolationPart::Expr(n) = part {
751 out.push(n.as_ref());
752 }
753 }
754 }
755 NodeKind::ResultConstruct { value, .. }
756 | NodeKind::Return { value }
757 | NodeKind::Break { value } => {
758 popt!(value.as_deref());
759 }
760 NodeKind::If {
761 let_pattern,
762 condition,
763 then_block,
764 else_block,
765 } => {
766 popt!(let_pattern.as_deref());
767 p!(condition);
768 p!(then_block);
769 popt!(else_block.as_deref());
770 }
771 NodeKind::Guard {
772 let_pattern,
773 condition,
774 else_block,
775 } => {
776 popt!(let_pattern.as_deref());
777 p!(condition);
778 p!(else_block);
779 }
780 NodeKind::Match { scrutinee, arms } => {
781 p!(scrutinee);
782 pvec!(arms);
783 }
784 NodeKind::MatchArm {
785 pattern,
786 guard,
787 body,
788 } => {
789 p!(pattern);
790 popt!(guard.as_deref());
791 p!(body);
792 }
793 NodeKind::For {
794 pattern,
795 iterable,
796 body,
797 } => {
798 p!(pattern);
799 p!(iterable);
800 p!(body);
801 }
802 NodeKind::While { condition, body } => {
803 p!(condition);
804 p!(body);
805 }
806 NodeKind::Loop { body } => p!(body),
807 NodeKind::Block { stmts, tail } => {
808 pvec!(stmts);
809 popt!(tail.as_deref());
810 }
811 NodeKind::LetBinding {
812 pattern, ty, value, ..
813 } => {
814 p!(pattern);
815 popt!(ty.as_deref());
816 p!(value);
817 }
818 NodeKind::EffectOp { args, .. } => {
819 for a in args {
820 out.push(&a.value);
821 }
822 }
823 NodeKind::HandlingBlock { handlers, body } => {
824 for h in handlers {
825 out.push(&h.handler);
826 }
827 p!(body);
828 }
829 NodeKind::ConstructorPat { fields, .. } => {
830 pvec!(fields);
831 }
832 NodeKind::ListPat { elems, rest } => {
833 pvec!(elems);
834 popt!(rest.as_deref());
835 }
836 NodeKind::OrPat { alternatives } => {
837 pvec!(alternatives);
838 }
839 NodeKind::GuardPat { pattern, guard } => {
840 p!(pattern);
841 p!(guard);
842 }
843 // Leaf / label-bearing kinds with no extra children to descend (the
844 // latter are handled by the caller's `match`).
845 NodeKind::ImportDecl { .. }
846 | NodeKind::RecordDecl { .. }
847 | NodeKind::FieldAccess { .. }
848 | NodeKind::RecordConstruct { .. }
849 | NodeKind::RecordPat { .. }
850 | NodeKind::TypeSelf
851 | NodeKind::Literal { .. }
852 | NodeKind::Identifier { .. }
853 | NodeKind::Placeholder
854 | NodeKind::Unreachable
855 | NodeKind::Continue
856 | NodeKind::WildcardPat
857 | NodeKind::BindPat { .. }
858 | NodeKind::LiteralPat { .. }
859 | NodeKind::RestPat
860 | NodeKind::Error
861 | NodeKind::EffectRef { .. } => {}
862 // `NodeKind` is `#[non_exhaustive]`. A future kind we have not taught
863 // this walker about contributes no field-label children, so the scan
864 // falls back to its old (harmless over-import) behavior for it — never
865 // under-import.
866 _ => {}
867 }
868 out
869}
870
871/// Count quoted-identifier-token occurrences of `name` in `rendered` — the
872/// number of `"name"` substrings in the AIR debug dump.
873fn quoted_token_count(rendered: &str, name: &str) -> usize {
874 rendered.matches(&format!("\"{name}\"")).count()
875}
876
877/// Whether a value expression in **binding/expression position** must be lowered
878/// to Python *statements* (assigning the binding) rather than emitted as a
879/// Python expression.
880///
881/// Python has no statement-admitting expression form (no value-`loop`, no
882/// IIFE), so these constructs cannot ride inside a `let x = …` expression:
883///
884/// - a `match` whose arms include a statement / diverging body
885/// (`_ => { return … }`, see [`crate::generator::match_has_statement_arm`]);
886/// - a `loop` / `while` (a value-`loop` yields via `break <v>`, which Python's
887/// valueless `break` cannot express);
888/// - an `if` that is itself a statement (both branches statement bodies) —
889/// it produces no expression value;
890/// - a `Block` carrying statements (a tail-only block is fine as an expression).
891///
892/// When true, [`PyEmitCtx::emit_value_binding`] hoists the construct into real
893/// Python statements. Otherwise the existing expression lowering (including the
894/// ternary `match`/`if` paths) is used unchanged.
895fn value_needs_stmt_form(value: &AIRNode) -> bool {
896 match &value.kind {
897 NodeKind::Match { arms, .. } => {
898 crate::generator::match_has_statement_arm(arms)
899 || control_flow_has_raise_branch(value)
900 // A let-EXPRESSION-position match (`let x = match … { … }`) must
901 // route the same payload/field-binding shapes to statement form
902 // as the function-tail/return value path: the `(lambda __v: …)`
903 // value chain cannot bind a record-pattern field or a user-enum
904 // constructor payload, so without this the binding is left free
905 // (`NameError`). Q-py-letexpr-match-namerror.
906 || match_value_needs_stmt_form(arms)
907 }
908 NodeKind::Loop { .. } | NodeKind::While { .. } => true,
909 NodeKind::If { .. } => {
910 crate::generator::node_is_statement(value) || control_flow_has_raise_branch(value)
911 }
912 NodeKind::Block { stmts, .. } => !stmts.is_empty(),
913 _ => false,
914 }
915}
916
917/// Whether `node` lowers to a Python **`raise` statement** — a diverging
918/// expression that yields no value: a `todo()` / `unreachable()` prelude call
919/// (see [`PyEmitCtx::map_prelude_call`]), or the `unreachable` AIR node. Such an
920/// expression is valid as a statement but **not** after `return` / `= `
921/// (`return raise NotImplementedError()` is a `SyntaxError`), so in value/tail
922/// position it must be emitted bare. The fall-through value is supplied by the
923/// surrounding control flow — the function simply never returns past the raise.
924fn is_raise_expr(node: &AIRNode) -> bool {
925 match &node.kind {
926 NodeKind::Unreachable => true,
927 NodeKind::Call { callee, .. } => matches!(
928 &callee.kind,
929 NodeKind::Identifier { name }
930 if matches!(name.name.as_str(), "todo" | "unreachable")
931 ),
932 _ => false,
933 }
934}
935
936/// Whether `node`'s subtree contains a `?` propagate operator that belongs to
937/// *this* function/method — used to decide whether the body must be wrapped in
938/// the `try: … except _BockPropagate: return …` envelope (see
939/// [`PyEmitCtx::emit_fn_body_with_propagate`]). The walk stops at a nested
940/// `FnDecl`/`Lambda`/`ClassDecl` boundary: a `?` inside a nested closure or
941/// method propagates from *that* inner function, so it gets its own wrapper and
942/// must not force one on the enclosing body.
943fn body_contains_propagate(node: &AIRNode) -> bool {
944 if matches!(node.kind, NodeKind::Propagate { .. }) {
945 return true;
946 }
947 // Do not descend into a nested scope: its `?` is the inner function's.
948 if matches!(
949 node.kind,
950 NodeKind::FnDecl { .. } | NodeKind::Lambda { .. } | NodeKind::ClassDecl { .. }
951 ) {
952 return false;
953 }
954 child_nodes(node).iter().any(|c| body_contains_propagate(c))
955}
956
957/// The tail/block value of `node` (for an `if`/`match` arm body), unwrapping a
958/// single-tail `Block`.
959fn unwrap_block_tail(node: &AIRNode) -> &AIRNode {
960 if let NodeKind::Block {
961 stmts,
962 tail: Some(t),
963 } = &node.kind
964 {
965 if stmts.is_empty() {
966 return t;
967 }
968 }
969 node
970}
971
972/// Whether an **expression-position** `if` (or `match`) has a branch/arm body
973/// that lowers to a diverging Python `raise` (`todo()` / `unreachable()`).
974/// Such a construct cannot ride inside a ternary (`return raise … if … else …`
975/// is a `SyntaxError`), so it must be hoisted to a statement-form `if`/`match`
976/// whose non-diverging branches `return` while the diverging branch `raise`s.
977fn control_flow_has_raise_branch(node: &AIRNode) -> bool {
978 match &node.kind {
979 NodeKind::If {
980 then_block,
981 else_block,
982 ..
983 } => {
984 is_raise_expr(unwrap_block_tail(then_block))
985 || control_flow_has_raise_branch(then_block)
986 || else_block.as_ref().is_some_and(|eb| {
987 is_raise_expr(unwrap_block_tail(eb)) || control_flow_has_raise_branch(eb)
988 })
989 }
990 NodeKind::Match { arms, .. } => arms.iter().any(|arm| {
991 if let NodeKind::MatchArm { body, .. } = &arm.kind {
992 is_raise_expr(unwrap_block_tail(body)) || control_flow_has_raise_branch(body)
993 } else {
994 false
995 }
996 }),
997 _ => false,
998 }
999}
1000
1001/// Whether a **value-position** `if` (one consumed as an expression — a
1002/// function tail, a `return` value) must be lowered to a statement-form
1003/// `if`/`elif`/`else` rather than a Python ternary.
1004///
1005/// The ternary form (`<then> if <cond> else <else>`) emits only each branch's
1006/// *tail* expression: any statements in a branch block — most importantly a
1007/// `let` binding — are silently dropped, so a later reference to that binding
1008/// becomes a `NameError` (the microservice `handle_delete_user` is the canonical
1009/// case: its `if (authorized) { let role = …; if (role == "admin") … }` lost the
1010/// `role` binding inside the ternary). Routing such an `if` to statement form
1011/// (each branch recursing through `emit_block_body`, which emits the `let` then
1012/// `return`s the tail) preserves the bindings. A branch is "droppable" when its
1013/// block carries statements (or nests another droppable `if`/`elif`).
1014fn if_value_needs_stmt_form(node: &AIRNode) -> bool {
1015 let NodeKind::If {
1016 then_block,
1017 else_block,
1018 ..
1019 } = &node.kind
1020 else {
1021 return false;
1022 };
1023 block_has_droppable_stmts(then_block)
1024 || else_block.as_deref().is_some_and(|eb| {
1025 if matches!(eb.kind, NodeKind::If { .. }) {
1026 if_value_needs_stmt_form(eb)
1027 } else {
1028 block_has_droppable_stmts(eb)
1029 }
1030 })
1031}
1032
1033/// True when a block carries leading statements that a value/ternary lowering
1034/// would drop (it emits only the tail). An empty-statement block is safe as a
1035/// ternary branch; a block with a `let` / expression statement is not. See
1036/// [`if_value_needs_stmt_form`].
1037fn block_has_droppable_stmts(node: &AIRNode) -> bool {
1038 matches!(&node.kind, NodeKind::Block { stmts, .. } if !stmts.is_empty())
1039}
1040
1041/// Whether a **value-position** `match` (one consumed as an expression — a
1042/// function tail, a `return` value) must be lowered to a statement-form
1043/// `match`/`case` rather than the `(lambda __v: …)` conditional chain.
1044///
1045/// The conditional chain can correctly express a flat dispatch that binds at
1046/// most a single payload: a literal, range, list, `Some(x)`/`Ok`/`Err`/`None`,
1047/// a whole-scrutinee bind, or a wildcard. It **cannot** test or bind:
1048///
1049/// - guards (it dropped the guard entirely),
1050/// - or / tuple / nested-constructor / range / list patterns (caught by the
1051/// shared [`crate::generator::match_needs_ifchain`]),
1052/// - **record patterns** — even a bare-bind one (`Point { x, .. } => "x=${x}"`),
1053/// whose field binding the chain left free (`(lambda __v: f"x={x}")(p)` →
1054/// `NameError: name 'x'`). The shared recogniser treats a bare-bind record
1055/// field as *not* structured, so it returns false for that shape; this
1056/// py-local predicate adds record patterns on top so the Python backend routes
1057/// them to the statement-form `emit_pattern`, which binds `case Point(x=x):`
1058/// by field name. (Kept py-local rather than widening the shared recogniser,
1059/// which the if-chain backends consult for their own switch fast-path.)
1060fn match_value_needs_stmt_form(arms: &[AIRNode]) -> bool {
1061 crate::generator::match_needs_ifchain(arms)
1062 || arms.iter().any(|arm| {
1063 matches!(
1064 &arm.kind,
1065 NodeKind::MatchArm { pattern, .. }
1066 if matches!(pattern.kind, NodeKind::RecordPat { .. })
1067 )
1068 })
1069 || arms
1070 .iter()
1071 .any(|arm| matches!(&arm.kind, NodeKind::MatchArm { pattern, .. } if arm_constructor_binds_payload(pattern)))
1072 || match_arm_drops_leading_stmts(arms)
1073}
1074
1075/// Whether `pattern` is a **constructor pattern that binds a payload the
1076/// `(lambda __v: …)` value chain cannot bind** — i.e. a user-enum variant such
1077/// as `Circle(r)` / `Rect(w, h)`. [`PyEmitCtx::emit_arm_value`] only binds the
1078/// payload of the runtime constructors `Some(x)`/`Ok(x)`/`Err(x)` (and the
1079/// payload-less `None`); for any *other* constructor it emits the arm body with
1080/// the field bindings left FREE, so a value-position / let-expression match
1081/// `let label = match s { Circle(r) => r … }` lowered to `(lambda __v: r …)(s)`
1082/// and raised `NameError: name 'r'` at run time (Q-py-letexpr-match-namerror /
1083/// Q-py-valuepos-match-payload-namebind — same root). Routing such a match to
1084/// the statement-form `match`/`case`, whose `emit_pattern` binds
1085/// `case Shape_Circle(_0=r):` by position, makes the binding resolve. The
1086/// chain-supported `Some`/`None`/`Ok`/`Err` shapes are left on the chain.
1087fn arm_constructor_binds_payload(pattern: &AIRNode) -> bool {
1088 let NodeKind::ConstructorPat { path, fields } = &pattern.kind else {
1089 return false;
1090 };
1091 // The runtime Optional/Result constructors are bound by the value chain.
1092 let leaf = path.segments.last().map_or("", |s| s.name.as_str());
1093 if matches!(leaf, "Some" | "None" | "Ok" | "Err") {
1094 return false;
1095 }
1096 // Any field that introduces a binding (a bare `BindPat`, or a nested
1097 // structured sub-pattern that itself binds) cannot be left free in the
1098 // expression chain.
1099 fields
1100 .iter()
1101 .any(|f| !matches!(f.kind, NodeKind::WildcardPat))
1102}
1103
1104/// Whether any **value-position** `match` arm carries a leading statement that
1105/// the `(lambda __v: …)` conditional chain cannot fold into an
1106/// immediately-applied lambda and would therefore *silently drop*.
1107///
1108/// The chain lowers an arm body block with a *value* tail via
1109/// [`PyEmitCtx::try_emit_block_stmts_as_expr`], which folds a leading simple
1110/// immutable `let` (`lambda x: …`) or a bare expression statement
1111/// (`lambda _: …`) into the expression. But a leading construct that has no
1112/// Python *expression* form — a loop (`for`/`while`/`loop`), an assignment, a
1113/// `return`/`break`/`continue`, a mutable or destructuring `let`, or a nested
1114/// block — makes `try_emit_block_stmts_as_expr` bail; the caller then falls
1115/// back to emitting just the block's tail, dropping the leading statement
1116/// (e.g. `Ok(n) => { for i in 0..n { log(i) } "ok" }` lost the whole loop).
1117///
1118/// When this predicate is true the match is routed to the statement-form
1119/// `match`/`case` ([`PyEmitCtx::emit_match`]), whose arm bodies recurse through
1120/// [`PyEmitCtx::emit_block_body`] — emitting each leading statement and then
1121/// `return`ing the tail — so the side effect runs *and* the value is produced.
1122/// The check mirrors `try_emit_block_stmts_as_expr`'s bail conditions exactly,
1123/// so the two stay in agreement (only arms the chain *can* express stay on it).
1124fn match_arm_drops_leading_stmts(arms: &[AIRNode]) -> bool {
1125 arms.iter().any(|arm| {
1126 let NodeKind::MatchArm { body, .. } = &arm.kind else {
1127 return false;
1128 };
1129 // Only a block with both leading statements *and* a value tail rides the
1130 // lambda chain; a statement-tail / tail-less arm is already routed to
1131 // statement form by `match_has_statement_arm`, and a tail-only block has
1132 // nothing to drop.
1133 let NodeKind::Block {
1134 stmts,
1135 tail: Some(_),
1136 } = &body.kind
1137 else {
1138 return false;
1139 };
1140 stmts.iter().any(stmt_not_lambda_expressible)
1141 })
1142}
1143
1144/// Whether a leading block statement has no Python *expression* form and so
1145/// cannot be folded into the `(lambda …: …)` chain by
1146/// [`PyEmitCtx::try_emit_block_stmts_as_expr`]. Mirrors that method's bail set
1147/// exactly (see [`match_arm_drops_leading_stmts`]).
1148fn stmt_not_lambda_expressible(stmt: &AIRNode) -> bool {
1149 match &stmt.kind {
1150 // A mutable or non-simple-bind (tuple/record/destructuring) `let` cannot
1151 // become a `lambda` parameter; a simple immutable `let` can.
1152 NodeKind::LetBinding {
1153 is_mut, pattern, ..
1154 } => *is_mut || PyEmitCtx::simple_bind_name(pattern).is_none(),
1155 // Statements with no expression form.
1156 NodeKind::Assign { .. }
1157 | NodeKind::While { .. }
1158 | NodeKind::For { .. }
1159 | NodeKind::Loop { .. }
1160 | NodeKind::Return { .. }
1161 | NodeKind::Break { .. }
1162 | NodeKind::Continue
1163 | NodeKind::Block { .. } => true,
1164 // A bare expression statement is foldable via `lambda _: …`.
1165 _ => false,
1166 }
1167}
1168
1169/// Compute the implicit cross-module imports for `module`: public symbols
1170/// declared in *other* reachable modules that `module` references but neither
1171/// declares locally nor imports explicitly. Returns `(module_path, name)`
1172/// pairs.
1173///
1174/// "References" is a conservative structural scan of the module's debug
1175/// rendering for the symbol name as an identifier token (mirroring
1176/// [`py_module_uses_optional`] and friends). It can only *over*-import a name
1177/// the program does not really use, which is harmless (a dead import), never
1178/// *under*-import — so it cannot reintroduce the `NameError` it exists to fix.
1179///
1180/// Exception: a name that appears *only* as a record/enum/class **field label**
1181/// (declaration, construction, pattern, or `.field` access — see
1182/// [`field_label_occurrences`]) is **not** a cross-module symbol reference.
1183/// Importing it anyway was the root cause of a Python import cycle when a record
1184/// field collided with a sibling module's public function (e.g.
1185/// `InventorySummary.total_value` vs. `service.total_value`, making `models`
1186/// import `service` which already imports `models`). We subtract the field-label
1187/// occurrences so such names are skipped while genuine references still import.
1188fn implicit_imports_for(
1189 module: &AIRModule,
1190 public_symbols: &HashMap<String, String>,
1191 own_path: &str,
1192) -> Vec<(String, String)> {
1193 let local = locally_declared_names(module);
1194 let explicit = explicitly_imported_names(module);
1195 let rendered = format!("{module:?}");
1196 let field_labels = field_label_occurrences(module);
1197 let mut out: Vec<(String, String)> = Vec::new();
1198 for (name, declaring_module) in public_symbols {
1199 if declaring_module == own_path || local.contains(name) || explicit.contains(name) {
1200 continue;
1201 }
1202 // Identifier-token match: the AIR debug rendering quotes identifier
1203 // names, so `"Iterable"` appears iff the name is referenced. Subtract
1204 // the field-label occurrences: a name reached *only* through field
1205 // labels is not a cross-module reference and must not be imported (it
1206 // would create an import cycle for field/function name collisions).
1207 let total = quoted_token_count(&rendered, name);
1208 let labels = field_labels.get(name).copied().unwrap_or(0);
1209 if total > labels {
1210 out.push((declaring_module.clone(), name.clone()));
1211 }
1212 }
1213 out
1214}
1215
1216/// The shared per-module runtime module name (without extension). In the
1217/// per-module (native-import) emission path the four runtime preludes
1218/// (`Optional`, `Result`, `Ordering`, concurrency) live in one file —
1219/// `_bock_runtime.py` at the build root — and every emitted module imports the
1220/// names it needs from it. A single shared definition keeps the tagged runtime
1221/// classes *identical objects* across files, so an `isinstance(x, _BockSome)`
1222/// in `main.py` still matches a `_BockSome` built in `core/option.py` (separate
1223/// per-file class definitions would not be `isinstance`-compatible).
1224const RUNTIME_MODULE_PY: &str = "_bock_runtime";
1225
1226/// The Ordering-runtime *singleton* name for an `Ordering` variant
1227/// (`Less`→`_bock_less`, …). Used at construction sites.
1228fn ordering_singleton_py(variant: &str) -> &'static str {
1229 match variant {
1230 "Less" => "_bock_less",
1231 "Equal" => "_bock_equal",
1232 _ => "_bock_greater",
1233 }
1234}
1235
1236/// The Ordering-runtime *class* name for an `Ordering` variant
1237/// (`Less`→`_BockOrderingLess`, …). Used as a `case` pattern.
1238fn ordering_class_py(variant: &str) -> &'static str {
1239 match variant {
1240 "Less" => "_BockOrderingLess",
1241 "Equal" => "_BockOrderingEqual",
1242 _ => "_BockOrderingGreater",
1243 }
1244}
1245
1246const CONCURRENCY_RUNTIME_PY: &str = "\
1247# ── Bock concurrency runtime ──
1248import asyncio as __bock_asyncio
1249
1250class __BockChannel:
1251 __slots__ = ('_q',)
1252 def __init__(self):
1253 self._q = __bock_asyncio.Queue()
1254 def send(self, v):
1255 self._q.put_nowait(v)
1256 async def recv(self):
1257 return await self._q.get()
1258 def close(self):
1259 pass
1260
1261def __bock_channel_new():
1262 ch = __BockChannel()
1263 return (ch, ch)
1264
1265def __bock_spawn(x):
1266 # If already a coroutine, wrap it in a Task so it starts eagerly.
1267 if __bock_asyncio.iscoroutine(x):
1268 return __bock_asyncio.create_task(x)
1269 return x
1270";
1271
1272/// Python code generator implementing the `CodeGenerator` trait.
1273#[derive(Debug)]
1274pub struct PyGenerator {
1275 profile: TargetProfile,
1276}
1277
1278impl PyGenerator {
1279 /// Creates a new Python code generator.
1280 #[must_use]
1281 pub fn new() -> Self {
1282 Self {
1283 profile: TargetProfile::python(),
1284 }
1285 }
1286}
1287
1288impl Default for PyGenerator {
1289 fn default() -> Self {
1290 Self::new()
1291 }
1292}
1293
1294impl CodeGenerator for PyGenerator {
1295 fn target(&self) -> &TargetProfile {
1296 &self.profile
1297 }
1298
1299 fn generate_module(&self, module: &AIRModule) -> Result<GeneratedCode, CodegenError> {
1300 // Shared pre-pass: hoist value-position diverging control flow (see
1301 // `hoist_value_cf`) into declare-then-assign temp blocks.
1302 let module =
1303 &crate::generator::hoist_value_cf(crate::generator::lower_blanket_into(module.clone()));
1304 let mut ctx = PyEmitCtx::new();
1305 ctx.enum_variants =
1306 crate::generator::collect_enum_variants(&[(module, std::path::Path::new(""))]);
1307 ctx.trait_decls =
1308 crate::generator::collect_trait_decls(&[(module, std::path::Path::new(""))]);
1309 ctx.const_names =
1310 crate::generator::collect_const_names(&[(module, std::path::Path::new(""))]);
1311 ctx.emit_node(module)?;
1312 let content = ctx.finish();
1313 let source_map = SourceMap {
1314 generated_file: String::new(),
1315 ..Default::default()
1316 };
1317 Ok(GeneratedCode {
1318 files: vec![OutputFile {
1319 path: PathBuf::new(),
1320 content,
1321 source_map: Some(source_map),
1322 }],
1323 })
1324 }
1325
1326 fn entry_invocation(&self, main_is_async: bool) -> Option<String> {
1327 if main_is_async {
1328 Some("if __name__ == \"__main__\":\n asyncio.run(main())\n".to_string())
1329 } else {
1330 Some("if __name__ == \"__main__\":\n main()\n".to_string())
1331 }
1332 }
1333
1334 /// Emit a per-module **native import tree** (spec §20.6.1; DQ19 resolved):
1335 /// each module the entry program reaches through a real `use` is emitted to
1336 /// its **own** Python file, and cross-module references resolve through real
1337 /// Python imports (`from core.option import or_else`). This is the sole
1338 /// `bock build` output path.
1339 ///
1340 /// Output-path mapping is keyed on each module's *declared* path, not its
1341 /// on-disk source path, so the file layout and the import path agree:
1342 /// `module core.option` ⇒ `core/option.py` and `from core.option import …`.
1343 /// The **entry** module (the one declaring `main`, else the last in
1344 /// dependency order) is always emitted as `main.py` so the run model
1345 /// (`python3 main.py` from the build root) is stable; Python adds the
1346 /// script's directory to `sys.path`, and `core` resolves as a PEP 420
1347 /// namespace package (no `__init__.py` needed).
1348 ///
1349 /// The four runtime preludes (`Optional`, `Result`, `Ordering`,
1350 /// concurrency) are emitted **once** into a shared `_bock_runtime.py`
1351 /// (see `RUNTIME_MODULE_PY`); every module that references one imports it
1352 /// (`from _bock_runtime import *`). A single shared definition keeps the
1353 /// tagged runtime classes identical across files so cross-module
1354 /// `isinstance` checks succeed.
1355 fn generate_project(
1356 &self,
1357 modules: &[(&AIRModule, &std::path::Path)],
1358 ) -> Result<GeneratedCode, CodegenError> {
1359 // Shared pre-pass: hoist value-position diverging control flow on every
1360 // module before registry collection or emission (see `hoist_value_cf`).
1361 let hoisted: Vec<(AIRModule, &std::path::Path)> = modules
1362 .iter()
1363 .map(|(m, p)| {
1364 (
1365 crate::generator::hoist_value_cf(crate::generator::lower_blanket_into(
1366 (*m).clone(),
1367 )),
1368 *p,
1369 )
1370 })
1371 .collect();
1372 let modules: Vec<(&AIRModule, &std::path::Path)> =
1373 hoisted.iter().map(|(m, p)| (m, *p)).collect();
1374 let modules = modules.as_slice();
1375 // Emit only modules the entry program actually `use`s (plus the entry
1376 // itself), dependency-ordered — never the prelude-only stdlib (see
1377 // `reachable_modules`).
1378 let reachable = crate::generator::reachable_modules(modules);
1379 let modules = reachable.as_slice();
1380 if modules.is_empty() {
1381 return Ok(GeneratedCode { files: vec![] });
1382 }
1383
1384 // The entry module names `main.py`; every other module is placed at the
1385 // path mirrored from its declared module-path.
1386 let entry_idx = modules
1387 .iter()
1388 .position(|(m, _)| crate::generator::module_declares_main_fn(m))
1389 .unwrap_or(modules.len() - 1);
1390
1391 // Enum-variant / trait registries are collected across the whole
1392 // reachable set so a reference in one file to a type declared in another
1393 // lowers identically to the bundling path.
1394 let enum_variants = crate::generator::collect_enum_variants(modules);
1395 let trait_decls = crate::generator::collect_trait_decls(modules);
1396 let const_names = crate::generator::collect_const_names(modules);
1397 // Map of public symbol → declaring module, for the implicit-import pass.
1398 let public_symbols = collect_public_symbol_modules(modules);
1399 // Program-wide field/method name-collision set (snake_cased). Built across
1400 // *all* reachable modules so a call site in `main.py` to a renamed method
1401 // declared in `core/error.py` agrees with that declaration.
1402 let mut field_method_collisions = std::collections::HashSet::new();
1403 for (module, _) in modules {
1404 field_method_collisions.extend(crate::generator::collect_record_field_names(
1405 module,
1406 to_snake_case,
1407 ));
1408 }
1409
1410 let main_is_async = modules
1411 .iter()
1412 .any(|(m, _)| crate::generator::module_main_fn_is_async(m));
1413 let invocation = self.entry_invocation(main_is_async);
1414
1415 let mut files: Vec<OutputFile> = Vec::with_capacity(modules.len() + 1);
1416 // Which runtime preludes any module references — drives `_bock_runtime.py`.
1417 let mut runtime_optional = false;
1418 let mut runtime_result = false;
1419 let mut runtime_ordering = false;
1420 let mut runtime_concurrency = false;
1421 let mut runtime_list_functional = false;
1422 let mut runtime_list_mutators = false;
1423 let mut runtime_propagate = false;
1424 let mut runtime_str = false;
1425
1426 for (i, (module, source_path)) in modules.iter().enumerate() {
1427 let mut ctx = PyEmitCtx::new();
1428 ctx.per_module = true;
1429 // Entry module (declares `main`, emitted as `main.py`) gets the
1430 // Windows UTF-8 stdout guard in its preamble — entry-only, since it
1431 // is a process-global side effect.
1432 ctx.is_entry_module =
1433 i == entry_idx && crate::generator::module_declares_main_fn(module);
1434 ctx.enum_variants = enum_variants.clone();
1435 ctx.trait_decls = trait_decls.clone();
1436 ctx.const_names = const_names.clone();
1437 ctx.field_method_collisions = field_method_collisions.clone();
1438 // Effect-op resolution needs the whole reachable set: a bare op in
1439 // one module may belong to an effect declared in another.
1440 ctx.seed_effect_registries(modules);
1441 ctx.implicit_imports =
1442 implicit_imports_for(module, &public_symbols, &module_path_string_of(module));
1443 ctx.emit_node(module)?;
1444 runtime_optional |= ctx.needs_runtime_optional;
1445 runtime_result |= ctx.needs_runtime_result;
1446 runtime_ordering |= ctx.needs_runtime_ordering;
1447 runtime_concurrency |= ctx.needs_runtime_concurrency;
1448 runtime_list_functional |= ctx.needs_runtime_list_functional;
1449 runtime_list_mutators |= ctx.needs_runtime_list_mutators;
1450 runtime_propagate |= ctx.needs_runtime_propagate;
1451 runtime_str |= ctx.needs_runtime_str;
1452 let mut content = ctx.finish();
1453
1454 // The entry file gets the `if __name__ == "__main__": main()`
1455 // invocation appended (exactly once, only when it declares `main`).
1456 if i == entry_idx && crate::generator::module_declares_main_fn(module) {
1457 if let Some(invoc) = invocation.as_ref() {
1458 if !content.is_empty() && !content.ends_with('\n') {
1459 content.push('\n');
1460 }
1461 content.push_str(invoc);
1462 }
1463 }
1464
1465 let out_path = self.module_output_path(module, source_path, i == entry_idx);
1466 let generated_file = out_path
1467 .file_name()
1468 .and_then(|s| s.to_str())
1469 .unwrap_or("")
1470 .to_string();
1471 let source_map = SourceMap {
1472 generated_file,
1473 ..Default::default()
1474 };
1475 files.push(OutputFile {
1476 path: out_path,
1477 content,
1478 source_map: Some(source_map),
1479 });
1480 }
1481
1482 // Emit the shared runtime module with exactly the preludes referenced.
1483 if runtime_optional
1484 || runtime_result
1485 || runtime_ordering
1486 || runtime_concurrency
1487 || runtime_list_functional
1488 || runtime_list_mutators
1489 || runtime_propagate
1490 || runtime_str
1491 {
1492 let mut content = String::new();
1493 // Every runtime name is underscore-prefixed, which `from … import *`
1494 // skips *unless* the module declares `__all__`. Build `__all__`
1495 // explicitly from the emitted preludes so the consuming modules'
1496 // `from _bock_runtime import *` pulls in `_BockSome` / `_bock_none`
1497 // / … (without it, those names resolve to `NameError` at run time).
1498 let mut all_names: Vec<&str> = Vec::new();
1499 if runtime_optional {
1500 content.push_str(OPTIONAL_RUNTIME_PY);
1501 content.push('\n');
1502 all_names.extend(["_BockSome", "_BockNone", "_bock_none"]);
1503 }
1504 if runtime_result {
1505 content.push_str(RESULT_RUNTIME_PY);
1506 content.push('\n');
1507 all_names.extend([
1508 "_BockOk",
1509 "_BockErr",
1510 "_bock_parse_int",
1511 "_bock_parse_float",
1512 ]);
1513 }
1514 if runtime_ordering {
1515 content.push_str(ORDERING_RUNTIME_PY);
1516 content.push('\n');
1517 all_names.extend([
1518 "_BockOrderingLess",
1519 "_BockOrderingEqual",
1520 "_BockOrderingGreater",
1521 "_bock_less",
1522 "_bock_equal",
1523 "_bock_greater",
1524 "_bock_compare",
1525 ]);
1526 }
1527 if runtime_concurrency {
1528 content.push_str(CONCURRENCY_RUNTIME_PY);
1529 content.push('\n');
1530 all_names.extend(["__BockChannel", "__bock_channel_new", "__bock_spawn"]);
1531 }
1532 if runtime_list_functional {
1533 // `_bock_find` references `_BockSome`/`_bock_none`; the ctx that
1534 // set `needs_runtime_list_functional` also set
1535 // `needs_runtime_optional`, so the Optional prelude is already in
1536 // `content` above this point.
1537 content.push_str(LIST_FUNCTIONAL_RUNTIME_PY);
1538 content.push('\n');
1539 all_names.extend(["_bock_reduce", "_bock_fold", "_bock_find", "_bock_for_each"]);
1540 }
1541 if runtime_list_mutators {
1542 content.push_str(LIST_MUTATOR_RUNTIME_PY);
1543 content.push('\n');
1544 all_names.push("_bock_list_abort");
1545 }
1546 if runtime_propagate {
1547 // `_bock_try` tests success tags by class *name*, so it has no
1548 // hard reference to `_BockOk`/`_BockSome` and can stand alone even
1549 // when only one of the Optional/Result preludes is present.
1550 content.push_str(PROPAGATE_RUNTIME_PY);
1551 content.push('\n');
1552 all_names.extend(["_BockPropagate", "_bock_try"]);
1553 }
1554 if runtime_str {
1555 content.push_str(STR_RUNTIME_PY);
1556 content.push('\n');
1557 all_names.push("_bock_str");
1558 }
1559 let all_list = all_names
1560 .iter()
1561 .map(|n| format!("\"{n}\""))
1562 .collect::<Vec<_>>()
1563 .join(", ");
1564 content.push_str(&format!("__all__ = [{all_list}]\n"));
1565 files.push(OutputFile {
1566 path: PathBuf::from(format!("{RUNTIME_MODULE_PY}.py")),
1567 content,
1568 source_map: Some(SourceMap {
1569 generated_file: format!("{RUNTIME_MODULE_PY}.py"),
1570 ..Default::default()
1571 }),
1572 });
1573 }
1574
1575 Ok(GeneratedCode { files })
1576 }
1577
1578 /// Transpile `@test` functions into a `test_bock.py` file (S7).
1579 ///
1580 /// `framework`: `"unittest"` emits a `unittest.TestCase` subclass with
1581 /// `self.assertEqual`/`assertTrue`/…; anything else (default `"pytest"`)
1582 /// emits module-level `def test_xxx():` with bare `assert` — both discovered
1583 /// by `pytest` and `python -m unittest`. Functions under test are imported by
1584 /// name from their emitted modules; the Optional/Result predicate assertions
1585 /// import the runtime tag classes from `_bock_runtime`.
1586 fn generate_tests(
1587 &self,
1588 modules: &[(&AIRModule, &std::path::Path)],
1589 framework: &str,
1590 ) -> Result<crate::generator::TestArtifacts, CodegenError> {
1591 let reachable = crate::generator::reachable_modules(modules);
1592 let modules = reachable.as_slice();
1593 let tests = crate::generator::collect_test_fns(modules);
1594 if tests.is_empty() {
1595 return Ok(crate::generator::TestArtifacts::default());
1596 }
1597 let entry_idx = modules
1598 .iter()
1599 .position(|(m, _)| crate::generator::module_declares_main_fn(m))
1600 .unwrap_or(modules.len().saturating_sub(1));
1601
1602 // Cross-module registries, mirroring `generate_project`, so the test
1603 // bodies lower references identically to the runtime tree.
1604 let enum_variants = crate::generator::collect_enum_variants(modules);
1605 let trait_decls = crate::generator::collect_trait_decls(modules);
1606 let const_names = crate::generator::collect_const_names(modules);
1607 let mut field_method_collisions = std::collections::HashSet::new();
1608 for (module, _) in modules {
1609 field_method_collisions.extend(crate::generator::collect_record_field_names(
1610 module,
1611 to_snake_case,
1612 ));
1613 }
1614 let mut ctx = PyEmitCtx::new();
1615 ctx.per_module = true;
1616 ctx.enum_variants = enum_variants;
1617 ctx.trait_decls = trait_decls;
1618 ctx.const_names = const_names;
1619 ctx.field_method_collisions = field_method_collisions;
1620 ctx.seed_effect_registries(modules);
1621
1622 // Import the functions under test, snake_cased, from each module.
1623 let mut import_lines: Vec<String> = Vec::new();
1624 for (i, (module, _)) in modules.iter().enumerate() {
1625 let mut import_names: Vec<String> = crate::generator::exportable_value_names(module)
1626 .into_iter()
1627 .filter(|e| e.is_fn)
1628 .map(|e| to_snake_case(&e.name))
1629 .collect();
1630 // Enum-variant constructors a `@test` body may reference *bare* as a
1631 // call argument (e.g. `apply_casing("x", Upper)` → emits an instance
1632 // of the `{enum}_{variant}` `@dataclass`, `Casing_Upper()`). The
1633 // runtime tree emits those dataclasses at module top level, but the
1634 // class name is emitted *verbatim* (no snake_case), so import it under
1635 // its exact value-name. Over-importing an unreferenced variant is a
1636 // harmless dead import; under-importing a referenced one is a
1637 // `NameError` at test runtime — so mirror the non-test path and
1638 // include every public variant value-name.
1639 import_names.extend(crate::generator::enum_variant_value_names(module));
1640 if import_names.is_empty() {
1641 continue;
1642 }
1643 let module_import = if i == entry_idx {
1644 "main".to_string()
1645 } else {
1646 crate::generator::module_path_string(module).unwrap_or_else(|| "main".to_string())
1647 };
1648 import_lines.push(format!(
1649 "from {module_import} import {}",
1650 import_names.join(", ")
1651 ));
1652 }
1653 import_lines.sort_unstable();
1654 import_lines.dedup();
1655
1656 // Import only the Optional/Result runtime tag classes the assertions
1657 // actually reference — `_bock_runtime.py` only defines the runtimes the
1658 // program uses, so importing an absent class (e.g. `_BockOk` in an
1659 // Optional-only program) would be an ImportError at test load.
1660 let mut runtime_imports: std::collections::BTreeSet<&str> =
1661 std::collections::BTreeSet::new();
1662 for (test_fn, _) in &tests {
1663 if let NodeKind::FnDecl { body, .. } = &test_fn.kind {
1664 collect_runtime_tag_imports(body, &mut runtime_imports);
1665 }
1666 }
1667
1668 let is_unittest = framework == "unittest";
1669 let mut out = String::new();
1670 if is_unittest {
1671 out.push_str("import unittest\n");
1672 }
1673 if !runtime_imports.is_empty() {
1674 let names: Vec<&str> = runtime_imports.iter().copied().collect();
1675 out.push_str(&format!("from _bock_runtime import {}\n", names.join(", ")));
1676 }
1677 for line in &import_lines {
1678 out.push_str(line);
1679 out.push('\n');
1680 }
1681 // Black/PEP 8 puts two blank lines between the import block and the first
1682 // top-level definition (`class`/`def`). Emitting them here keeps the
1683 // transpiled test file `black --check`-clean (§20.6.2 codegen-formatter
1684 // agreement), which CI enforces on the certifying lane.
1685 out.push_str("\n\n");
1686
1687 if is_unittest {
1688 out.push_str("class TestBock(unittest.TestCase):\n");
1689 for (i, (test_fn, _module_path)) in tests.iter().enumerate() {
1690 let NodeKind::FnDecl { name, body, .. } = &test_fn.kind else {
1691 continue;
1692 };
1693 if i > 0 {
1694 out.push('\n');
1695 }
1696 out.push_str(&format!(" def {}(self):\n", to_snake_case(&name.name)));
1697 ctx.emit_py_test_body(body, true, 2, &mut out)?;
1698 }
1699 out.push_str("\n\nif __name__ == \"__main__\":\n unittest.main()\n");
1700 } else {
1701 // Two blank lines between top-level `def`s (Black/PEP 8) and exactly
1702 // one trailing newline at end of file.
1703 for (i, (test_fn, _module_path)) in tests.iter().enumerate() {
1704 let NodeKind::FnDecl { name, body, .. } = &test_fn.kind else {
1705 continue;
1706 };
1707 if i > 0 {
1708 out.push_str("\n\n");
1709 }
1710 out.push_str(&format!("def {}():\n", to_snake_case(&name.name)));
1711 ctx.emit_py_test_body(body, false, 1, &mut out)?;
1712 }
1713 }
1714
1715 Ok(crate::generator::TestArtifacts {
1716 files: vec![OutputFile {
1717 path: PathBuf::from("test_bock.py"),
1718 content: out,
1719 source_map: None,
1720 }],
1721 entry_append: None,
1722 })
1723 }
1724}
1725
1726impl PyGenerator {
1727 /// Output path for one module in the per-module native-import tree.
1728 ///
1729 /// The entry module is always `main.py` (mirrored from its source path) so
1730 /// the run model `python3 main.py` is stable. Every other module is placed
1731 /// at the path mirrored from its **declared** module-path so the file
1732 /// location and the Python import path agree:
1733 /// `module core.option` ⇒ `core/option.py` (imported as `core.option`).
1734 /// A module without a declared path falls back to its source-mirrored path.
1735 fn module_output_path(
1736 &self,
1737 module: &AIRModule,
1738 source_path: &std::path::Path,
1739 is_entry: bool,
1740 ) -> PathBuf {
1741 if is_entry {
1742 return crate::generator::derive_output_path(source_path, self.target());
1743 }
1744 match crate::generator::module_path_string(module) {
1745 Some(path) if !path.is_empty() => {
1746 let rel: PathBuf = path.split('.').collect();
1747 rel.with_extension(&self.target().conventions.file_extension)
1748 }
1749 _ => crate::generator::derive_output_path(source_path, self.target()),
1750 }
1751 }
1752}
1753
1754// ─── Emission context ────────────────────────────────────────────────────────
1755
1756/// One lexical-block frame on [`PyEmitCtx::shadow_scopes`].
1757///
1758/// Python has function scope, not block scope, for `=`. A Bock `let` that
1759/// shadows a name bound in an enclosing block would therefore, if emitted as a
1760/// plain `name = …`, permanently stomp the outer binding — code after the nested
1761/// block then reads the inner value. Each block frame tracks the Python names
1762/// bound *directly within it* (`bound`) and, for any name it shadows from an
1763/// enclosing frame, the fresh alias it was renamed to (`renames`). Identifier
1764/// emission consults the frame stack innermost-first, so a shadowed name resolves
1765/// to its alias inside the nested block and to the original once the block ends.
1766#[derive(Default)]
1767struct ShadowScope {
1768 /// Python names bound directly in this block (so a *same-block* re-bind is a
1769 /// plain rebind, never renamed — `let acc = …; let acc = acc + 1`).
1770 bound: std::collections::HashSet<String>,
1771 /// Original-python-name → alias for names this block shadows from an
1772 /// enclosing frame.
1773 renames: HashMap<String, String>,
1774}
1775
1776/// Internal state for Python emission.
1777struct PyEmitCtx {
1778 buf: String,
1779 indent: usize,
1780 needs_dataclass_import: bool,
1781 needs_abc_import: bool,
1782 /// Set when any `async def` is emitted; forces `import asyncio` in the
1783 /// preamble so awaited calls, `asyncio.run`, and `asyncio.create_task`
1784 /// resolve at runtime.
1785 needs_asyncio_import: bool,
1786 /// Set when Duration/Instant codegen emits `time.monotonic_ns()`.
1787 needs_time_import: bool,
1788 /// Set when a numeric primitive method emits `math.*` (`Float.floor`/`ceil`/
1789 /// `sqrt`/`is_nan`/`is_infinite`), forcing `import math` in the preamble.
1790 needs_math_import: bool,
1791 /// Names bound in the current block whose call value should be wrapped
1792 /// in `asyncio.create_task(...)` because the binding is later `await`ed
1793 /// within the same block. See [`Self::collect_task_bindings`].
1794 task_bound_names: std::collections::HashSet<String>,
1795 /// Maps effect operation name → effect type name (e.g., "log" → "Logger").
1796 effect_ops: HashMap<String, String>,
1797 /// Maps effect type name → current handler variable name in scope.
1798 current_handler_vars: HashMap<String, String>,
1799 /// Maps function name → effect type names from its `with` clause.
1800 fn_effects: HashMap<String, Vec<String>>,
1801 /// Maps composite effect name → component effect names.
1802 composite_effects: HashMap<String, Vec<String>>,
1803 /// Monotonically-increasing counter used to generate unique handler
1804 /// variable names per handling block. Python lacks block scope for
1805 /// `=` bindings, so without a suffix, nested `handling (...)` blocks
1806 /// would overwrite each other's handler variables.
1807 handling_counter: usize,
1808 /// Trait impls keyed by target record name, collected up front from the
1809 /// current module's items so `RecordDecl` emission can inline the impl
1810 /// methods as class members instead of leaving orphan module-level
1811 /// functions that never get bound to the handler instance.
1812 impls_by_target: HashMap<String, Vec<AIRNode>>,
1813 /// Set once the Optional runtime prelude has been emitted in the
1814 /// single-module self-contained path ([`PyGenerator::generate_module`]), so
1815 /// a module referencing it more than once still inlines it at most once
1816 /// (redefining the `_BockSome`/`_BockNone` helpers is wasteful and risks
1817 /// shadowing surprises). The per-module project path imports the runtime
1818 /// from the shared `RUNTIME_MODULE_PY` module instead.
1819 optional_runtime_emitted: bool,
1820 /// Set once the `Result` runtime prelude has been emitted; deduped exactly as
1821 /// [`Self::optional_runtime_emitted`] (redefining the `_BockOk`/`_BockErr`
1822 /// classes is wasteful).
1823 result_runtime_emitted: bool,
1824 /// Set once the [`ORDERING_RUNTIME_PY`] prelude has been emitted; deduped
1825 /// exactly as [`Self::optional_runtime_emitted`].
1826 ordering_runtime_emitted: bool,
1827 /// Set once the concurrency runtime prelude has been emitted; deduped exactly
1828 /// as [`Self::optional_runtime_emitted`].
1829 concurrency_runtime_emitted: bool,
1830 /// Set once the [`LIST_FUNCTIONAL_RUNTIME_PY`] prelude has been emitted;
1831 /// deduped exactly as [`Self::optional_runtime_emitted`].
1832 list_functional_runtime_emitted: bool,
1833 /// Set once the [`LIST_MUTATOR_RUNTIME_PY`] prelude (`_bock_list_abort`)
1834 /// has been emitted; deduped exactly as
1835 /// [`Self::optional_runtime_emitted`].
1836 list_mutator_runtime_emitted: bool,
1837 /// Set once the [`PROPAGATE_RUNTIME_PY`] prelude (`_bock_try` /
1838 /// `_BockPropagate`) has been emitted; deduped exactly as
1839 /// [`Self::optional_runtime_emitted`].
1840 propagate_runtime_emitted: bool,
1841 /// Set once the display-string runtime ([`STR_RUNTIME_PY`]) has been inlined
1842 /// in the single-module self-contained path. (Q-displayable-interpolation-dispatch.)
1843 str_runtime_emitted: bool,
1844 /// Set when an enum decl emits a `Name = Union[...]` alias, so the preamble
1845 /// imports `Union` from `typing`.
1846 needs_union_import: bool,
1847 /// Typing-import needs accumulated while lowering type annotations. These
1848 /// are `Cell`s because `type_to_py`/`ast_type_to_py` (where the relevant
1849 /// `Callable`/`Any`/`Self`/`Never`/`TypeVar` names are emitted) take
1850 /// `&self` — many of their call sites borrow `self` immutably inside
1851 /// closures, so promoting them to `&mut self` would fight the borrow
1852 /// checker. The `finish` preamble reads them to emit a single merged
1853 /// `from typing import …` line.
1854 needs_typing_callable: Cell<bool>,
1855 needs_typing_any: Cell<bool>,
1856 needs_typing_self: Cell<bool>,
1857 needs_typing_never: Cell<bool>,
1858 /// Set when a generic decl emits `T = TypeVar("T")`, so the preamble
1859 /// imports `TypeVar` (and `Generic`, used in the class base list).
1860 needs_typing_typevar: Cell<bool>,
1861 /// Names already emitted as `T = TypeVar("T")`, deduped within the file so
1862 /// a type parameter shared by several decls is declared exactly once.
1863 emitted_typevars: std::collections::HashSet<String>,
1864 /// User-enum-variant registry (DV14). Routes a construction/pattern to the
1865 /// `{enum}_{variant}` dataclass and recognises a unit variant (needs `()`
1866 /// instantiation). Built-in Optional/Result pre-seeds filtered out where
1867 /// the bespoke `_BockSome`/`_BockNone` lowering applies. Pre-scanned across
1868 /// the reached modules.
1869 enum_variants: crate::generator::EnumVariantRegistry,
1870 /// The reached modules' user-declared traits (keyed by name). Distinguishes a
1871 /// `T: Equatable` bound that is a real user trait from the compiler-provided
1872 /// sealed-core conformance, which must drop the `bound=` on the `TypeVar` and
1873 /// lower `.eq`/`.compare` to native operators (GAP-C). See
1874 /// [`crate::generator::is_unimplemented_sealed_core_trait`].
1875 trait_decls: crate::generator::TraitDeclRegistry,
1876 /// True in the **per-module native-import** emission path
1877 /// ([`PyGenerator::generate_project`], the sole real-build path). When set,
1878 /// the `Module` arm imports each needed runtime prelude from the shared
1879 /// `RUNTIME_MODULE_PY` module instead of inlining its definitions, and the
1880 /// `ImportDecl` arm emits a real `from <module> import …` rather than a
1881 /// no-op. When clear, the module is emitted as a single self-contained file
1882 /// with its runtime preludes inlined — the [`PyGenerator::generate_module`]
1883 /// path used by unit tests.
1884 per_module: bool,
1885 /// In the per-module path, records which shared-runtime names this module
1886 /// must import from `RUNTIME_MODULE_PY`: Optional, Result, Ordering,
1887 /// concurrency — set from the same structural scans the bundling path uses
1888 /// to decide whether to inline a prelude. `finish` turns these into the
1889 /// module's `from _bock_runtime import …` line.
1890 needs_runtime_optional: bool,
1891 needs_runtime_result: bool,
1892 needs_runtime_ordering: bool,
1893 needs_runtime_concurrency: bool,
1894 needs_runtime_list_functional: bool,
1895 /// In the per-module path, set when this module uses a DQ30 in-place
1896 /// `List` mutator with a bounds pre-check (`remove_at`/`insert`/`set`),
1897 /// so it imports `_bock_list_abort` from the shared `RUNTIME_MODULE_PY`.
1898 /// Mirrors [`Self::needs_runtime_optional`].
1899 needs_runtime_list_mutators: bool,
1900 /// In the per-module path, set when this module uses the `?` propagate
1901 /// operator, so it imports `_bock_try` / `_BockPropagate` from the shared
1902 /// `RUNTIME_MODULE_PY`. Mirrors [`Self::needs_runtime_optional`].
1903 needs_runtime_propagate: bool,
1904 /// As [`Self::needs_runtime_propagate`], for the display-string runtime
1905 /// (`_bock_str`). (Q-displayable-interpolation-dispatch.)
1906 needs_runtime_str: bool,
1907 /// Implicit cross-module imports for the per-module path, as
1908 /// `(module_path, symbol_name)` pairs — names this module references but
1909 /// neither declares locally nor imports via an explicit `use` (e.g. a
1910 /// §18.2-prelude trait used as a base class). The `Module` arm emits a
1911 /// `from <module_path> import <symbol_name>` for each, grouped by module,
1912 /// after the explicit imports. Computed in `generate_project`.
1913 implicit_imports: Vec<(String, String)>,
1914 /// Snake-cased record/class field names across the reachable program, used to
1915 /// disambiguate a method whose snake_cased name collides with a field name
1916 /// (`core.error`'s `message` field + `message()` method). A `@dataclass`
1917 /// field overwrites a same-named method attribute on the class, so the
1918 /// *method* is renamed (`message_method`) at its definition and every call
1919 /// site via [`Self::py_method_name`]; the field keeps its name. Pre-seeded
1920 /// program-wide by `generate_project` (and extended per-module by the
1921 /// `Module` arm for the single-module `generate_module` path). Shared policy
1922 /// with go/js/ts.
1923 field_method_collisions: std::collections::HashSet<String>,
1924 /// Set on the **entry** module (the one declaring `main`, emitted as
1925 /// `main.py`) in the per-module path. When set, `finish` prepends a
1926 /// `sys.stdout.reconfigure(encoding="utf-8")` guard so unicode `print`
1927 /// output is correct on Windows, whose Python defaults stdout to the locale
1928 /// codepage rather than UTF-8. Entry-only: the reconfigure is a
1929 /// process-global side effect, so it belongs at the single program entry,
1930 /// not in every imported module.
1931 is_entry_module: bool,
1932 /// Stack of "current loop's value target" used to lower an
1933 /// **expression-position `loop`** assigned to a binding
1934 /// (`let r = loop { … break v }`). Python's `break` carries no value, so a
1935 /// `loop` that yields a value cannot be an expression. When such a loop is
1936 /// hoisted to statement form by [`Self::emit_value_binding`], the target
1937 /// variable is pushed here; a `break <value>` inside then lowers to
1938 /// `<target> = <value>` followed by `break`. `None` is pushed for ordinary
1939 /// statement-position loops (no value), so a bare `break` stays a bare
1940 /// `break`. Only the innermost frame is consulted.
1941 loop_value_targets: Vec<Option<String>>,
1942 /// Declared names of module-scope `const`s, pre-scanned across the reachable
1943 /// program. A const is emitted verbatim at both its declaration and every use
1944 /// so the two agree — the def's `to_snake_case` (`FIZZ_NUM` → `fizz_num`) and
1945 /// the use site's uppercase-preserving `identifier_to_py` (`FIZZ_NUM`) would
1946 /// otherwise disagree, raising `NameError`. See
1947 /// [`crate::generator::collect_const_names`].
1948 const_names: std::collections::HashSet<String>,
1949 /// Stack of lexical-block frames for nested-block `let`-shadow renaming (see
1950 /// [`ShadowScope`]). A frame is pushed on entering a Bock `{ }` block (every
1951 /// function/method body, value-block, `if`/`else`/`match`-arm/loop/guard
1952 /// body) and popped on leaving it.
1953 shadow_scopes: Vec<ShadowScope>,
1954 /// Monotonic counter for generating fresh shadow-alias names
1955 /// (`{name}__s{N}`), unique per emission context.
1956 shadow_counter: usize,
1957 /// Names to seed into the *next* shadow frame pushed by
1958 /// [`Self::emit_block_body`] — used to put a function/method's parameters in
1959 /// the same frame as its body block, so a body-level `let` re-binding a param
1960 /// is a plain Python rebind (the idiom) while a *nested*-block `let`
1961 /// shadowing the param is renamed. Drained (cleared) on the next push.
1962 pending_scope_seed: Vec<String>,
1963 /// `true` while emitting the **immediate** body of a `for`/`while`/`loop`
1964 /// (set by [`Self::emit_loop_body`]). A loop body is statement position: its
1965 /// tail expression is *discarded* (a Bock loop evaluates to Unit), so
1966 /// [`Self::emit_block_body_inner`] must emit the tail as a bare expression
1967 /// statement (`<value>`) rather than a function-body `return <value>` — a
1968 /// `return` inside a loop aborts the enclosing function after one iteration
1969 /// (the fizzbuzz / inventory-system truncation). Saved/restored around the
1970 /// loop body and cleared while emitting any *nested* value context (a
1971 /// value-binding hoist, a value-`if`/`match` arm), so the discard applies
1972 /// only to the loop's own tail and never leaks into a value position. A
1973 /// `break v` value still flows through the separate `loop_value_targets`
1974 /// stack, not this flag.
1975 in_loop_body_tail: bool,
1976 /// `true` while emitting the arm/branch bodies of a **statement-position**
1977 /// control-flow construct — a `match` or an `if`/`else` that sits mid-block
1978 /// as a side-effecting statement, not as the block/function tail nor a
1979 /// value-binding RHS (set by [`Self::emit_stmt`]'s `Match` and `If` arms).
1980 /// Like [`Self::in_loop_body_tail`], such a construct evaluates to Unit:
1981 /// each arm's/branch's tail expression is *discarded*, so
1982 /// [`Self::emit_block_body_inner`] must emit it as a bare expression statement
1983 /// (`<value>`) rather than a function-body `return <value>`. Emitting `return`
1984 /// here aborts the enclosing function after the matched arm/taken branch runs
1985 /// — the chat-protocol truncation, where `match decoded { Ok(m) =>
1986 /// println(..) … }` returned out of `main` after the first arm instead of
1987 /// falling through to the rest of the body (#259), and its `if`/`else`
1988 /// sibling (Q-python-ifelse-truncation), where `if c { println(..) } else {
1989 /// println(..) }` returned out after either branch. Saved/restored around
1990 /// the arm/branch bodies and cleared while emitting any *nested* value
1991 /// context (a nested `fn`/method body, a value-binding hoist), so the
1992 /// discard applies only to the statement construct's own tails and never
1993 /// leaks into a value position.
1994 in_stmt_construct_arm: bool,
1995}
1996
1997impl PyEmitCtx {
1998 fn new() -> Self {
1999 Self {
2000 buf: String::with_capacity(4096),
2001 indent: 0,
2002 needs_dataclass_import: false,
2003 needs_abc_import: false,
2004 needs_asyncio_import: false,
2005 needs_time_import: false,
2006 needs_math_import: false,
2007 task_bound_names: std::collections::HashSet::new(),
2008 effect_ops: HashMap::new(),
2009 current_handler_vars: HashMap::new(),
2010 fn_effects: HashMap::new(),
2011 composite_effects: HashMap::new(),
2012 handling_counter: 0,
2013 impls_by_target: HashMap::new(),
2014 optional_runtime_emitted: false,
2015 result_runtime_emitted: false,
2016 ordering_runtime_emitted: false,
2017 concurrency_runtime_emitted: false,
2018 list_functional_runtime_emitted: false,
2019 list_mutator_runtime_emitted: false,
2020 propagate_runtime_emitted: false,
2021 str_runtime_emitted: false,
2022 needs_union_import: false,
2023 needs_typing_callable: Cell::new(false),
2024 needs_typing_any: Cell::new(false),
2025 needs_typing_self: Cell::new(false),
2026 needs_typing_never: Cell::new(false),
2027 needs_typing_typevar: Cell::new(false),
2028 emitted_typevars: std::collections::HashSet::new(),
2029 enum_variants: crate::generator::EnumVariantRegistry::new(),
2030 trait_decls: crate::generator::TraitDeclRegistry::new(),
2031 per_module: false,
2032 needs_runtime_optional: false,
2033 needs_runtime_result: false,
2034 needs_runtime_ordering: false,
2035 needs_runtime_concurrency: false,
2036 needs_runtime_list_functional: false,
2037 needs_runtime_list_mutators: false,
2038 needs_runtime_propagate: false,
2039 needs_runtime_str: false,
2040 implicit_imports: Vec::new(),
2041 field_method_collisions: std::collections::HashSet::new(),
2042 const_names: std::collections::HashSet::new(),
2043 is_entry_module: false,
2044 loop_value_targets: Vec::new(),
2045 shadow_scopes: Vec::new(),
2046 shadow_counter: 0,
2047 pending_scope_seed: Vec::new(),
2048 in_loop_body_tail: false,
2049 in_stmt_construct_arm: false,
2050 }
2051 }
2052
2053 /// The Python method name for a Bock method, disambiguated against the
2054 /// program's field names so a method whose snake_cased name collides with a
2055 /// field gets a `_method` suffix (`message` → `message_method`). Applied
2056 /// identically at the method definition and every call site (shared policy
2057 /// with go/js/ts — see [`crate::generator::disambiguate_method_name`]).
2058 fn py_method_name(&self, name: &str) -> String {
2059 // A method/associated-fn whose snake-cased name is a Python *keyword*
2060 // (e.g. a `From` impl's `from`) cannot be a `def` name or an attribute
2061 // access — `def from()` and `Type.from(...)` are both syntax errors. Such
2062 // names are escaped with a trailing `_` (`from` → `from_`), applied
2063 // identically at the definition and every call site. Ordinary member
2064 // names (`default`, etc.) are legal Python attributes and are not
2065 // escaped; only true keywords are.
2066 let snake = to_snake_case(name);
2067 let escaped =
2068 if crate::generator::is_target_keyword(&snake, crate::generator::KeywordTarget::Python)
2069 {
2070 format!("{snake}_")
2071 } else {
2072 snake
2073 };
2074 crate::generator::disambiguate_method_name(
2075 escaped,
2076 &self.field_method_collisions,
2077 "_method",
2078 )
2079 }
2080
2081 fn finish(mut self) -> String {
2082 // An empty module emits nothing at all — not even a preamble (an empty
2083 // `.py` is the expected output, and a bare `from __future__` import on
2084 // its own would be surprising noise).
2085 if self.buf.is_empty() {
2086 return self.buf;
2087 }
2088 let mut preamble = String::new();
2089 // PEP 563: defer evaluation of every annotation to a string. A method
2090 // declared inside a class body that annotates a parameter with the class
2091 // itself — `class Tag: def equals(self, other: Tag)`, emitted for an
2092 // `impl Eq for Tag` whose `other: Self` resolves to `Tag` — references a
2093 // name that is not yet bound while the class body executes, raising
2094 // `NameError` at import time. `from __future__ import annotations` makes
2095 // all annotations lazy strings, so such (and any other) forward
2096 // references never evaluate eagerly. It must be the first statement in
2097 // the module, so it is prepended ahead of every other import.
2098 preamble.push_str("from __future__ import annotations\n");
2099 // Windows UTF-8 stdout (entry module only). On Windows, Python's stdout
2100 // defaults to the locale codepage, so a unicode `print` (`✓`, `→`, CJK,
2101 // …) raises `UnicodeEncodeError` or mojibakes. Reconfiguring stdout/stderr
2102 // to UTF-8 (py3.7+ `TextIOWrapper.reconfigure`) makes output consistent
2103 // with the POSIX targets. It is a process-global side effect, so it is
2104 // emitted only at the single program entry, never in imported modules.
2105 // The `getattr` guard keeps it a no-op when the stream is not a
2106 // reconfigurable `TextIOWrapper` (e.g. already wrapped/redirected).
2107 if self.is_entry_module {
2108 preamble.push_str(
2109 "import sys as _sys\n\
2110 if hasattr(_sys.stdout, \"reconfigure\"):\n \
2111 _sys.stdout.reconfigure(encoding=\"utf-8\")\n\
2112 if hasattr(_sys.stderr, \"reconfigure\"):\n \
2113 _sys.stderr.reconfigure(encoding=\"utf-8\")\n",
2114 );
2115 }
2116 // Per-module native-import path: pull the runtime-prelude names this
2117 // module references from the shared `_bock_runtime` module so the
2118 // tagged runtime classes are shared (and `isinstance`-compatible)
2119 // across every emitted file (see `RUNTIME_MODULE_PY`). A `*` import
2120 // is intentional — the runtime exposes a small, fixed, underscore-
2121 // prefixed surface and the exact set of referenced names varies with
2122 // how each prelude is used (constructors, singletons, match classes).
2123 if self.per_module
2124 && (self.needs_runtime_optional
2125 || self.needs_runtime_result
2126 || self.needs_runtime_ordering
2127 || self.needs_runtime_concurrency
2128 || self.needs_runtime_list_functional
2129 || self.needs_runtime_list_mutators
2130 || self.needs_runtime_propagate
2131 || self.needs_runtime_str)
2132 {
2133 let _ = writeln!(preamble, "from {RUNTIME_MODULE_PY} import *");
2134 }
2135 if self.needs_asyncio_import {
2136 preamble.push_str("import asyncio\n");
2137 }
2138 if self.needs_time_import {
2139 preamble.push_str("import time\n");
2140 }
2141 if self.needs_math_import {
2142 preamble.push_str("import math\n");
2143 }
2144 // Merge every `typing` need into one `from typing import …` line so a
2145 // module that uses, e.g., both a `Callable` annotation and a generic
2146 // type does not emit two separate (potentially conflicting) imports.
2147 let mut typing_names: Vec<&str> = Vec::new();
2148 if self.needs_union_import {
2149 typing_names.push("Union");
2150 }
2151 if self.needs_typing_callable.get() {
2152 typing_names.push("Callable");
2153 }
2154 if self.needs_typing_any.get() {
2155 typing_names.push("Any");
2156 }
2157 if self.needs_typing_self.get() {
2158 typing_names.push("Self");
2159 }
2160 if self.needs_typing_never.get() {
2161 typing_names.push("Never");
2162 }
2163 if self.needs_typing_typevar.get() {
2164 // `Generic` is always paired with `TypeVar`: a generic class lists
2165 // `Generic[T, …]` in its bases and `T` is a `TypeVar`.
2166 typing_names.push("TypeVar");
2167 typing_names.push("Generic");
2168 }
2169 if !typing_names.is_empty() {
2170 // Stable, de-duplicated ordering for deterministic output.
2171 typing_names.sort_unstable();
2172 typing_names.dedup();
2173 preamble.push_str(&format!("from typing import {}\n", typing_names.join(", ")));
2174 }
2175 if self.needs_abc_import {
2176 preamble.push_str("from abc import ABC, abstractmethod\n");
2177 }
2178 if self.needs_dataclass_import {
2179 preamble.push_str("from dataclasses import dataclass\n");
2180 }
2181 if !preamble.is_empty() {
2182 preamble.push('\n');
2183 self.buf.insert_str(0, &preamble);
2184 }
2185 self.buf
2186 }
2187
2188 /// Emit a `@test` function body (S7) into `out`, lowering `expect(...)`
2189 /// assertion chains to pytest-style `assert` (or `self.assert*` for
2190 /// unittest) and other statements to plain expression/`=` statements.
2191 ///
2192 /// `use_self` selects the unittest idiom (`self.assertEqual(a, e)`); `indent`
2193 /// is the base indentation level (1 for a module-level `def`, 2 for a method
2194 /// inside a `TestCase`). A body with no statements emits `pass`.
2195 fn emit_py_test_body(
2196 &mut self,
2197 body: &AIRNode,
2198 use_self: bool,
2199 indent: usize,
2200 out: &mut String,
2201 ) -> Result<(), CodegenError> {
2202 let pad = " ".repeat(indent);
2203 let stmts: Vec<&AIRNode> = match &body.kind {
2204 NodeKind::Block { stmts, tail } => stmts.iter().chain(tail.as_deref()).collect(),
2205 _ => vec![body],
2206 };
2207 let mut emitted_any = false;
2208 for stmt in stmts {
2209 emitted_any = true;
2210 if let Some((assertion, actual, expected)) = crate::generator::classify_assertion(stmt)
2211 {
2212 let a = self.expr_to_string(actual)?;
2213 use crate::generator::TestAssertion as T;
2214 let line = if use_self {
2215 match assertion {
2216 T::Equal => {
2217 let e = match expected {
2218 Some(e) => self.expr_to_string(e)?,
2219 None => "None".to_string(),
2220 };
2221 format!("self.assertEqual({a}, {e})")
2222 }
2223 T::BeTrue => format!("self.assertTrue({a})"),
2224 T::BeFalse => format!("self.assertFalse({a})"),
2225 T::BeSome => format!("self.assertIsInstance({a}, _BockSome)"),
2226 T::BeNone => format!("self.assertIsInstance({a}, _BockNone)"),
2227 T::BeOk => format!("self.assertIsInstance({a}, _BockOk)"),
2228 T::BeErr => format!("self.assertIsInstance({a}, _BockErr)"),
2229 }
2230 } else {
2231 match assertion {
2232 T::Equal => {
2233 let e = match expected {
2234 Some(e) => self.expr_to_string(e)?,
2235 None => "None".to_string(),
2236 };
2237 format!("assert ({a}) == ({e})")
2238 }
2239 T::BeTrue => format!("assert ({a}) is True"),
2240 T::BeFalse => format!("assert ({a}) is False"),
2241 T::BeSome => format!("assert isinstance({a}, _BockSome)"),
2242 T::BeNone => format!("assert isinstance({a}, _BockNone)"),
2243 T::BeOk => format!("assert isinstance({a}, _BockOk)"),
2244 T::BeErr => format!("assert isinstance({a}, _BockErr)"),
2245 }
2246 };
2247 out.push_str(&format!("{pad}{line}\n"));
2248 } else if let NodeKind::LetBinding { pattern, value, .. } = &stmt.kind {
2249 let name = match &pattern.kind {
2250 NodeKind::BindPat { name, .. } => to_snake_case(&name.name),
2251 _ => {
2252 emitted_any = false;
2253 continue;
2254 }
2255 };
2256 let v = self.expr_to_string(value)?;
2257 out.push_str(&format!("{pad}{name} = {v}\n"));
2258 } else {
2259 let s = self.expr_to_string(stmt)?;
2260 out.push_str(&format!("{pad}{s}\n"));
2261 }
2262 }
2263 if !emitted_any {
2264 out.push_str(&format!("{pad}pass\n"));
2265 }
2266 Ok(())
2267 }
2268
2269 /// Pre-seed the effect registries (`effect_ops`, `composite_effects`) from
2270 /// every module's top-level `EffectDecl`s. In the per-module path each
2271 /// module is emitted by its own context, so a bare op `log(...)` used in
2272 /// `main` whose effect `Log` is declared in another module would not be
2273 /// recognised as an effect op (and not rewritten to `handler.log(...)`)
2274 /// without pre-seeding from the whole reachable set. Mirrors how
2275 /// `enum_variants` / `trait_decls` are collected across the reached modules.
2276 fn seed_effect_registries(&mut self, modules: &[(&AIRModule, &std::path::Path)]) {
2277 for (module, _) in modules {
2278 let NodeKind::Module { items, .. } = &module.kind else {
2279 continue;
2280 };
2281 for item in items {
2282 let NodeKind::EffectDecl {
2283 name,
2284 components,
2285 operations,
2286 ..
2287 } = &item.kind
2288 else {
2289 continue;
2290 };
2291 if !components.is_empty() {
2292 let comp_names: Vec<String> = components
2293 .iter()
2294 .map(|tp| {
2295 tp.segments
2296 .last()
2297 .map_or("effect".to_string(), |s| s.name.clone())
2298 })
2299 .collect();
2300 self.composite_effects.insert(name.name.clone(), comp_names);
2301 continue;
2302 }
2303 for op in operations {
2304 if let NodeKind::FnDecl { name: op_name, .. } = &op.kind {
2305 self.effect_ops
2306 .insert(op_name.name.clone(), name.name.clone());
2307 }
2308 }
2309 }
2310 }
2311 }
2312
2313 /// Variant info for `path` when its last segment is a registered *user*
2314 /// enum variant (built-in Optional/Result pre-seeds excluded — those go
2315 /// through the bespoke `_BockSome`/`_BockNone` lowering).
2316 fn user_variant_for_path(
2317 &self,
2318 path: &bock_ast::TypePath,
2319 ) -> Option<&crate::generator::EnumVariantInfo> {
2320 let info = crate::generator::registered_variant(&self.enum_variants, path)?;
2321 if matches!(info.enum_name.as_str(), "Optional" | "Result") {
2322 return None;
2323 }
2324 Some(info)
2325 }
2326
2327 /// As [`Self::user_variant_for_path`] but keyed by a bare identifier name.
2328 fn user_variant_for_name(&self, name: &str) -> Option<&crate::generator::EnumVariantInfo> {
2329 let info = self.enum_variants.get(name)?;
2330 if matches!(info.enum_name.as_str(), "Optional" | "Result") {
2331 return None;
2332 }
2333 Some(info)
2334 }
2335
2336 fn indent_str(&self) -> String {
2337 " ".repeat(self.indent)
2338 }
2339
2340 fn write_indent(&mut self) {
2341 let indent = self.indent_str();
2342 self.buf.push_str(&indent);
2343 }
2344
2345 fn writeln(&mut self, s: &str) {
2346 self.write_indent();
2347 self.buf.push_str(s);
2348 self.buf.push('\n');
2349 }
2350
2351 // ── Prelude function mapping ──────────────────────────────────────────
2352
2353 /// Emit an expression into a temporary buffer and return the string.
2354 fn expr_to_string(&mut self, node: &AIRNode) -> Result<String, CodegenError> {
2355 let start = self.buf.len();
2356 self.emit_expr(node)?;
2357 let s = self.buf[start..].to_string();
2358 self.buf.truncate(start);
2359 Ok(s)
2360 }
2361
2362 /// Render a pattern node to a Python `case` sub-pattern string by running
2363 /// [`Self::emit_pattern`] against a scratch slice of the buffer. Lets a
2364 /// constructor / record field embed a *nested* sub-pattern (`_BockSome(x)`,
2365 /// `_BockOk(v)`, a nested tuple) instead of a flat binding name — the fix for
2366 /// `Some(Ok(v))` losing its inner bindings.
2367 fn pattern_to_py(&mut self, pat: &AIRNode) -> Result<String, CodegenError> {
2368 let start = self.buf.len();
2369 self.emit_pattern(pat)?;
2370 let s = self.buf[start..].to_string();
2371 self.buf.truncate(start);
2372 Ok(s)
2373 }
2374
2375 /// Map Bock prelude functions to Python equivalents.
2376 fn map_prelude_call(
2377 &mut self,
2378 callee: &AIRNode,
2379 args: &[bock_air::AirArg],
2380 ) -> Result<Option<String>, CodegenError> {
2381 let name = match &callee.kind {
2382 NodeKind::Identifier { name } => name.name.as_str(),
2383 _ => return Ok(None),
2384 };
2385 let arg_strs: Vec<String> = args
2386 .iter()
2387 .map(|a| self.expr_to_string(&a.value))
2388 .collect::<Result<_, _>>()?;
2389 let code = match name {
2390 "println" => {
2391 let a = arg_strs.first().map_or(String::new(), |s| s.clone());
2392 format!("print({a})")
2393 }
2394 "print" => {
2395 let a = arg_strs.first().map_or(String::new(), |s| s.clone());
2396 format!("print({a}, end=\"\")")
2397 }
2398 "debug" => {
2399 let a = arg_strs.first().map_or(String::new(), |s| s.clone());
2400 format!("print(repr({a}))")
2401 }
2402 "assert" => {
2403 let a = arg_strs.first().map_or(String::new(), |s| s.clone());
2404 format!("assert {a}")
2405 }
2406 "todo" => "raise NotImplementedError()".to_string(),
2407 "unreachable" => "raise RuntimeError(\"unreachable\")".to_string(),
2408 // Optional `Some(x)` constructor → tagged runtime value (see
2409 // `OPTIONAL_RUNTIME_PY`). `None` is not a call; it lowers in the
2410 // `Identifier` arm to the `_bock_none` singleton.
2411 "Some" => {
2412 let a = arg_strs.first().map_or(String::new(), |s| s.clone());
2413 format!("_BockSome({a})")
2414 }
2415 // Result `Ok(x)` / `Err(e)` constructors → tagged runtime values
2416 // (see `RESULT_RUNTIME_PY`), mirroring the `Some` handling above so
2417 // construction agrees with the `case _BockOk(..)` / `_BockErr(..)`
2418 // match arms.
2419 "Ok" => {
2420 let a = arg_strs.first().map_or(String::new(), |s| s.clone());
2421 format!("_BockOk({a})")
2422 }
2423 "Err" => {
2424 let a = arg_strs.first().map_or(String::new(), |s| s.clone());
2425 format!("_BockErr({a})")
2426 }
2427 "sleep" => {
2428 let a = arg_strs.first().map_or(String::new(), |s| s.clone());
2429 // Route through an installed `Clock` handler if one is in scope;
2430 // otherwise fall through to the host primitive (default).
2431 if let Some(handler) = self.clock_handler_var() {
2432 format!("{handler}.{}({a})", to_snake_case("sleep"))
2433 } else {
2434 self.needs_asyncio_import = true;
2435 // Duration is ns → asyncio.sleep takes seconds.
2436 format!("asyncio.sleep(({a}) / 1_000_000_000)")
2437 }
2438 }
2439 _ => return Ok(None),
2440 };
2441 Ok(Some(code))
2442 }
2443
2444 /// Emit a built-in `Optional`/`Result` method call to its Python form.
2445 ///
2446 /// Recognised via the checker's `recv_kind` annotation
2447 /// ([`crate::generator::desugared_optional_method`] /
2448 /// [`crate::generator::desugared_result_method`]) so the overloaded names
2449 /// (`unwrap`/`unwrap_or`/`map`) dispatch to the right `isinstance` test on the
2450 /// tagged runtime classes (`_BockSome`/`_BockOk` carry the payload as `._0`).
2451 /// The receiver is bound once in a `lambda` so it is evaluated exactly once.
2452 /// Returns `true` if handled.
2453 fn try_emit_container_method(
2454 &mut self,
2455 node: &AIRNode,
2456 callee: &AIRNode,
2457 args: &[bock_air::AirArg],
2458 ) -> Result<bool, CodegenError> {
2459 if let Some((recv, method, rest)) =
2460 crate::generator::desugared_optional_method(node, callee, args)
2461 {
2462 // Optional: present = `_BockSome`; the "map" reconstruction also uses
2463 // `_BockSome` for the present case and the receiver `__c` (a
2464 // `_bock_none`) for the empty case.
2465 self.emit_tagged_container_method(recv, method, rest, "_BockSome", "_BockSome")?;
2466 return Ok(true);
2467 }
2468 if let Some((recv, method, rest)) =
2469 crate::generator::desugared_result_method(node, callee, args)
2470 {
2471 self.emit_tagged_container_method(recv, method, rest, "_BockOk", "_BockErr")?;
2472 return Ok(true);
2473 }
2474 Ok(false)
2475 }
2476
2477 /// Lower a tagged-container method on `recv`. `present_cls` is the
2478 /// payload-carrying runtime class (`_BockSome`/`_BockOk`); `err_cls` is the
2479 /// other class (`_BockNone` for Optional — unused as a constructor since the
2480 /// empty case passes the receiver through; `_BockErr` for Result, used by
2481 /// `map_err`).
2482 fn emit_tagged_container_method(
2483 &mut self,
2484 recv: &AIRNode,
2485 method: &str,
2486 rest: &[bock_air::AirArg],
2487 present_cls: &str,
2488 err_cls: &str,
2489 ) -> Result<(), CodegenError> {
2490 // Tag tests read the receiver once → emit inline.
2491 match method {
2492 "is_some" | "is_ok" => {
2493 self.buf.push_str("isinstance(");
2494 self.emit_expr(recv)?;
2495 let _ = write!(self.buf, ", {present_cls})");
2496 return Ok(());
2497 }
2498 "is_none" | "is_err" => {
2499 self.buf.push_str("(not isinstance(");
2500 self.emit_expr(recv)?;
2501 let _ = write!(self.buf, ", {present_cls}))");
2502 return Ok(());
2503 }
2504 _ => {}
2505 }
2506 self.buf.push_str("(lambda __c: ");
2507 match method {
2508 "unwrap" => {
2509 let _ = write!(
2510 self.buf,
2511 "__c._0 if isinstance(__c, {present_cls}) else None"
2512 );
2513 }
2514 "unwrap_or" => {
2515 let _ = write!(self.buf, "__c._0 if isinstance(__c, {present_cls}) else (");
2516 if let Some(d) = rest.first() {
2517 self.emit_expr(&d.value)?;
2518 } else {
2519 self.buf.push_str("None");
2520 }
2521 self.buf.push(')');
2522 }
2523 "map" => {
2524 let _ = write!(self.buf, "{present_cls}((");
2525 if let Some(f) = rest.first() {
2526 self.emit_expr(&f.value)?;
2527 } else {
2528 self.buf.push_str("lambda x: x");
2529 }
2530 let _ = write!(
2531 self.buf,
2532 ")(__c._0)) if isinstance(__c, {present_cls}) else __c"
2533 );
2534 }
2535 "flat_map" => {
2536 let _ = write!(self.buf, "(");
2537 if let Some(f) = rest.first() {
2538 self.emit_expr(&f.value)?;
2539 } else {
2540 self.buf.push_str("lambda x: x");
2541 }
2542 let _ = write!(
2543 self.buf,
2544 ")(__c._0) if isinstance(__c, {present_cls}) else __c"
2545 );
2546 }
2547 "map_err" => {
2548 let _ = write!(self.buf, "{err_cls}((");
2549 if let Some(f) = rest.first() {
2550 self.emit_expr(&f.value)?;
2551 } else {
2552 self.buf.push_str("lambda x: x");
2553 }
2554 let _ = write!(
2555 self.buf,
2556 ")(__c._0)) if isinstance(__c, {err_cls}) else __c"
2557 );
2558 }
2559 _ => self.buf.push_str("None"),
2560 }
2561 self.buf.push_str(")(");
2562 self.emit_expr(recv)?;
2563 self.buf.push(')');
2564 Ok(())
2565 }
2566
2567 /// Emit a read-only `List` built-in method call to its Python form.
2568 ///
2569 /// Python lists are native, so `len`/`is_empty`/`contains`/`concat` map to
2570 /// `len(r)`/`(len(r) == 0)`/`(x in r)`/`(r + o)`. `Optional`-returning
2571 /// methods (`get`/`first`/`last`/`index_of`) build the tagged Optional
2572 /// runtime values (`_BockSome(v)` / `_bock_none`); they wrap the receiver in
2573 /// a `lambda` so it is evaluated exactly once.
2574 fn try_emit_list_method(
2575 &mut self,
2576 node: &AIRNode,
2577 callee: &AIRNode,
2578 args: &[bock_air::AirArg],
2579 ) -> Result<bool, CodegenError> {
2580 let Some((recv, method, rest)) =
2581 crate::generator::desugared_list_method(node, callee, args)
2582 else {
2583 return Ok(false);
2584 };
2585 match method {
2586 "len" | "length" | "count" => {
2587 self.buf.push_str("len(");
2588 self.emit_expr(recv)?;
2589 self.buf.push(')');
2590 }
2591 "is_empty" => {
2592 self.buf.push_str("(len(");
2593 self.emit_expr(recv)?;
2594 self.buf.push_str(") == 0)");
2595 }
2596 "get" => {
2597 let Some(idx) = rest.first() else {
2598 return Ok(false);
2599 };
2600 self.buf
2601 .push_str("(lambda __r, __i: _BockSome(__r[__i]) if 0 <= __i < len(__r) else _bock_none)(");
2602 self.emit_expr(recv)?;
2603 self.buf.push_str(", ");
2604 self.emit_expr(&idx.value)?;
2605 self.buf.push(')');
2606 }
2607 "first" => {
2608 self.buf
2609 .push_str("(lambda __r: _BockSome(__r[0]) if len(__r) > 0 else _bock_none)(");
2610 self.emit_expr(recv)?;
2611 self.buf.push(')');
2612 }
2613 "last" => {
2614 self.buf
2615 .push_str("(lambda __r: _BockSome(__r[-1]) if len(__r) > 0 else _bock_none)(");
2616 self.emit_expr(recv)?;
2617 self.buf.push(')');
2618 }
2619 "contains" => {
2620 let Some(x) = rest.first() else {
2621 return Ok(false);
2622 };
2623 self.buf.push('(');
2624 self.emit_expr(&x.value)?;
2625 self.buf.push_str(" in ");
2626 self.emit_expr(recv)?;
2627 self.buf.push(')');
2628 }
2629 "index_of" => {
2630 let Some(x) = rest.first() else {
2631 return Ok(false);
2632 };
2633 self.buf.push_str(
2634 "(lambda __r, __x: _BockSome(__r.index(__x)) if __x in __r else _bock_none)(",
2635 );
2636 self.emit_expr(recv)?;
2637 self.buf.push_str(", ");
2638 self.emit_expr(&x.value)?;
2639 self.buf.push(')');
2640 }
2641 "concat" => {
2642 let Some(o) = rest.first() else {
2643 return Ok(false);
2644 };
2645 self.buf.push('(');
2646 self.emit_expr(recv)?;
2647 self.buf.push_str(" + ");
2648 self.emit_expr(&o.value)?;
2649 self.buf.push(')');
2650 }
2651 "join" => {
2652 let Some(sep) = rest.first() else {
2653 return Ok(false);
2654 };
2655 self.buf.push('(');
2656 self.emit_expr(&sep.value)?;
2657 self.buf.push_str(").join(");
2658 self.emit_expr(recv)?;
2659 self.buf.push(')');
2660 }
2661 _ => return Ok(false),
2662 }
2663 Ok(true)
2664 }
2665
2666 /// Emit an in-place `List` mutator (`push`/`append`, DQ18) to its Python
2667 /// form.
2668 ///
2669 /// Recognised via [`crate::generator::desugared_list_mutating_method`].
2670 /// Python lists grow in place with `.append(x)`, so `recv.push(x)` lowers to
2671 /// `(recv).append(x)`. The checker types these as `Void`, so they appear in
2672 /// statement position (Python's `list.append` returns `None`); the receiver
2673 /// is a `mut` lvalue (ownership-enforced), evaluated once.
2674 fn try_emit_list_mutating_method(
2675 &mut self,
2676 node: &AIRNode,
2677 callee: &AIRNode,
2678 args: &[bock_air::AirArg],
2679 ) -> Result<bool, CodegenError> {
2680 let Some((recv, _method, rest)) =
2681 crate::generator::desugared_list_mutating_method(node, callee, args)
2682 else {
2683 return Ok(false);
2684 };
2685 let Some(x) = rest.first() else {
2686 return Ok(false);
2687 };
2688 self.buf.push('(');
2689 self.emit_expr(recv)?;
2690 self.buf.push_str(").append(");
2691 self.emit_expr(&x.value)?;
2692 self.buf.push(')');
2693 Ok(true)
2694 }
2695
2696 /// Emit a DQ30 in-place `List` mutator
2697 /// (`pop`/`remove_at`/`insert`/`reverse`/`set`) to its Python form.
2698 ///
2699 /// Recognised via [`crate::generator::desugared_list_inplace_mutator`].
2700 /// Python lists mutate in place natively, and a `lambda` parameter aliases
2701 /// the receiver, so each lowering is a single-evaluation conditional
2702 /// expression:
2703 ///
2704 /// - `pop` → `(lambda __r: _BockSome(__r.pop()) if len(__r) > 0 else
2705 /// _bock_none)(recv)` — emptiness is `None`, never an abort;
2706 /// - `remove_at(i)` → pre-check + `__r.pop(__i)` — the `0 <= __i` half is
2707 /// load-bearing (native `pop(-1)` would remove from the end);
2708 /// - `insert(i, x)` → pre-check (`0 <= __i <= len`) + `__r.insert(__i,
2709 /// __x)` — the pre-check is REQUIRED: native `insert` clamps
2710 /// out-of-range indices instead of failing;
2711 /// - `reverse` → native in-place `(recv).reverse()`;
2712 /// - `set(i, x)` → pre-check + `__r.__setitem__(__i, __x)` (the statement
2713 /// form `__r[__i] = __x` is not an expression; the dunder call is) —
2714 /// `0 <= __i` again excludes Python's negative indexing.
2715 ///
2716 /// The violated-contract branches call the raising helper
2717 /// `_bock_list_abort(op, i, len)` ([`LIST_MUTATOR_RUNTIME_PY`]), producing
2718 /// the normalized abort message `List.<op>: index <i> out of bounds
2719 /// (len <n>)`.
2720 fn try_emit_list_inplace_mutator(
2721 &mut self,
2722 node: &AIRNode,
2723 callee: &AIRNode,
2724 args: &[bock_air::AirArg],
2725 ) -> Result<bool, CodegenError> {
2726 let Some((recv, method, rest)) =
2727 crate::generator::desugared_list_inplace_mutator(node, callee, args)
2728 else {
2729 return Ok(false);
2730 };
2731 match method {
2732 "pop" => {
2733 self.buf.push_str(
2734 "(lambda __r: _BockSome(__r.pop()) if len(__r) > 0 else _bock_none)(",
2735 );
2736 self.emit_expr(recv)?;
2737 self.buf.push(')');
2738 }
2739 "remove_at" => {
2740 let Some(idx) = rest.first() else {
2741 return Ok(false);
2742 };
2743 self.buf.push_str(
2744 "(lambda __r, __i: __r.pop(__i) if 0 <= __i < len(__r) \
2745 else _bock_list_abort('remove_at', __i, len(__r)))(",
2746 );
2747 self.emit_expr(recv)?;
2748 self.buf.push_str(", ");
2749 self.emit_expr(&idx.value)?;
2750 self.buf.push(')');
2751 }
2752 "insert" => {
2753 let (Some(idx), Some(x)) = (rest.first(), rest.get(1)) else {
2754 return Ok(false);
2755 };
2756 self.buf.push_str(
2757 "(lambda __r, __i, __x: __r.insert(__i, __x) if 0 <= __i <= len(__r) \
2758 else _bock_list_abort('insert', __i, len(__r)))(",
2759 );
2760 self.emit_expr(recv)?;
2761 self.buf.push_str(", ");
2762 self.emit_expr(&idx.value)?;
2763 self.buf.push_str(", ");
2764 self.emit_expr(&x.value)?;
2765 self.buf.push(')');
2766 }
2767 "reverse" => {
2768 self.buf.push('(');
2769 self.emit_expr(recv)?;
2770 self.buf.push_str(").reverse()");
2771 }
2772 "set" => {
2773 let (Some(idx), Some(x)) = (rest.first(), rest.get(1)) else {
2774 return Ok(false);
2775 };
2776 self.buf.push_str(
2777 "(lambda __r, __i, __x: __r.__setitem__(__i, __x) if 0 <= __i < len(__r) \
2778 else _bock_list_abort('set', __i, len(__r)))(",
2779 );
2780 self.emit_expr(recv)?;
2781 self.buf.push_str(", ");
2782 self.emit_expr(&idx.value)?;
2783 self.buf.push_str(", ");
2784 self.emit_expr(&x.value)?;
2785 self.buf.push(')');
2786 }
2787 _ => return Ok(false),
2788 }
2789 Ok(true)
2790 }
2791
2792 /// Emit a functional (closure-taking) `List` built-in method call to its
2793 /// Python form.
2794 ///
2795 /// Recognised via [`crate::generator::desugared_list_functional_method`].
2796 /// `map`/`filter` lower to `list(map(cb, r))` / `list(filter(cb, r))`;
2797 /// `any`/`all` to the `any(...)`/`all(...)` builtins over `map`; `flat_map`
2798 /// to a nested comprehension. `reduce`/`fold`/`find`/`for_each` lower to the
2799 /// `_bock_*` helpers of [`LIST_FUNCTIONAL_RUNTIME_PY`] (a left fold and the
2800 /// tagged-`Optional` `find` cannot be expressed as a single Python builtin
2801 /// call). In all cases the closure is passed *once* — the desugared
2802 /// `recv.map(recv, cb)` shape the generic fall-through would emit fails on a
2803 /// `list` (`'list' object has no attribute 'map'`).
2804 fn try_emit_list_functional_method(
2805 &mut self,
2806 node: &AIRNode,
2807 callee: &AIRNode,
2808 args: &[bock_air::AirArg],
2809 ) -> Result<bool, CodegenError> {
2810 let Some((recv, method, rest)) =
2811 crate::generator::desugared_list_functional_method(node, callee, args)
2812 else {
2813 return Ok(false);
2814 };
2815 match method {
2816 "map" | "filter" => {
2817 let Some(cb) = rest.first() else {
2818 return Ok(false);
2819 };
2820 let _ = write!(self.buf, "list({method}(");
2821 self.emit_expr(&cb.value)?;
2822 self.buf.push_str(", ");
2823 self.emit_expr(recv)?;
2824 self.buf.push_str("))");
2825 }
2826 "any" | "all" => {
2827 let Some(cb) = rest.first() else {
2828 return Ok(false);
2829 };
2830 let _ = write!(self.buf, "{method}(map(");
2831 self.emit_expr(&cb.value)?;
2832 self.buf.push_str(", ");
2833 self.emit_expr(recv)?;
2834 self.buf.push_str("))");
2835 }
2836 "flat_map" => {
2837 let Some(cb) = rest.first() else {
2838 return Ok(false);
2839 };
2840 self.buf.push_str("[__y for __x in ");
2841 self.emit_expr(recv)?;
2842 self.buf.push_str(" for __y in (");
2843 self.emit_expr(&cb.value)?;
2844 self.buf.push_str(")(__x)]");
2845 }
2846 "reduce" => {
2847 let Some(cb) = rest.first() else {
2848 return Ok(false);
2849 };
2850 self.buf.push_str("_bock_reduce(");
2851 self.emit_expr(recv)?;
2852 self.buf.push_str(", ");
2853 self.emit_expr(&cb.value)?;
2854 self.buf.push(')');
2855 }
2856 "fold" => {
2857 let (Some(init), Some(cb)) = (rest.first(), rest.get(1)) else {
2858 return Ok(false);
2859 };
2860 self.buf.push_str("_bock_fold(");
2861 self.emit_expr(recv)?;
2862 self.buf.push_str(", ");
2863 self.emit_expr(&init.value)?;
2864 self.buf.push_str(", ");
2865 self.emit_expr(&cb.value)?;
2866 self.buf.push(')');
2867 }
2868 "find" => {
2869 let Some(cb) = rest.first() else {
2870 return Ok(false);
2871 };
2872 self.buf.push_str("_bock_find(");
2873 self.emit_expr(recv)?;
2874 self.buf.push_str(", ");
2875 self.emit_expr(&cb.value)?;
2876 self.buf.push(')');
2877 }
2878 "for_each" => {
2879 let Some(cb) = rest.first() else {
2880 return Ok(false);
2881 };
2882 self.buf.push_str("_bock_for_each(");
2883 self.emit_expr(recv)?;
2884 self.buf.push_str(", ");
2885 self.emit_expr(&cb.value)?;
2886 self.buf.push(')');
2887 }
2888 _ => return Ok(false),
2889 }
2890 Ok(true)
2891 }
2892
2893 /// Emit a built-in `Map[K, V]` method call to its Python form (native
2894 /// `dict`).
2895 ///
2896 /// Recognised via [`crate::generator::desugared_map_method`] (gated on
2897 /// `recv_kind = "Map"`) and wired *before* [`Self::try_emit_list_method`],
2898 /// so a `Map` receiver's `get`/`contains_key`/`len` no longer route through
2899 /// the `List` path (where `get` would index `__m[__i]` instead of testing
2900 /// key membership, and `set`/`contains_key` would call non-existent `dict`
2901 /// methods). `get` returns the tagged `Optional` rep
2902 /// (`_BockSome(v)`/`_bock_none`). Mutating methods (`set`/`delete`/`merge`)
2903 /// mutate in place via the `(side_effect, recv)[1]` tuple idiom (Python
2904 /// lambdas are expression-only) and return the receiver. Returns `true` if
2905 /// handled.
2906 fn try_emit_map_method(
2907 &mut self,
2908 node: &AIRNode,
2909 callee: &AIRNode,
2910 args: &[bock_air::AirArg],
2911 ) -> Result<bool, CodegenError> {
2912 let Some((recv, method, rest)) = crate::generator::desugared_map_method(node, callee, args)
2913 else {
2914 return Ok(false);
2915 };
2916 match method {
2917 "len" | "length" | "count" => {
2918 self.buf.push_str("len(");
2919 self.emit_expr(recv)?;
2920 self.buf.push(')');
2921 }
2922 "is_empty" => {
2923 self.buf.push_str("(len(");
2924 self.emit_expr(recv)?;
2925 self.buf.push_str(") == 0)");
2926 }
2927 "contains_key" => {
2928 let Some(k) = rest.first() else {
2929 return Ok(false);
2930 };
2931 self.buf.push('(');
2932 self.emit_expr(&k.value)?;
2933 self.buf.push_str(" in ");
2934 self.emit_expr(recv)?;
2935 self.buf.push(')');
2936 }
2937 "get" => {
2938 let Some(k) = rest.first() else {
2939 return Ok(false);
2940 };
2941 self.buf.push_str(
2942 "(lambda __m, __k: _BockSome(__m[__k]) if __k in __m else _bock_none)(",
2943 );
2944 self.emit_expr(recv)?;
2945 self.buf.push_str(", ");
2946 self.emit_expr(&k.value)?;
2947 self.buf.push(')');
2948 }
2949 "set" => {
2950 let (Some(k), Some(v)) = (rest.first(), rest.get(1)) else {
2951 return Ok(false);
2952 };
2953 self.buf
2954 .push_str("(lambda __m, __k, __v: (__m.__setitem__(__k, __v), __m)[1])(");
2955 self.emit_expr(recv)?;
2956 self.buf.push_str(", ");
2957 self.emit_expr(&k.value)?;
2958 self.buf.push_str(", ");
2959 self.emit_expr(&v.value)?;
2960 self.buf.push(')');
2961 }
2962 "delete" => {
2963 let Some(k) = rest.first() else {
2964 return Ok(false);
2965 };
2966 self.buf
2967 .push_str("(lambda __m, __k: (__m.pop(__k, None), __m)[1])(");
2968 self.emit_expr(recv)?;
2969 self.buf.push_str(", ");
2970 self.emit_expr(&k.value)?;
2971 self.buf.push(')');
2972 }
2973 "merge" => {
2974 let Some(o) = rest.first() else {
2975 return Ok(false);
2976 };
2977 self.buf
2978 .push_str("(lambda __m, __o: (__m.update(__o), __m)[1])(");
2979 self.emit_expr(recv)?;
2980 self.buf.push_str(", ");
2981 self.emit_expr(&o.value)?;
2982 self.buf.push(')');
2983 }
2984 "filter" => {
2985 let Some(f) = rest.first() else {
2986 return Ok(false);
2987 };
2988 self.buf.push_str(
2989 "(lambda __m, __f: {__k: __v for __k, __v in __m.items() if __f(__k, __v)})(",
2990 );
2991 self.emit_expr(recv)?;
2992 self.buf.push_str(", ");
2993 self.emit_expr(&f.value)?;
2994 self.buf.push(')');
2995 }
2996 "keys" => {
2997 self.buf.push_str("list(");
2998 self.emit_expr(recv)?;
2999 self.buf.push_str(".keys())");
3000 }
3001 "values" => {
3002 self.buf.push_str("list(");
3003 self.emit_expr(recv)?;
3004 self.buf.push_str(".values())");
3005 }
3006 "entries" | "to_list" => {
3007 self.buf.push_str("list(");
3008 self.emit_expr(recv)?;
3009 self.buf.push_str(".items())");
3010 }
3011 "for_each" => {
3012 let Some(f) = rest.first() else {
3013 return Ok(false);
3014 };
3015 self.buf.push_str("[(");
3016 self.emit_expr(&f.value)?;
3017 self.buf.push_str(")(__k, __v) for __k, __v in (");
3018 self.emit_expr(recv)?;
3019 self.buf.push_str(").items()]");
3020 }
3021 _ => return Ok(false),
3022 }
3023 Ok(true)
3024 }
3025
3026 /// Emit a built-in `Set[E]` method call to its Python form (native `set`).
3027 ///
3028 /// Recognised via [`crate::generator::desugared_set_method`] (gated on
3029 /// `recv_kind = "Set"`) and wired *before* [`Self::try_emit_list_method`].
3030 /// Set algebra maps to Python's operators (`|`/`&`/`-`/`<=`/`>=`). Mutating
3031 /// methods (`add`/`remove`) mutate in place via the `(side_effect, recv)[1]`
3032 /// idiom and return the receiver.
3033 fn try_emit_set_method(
3034 &mut self,
3035 node: &AIRNode,
3036 callee: &AIRNode,
3037 args: &[bock_air::AirArg],
3038 ) -> Result<bool, CodegenError> {
3039 let Some((recv, method, rest)) = crate::generator::desugared_set_method(node, callee, args)
3040 else {
3041 return Ok(false);
3042 };
3043 match method {
3044 "len" | "length" | "count" => {
3045 self.buf.push_str("len(");
3046 self.emit_expr(recv)?;
3047 self.buf.push(')');
3048 }
3049 "is_empty" => {
3050 self.buf.push_str("(len(");
3051 self.emit_expr(recv)?;
3052 self.buf.push_str(") == 0)");
3053 }
3054 "contains" => {
3055 let Some(x) = rest.first() else {
3056 return Ok(false);
3057 };
3058 self.buf.push('(');
3059 self.emit_expr(&x.value)?;
3060 self.buf.push_str(" in ");
3061 self.emit_expr(recv)?;
3062 self.buf.push(')');
3063 }
3064 "add" => {
3065 let Some(x) = rest.first() else {
3066 return Ok(false);
3067 };
3068 self.buf
3069 .push_str("(lambda __s, __x: (__s.add(__x), __s)[1])(");
3070 self.emit_expr(recv)?;
3071 self.buf.push_str(", ");
3072 self.emit_expr(&x.value)?;
3073 self.buf.push(')');
3074 }
3075 "remove" => {
3076 let Some(x) = rest.first() else {
3077 return Ok(false);
3078 };
3079 self.buf
3080 .push_str("(lambda __s, __x: (__s.discard(__x), __s)[1])(");
3081 self.emit_expr(recv)?;
3082 self.buf.push_str(", ");
3083 self.emit_expr(&x.value)?;
3084 self.buf.push(')');
3085 }
3086 "union" => {
3087 let Some(o) = rest.first() else {
3088 return Ok(false);
3089 };
3090 self.buf.push('(');
3091 self.emit_expr(recv)?;
3092 self.buf.push_str(" | ");
3093 self.emit_expr(&o.value)?;
3094 self.buf.push(')');
3095 }
3096 "intersection" => {
3097 let Some(o) = rest.first() else {
3098 return Ok(false);
3099 };
3100 self.buf.push('(');
3101 self.emit_expr(recv)?;
3102 self.buf.push_str(" & ");
3103 self.emit_expr(&o.value)?;
3104 self.buf.push(')');
3105 }
3106 "difference" => {
3107 let Some(o) = rest.first() else {
3108 return Ok(false);
3109 };
3110 self.buf.push('(');
3111 self.emit_expr(recv)?;
3112 self.buf.push_str(" - ");
3113 self.emit_expr(&o.value)?;
3114 self.buf.push(')');
3115 }
3116 "is_subset" => {
3117 let Some(o) = rest.first() else {
3118 return Ok(false);
3119 };
3120 self.buf.push('(');
3121 self.emit_expr(recv)?;
3122 self.buf.push_str(" <= ");
3123 self.emit_expr(&o.value)?;
3124 self.buf.push(')');
3125 }
3126 "is_superset" => {
3127 let Some(o) = rest.first() else {
3128 return Ok(false);
3129 };
3130 self.buf.push('(');
3131 self.emit_expr(recv)?;
3132 self.buf.push_str(" >= ");
3133 self.emit_expr(&o.value)?;
3134 self.buf.push(')');
3135 }
3136 "filter" => {
3137 let Some(f) = rest.first() else {
3138 return Ok(false);
3139 };
3140 self.buf.push_str("{__x for __x in (");
3141 self.emit_expr(recv)?;
3142 self.buf.push_str(") if (");
3143 self.emit_expr(&f.value)?;
3144 self.buf.push_str(")(__x)}");
3145 }
3146 "map" => {
3147 let Some(f) = rest.first() else {
3148 return Ok(false);
3149 };
3150 self.buf.push_str("{(");
3151 self.emit_expr(&f.value)?;
3152 self.buf.push_str(")(__x) for __x in (");
3153 self.emit_expr(recv)?;
3154 self.buf.push_str(")}");
3155 }
3156 "to_list" => {
3157 self.buf.push_str("list(");
3158 self.emit_expr(recv)?;
3159 self.buf.push(')');
3160 }
3161 "for_each" => {
3162 let Some(f) = rest.first() else {
3163 return Ok(false);
3164 };
3165 self.buf.push_str("[(");
3166 self.emit_expr(&f.value)?;
3167 self.buf.push_str(")(__x) for __x in (");
3168 self.emit_expr(recv)?;
3169 self.buf.push_str(")]");
3170 }
3171 _ => return Ok(false),
3172 }
3173 Ok(true)
3174 }
3175
3176 /// Lower a primitive trait-bridge method call (`compare`/`eq`/`to_string`/
3177 /// `display` on a primitive receiver) to its Python form.
3178 ///
3179 /// `(1).compare(2)` resolves to `Ordering`; this produces the
3180 /// Ordering-runtime singleton (`_bock_less` / `_bock_equal` /
3181 /// `_bock_greater`) via a conditional expression, matching the
3182 /// construction/`case` sides. `eq` → `==`; `to_string`/`display` → `str(x)`.
3183 /// Lower a desugared `String` built-in method call (`recv_kind =
3184 /// "Primitive:String"`) to its native Python string op. Wired into the
3185 /// `Call` arm *before* `try_emit_list_method` so a String receiver's
3186 /// `len`/`contains`/`is_empty` dispatch here, not through the List path.
3187 ///
3188 /// `len` is the Unicode SCALAR count: Python `str` is a sequence of code
3189 /// points, so `len(s)` already yields the scalar count (spec §18.3).
3190 /// `byte_len` encodes to UTF-8 first (`len(s.encode())`). `replace` replaces
3191 /// ALL occurrences (Python's default). `split` returns a Python list, the
3192 /// List runtime rep.
3193 ///
3194 /// Gated on `recv_kind = "Primitive:String"` directly (not the cross-backend
3195 /// [`crate::generator::desugared_string_method`] subset) so Python can lower
3196 /// the wider resolved String surface — `slice`/`substring`/`char_at`/
3197 /// `index_of`/`repeat`/`reverse`/`trim_start`/`trim_end` — to native ops,
3198 /// matching the Rust backend. Python `str` is already a code-point sequence,
3199 /// so scalar slicing is plain `s[a:b]` and `reverse` is `s[::-1]`.
3200 /// `char_at`/`index_of` build the tagged `Optional` runtime (`_BockSome(v)` /
3201 /// `_bock_none`); the Optional prelude is pulled in by the structural scan
3202 /// over the (Optional-typed) call.
3203 fn try_emit_string_method(
3204 &mut self,
3205 node: &AIRNode,
3206 callee: &AIRNode,
3207 args: &[bock_air::AirArg],
3208 ) -> Result<bool, CodegenError> {
3209 if crate::generator::primitive_recv_kind(node) != Some("String") {
3210 return Ok(false);
3211 }
3212 let Some((recv, field, rest)) = crate::generator::desugared_self_call(callee, args) else {
3213 return Ok(false);
3214 };
3215 let method = field.name.as_str();
3216 let recv_str = self.expr_to_string(recv)?;
3217 let arg0 = |this: &mut Self| -> Result<Option<String>, CodegenError> {
3218 rest.first()
3219 .map(|a| this.expr_to_string(&a.value))
3220 .transpose()
3221 };
3222 let code = match method {
3223 "len" | "length" | "count" => format!("len({recv_str})"),
3224 "byte_len" => format!("len(({recv_str}).encode())"),
3225 "is_empty" => format!("(len({recv_str}) == 0)"),
3226 "to_upper" => format!("({recv_str}).upper()"),
3227 "to_lower" => format!("({recv_str}).lower()"),
3228 "trim" => format!("({recv_str}).strip()"),
3229 "trim_start" => format!("({recv_str}).lstrip()"),
3230 "trim_end" => format!("({recv_str}).rstrip()"),
3231 "reverse" => format!("({recv_str})[::-1]"),
3232 "to_string" | "display" => format!("str({recv_str})"),
3233 "repeat" => {
3234 let Some(n) = arg0(self)? else {
3235 return Ok(false);
3236 };
3237 format!("(({recv_str}) * ({n}))")
3238 }
3239 "contains" => {
3240 let Some(p) = arg0(self)? else {
3241 return Ok(false);
3242 };
3243 format!("(({p}) in ({recv_str}))")
3244 }
3245 "starts_with" => {
3246 let Some(p) = arg0(self)? else {
3247 return Ok(false);
3248 };
3249 format!("({recv_str}).startswith({p})")
3250 }
3251 "ends_with" => {
3252 let Some(p) = arg0(self)? else {
3253 return Ok(false);
3254 };
3255 format!("({recv_str}).endswith({p})")
3256 }
3257 "replace" => {
3258 let Some(from) = arg0(self)? else {
3259 return Ok(false);
3260 };
3261 let Some(to) = rest
3262 .get(1)
3263 .map(|a| self.expr_to_string(&a.value))
3264 .transpose()?
3265 else {
3266 return Ok(false);
3267 };
3268 format!("({recv_str}).replace({from}, {to})")
3269 }
3270 "split" => {
3271 let Some(sep) = arg0(self)? else {
3272 return Ok(false);
3273 };
3274 format!("({recv_str}).split({sep})")
3275 }
3276 // `slice`/`substring(start, end)`: scalar-index half-open substring
3277 // (spec §18.3). Python `str` slicing is already code-point based, and
3278 // out-of-range indices clamp rather than raise — matching the spec.
3279 "slice" | "substring" => {
3280 let Some(start) = arg0(self)? else {
3281 return Ok(false);
3282 };
3283 let Some(end) = rest
3284 .get(1)
3285 .map(|a| self.expr_to_string(&a.value))
3286 .transpose()?
3287 else {
3288 return Ok(false);
3289 };
3290 format!("({recv_str})[{start}:{end}]")
3291 }
3292 // `char_at(i)` returns `Optional[Char]` — `None` when out of range.
3293 "char_at" => {
3294 let Some(i) = arg0(self)? else {
3295 return Ok(false);
3296 };
3297 format!(
3298 "(lambda __s, __i: _BockSome(__s[__i]) if 0 <= __i < len(__s) else _bock_none)({recv_str}, {i})"
3299 )
3300 }
3301 // `index_of(needle)` returns `Optional[Int]` — scalar index of the
3302 // first match, or `None`. Python `str.find` is already code-point based.
3303 "index_of" => {
3304 let Some(p) = arg0(self)? else {
3305 return Ok(false);
3306 };
3307 format!(
3308 "(lambda __s, __p: (lambda __b: _BockSome(__b) if __b >= 0 else _bock_none)(__s.find(__p)))({recv_str}, {p})"
3309 )
3310 }
3311 _ => return Ok(false),
3312 };
3313 self.buf.push_str(&code);
3314 Ok(true)
3315 }
3316
3317 /// Q-prim-assoc: lower a primitive associated-conversion call
3318 /// (`Float.from(x)` / `Int.try_from(s)` / `String.from(c)`) to Python's
3319 /// native conversion. CRITICAL on Python: `from` is a keyword, so the
3320 /// generic associated-call form would emit `Float.from_(...)` (an undefined
3321 /// name and the wrong shape). `from` becomes `float(...)`/`int(...)`/
3322 /// `str(...)`; `try_from` calls the self-contained `_bock_parse_int` /
3323 /// `_bock_parse_float` runtime helpers (which return `_BockOk`/`_BockErr`),
3324 /// passing a `ConvertError`-factory lambda so the helper need not import the
3325 /// stdlib type (`ConvertError` is in scope at the call site via the
3326 /// `Result[T, ConvertError]` return type). Returns `true` when handled.
3327 fn try_emit_primitive_conversion(
3328 &mut self,
3329 node: &AIRNode,
3330 callee: &AIRNode,
3331 args: &[bock_air::AirArg],
3332 ) -> Result<bool, CodegenError> {
3333 let Some((target, method, arg)) =
3334 crate::generator::primitive_conversion_call(node, callee, args)
3335 else {
3336 return Ok(false);
3337 };
3338 let arg_str = self.expr_to_string(arg)?;
3339 let code = match (target, method) {
3340 ("Float", "from") => format!("float({arg_str})"),
3341 ("Int", "from") => format!("int({arg_str})"),
3342 ("String", "from") => format!("str({arg_str})"),
3343 ("Int", "try_from") => {
3344 self.needs_runtime_result = true;
3345 format!("_bock_parse_int({arg_str}, lambda __m: ConvertError(message=__m))")
3346 }
3347 ("Float", "try_from") => {
3348 self.needs_runtime_result = true;
3349 format!("_bock_parse_float({arg_str}, lambda __m: ConvertError(message=__m))")
3350 }
3351 _ => return Ok(false),
3352 };
3353 self.buf.push_str(&code);
3354 Ok(true)
3355 }
3356
3357 /// Lower a desugared numeric/`Char`/`Bool` primitive method (`recv_kind =
3358 /// "Primitive:Int" | "Primitive:Float" | "Primitive:Char" | "Primitive:Bool"`)
3359 /// to its native Python form. Covers the conversion and math methods the
3360 /// checker resolves on the scalar primitives — `to_float`/`to_int`/`abs`/`min`/
3361 /// `max`/`clamp`/`floor`/`ceil`/`round`/`sqrt`/… . Wired into the `Call` arm
3362 /// alongside [`Self::try_emit_string_method`], before the generic
3363 /// desugared-self-call fall-through (which would emit `n.to_float(n)`).
3364 /// `floor`/`ceil`/`sqrt` need `math`, so they set `needs_math_import`.
3365 /// `compare`/`eq`/`to_string`/`display`/`hash_code` stay on the primitive
3366 /// *bridge* path. `Char` is a one-code-point Python `str`.
3367 fn try_emit_numeric_method(
3368 &mut self,
3369 node: &AIRNode,
3370 callee: &AIRNode,
3371 args: &[bock_air::AirArg],
3372 ) -> Result<bool, CodegenError> {
3373 let prim = match crate::generator::primitive_recv_kind(node) {
3374 Some(p @ ("Int" | "Float" | "Char" | "Bool")) => p,
3375 _ => return Ok(false),
3376 };
3377 let Some((recv, field, rest)) = crate::generator::desugared_self_call(callee, args) else {
3378 return Ok(false);
3379 };
3380 let method = field.name.as_str();
3381 let recv_str = self.expr_to_string(recv)?;
3382 let arg = |this: &mut Self, i: usize| -> Result<Option<String>, CodegenError> {
3383 rest.get(i)
3384 .map(|a| this.expr_to_string(&a.value))
3385 .transpose()
3386 };
3387 let code = match (prim, method) {
3388 // Conversions.
3389 ("Int", "to_float") => format!("float({recv_str})"),
3390 ("Float", "to_int") => format!("int({recv_str})"),
3391 ("Char", "to_int") => format!("ord({recv_str})"),
3392 ("Bool", "to_int") => format!("(1 if ({recv_str}) else 0)"),
3393 // Int math.
3394 ("Int", "abs") => format!("abs({recv_str})"),
3395 ("Int" | "Float", "min") => {
3396 let Some(o) = arg(self, 0)? else {
3397 return Ok(false);
3398 };
3399 format!("min({recv_str}, {o})")
3400 }
3401 ("Int" | "Float", "max") => {
3402 let Some(o) = arg(self, 0)? else {
3403 return Ok(false);
3404 };
3405 format!("max({recv_str}, {o})")
3406 }
3407 ("Int" | "Float", "clamp") => {
3408 let (Some(lo), Some(hi)) = (arg(self, 0)?, arg(self, 1)?) else {
3409 return Ok(false);
3410 };
3411 format!("min(max({recv_str}, {lo}), {hi})")
3412 }
3413 ("Int", "shift_left") => {
3414 let Some(o) = arg(self, 0)? else {
3415 return Ok(false);
3416 };
3417 format!("(({recv_str}) << ({o}))")
3418 }
3419 ("Int", "shift_right") => {
3420 let Some(o) = arg(self, 0)? else {
3421 return Ok(false);
3422 };
3423 format!("(({recv_str}) >> ({o}))")
3424 }
3425 // Float math.
3426 ("Float", "abs") => format!("abs({recv_str})"),
3427 ("Float", "floor") => {
3428 self.needs_math_import = true;
3429 format!("float(math.floor({recv_str}))")
3430 }
3431 ("Float", "ceil") => {
3432 self.needs_math_import = true;
3433 format!("float(math.ceil({recv_str}))")
3434 }
3435 ("Float", "round") => format!("float(round({recv_str}))"),
3436 ("Float", "sqrt") => {
3437 self.needs_math_import = true;
3438 format!("math.sqrt({recv_str})")
3439 }
3440 ("Float", "is_nan") => {
3441 self.needs_math_import = true;
3442 format!("math.isnan({recv_str})")
3443 }
3444 ("Float", "is_infinite") => {
3445 self.needs_math_import = true;
3446 format!("math.isinf({recv_str})")
3447 }
3448 // Bool.
3449 ("Bool", "negate") => format!("(not ({recv_str}))"),
3450 // `Bool.to_string()` / `.display()` must yield the canonical lowercase
3451 // `"true"`/`"false"` (§3.5), not Python's `str(b)` → `"True"`/`"False"`.
3452 // Handled here (before the primitive *bridge* path that maps
3453 // `to_string` → `str(..)`) so the Bool case is intercepted.
3454 ("Bool", "to_string" | "display") => {
3455 format!("('true' if ({recv_str}) else 'false')")
3456 }
3457 // Char (a one-code-point Python `str`).
3458 ("Char", "to_upper") => format!("({recv_str}).upper()"),
3459 ("Char", "to_lower") => format!("({recv_str}).lower()"),
3460 ("Char", "is_alpha") => format!("({recv_str}).isalpha()"),
3461 ("Char", "is_digit") => format!("({recv_str}).isdigit()"),
3462 ("Char", "is_whitespace") => format!("({recv_str}).isspace()"),
3463 _ => return Ok(false),
3464 };
3465 self.buf.push_str(&code);
3466 Ok(true)
3467 }
3468
3469 fn try_emit_primitive_bridge(
3470 &mut self,
3471 node: &AIRNode,
3472 callee: &AIRNode,
3473 args: &[bock_air::AirArg],
3474 ) -> Result<bool, CodegenError> {
3475 let Some((recv, method, rest, _prim)) =
3476 crate::generator::primitive_bridge_call(node, callee, args)
3477 else {
3478 return Ok(false);
3479 };
3480 self.emit_bridge_method(recv, method, rest)
3481 }
3482
3483 /// Lower a sealed-core-trait bridge method on a *bounded generic type
3484 /// variable* (`a.eq(b)` / `a.compare(b)` inside `eq_check[T: Equatable]`) to
3485 /// its Python form (GAP-C). The method body is identical to the
3486 /// `Primitive:<Ty>` bridge; the `bound=Equatable` on the `TypeVar` is
3487 /// separately dropped (see the generic-decl emission). Fires only when the
3488 /// bound trait is sealed-core and NOT a user-declared trait.
3489 fn try_emit_trait_bound_bridge(
3490 &mut self,
3491 node: &AIRNode,
3492 callee: &AIRNode,
3493 args: &[bock_air::AirArg],
3494 ) -> Result<bool, CodegenError> {
3495 let Some((recv, method, rest, _tr)) =
3496 crate::generator::trait_bound_bridge_call(node, callee, args, &self.trait_decls)
3497 else {
3498 return Ok(false);
3499 };
3500 // Q-bounded-comparable-codegen: a bounded `T: Comparable` `compare`
3501 // receiver may be instantiated with a RECORD whose ordering lives in its
3502 // own `compare` method — the native `<`/`==` ternary the
3503 // `emit_bridge_method` `compare` arm emits raises `TypeError: '<' not
3504 // supported between instances of 'Money'`. Route through the
3505 // `_bock_compare` runtime helper, which calls the value's `compare`
3506 // method when present and falls back to the native ternary for a
3507 // primitive instantiation.
3508 if method == "compare" {
3509 let Some(other) = rest.first() else {
3510 return Ok(false);
3511 };
3512 let recv_str = self.expr_to_string(recv)?;
3513 let other = self.expr_to_string(&other.value)?;
3514 self.needs_runtime_ordering = true;
3515 let _ = write!(self.buf, "_bock_compare({recv_str}, {other})");
3516 return Ok(true);
3517 }
3518 self.emit_bridge_method(recv, method, rest)
3519 }
3520
3521 /// Shared body of the primitive / trait-bound bridges: emit the native Python
3522 /// form of `compare` (the `_bock_less`/`_bock_equal`/`_bock_greater`
3523 /// conditional), `eq` (`==`), or `to_string`/`display` (`str(..)`).
3524 fn emit_bridge_method(
3525 &mut self,
3526 recv: &AIRNode,
3527 method: &str,
3528 rest: &[bock_air::AirArg],
3529 ) -> Result<bool, CodegenError> {
3530 let recv_str = self.expr_to_string(recv)?;
3531 match method {
3532 "compare" => {
3533 let Some(other) = rest.first() else {
3534 return Ok(false);
3535 };
3536 let other = self.expr_to_string(&other.value)?;
3537 let _ = write!(
3538 self.buf,
3539 "(_bock_less if ({recv_str}) < ({other}) else \
3540 (_bock_equal if ({recv_str}) == ({other}) else _bock_greater))"
3541 );
3542 }
3543 "eq" => {
3544 let Some(other) = rest.first() else {
3545 return Ok(false);
3546 };
3547 let other = self.expr_to_string(&other.value)?;
3548 let _ = write!(self.buf, "(({recv_str}) == ({other}))");
3549 }
3550 "to_string" | "display" => {
3551 let _ = write!(self.buf, "str({recv_str})");
3552 }
3553 _ => return Ok(false),
3554 }
3555 Ok(true)
3556 }
3557
3558 /// Recognise `Duration.xxx(...)` / `Instant.xxx(...)` associated-function
3559 /// calls and emit inline arithmetic. Durations are ints (nanoseconds);
3560 /// Instants are ints representing `time.monotonic_ns()`.
3561 fn try_emit_time_assoc_call(
3562 &mut self,
3563 callee: &AIRNode,
3564 args: &[bock_air::AirArg],
3565 ) -> Result<bool, CodegenError> {
3566 let NodeKind::FieldAccess { object, field } = &callee.kind else {
3567 return Ok(false);
3568 };
3569 let NodeKind::Identifier { name: type_name } = &object.kind else {
3570 return Ok(false);
3571 };
3572 let arg_strs: Vec<String> = args
3573 .iter()
3574 .map(|a| self.expr_to_string(&a.value))
3575 .collect::<Result<_, _>>()?;
3576 let arg0 = || arg_strs.first().cloned().unwrap_or_default();
3577 let code = match (type_name.name.as_str(), field.name.as_str()) {
3578 ("Duration", "zero") => "0".to_string(),
3579 ("Duration", "nanos") => arg0(),
3580 ("Duration", "micros") => format!("(({}) * 1_000)", arg0()),
3581 ("Duration", "millis") => format!("(({}) * 1_000_000)", arg0()),
3582 ("Duration", "seconds") => format!("(({}) * 1_000_000_000)", arg0()),
3583 ("Duration", "minutes") => format!("(({}) * 60_000_000_000)", arg0()),
3584 ("Duration", "hours") => format!("(({}) * 3_600_000_000_000)", arg0()),
3585 ("Instant", "now") => {
3586 // Route through an installed `Clock` handler's `now_monotonic`
3587 // op if one is in scope; otherwise emit the host primitive.
3588 if let Some(handler) = self.clock_handler_var() {
3589 format!("{handler}.{}()", to_snake_case("now_monotonic"))
3590 } else {
3591 self.needs_time_import = true;
3592 "time.monotonic_ns()".to_string()
3593 }
3594 }
3595 _ => return Ok(false),
3596 };
3597 self.buf.push_str(&code);
3598 Ok(true)
3599 }
3600
3601 /// Recognise `Channel.new()`, `spawn(...)`, and method calls on a
3602 /// channel value (`send`, `recv`, `close`) and emit the Python
3603 /// runtime helper equivalents.
3604 fn try_emit_concurrency_call(
3605 &mut self,
3606 callee: &AIRNode,
3607 args: &[bock_air::AirArg],
3608 ) -> Result<bool, CodegenError> {
3609 if let NodeKind::Identifier { name } = &callee.kind {
3610 if name.name == "spawn" {
3611 self.buf.push_str("__bock_spawn(");
3612 for (i, arg) in args.iter().enumerate() {
3613 if i > 0 {
3614 self.buf.push_str(", ");
3615 }
3616 self.emit_expr(&arg.value)?;
3617 }
3618 self.buf.push(')');
3619 return Ok(true);
3620 }
3621 }
3622 let NodeKind::FieldAccess { object, field } = &callee.kind else {
3623 return Ok(false);
3624 };
3625 if let NodeKind::Identifier { name: type_name } = &object.kind {
3626 if type_name.name == "Channel" && field.name == "new" {
3627 self.buf.push_str("__bock_channel_new()");
3628 return Ok(true);
3629 }
3630 }
3631 if matches!(field.name.as_str(), "send" | "recv" | "close") {
3632 self.emit_expr(object)?;
3633 let _ = write!(self.buf, ".{}", field.name);
3634 self.buf.push('(');
3635 for (i, arg) in args.iter().skip(1).enumerate() {
3636 if i > 0 {
3637 self.buf.push_str(", ");
3638 }
3639 self.emit_expr(&arg.value)?;
3640 }
3641 self.buf.push(')');
3642 return Ok(true);
3643 }
3644 Ok(false)
3645 }
3646
3647 /// Recognise desugared method calls `Call(FieldAccess(recv, m), [recv, ...args])`
3648 /// on Duration/Instant values and emit inline arithmetic.
3649 ///
3650 /// `node` is the full `Call` AIR node, consulted only to *exclude* primitive
3651 /// receivers: [`is_time_method_name`] alone is ambiguous (`abs` is both
3652 /// `Duration.abs` and `Int.abs`/`Float.abs`), so when the checker has stamped
3653 /// `recv_kind = "Primitive:<Ty>"` this is a numeric method, not a time method —
3654 /// bail so [`Self::try_emit_numeric_method`] handles it.
3655 fn try_emit_time_desugared_method(
3656 &mut self,
3657 node: &AIRNode,
3658 callee: &AIRNode,
3659 args: &[bock_air::AirArg],
3660 ) -> Result<bool, CodegenError> {
3661 if crate::generator::primitive_recv_kind(node).is_some() {
3662 return Ok(false);
3663 }
3664 let NodeKind::FieldAccess { object, field } = &callee.kind else {
3665 return Ok(false);
3666 };
3667 if let NodeKind::Identifier { name } = &object.kind {
3668 if matches!(name.name.as_str(), "Duration" | "Instant") {
3669 return Ok(false);
3670 }
3671 }
3672 if !is_time_method_name(&field.name) {
3673 return Ok(false);
3674 }
3675 let remaining: Vec<bock_air::AirArg> = args.iter().skip(1).cloned().collect();
3676 self.try_emit_time_method(object, &field.name, &remaining)
3677 }
3678
3679 /// Recognise instance methods on Duration/Instant values and emit inline
3680 /// arithmetic.
3681 fn try_emit_time_method(
3682 &mut self,
3683 receiver: &AIRNode,
3684 method: &str,
3685 args: &[bock_air::AirArg],
3686 ) -> Result<bool, CodegenError> {
3687 let recv_str = self.expr_to_string(receiver)?;
3688 let arg_strs: Vec<String> = args
3689 .iter()
3690 .map(|a| self.expr_to_string(&a.value))
3691 .collect::<Result<_, _>>()?;
3692 let code = match method {
3693 "as_nanos" => format!("({recv_str})"),
3694 "as_millis" => format!("(({recv_str}) // 1_000_000)"),
3695 "as_seconds" => format!("(({recv_str}) // 1_000_000_000)"),
3696 "is_zero" => format!("(({recv_str}) == 0)"),
3697 "is_negative" => format!("(({recv_str}) < 0)"),
3698 "abs" => format!("abs({recv_str})"),
3699 "elapsed" => {
3700 // `instant.elapsed()` is derived: `now - instant`. Route the
3701 // "now" read through an installed `Clock` handler if in scope;
3702 // otherwise read the host monotonic clock (default).
3703 if let Some(handler) = self.clock_handler_var() {
3704 format!(
3705 "({handler}.{}() - ({recv_str}))",
3706 to_snake_case("now_monotonic")
3707 )
3708 } else {
3709 self.needs_time_import = true;
3710 format!("(time.monotonic_ns() - ({recv_str}))")
3711 }
3712 }
3713 "duration_since" => {
3714 let other = arg_strs.first().cloned().unwrap_or_default();
3715 format!("(({recv_str}) - ({other}))")
3716 }
3717 _ => return Ok(false),
3718 };
3719 self.buf.push_str(&code);
3720 Ok(true)
3721 }
3722
3723 // ── Top-level dispatch ──────────────────────────────────────────────────
3724
3725 fn emit_node(&mut self, node: &AIRNode) -> Result<(), CodegenError> {
3726 match &node.kind {
3727 NodeKind::Module { items, imports, .. } => {
3728 // Field/method name-collision set (snake_cased). Pre-seeded
3729 // program-wide by `generate_project` so a call site in one file
3730 // agrees with the renamed method declared in another; extended
3731 // here so the single-module `generate_module` path (no pre-seed)
3732 // is also covered.
3733 self.field_method_collisions
3734 .extend(crate::generator::collect_record_field_names(
3735 node,
3736 to_snake_case,
3737 ));
3738 if self.per_module {
3739 // Per-module native-import path (the real build): each module
3740 // is emitted to its own file and the shared runtime preludes
3741 // live in `_bock_runtime.py`. Record which prelude names this
3742 // module references; `finish` emits the single
3743 // `from _bock_runtime import *` line.
3744 if py_module_uses_optional(items) {
3745 self.needs_runtime_optional = true;
3746 }
3747 if py_module_uses_result(items) {
3748 self.needs_runtime_result = true;
3749 }
3750 if py_module_uses_ordering(items) {
3751 self.needs_runtime_ordering = true;
3752 }
3753 if py_module_uses_concurrency(items) {
3754 self.needs_runtime_concurrency = true;
3755 }
3756 if py_module_uses_list_functional(items) {
3757 self.needs_runtime_list_functional = true;
3758 // `_bock_find` builds tagged `Optional` runtime values, so
3759 // the Optional prelude must be present alongside it.
3760 self.needs_runtime_optional = true;
3761 }
3762 if py_module_uses_list_mutators(items) {
3763 self.needs_runtime_list_mutators = true;
3764 }
3765 if py_module_uses_propagate(items) {
3766 self.needs_runtime_propagate = true;
3767 }
3768 if py_module_uses_str(items) {
3769 self.needs_runtime_str = true;
3770 }
3771 } else {
3772 // Single-module self-contained emit (`generate_module`, used
3773 // by unit tests): the module's runtime preludes are inlined
3774 // into this one file and `ImportDecl`s are dropped. Each
3775 // prelude is inlined at most once, gated on a ctx flag.
3776 if !self.optional_runtime_emitted && py_module_uses_optional(items) {
3777 self.buf.push_str(OPTIONAL_RUNTIME_PY);
3778 self.buf.push('\n');
3779 self.optional_runtime_emitted = true;
3780 }
3781 if !self.result_runtime_emitted && py_module_uses_result(items) {
3782 self.buf.push_str(RESULT_RUNTIME_PY);
3783 self.buf.push('\n');
3784 self.result_runtime_emitted = true;
3785 }
3786 if !self.ordering_runtime_emitted && py_module_uses_ordering(items) {
3787 self.buf.push_str(ORDERING_RUNTIME_PY);
3788 self.buf.push('\n');
3789 self.ordering_runtime_emitted = true;
3790 }
3791 if !self.concurrency_runtime_emitted && py_module_uses_concurrency(items) {
3792 self.buf.push_str(CONCURRENCY_RUNTIME_PY);
3793 self.buf.push('\n');
3794 self.concurrency_runtime_emitted = true;
3795 }
3796 // `_bock_find` references `_BockSome`/`_bock_none`, so the
3797 // Optional prelude (emitted just above when used) must precede
3798 // this one; both are inlined in source order here.
3799 if !self.list_functional_runtime_emitted
3800 && py_module_uses_list_functional(items)
3801 {
3802 if !self.optional_runtime_emitted {
3803 self.buf.push_str(OPTIONAL_RUNTIME_PY);
3804 self.buf.push('\n');
3805 self.optional_runtime_emitted = true;
3806 }
3807 self.buf.push_str(LIST_FUNCTIONAL_RUNTIME_PY);
3808 self.buf.push('\n');
3809 self.list_functional_runtime_emitted = true;
3810 }
3811 if !self.list_mutator_runtime_emitted && py_module_uses_list_mutators(items) {
3812 self.buf.push_str(LIST_MUTATOR_RUNTIME_PY);
3813 self.buf.push('\n');
3814 self.list_mutator_runtime_emitted = true;
3815 }
3816 if !self.propagate_runtime_emitted && py_module_uses_propagate(items) {
3817 self.buf.push_str(PROPAGATE_RUNTIME_PY);
3818 self.buf.push('\n');
3819 self.propagate_runtime_emitted = true;
3820 }
3821 if !self.str_runtime_emitted && py_module_uses_str(items) {
3822 self.buf.push_str(STR_RUNTIME_PY);
3823 self.buf.push('\n');
3824 self.str_runtime_emitted = true;
3825 }
3826 }
3827 // Per-module path: emit the module's cross-module imports as
3828 // real Python `from <module> import …` statements at the top of
3829 // the body (the runtime-prelude import is emitted into the
3830 // preamble by `finish`). The single-module path drops these.
3831 if self.per_module {
3832 for import in imports {
3833 self.emit_node(import)?;
3834 }
3835 // Implicit imports: prelude-visible names this module
3836 // references but does not explicitly `use` (e.g. a base
3837 // trait). Grouped per declaring module for one import line
3838 // each, in deterministic (sorted) order.
3839 let mut by_module: std::collections::BTreeMap<String, Vec<String>> =
3840 std::collections::BTreeMap::new();
3841 for (module_path, name) in &self.implicit_imports {
3842 by_module
3843 .entry(module_path.clone())
3844 .or_default()
3845 .push(name.clone());
3846 }
3847 let import_lines: Vec<String> = by_module
3848 .into_iter()
3849 .map(|(module_path, mut names)| {
3850 names.sort_unstable();
3851 names.dedup();
3852 format!("from {module_path} import {}", names.join(", "))
3853 })
3854 .collect();
3855 for line in import_lines {
3856 self.writeln(&line);
3857 }
3858 }
3859 // Pre-scan impl blocks so we can attach their methods to the
3860 // target record/class body instead of leaving them as orphan
3861 // module-level functions with a `self` parameter. Both trait
3862 // impls (`impl Trait for T`) and bare inherent impls (`impl T`)
3863 // are folded; only impls whose target is a record/class
3864 // declared in this module are consumed (others stay as-is).
3865 self.impls_by_target.clear();
3866 let class_targets: std::collections::HashSet<String> = items
3867 .iter()
3868 .filter_map(|it| match &it.kind {
3869 NodeKind::RecordDecl { name, .. } | NodeKind::ClassDecl { name, .. } => {
3870 Some(name.name.clone())
3871 }
3872 _ => None,
3873 })
3874 .collect();
3875 let mut consumed_impls: std::collections::HashSet<bock_air::NodeId> =
3876 std::collections::HashSet::new();
3877 for item in items.iter() {
3878 if let NodeKind::ImplBlock { target, .. } = &item.kind {
3879 if let Some(target_name) = ast_type_name(target) {
3880 if class_targets.contains(&target_name) {
3881 self.impls_by_target
3882 .entry(target_name)
3883 .or_default()
3884 .push(item.clone());
3885 consumed_impls.insert(item.id);
3886 }
3887 }
3888 }
3889 }
3890 // A trait/base class becomes a Python base class of every type
3891 // that subclasses it (`class Sub(Base):`). Python evaluates the
3892 // base list at `class` statement time, so the base MUST already be
3893 // defined — else `NameError: name 'Base' is not defined`. Source
3894 // order does not guarantee this: a `trait` may be declared after
3895 // the record/class that impls it (chat-protocol's `Serializable`),
3896 // and a `record`+inlined-impl is emitted at the record's source
3897 // position, which can precede the trait. Reorder the *type
3898 // declarations* so each base precedes its subclasses, keeping the
3899 // emission otherwise stable (Q-py-impl-before-trait, py slice).
3900 let order = type_decl_emission_order(items, &self.impls_by_target);
3901 for (idx, &i) in order.iter().enumerate() {
3902 let item = &items[i];
3903 if consumed_impls.contains(&item.id) {
3904 continue;
3905 }
3906 // `@test` functions are transpiled separately into pytest/
3907 // unittest test files (project mode, §20.6.2 — see
3908 // `generate_tests`), never into the runtime module tree: their
3909 // `expect(...)` assertion DSL has no runtime definition in the
3910 // emitted source.
3911 if crate::generator::fn_is_test(item) {
3912 continue;
3913 }
3914 if idx > 0 && !self.buf.is_empty() && !self.buf.ends_with("\n\n") {
3915 self.buf.push('\n');
3916 }
3917 self.emit_node(item)?;
3918 }
3919 Ok(())
3920 }
3921 NodeKind::ImportDecl { path, items } => {
3922 if !self.per_module {
3923 // Single-module self-contained emit: there is no sibling file
3924 // to import from, so the import is a no-op. (Only
3925 // `generate_module` — the unit-test path — takes this branch;
3926 // the per-module project path emits real imports below.)
3927 return Ok(());
3928 }
3929 // Per-module native-import path (Python S1): emit a real Python
3930 // import. The module path's dotted form is both the on-disk
3931 // package path (`core.option` ⇒ `core/option.py`, emitted by
3932 // `generate_project`) and the import path, so this resolves when
3933 // the entry is run from the build root (Python adds the script's
3934 // dir to `sys.path`, and `core` resolves as a PEP 420 namespace
3935 // package).
3936 let module_path = path
3937 .segments
3938 .iter()
3939 .map(|s| s.name.as_str())
3940 .collect::<Vec<_>>()
3941 .join(".");
3942 if module_path.is_empty() {
3943 return Ok(());
3944 }
3945 match items {
3946 bock_ast::ImportItems::Named(names) => {
3947 // A braced cross-module enum VARIANT (`use core.compare.
3948 // {Ordering, Less, Equal, Greater}`) is NOT a free
3949 // module-level symbol in the emitted Python: the py backend
3950 // lowers a user enum variant to a dataclass named
3951 // `{Enum}_{Variant}` (`Ordering_Less`), never the bare
3952 // `Less`, so `from core.compare import Less` raises
3953 // `ImportError: cannot import name 'Less'` at runtime. Drop
3954 // the (unaliased) variant leaf names here: the variant is
3955 // reached at its use sites as the `Ordering_Less` dataclass
3956 // (the use-site lowering already emits that name), and the
3957 // implicit-import pass (`implicit_imports_for`) pulls that
3958 // dataclass into the module. This mirrors the js/ts filter
3959 // (js.rs `ImportItems::Named`, which drops non-js-value
3960 // leaves) and the Rust fix (rs.rs `emit_cross_module_uses`,
3961 // which reaches a variant via its enum type). The enum TYPE
3962 // name (`Ordering`) IS a real module-level symbol
3963 // (`Ordering = Union[Ordering_Less, …]`) and is kept, as is
3964 // any non-variant leaf. (`user_variant_for_name` returns
3965 // `Some` only for user enum variants and excludes the
3966 // built-in `Optional`/`Result`.) An *aliased* variant
3967 // (`{Less as L}`) is left untouched — aliased-variant
3968 // rebinding is a separate, unexercised concern.
3969 let rendered: Vec<String> = names
3970 .iter()
3971 .filter(|n| {
3972 n.alias.is_some()
3973 || self.user_variant_for_name(&n.name.name).is_none()
3974 })
3975 .map(|n| match &n.alias {
3976 Some(alias) => format!("{} as {}", n.name.name, alias.name),
3977 None => n.name.name.clone(),
3978 })
3979 .collect();
3980 if rendered.is_empty() {
3981 // A genuinely-empty braced list (`use mod.{}`) keeps the
3982 // `import {module_path}` fallback. But if filtering the
3983 // variant leaves emptied a *non-empty* original list,
3984 // emit nothing — the dropped variants are covered by the
3985 // implicit-import pass + use sites, exactly as js does.
3986 if names.is_empty() {
3987 self.writeln(&format!("import {module_path}"));
3988 }
3989 } else {
3990 self.writeln(&format!(
3991 "from {module_path} import {}",
3992 rendered.join(", ")
3993 ));
3994 }
3995 }
3996 bock_ast::ImportItems::Glob => {
3997 self.writeln(&format!("from {module_path} import *"));
3998 }
3999 bock_ast::ImportItems::Module => {
4000 // `use Foo` brings the module's exported names into
4001 // scope unqualified in Bock; a `*` import mirrors that.
4002 self.writeln(&format!("from {module_path} import *"));
4003 }
4004 }
4005 Ok(())
4006 }
4007 NodeKind::FnDecl {
4008 visibility,
4009 is_async,
4010 name,
4011 generic_params,
4012 params,
4013 return_type,
4014 effect_clause,
4015 body,
4016 ..
4017 } => self.emit_fn_decl(
4018 *visibility,
4019 *is_async,
4020 &name.name,
4021 generic_params,
4022 params,
4023 return_type.as_deref(),
4024 effect_clause,
4025 body,
4026 ),
4027 NodeKind::RecordDecl {
4028 name,
4029 fields,
4030 generic_params,
4031 ..
4032 } => {
4033 // A `record R[T] { … }` needs `T` to resolve at class-eval time:
4034 // emit `T = TypeVar("T")` and add `Generic[T, …]` to the bases,
4035 // else the field annotation `value: T` raises `NameError`
4036 // (DV12, Python slice).
4037 self.emit_typevars(generic_params);
4038 // Pull any previously-collected `impl Trait for Name` blocks
4039 // so their methods become part of this class body and the
4040 // class inherits from every implemented trait — giving real
4041 // method dispatch (a bare instance has no orphan methods).
4042 let impls = self.impls_by_target.remove(&name.name).unwrap_or_default();
4043 let mut bases: Vec<String> = impls
4044 .iter()
4045 .filter_map(|im| {
4046 if let NodeKind::ImplBlock {
4047 trait_path: Some(tp),
4048 ..
4049 } = &im.kind
4050 {
4051 let trait_name = tp.segments.last().map(|s| s.name.clone())?;
4052 // A prelude (compiler-sealed) trait with no user
4053 // `trait` declaration — `Comparable`/`Equatable`/
4054 // `Displayable`/`Hashable` used without a definition,
4055 // §18.2 — emits NO Python ABC, so it must not be a
4056 // base class (`class Foo(Comparable)` raises
4057 // `NameError: Comparable`). Its `compare`/`eq`/… is
4058 // emitted directly on the class, which Python dispatches
4059 // by duck typing. (Q-prelude-impl-missing-import.)
4060 if crate::generator::is_unimplemented_sealed_core_trait(
4061 &trait_name,
4062 &self.trait_decls,
4063 ) {
4064 return None;
4065 }
4066 // An impl with no instance methods (e.g. `From`, whose
4067 // only method `from` is associated) carries no
4068 // instance contract and is often a prelude trait not
4069 // emitted here, so it must not be a Python base class
4070 // (`class Foot(From)` would raise `NameError`). Its
4071 // `from` static method is emitted directly on the
4072 // class.
4073 if crate::generator::impl_has_instance_method(im, &self.effect_ops) {
4074 Some(trait_name)
4075 } else {
4076 None
4077 }
4078 } else {
4079 None
4080 }
4081 })
4082 .collect();
4083 bases.extend(self.generic_base(generic_params));
4084 let base_list = if bases.is_empty() {
4085 String::new()
4086 } else {
4087 format!("({})", bases.join(", "))
4088 };
4089 // DQ31 (§18.5): a record with an explicit `impl Equatable` (its
4090 // custom `eq` IS its equality) gets `@dataclass(eq=False)` plus
4091 // a `__eq__` that DELEGATES to that `eq`. Otherwise the
4092 // dataclass-generated structural `__eq__` would shadow the
4093 // custom `eq`, and a `list`/`dict`/`set`/`tuple` of the type
4094 // would compare structurally — silently ignoring the user's
4095 // equality and diverging from the other targets. With the
4096 // delegating `__eq__`, native container `==` (and `dict`-value
4097 // / `list` element comparison) route through the custom `eq`,
4098 // giving the type its ONE equality inside containers as outside.
4099 let custom_eq = matches!(
4100 node.metadata.get(bock_types::checker::CUSTOM_EQ_META_KEY),
4101 Some(bock_air::Value::Bool(true))
4102 );
4103 // `@dataclass` is only appropriate when the class actually
4104 // carries data. Empty handler structs are cleaner as plain
4105 // classes — `@dataclass` on an ABC subclass without fields
4106 // adds no value and drags in the dataclass metaclass.
4107 if !fields.is_empty() {
4108 self.needs_dataclass_import = true;
4109 if custom_eq {
4110 self.writeln("@dataclass(eq=False)");
4111 } else {
4112 self.writeln("@dataclass");
4113 }
4114 }
4115 self.writeln(&format!("class {}{base_list}:", name.name));
4116 self.indent += 1;
4117 let has_members = !fields.is_empty()
4118 || impls
4119 .iter()
4120 .any(|im| matches!(&im.kind, NodeKind::ImplBlock { methods, .. } if !methods.is_empty()));
4121 if !has_members {
4122 self.writeln("pass");
4123 } else {
4124 for f in fields {
4125 let type_hint = self.ast_type_to_py(&f.ty);
4126 // `py_field_ident`: a field named after a Python
4127 // keyword (`pass`) must be escaped (`pass_`) or the
4128 // dataclass declaration is a SyntaxError.
4129 self.writeln(&format!("{}: {type_hint}", py_field_ident(&f.name.name)));
4130 }
4131 for method in Self::dedup_impl_methods(&impls) {
4132 self.buf.push('\n');
4133 self.emit_class_method(method)?;
4134 }
4135 if custom_eq {
4136 self.buf.push('\n');
4137 self.writeln("def __eq__(self, other: object) -> bool:");
4138 self.indent += 1;
4139 self.writeln(&format!("if not isinstance(other, {}):", name.name));
4140 self.indent += 1;
4141 self.writeln("return NotImplemented");
4142 self.indent -= 1;
4143 self.writeln("return self.eq(other)");
4144 self.indent -= 1;
4145 }
4146 }
4147 self.indent -= 1;
4148 Ok(())
4149 }
4150 NodeKind::EnumDecl {
4151 name,
4152 variants,
4153 generic_params,
4154 ..
4155 } => {
4156 self.needs_dataclass_import = true;
4157 // A generic `enum E[T]` whose variants carry `T`-typed payloads
4158 // needs `T = TypeVar("T")` so those field annotations resolve.
4159 // (Full generic-enum codegen — `Generic[T]` variant bases — is
4160 // tracked separately under DV12/P1; the TypeVar declaration is
4161 // the minimum that keeps the module from raising `NameError`.)
4162 self.emit_typevars(generic_params);
4163 for (i, variant) in variants.iter().enumerate() {
4164 if i > 0 {
4165 self.buf.push('\n');
4166 }
4167 self.emit_enum_variant(&name.name, variant)?;
4168 }
4169 // A union type alias so the enum's *name* (`Shape`) resolves as
4170 // a type annotation — `def area(s: Shape)` evaluates `Shape` at
4171 // def time, so without this alias the module raises `NameError`
4172 // before `main` ever runs (DV14).
4173 let variant_types: Vec<String> = variants
4174 .iter()
4175 .filter_map(|v| {
4176 if let NodeKind::EnumVariant { name: vname, .. } = &v.kind {
4177 Some(format!("{}_{}", name.name, vname.name))
4178 } else {
4179 None
4180 }
4181 })
4182 .collect();
4183 if !variant_types.is_empty() {
4184 self.needs_union_import = true;
4185 self.writeln(&format!(
4186 "{} = Union[{}]",
4187 name.name,
4188 variant_types.join(", ")
4189 ));
4190 }
4191 Ok(())
4192 }
4193 NodeKind::ClassDecl {
4194 name,
4195 fields,
4196 methods,
4197 generic_params,
4198 base,
4199 traits,
4200 ..
4201 } => {
4202 // A generic `class C[T]` needs `T = TypeVar("T")` + a
4203 // `Generic[T, …]` base so `T`-typed members resolve (DV12).
4204 self.emit_typevars(generic_params);
4205 // Pull any `impl T { … }` / `impl Trait for T { … }` blocks
4206 // collected up front (Module pre-scan) so their methods become
4207 // part of THIS class body — the same path records already use.
4208 // Without this the inherent/trait methods were silently dropped:
4209 // the emitted class had only `__init__`, so `t.render()` raised
4210 // `AttributeError` at runtime (Q-class-codegen, py slice).
4211 let impls = self.impls_by_target.remove(&name.name).unwrap_or_default();
4212 // Bases: the declared `base` class, then every implemented trait
4213 // (both the class-decl `traits` list and any `impl Trait for T`
4214 // trait paths), then `Generic[..]` for generic params. Dedup so a
4215 // trait named both on the class header and via an impl block isn't
4216 // listed twice.
4217 let mut bases: Vec<String> = Vec::new();
4218 if let Some(b) = base {
4219 bases.push(
4220 b.segments
4221 .last()
4222 .map(|s| s.name.clone())
4223 .unwrap_or_default(),
4224 );
4225 }
4226 for tp in traits {
4227 if let Some(seg) = tp.segments.last() {
4228 // Skip a prelude (compiler-sealed) trait with no user
4229 // `trait` decl: it emits no Python ABC, so it cannot be a
4230 // base class. See the record-decl arm
4231 // (Q-prelude-impl-missing-import).
4232 if crate::generator::is_unimplemented_sealed_core_trait(
4233 &seg.name,
4234 &self.trait_decls,
4235 ) {
4236 continue;
4237 }
4238 bases.push(seg.name.clone());
4239 }
4240 }
4241 for im in &impls {
4242 if let NodeKind::ImplBlock {
4243 trait_path: Some(tp),
4244 ..
4245 } = &im.kind
4246 {
4247 if let Some(seg) = tp.segments.last() {
4248 if crate::generator::is_unimplemented_sealed_core_trait(
4249 &seg.name,
4250 &self.trait_decls,
4251 ) {
4252 continue;
4253 }
4254 bases.push(seg.name.clone());
4255 }
4256 }
4257 }
4258 // Order-preserving dedup: a trait named on both the class header
4259 // and an `impl` block would otherwise repeat in the base list
4260 // (which Python rejects — duplicate bases are a `TypeError`).
4261 let mut seen_bases: std::collections::HashSet<String> =
4262 std::collections::HashSet::new();
4263 bases.retain(|b| seen_bases.insert(b.clone()));
4264 bases.extend(self.generic_base(generic_params));
4265 let base_list = if bases.is_empty() {
4266 String::new()
4267 } else {
4268 format!("({})", bases.join(", "))
4269 };
4270 self.writeln(&format!("class {}{base_list}:", name.name));
4271 self.indent += 1;
4272 // __init__
4273 if !fields.is_empty() {
4274 let params: Vec<String> = fields
4275 .iter()
4276 .map(|f| {
4277 // `py_field_ident`: keyword-named fields (`pass`)
4278 // must be escaped (`pass_`) in the `__init__`
4279 // parameter list and attribute assignments alike.
4280 let fname = py_field_ident(&f.name.name);
4281 let type_hint = self.ast_type_to_py(&f.ty);
4282 format!("{fname}: {type_hint}")
4283 })
4284 .collect();
4285 self.writeln(&format!("def __init__(self, {}):", params.join(", ")));
4286 self.indent += 1;
4287 for f in fields {
4288 let fname = py_field_ident(&f.name.name);
4289 self.writeln(&format!("self.{fname} = {fname}"));
4290 }
4291 self.indent -= 1;
4292 }
4293 // Names already taken by an inline `class T { fn … }` method
4294 // (rare in surface Bock, which puts methods in `impl` blocks, but
4295 // kept for completeness) — so a same-named impl method does not
4296 // re-emit and shadow them.
4297 let mut inline_names: std::collections::HashSet<String> =
4298 std::collections::HashSet::new();
4299 for method in methods {
4300 if let NodeKind::FnDecl { name, .. } = &method.kind {
4301 inline_names.insert(name.name.clone());
4302 }
4303 self.buf.push('\n');
4304 self.emit_class_method(method)?;
4305 }
4306 // Methods pulled in from inherent + trait impl blocks, deduped by
4307 // name (inherent precedence) so a delegating trait method never
4308 // overwrites and self-recurses the inherent one.
4309 let impl_methods: Vec<&AIRNode> = Self::dedup_impl_methods(&impls)
4310 .into_iter()
4311 .filter(|m| {
4312 !matches!(&m.kind, NodeKind::FnDecl { name, .. } if inline_names.contains(&name.name))
4313 })
4314 .collect();
4315 let has_impl_methods = !impl_methods.is_empty();
4316 for method in impl_methods {
4317 self.buf.push('\n');
4318 self.emit_class_method(method)?;
4319 }
4320 if fields.is_empty() && methods.is_empty() && !has_impl_methods {
4321 self.writeln("pass");
4322 }
4323 self.indent -= 1;
4324 Ok(())
4325 }
4326 NodeKind::TraitDecl { name, methods, .. } => {
4327 // Traits → abstract base class (comment + class with pass methods).
4328 self.writeln(&format!("# trait {}", name.name));
4329 self.writeln(&format!("class {}:", name.name));
4330 self.indent += 1;
4331 if methods.is_empty() {
4332 self.writeln("pass");
4333 } else {
4334 for (i, method) in methods.iter().enumerate() {
4335 if i > 0 {
4336 self.buf.push('\n');
4337 }
4338 self.emit_class_method(method)?;
4339 }
4340 }
4341 self.indent -= 1;
4342 Ok(())
4343 }
4344 NodeKind::ImplBlock {
4345 trait_path,
4346 target,
4347 methods,
4348 ..
4349 } => {
4350 let target_name = self.type_expr_to_string(target);
4351 if let Some(tp) = trait_path {
4352 let trait_name = tp
4353 .segments
4354 .iter()
4355 .map(|s| s.name.as_str())
4356 .collect::<Vec<_>>()
4357 .join(".");
4358 self.writeln(&format!("# impl {trait_name} for {target_name}"));
4359 } else {
4360 self.writeln(&format!("# impl {target_name}"));
4361 }
4362 for method in methods {
4363 if let NodeKind::FnDecl {
4364 is_async,
4365 name,
4366 params,
4367 return_type,
4368 effect_clause,
4369 body,
4370 ..
4371 } = &method.kind
4372 {
4373 if *is_async {
4374 self.needs_asyncio_import = true;
4375 }
4376 let async_kw = if *is_async { "async " } else { "" };
4377 let rest = match params.first().map(crate::generator::param_binds_self) {
4378 Some(Some(_)) => ¶ms[1..],
4379 _ => ¶ms[..],
4380 };
4381 let param_strs = self.collect_param_strs(rest);
4382 let effects = self.effects_params(effect_clause);
4383 let mut all_params = vec!["self".to_string()];
4384 all_params.extend(param_strs);
4385 all_params.extend(effects);
4386 let ret = return_type
4387 .as_deref()
4388 .map(|t| format!(" -> {}", self.type_to_py(t)))
4389 .unwrap_or_default();
4390 // Rename a field-colliding method consistently with the
4391 // inlined-impl path (`emit_class_method`) and call sites.
4392 let fn_name = self.py_method_name(&name.name);
4393 self.writeln(&format!(
4394 "{async_kw}def {fn_name}({}){}:",
4395 all_params.join(", "),
4396 ret,
4397 ));
4398 self.indent += 1;
4399 let old_handler_vars = self.current_handler_vars.clone();
4400 let expanded = self.expand_effect_names(effect_clause);
4401 for ename in &expanded {
4402 self.current_handler_vars
4403 .insert(ename.clone(), to_snake_case(ename));
4404 }
4405 self.emit_block_body(body)?;
4406 self.current_handler_vars = old_handler_vars;
4407 self.indent -= 1;
4408 }
4409 }
4410 Ok(())
4411 }
4412 NodeKind::EffectDecl {
4413 name,
4414 components,
4415 operations,
4416 ..
4417 } => {
4418 if !components.is_empty() {
4419 let comp_names: Vec<String> = components
4420 .iter()
4421 .map(|tp| {
4422 tp.segments
4423 .last()
4424 .map_or("effect".to_string(), |s| s.name.clone())
4425 })
4426 .collect();
4427 self.writeln(&format!(
4428 "# composite effect {} = {}",
4429 name.name,
4430 comp_names.join(" + ")
4431 ));
4432 self.composite_effects.insert(name.name.clone(), comp_names);
4433 return Ok(());
4434 }
4435 // Record effect operations for Call → handler.op rewriting.
4436 for op in operations {
4437 if let NodeKind::FnDecl { name: op_name, .. } = &op.kind {
4438 self.effect_ops
4439 .insert(op_name.name.clone(), name.name.clone());
4440 }
4441 }
4442 // Effects → abstract base class with @abstractmethod.
4443 self.needs_abc_import = true;
4444 self.writeln(&format!("class {}(ABC):", name.name));
4445 self.indent += 1;
4446 if operations.is_empty() {
4447 self.writeln("pass");
4448 } else {
4449 for (i, op) in operations.iter().enumerate() {
4450 if i > 0 {
4451 self.buf.push('\n');
4452 }
4453 if let NodeKind::FnDecl {
4454 name,
4455 params,
4456 return_type,
4457 ..
4458 } = &op.kind
4459 {
4460 self.writeln("@abstractmethod");
4461 let rest = match params.first().map(crate::generator::param_binds_self)
4462 {
4463 Some(Some(_)) => ¶ms[1..],
4464 _ => ¶ms[..],
4465 };
4466 let param_strs = self.collect_param_strs(rest);
4467 let mut all_params = vec!["self".to_string()];
4468 all_params.extend(param_strs);
4469 let ret = return_type
4470 .as_deref()
4471 .map(|t| format!(" -> {}", self.type_to_py(t)))
4472 .unwrap_or_default();
4473 let fn_name = to_snake_case(&name.name);
4474 self.writeln(&format!(
4475 "def {fn_name}({}){}:",
4476 all_params.join(", "),
4477 ret,
4478 ));
4479 self.indent += 1;
4480 self.writeln("...");
4481 self.indent -= 1;
4482 }
4483 }
4484 }
4485 self.indent -= 1;
4486 Ok(())
4487 }
4488 NodeKind::TypeAlias { name, .. } => {
4489 self.writeln(&format!("# type {} = ...", name.name));
4490 Ok(())
4491 }
4492 NodeKind::ConstDecl {
4493 name, value, ty, ..
4494 } => {
4495 let type_hint = format!(": {}", self.type_to_py(ty));
4496 let ind = self.indent_str();
4497 // Emit the const's declared name verbatim (not snake_cased) so it
4498 // matches the verbatim spelling the `Identifier` use-site arm emits
4499 // for a known const — `to_snake_case` would lower `FIZZ_NUM` to
4500 // `fizz_num` here while the use site keeps `FIZZ_NUM`, a `NameError`.
4501 let _ = write!(self.buf, "{ind}{}{type_hint} = ", name.name);
4502 self.emit_expr(value)?;
4503 self.buf.push('\n');
4504 Ok(())
4505 }
4506 NodeKind::ModuleHandle { effect, handler } => {
4507 // Emit `__<effect>: Effect = Handler()` at module scope and
4508 // register it as the default handler. Effectful calls later
4509 // in the module will pick it up via `current_handler_vars`
4510 // unless a local handling block overrides it.
4511 let effect_name = effect.segments.last().map_or("effect", |s| s.name.as_str());
4512 let var_name = format!("__{}", to_snake_case(effect_name));
4513 let ind = self.indent_str();
4514 let _ = write!(self.buf, "{ind}{var_name}: {effect_name} = ");
4515 self.emit_expr(handler)?;
4516 self.buf.push('\n');
4517 self.current_handler_vars
4518 .insert(effect_name.to_string(), var_name);
4519 Ok(())
4520 }
4521 NodeKind::PropertyTest { name, body, .. } => {
4522 self.writeln(&format!("# property test: {name}"));
4523 self.writeln("# (property tests are not emitted in Python output)");
4524 let _ = body;
4525 Ok(())
4526 }
4527 // Statement / expression nodes at top level:
4528 NodeKind::LetBinding { .. }
4529 | NodeKind::If { .. }
4530 | NodeKind::For { .. }
4531 | NodeKind::While { .. }
4532 | NodeKind::Loop { .. }
4533 | NodeKind::Return { .. }
4534 | NodeKind::Break { .. }
4535 | NodeKind::Continue
4536 | NodeKind::Guard { .. }
4537 | NodeKind::Match { .. }
4538 | NodeKind::Block { .. }
4539 | NodeKind::HandlingBlock { .. }
4540 | NodeKind::Assign { .. } => self.emit_stmt(node),
4541 // Expression nodes that appear as statements:
4542 _ => {
4543 self.write_indent();
4544 self.emit_expr(node)?;
4545 self.buf.push('\n');
4546 Ok(())
4547 }
4548 }
4549 }
4550
4551 // ── Generic type parameters ─────────────────────────────────────────────
4552
4553 /// Emit `T = TypeVar("T")` for each generic parameter not already declared,
4554 /// deduped within the file via [`Self::emitted_typevars`]. A param
4555 /// with a single bound (`T: Comparable`) becomes
4556 /// `T = TypeVar("T", bound=Comparable)` so static checkers see the
4557 /// constraint; multiple bounds collapse to the first (Python `TypeVar`
4558 /// takes one `bound=`). Sets [`Self::needs_typing_typevar`] when anything is
4559 /// emitted so the preamble imports `TypeVar`/`Generic`.
4560 fn emit_typevars(&mut self, generic_params: &[bock_ast::GenericParam]) {
4561 for gp in generic_params {
4562 let name = gp.name.name.clone();
4563 if !self.emitted_typevars.insert(name.clone()) {
4564 continue;
4565 }
4566 self.needs_typing_typevar.set(true);
4567 // A bound becomes `bound=<Name>`. Python's `TypeVar` accepts a
4568 // single `bound`; if Bock ever allows several, the first wins and
4569 // the rest are dropped (a static-checker approximation only —
4570 // Python erases generics at runtime regardless). A compiler-provided
4571 // sealed-core bound (`Equatable`/…) with no user `impl` is dropped
4572 // entirely: there is no such Python class, so `bound=Equatable` raises
4573 // `NameError` at def time (GAP-C). The `.eq`/`.compare` call is lowered
4574 // to a native operator by `try_emit_trait_bound_bridge`.
4575 let bound = gp
4576 .bounds
4577 .first()
4578 .and_then(|tp| tp.segments.last())
4579 .filter(|seg| {
4580 !crate::generator::is_unimplemented_sealed_core_trait(
4581 &seg.name,
4582 &self.trait_decls,
4583 )
4584 })
4585 .map(|seg| format!(", bound={}", self.map_type_name(&seg.name)))
4586 .unwrap_or_default();
4587 self.writeln(&format!("{name} = TypeVar(\"{name}\"{bound})"));
4588 }
4589 }
4590
4591 /// Build the `Generic[T, …]` base-class fragment for a generic decl. Returns
4592 /// an empty `Vec` when there are no type parameters. Also sets
4593 /// [`Self::needs_typing_typevar`] (the typevars are emitted separately by
4594 /// [`Self::emit_typevars`]).
4595 fn generic_base(&self, generic_params: &[bock_ast::GenericParam]) -> Vec<String> {
4596 if generic_params.is_empty() {
4597 return Vec::new();
4598 }
4599 self.needs_typing_typevar.set(true);
4600 let names: Vec<String> = generic_params
4601 .iter()
4602 .map(|gp| gp.name.name.clone())
4603 .collect();
4604 vec![format!("Generic[{}]", names.join(", "))]
4605 }
4606
4607 // ── Function declarations ───────────────────────────────────────────────
4608
4609 #[allow(clippy::too_many_arguments)]
4610 fn emit_fn_decl(
4611 &mut self,
4612 _visibility: Visibility,
4613 is_async: bool,
4614 name: &str,
4615 generic_params: &[bock_ast::GenericParam],
4616 params: &[AIRNode],
4617 return_type: Option<&AIRNode>,
4618 effect_clause: &[bock_ast::TypePath],
4619 body: &AIRNode,
4620 ) -> Result<(), CodegenError> {
4621 if is_async {
4622 self.needs_asyncio_import = true;
4623 }
4624 // A generic `fn f[T](…) -> T` references `T` in its param/return
4625 // annotations, which Python evaluates at def time — declare
4626 // `T = TypeVar("T")` first so those names resolve (DV12).
4627 self.emit_typevars(generic_params);
4628 let async_kw = if is_async { "async " } else { "" };
4629 let param_strs = self.collect_param_strs(params);
4630 let effects = self.effects_params(effect_clause);
4631 let mut all_params = param_strs;
4632 all_params.extend(effects);
4633 let ret = return_type
4634 .map(|t| format!(" -> {}", self.type_to_py(t)))
4635 .unwrap_or_default();
4636 if !effect_clause.is_empty() {
4637 let effect_names = self.expand_effect_names(effect_clause);
4638 self.fn_effects.insert(name.to_string(), effect_names);
4639 }
4640 let fn_name = py_value_ident(name);
4641 self.writeln(&format!(
4642 "{async_kw}def {fn_name}({}){}:",
4643 all_params.join(", "),
4644 ret,
4645 ));
4646 self.indent += 1;
4647 let old_handler_vars = self.current_handler_vars.clone();
4648 let expanded = self.expand_effect_names(effect_clause);
4649 for ename in &expanded {
4650 self.current_handler_vars
4651 .insert(ename.clone(), to_snake_case(ename));
4652 }
4653 // Seed the body's shadow frame with the parameters (so a body-level `let`
4654 // re-binding a param is a plain rebind, a nested-block one is renamed).
4655 self.pending_scope_seed = Self::param_value_names(params);
4656 // A function-body tail is the function's return value, even for a `fn`
4657 // *nested inside a loop body or a statement-`match`/`if` arm*: clear the discard
4658 // flags so this body returns its tail rather than dropping it.
4659 let prev_discard = std::mem::replace(&mut self.in_loop_body_tail, false);
4660 let prev_match = std::mem::replace(&mut self.in_stmt_construct_arm, false);
4661 let body_res = self.emit_fn_body(body);
4662 self.in_loop_body_tail = prev_discard;
4663 self.in_stmt_construct_arm = prev_match;
4664 body_res?;
4665 self.current_handler_vars = old_handler_vars;
4666 self.indent -= 1;
4667 Ok(())
4668 }
4669
4670 fn emit_class_method(&mut self, method: &AIRNode) -> Result<(), CodegenError> {
4671 if let NodeKind::FnDecl {
4672 is_async,
4673 name,
4674 params,
4675 return_type,
4676 effect_clause,
4677 body,
4678 ..
4679 } = &method.kind
4680 {
4681 if *is_async {
4682 self.needs_asyncio_import = true;
4683 }
4684 let async_kw = if *is_async { "async " } else { "" };
4685 // An associated function (no `self` receiver, e.g. a `From` impl's
4686 // `from`) is a `@staticmethod`: it is called as `Type.method(...)`
4687 // and takes no implicit `self`. A regular method takes `self`.
4688 let is_assoc = crate::generator::is_associated_impl_method(method, &self.effect_ops);
4689 // The AIR keeps `self` as a leading `Param`; Python methods need
4690 // exactly one explicit `self`. Skip the bound `self` param if
4691 // present so it isn't emitted twice (`def m(self, self)`).
4692 let rest = match params.first().map(crate::generator::param_binds_self) {
4693 Some(Some(_)) => ¶ms[1..],
4694 _ => ¶ms[..],
4695 };
4696 let param_strs = self.collect_param_strs(rest);
4697 let effects = self.effects_params(effect_clause);
4698 let mut all_params = if is_assoc {
4699 Vec::new()
4700 } else {
4701 vec!["self".to_string()]
4702 };
4703 all_params.extend(param_strs);
4704 all_params.extend(effects);
4705 let ret = return_type
4706 .as_deref()
4707 .map(|t| format!(" -> {}", self.type_to_py(t)))
4708 .unwrap_or_default();
4709 // A method whose name collides with a field is renamed (`message`
4710 // → `message_method`); the dataclass field would otherwise overwrite
4711 // the method attribute. Renamed identically at every call site.
4712 let fn_name = self.py_method_name(&name.name);
4713 if is_assoc {
4714 self.writeln("@staticmethod");
4715 }
4716 self.writeln(&format!(
4717 "{async_kw}def {fn_name}({}){}:",
4718 all_params.join(", "),
4719 ret,
4720 ));
4721 self.indent += 1;
4722 let old_handler_vars = self.current_handler_vars.clone();
4723 let expanded = self.expand_effect_names(effect_clause);
4724 for ename in &expanded {
4725 self.current_handler_vars
4726 .insert(ename.clone(), to_snake_case(ename));
4727 }
4728 // Seed the body frame with `self` (regular methods only) + the
4729 // method params (see `emit_fn_decl`). An associated `@staticmethod`
4730 // has no `self`.
4731 let mut seed = if is_assoc {
4732 Vec::new()
4733 } else {
4734 vec!["self".to_string()]
4735 };
4736 seed.extend(Self::param_value_names(rest));
4737 self.pending_scope_seed = seed;
4738 // A method body's tail is its return value — clear any enclosing
4739 // discard flags (loop-body or statement-`match`/`if` arm) so it returns
4740 // rather than dropping its tail.
4741 let prev_discard = std::mem::replace(&mut self.in_loop_body_tail, false);
4742 let prev_match = std::mem::replace(&mut self.in_stmt_construct_arm, false);
4743 let body_res = self.emit_fn_body(body);
4744 self.in_loop_body_tail = prev_discard;
4745 self.in_stmt_construct_arm = prev_match;
4746 body_res?;
4747 self.current_handler_vars = old_handler_vars;
4748 self.indent -= 1;
4749 }
4750 Ok(())
4751 }
4752
4753 /// Flatten the methods of a type's impl blocks into the emission order for a
4754 /// single Python class body, **deduplicating by method name** so the same
4755 /// `def` is never emitted twice into one class.
4756 ///
4757 /// A type can have both an inherent impl (`impl T { fn render }`) and a trait
4758 /// impl (`impl Trait for T { fn render }`) whose methods share a name. In
4759 /// Bock those are distinct (the trait method typically delegates to the
4760 /// inherent one via `self.render()`), but Python has a single per-class
4761 /// method namespace: emitting both means the second `def render` silently
4762 /// overwrites the first, and a delegating trait body (`return self.render()`)
4763 /// then calls *itself* — unbounded recursion (`RecursionError`, seen on
4764 /// react-components' `Button`). The **inherent** method is the concrete
4765 /// implementation and the one a direct `btn.render()` call resolves to, so it
4766 /// wins; a colliding trait method (which would only shadow it and recurse) is
4767 /// dropped. Method order is otherwise preserved (inherent impls, then trait
4768 /// impls, in source order).
4769 fn dedup_impl_methods<'a>(impls: &'a [AIRNode]) -> Vec<&'a AIRNode> {
4770 // Inherent impls (no trait_path) take precedence, so visit them first;
4771 // within each group, source order is preserved.
4772 let mut out: Vec<&'a AIRNode> = Vec::new();
4773 let mut seen: std::collections::HashSet<String> = std::collections::HashSet::new();
4774 let inherent_first = impls.iter().filter(|im| {
4775 matches!(
4776 &im.kind,
4777 NodeKind::ImplBlock {
4778 trait_path: None,
4779 ..
4780 }
4781 )
4782 });
4783 let trait_after = impls.iter().filter(|im| {
4784 matches!(
4785 &im.kind,
4786 NodeKind::ImplBlock {
4787 trait_path: Some(_),
4788 ..
4789 }
4790 )
4791 });
4792 for im in inherent_first.chain(trait_after) {
4793 if let NodeKind::ImplBlock { methods, .. } = &im.kind {
4794 for method in methods {
4795 if let NodeKind::FnDecl { name, .. } = &method.kind {
4796 if seen.insert(name.name.clone()) {
4797 out.push(method);
4798 }
4799 }
4800 }
4801 }
4802 }
4803 out
4804 }
4805
4806 /// The Python value-names a parameter list binds (simple `BindPat` params
4807 /// only). Used to seed the function/method body's shadow frame so a body
4808 /// `let` re-binding a param is a plain rebind, while a nested-block `let`
4809 /// shadowing the param is renamed.
4810 fn param_value_names(params: &[AIRNode]) -> Vec<String> {
4811 params
4812 .iter()
4813 .filter_map(|p| {
4814 if let NodeKind::Param { pattern, .. } = &p.kind {
4815 Self::simple_bind_name(pattern)
4816 } else {
4817 None
4818 }
4819 })
4820 .collect()
4821 }
4822
4823 /// Collect parameter strings for a `def`/method signature.
4824 ///
4825 /// Each emitted param carries its `: type` annotation and any default.
4826 /// Use [`Self::collect_param_strs_bare`] for `lambda` params, where Python
4827 /// forbids type annotations (`lambda x: int: body` is a `SyntaxError`).
4828 fn collect_param_strs(&self, params: &[AIRNode]) -> Vec<String> {
4829 self.collect_param_strs_inner(params, true)
4830 }
4831
4832 /// Collect bare parameter names (no `: type` annotations) for a `lambda`.
4833 ///
4834 /// A Python `lambda` parameter list cannot carry annotations: emitting one
4835 /// produces `lambda x: int: body`, where the first `:` ends the parameter
4836 /// list, so the type hint becomes a second, syntactically invalid `:`.
4837 fn collect_param_strs_bare(&self, params: &[AIRNode]) -> Vec<String> {
4838 self.collect_param_strs_inner(params, false)
4839 }
4840
4841 /// Shared implementation of [`Self::collect_param_strs`] and
4842 /// [`Self::collect_param_strs_bare`]. When `hints` is `false`, the `: type`
4843 /// annotation is omitted (required for `lambda` params).
4844 fn collect_param_strs_inner(&self, params: &[AIRNode], hints: bool) -> Vec<String> {
4845 params
4846 .iter()
4847 .filter_map(|p| {
4848 if let NodeKind::Param {
4849 pattern,
4850 ty,
4851 default,
4852 } = &p.kind
4853 {
4854 let name = to_snake_case(&self.pattern_to_binding_name(pattern));
4855 let type_hint = if hints {
4856 ty.as_ref()
4857 .map(|t| format!(": {}", self.type_to_py(t)))
4858 .unwrap_or_default()
4859 } else {
4860 String::new()
4861 };
4862 if let Some(def) = default {
4863 let mut ctx = PyEmitCtx::new();
4864 ctx.indent = self.indent;
4865 ctx.enum_variants = self.enum_variants.clone();
4866 if ctx.emit_expr(def).is_ok() {
4867 let def_str = ctx.buf;
4868 return Some(format!("{name}{type_hint} = {def_str}"));
4869 }
4870 }
4871 Some(format!("{name}{type_hint}"))
4872 } else {
4873 None
4874 }
4875 })
4876 .collect()
4877 }
4878
4879 /// Expand effect names, replacing composite effects with their components.
4880 fn expand_effect_names(&self, effects: &[bock_ast::TypePath]) -> Vec<String> {
4881 let mut result = Vec::new();
4882 for tp in effects {
4883 let name = tp
4884 .segments
4885 .last()
4886 .map_or("effect".to_string(), |s| s.name.clone());
4887 if let Some(components) = self.composite_effects.get(&name) {
4888 result.extend(components.iter().cloned());
4889 } else {
4890 result.push(name);
4891 }
4892 }
4893 result
4894 }
4895
4896 /// Effects → keyword arguments: `*, log: Log, clock: Clock`.
4897 fn effects_params(&self, effects: &[bock_ast::TypePath]) -> Vec<String> {
4898 if effects.is_empty() {
4899 return vec![];
4900 }
4901 let expanded = self.expand_effect_names(effects);
4902 let mut result = vec!["*".to_string()];
4903 for name in &expanded {
4904 let param_name = to_snake_case(name);
4905 result.push(format!("{param_name}: {name}"));
4906 }
4907 result
4908 }
4909
4910 /// The in-scope `Clock` effect handler variable, if one is installed.
4911 ///
4912 /// Returns the emitted name of the handler bound for the `Clock` effect at
4913 /// the current point (a `with Clock` parameter such as `clock`, or a
4914 /// `handling (Clock with ...)` block's synthesised `__clock_hN`). When this
4915 /// is `Some`, the `Clock` time operations (`Instant.now`, `sleep`, `elapsed`)
4916 /// are routed through the handler instead of inlining the host primitive
4917 /// (Q-clock-handler-routing, §18.3.1/§18.4); when `None`, no handler is in
4918 /// scope and the default host primitive is emitted.
4919 fn clock_handler_var(&self) -> Option<&str> {
4920 self.current_handler_vars.get("Clock").map(String::as_str)
4921 }
4922
4923 /// Build `effect=handler_var, ...` keyword arguments for calling an effectful function.
4924 fn build_effects_call_args_py(&self, fn_name: &str) -> Option<String> {
4925 let effects = self.fn_effects.get(fn_name)?;
4926 let entries: Vec<String> = effects
4927 .iter()
4928 .filter_map(|e| {
4929 let handler_var = self.current_handler_vars.get(e)?;
4930 let param_name = to_snake_case(e);
4931 Some(format!("{param_name}={handler_var}"))
4932 })
4933 .collect();
4934 if entries.is_empty() {
4935 return None;
4936 }
4937 Some(entries.join(", "))
4938 }
4939
4940 // ── Enum variant factories ──────────────────────────────────────────────
4941
4942 fn emit_enum_variant(
4943 &mut self,
4944 enum_name: &str,
4945 variant: &AIRNode,
4946 ) -> Result<(), CodegenError> {
4947 if let NodeKind::EnumVariant { name, payload } = &variant.kind {
4948 let vname = &name.name;
4949 match payload {
4950 EnumVariantPayload::Unit => {
4951 self.writeln("@dataclass(frozen=True)");
4952 self.writeln(&format!("class {enum_name}_{vname}:"));
4953 self.indent += 1;
4954 self.writeln(&format!("_tag: str = \"{vname}\""));
4955 self.indent -= 1;
4956 }
4957 EnumVariantPayload::Struct(fields) => {
4958 self.writeln("@dataclass");
4959 self.writeln(&format!("class {enum_name}_{vname}:"));
4960 self.indent += 1;
4961 for f in fields {
4962 let type_hint = self.ast_type_to_py(&f.ty);
4963 // `py_field_ident`: keyword-named payload fields
4964 // (`lambda`) must be escaped (`lambda_`).
4965 self.writeln(&format!("{}: {type_hint}", py_field_ident(&f.name.name)));
4966 }
4967 self.writeln(&format!("_tag: str = \"{vname}\""));
4968 self.indent -= 1;
4969 }
4970 EnumVariantPayload::Tuple(elems) => {
4971 self.writeln("@dataclass");
4972 self.writeln(&format!("class {enum_name}_{vname}:"));
4973 self.indent += 1;
4974 for (i, elem) in elems.iter().enumerate() {
4975 let type_hint = self.type_to_py(elem);
4976 self.writeln(&format!("_{i}: {type_hint}"));
4977 }
4978 self.writeln(&format!("_tag: str = \"{vname}\""));
4979 self.indent -= 1;
4980 }
4981 }
4982 }
4983 Ok(())
4984 }
4985
4986 // ── Statements ──────────────────────────────────────────────────────────
4987
4988 fn emit_stmt(&mut self, node: &AIRNode) -> Result<(), CodegenError> {
4989 match &node.kind {
4990 NodeKind::LetBinding {
4991 pattern, value, ty, ..
4992 } => {
4993 // Nested-block `let`-shadow handling (simple `BindPat` only): a
4994 // binding that shadows an enclosing-scope name is renamed to a
4995 // fresh alias so Python's function-scoped `=` doesn't stomp the
4996 // outer binding. The rename is *planned* now (so the LHS uses the
4997 // alias) but *committed* only after the RHS is emitted — the RHS
4998 // reads the prior binding (`let y = y + 10` reads the outer `y`).
4999 let raw_name = Self::simple_bind_name(pattern);
5000 let (binding, pending) = match &raw_name {
5001 Some(n) => self.plan_shadow_let(n),
5002 None => (self.pattern_to_py_binding(pattern), None),
5003 };
5004 let type_hint = ty
5005 .as_ref()
5006 .map(|t| format!(": {}", self.type_to_py(t)))
5007 .unwrap_or_default();
5008 // Declare-only temp from the shared value-CF hoist: Python has no
5009 // declarations, so pre-bind `name = None`; the relocated control
5010 // flow that follows assigns it on every non-diverging path.
5011 if node.metadata.contains_key(crate::generator::DECL_ONLY_META) {
5012 let ind = self.indent_str();
5013 let _ = writeln!(self.buf, "{ind}{binding}{type_hint} = None");
5014 if let Some(n) = &raw_name {
5015 self.commit_shadow_let(n, pending);
5016 }
5017 return Ok(());
5018 }
5019 // Expression-position control flow (a value-`loop`, a `match`
5020 // with a diverging/statement arm, a statement-`if`) cannot be a
5021 // Python expression. Pre-declare the binding (so it is always
5022 // bound, including the diverging-arm path) and fill it in via
5023 // real statements. See `value_needs_stmt_form`.
5024 if value_needs_stmt_form(value) {
5025 let ind = self.indent_str();
5026 let _ = writeln!(self.buf, "{ind}{binding}{type_hint} = None");
5027 let r = self.emit_value_binding(&binding, value);
5028 if let Some(n) = &raw_name {
5029 self.commit_shadow_let(n, pending);
5030 }
5031 return r;
5032 }
5033 // `let x = todo()` — the value diverges (a `raise`), so it cannot
5034 // sit on the RHS of `=`. Emit the raise bare; the binding is never
5035 // reached.
5036 if is_raise_expr(value) {
5037 self.write_indent();
5038 self.emit_expr(value)?;
5039 self.buf.push('\n');
5040 return Ok(());
5041 }
5042 let ind = self.indent_str();
5043 let _ = write!(self.buf, "{ind}{binding}{type_hint} = ");
5044 let wrap_task = matches!(&value.kind, NodeKind::Call { .. })
5045 && self.task_bound_names.contains(&binding);
5046 if wrap_task {
5047 self.needs_asyncio_import = true;
5048 self.buf.push_str("asyncio.create_task(");
5049 self.emit_expr(value)?;
5050 self.buf.push(')');
5051 } else {
5052 self.emit_expr(value)?;
5053 }
5054 self.buf.push('\n');
5055 // Commit the rename only now — after the RHS read the prior binding.
5056 if let Some(n) = &raw_name {
5057 self.commit_shadow_let(n, pending);
5058 }
5059 Ok(())
5060 }
5061 NodeKind::If { .. } => {
5062 // Statement position: a mid-block `if`/`else` is a Unit
5063 // statement, so each branch's tail expression is discarded
5064 // (emitted as a bare expression statement) rather than
5065 // `return`ed — the same contract as the statement-`match` arm
5066 // below (Q-python-ifelse-truncation; sibling of the #259
5067 // chat-protocol truncation). Without the flag, a branch whose
5068 // body is a bare expression (`if c { println(..) }`) emitted
5069 // `return print(..)`, aborting the enclosing function after
5070 // the taken branch so every following statement was skipped.
5071 // Saved/restored, and cleared inside any nested value context
5072 // (a nested fn/method body, a value-binding hoist), so it
5073 // scopes only to this statement's own branch tails.
5074 let prev = std::mem::replace(&mut self.in_stmt_construct_arm, true);
5075 let r = self.emit_stmt_if(node, false);
5076 self.in_stmt_construct_arm = prev;
5077 r
5078 }
5079 NodeKind::For {
5080 pattern,
5081 iterable,
5082 body,
5083 } => {
5084 let binding = self.pattern_to_py_binding(pattern);
5085 let ind = self.indent_str();
5086 let _ = write!(self.buf, "{ind}for {binding} in ");
5087 self.emit_expr(iterable)?;
5088 self.buf.push_str(":\n");
5089 self.indent += 1;
5090 self.loop_value_targets.push(None);
5091 // Loop body is statement position: a tail expression is
5092 // discarded, not `return`ed (see `emit_loop_body`).
5093 self.emit_loop_body(body)?;
5094 self.loop_value_targets.pop();
5095 self.indent -= 1;
5096 Ok(())
5097 }
5098 NodeKind::While { condition, body } => {
5099 let ind = self.indent_str();
5100 let _ = write!(self.buf, "{ind}while ");
5101 self.emit_expr(condition)?;
5102 self.buf.push_str(":\n");
5103 self.indent += 1;
5104 self.loop_value_targets.push(None);
5105 // Loop body is statement position: a tail expression is
5106 // discarded, not `return`ed (see `emit_loop_body`).
5107 self.emit_loop_body(body)?;
5108 self.loop_value_targets.pop();
5109 self.indent -= 1;
5110 Ok(())
5111 }
5112 NodeKind::Loop { body } => {
5113 self.writeln("while True:");
5114 self.indent += 1;
5115 // Statement-position loop yields no value; push a `None` frame so
5116 // a bare `break` inside stays a bare `break` (and isn't mistaken
5117 // for an enclosing value-loop's break).
5118 self.loop_value_targets.push(None);
5119 // Loop body is statement position: a tail expression is
5120 // discarded, not `return`ed (see `emit_loop_body`).
5121 self.emit_loop_body(body)?;
5122 self.loop_value_targets.pop();
5123 self.indent -= 1;
5124 Ok(())
5125 }
5126 NodeKind::Return { value } => {
5127 if let Some(val) = value {
5128 let ind = self.indent_str();
5129 let _ = write!(self.buf, "{ind}return ");
5130 self.emit_expr(val)?;
5131 self.buf.push('\n');
5132 } else {
5133 self.writeln("return");
5134 }
5135 Ok(())
5136 }
5137 NodeKind::Break { value } => {
5138 if let Some(val) = value {
5139 // A value-`loop` hoisted by `emit_value_binding` records its
5140 // assignment target; `break <v>` lowers to `<target> = <v>`
5141 // then `break`. Python's `break` itself carries no value.
5142 if let Some(Some(target)) = self.loop_value_targets.last() {
5143 let target = target.clone();
5144 let ind = self.indent_str();
5145 let _ = write!(self.buf, "{ind}{target} = ");
5146 self.emit_expr(val)?;
5147 self.buf.push('\n');
5148 self.writeln("break");
5149 } else {
5150 // No value target in scope (statement-position loop):
5151 // record the value as a comment, then break.
5152 let ind = self.indent_str();
5153 let _ = write!(self.buf, "{ind}# break value: ");
5154 self.emit_expr(val)?;
5155 self.buf.push('\n');
5156 self.writeln("break");
5157 }
5158 } else {
5159 self.writeln("break");
5160 }
5161 Ok(())
5162 }
5163 NodeKind::Continue => {
5164 self.writeln("continue");
5165 Ok(())
5166 }
5167 NodeKind::Guard {
5168 let_pattern,
5169 condition,
5170 else_block,
5171 } => {
5172 // The guard `else` block is statement position. Per §8.4 it
5173 // must diverge (`return`/`break`/`continue`/`Never`) — a
5174 // diverging tail is a statement and unaffected by the discard
5175 // flag — but the checker does not currently enforce the
5176 // divergence (surfaced as OPEN with this fix), and for an
5177 // accepted non-diverging else every other backend (js/ts/go/
5178 // rust and the interpreter) falls through to the statements
5179 // after the guard. Without the flag the bare-expression tail
5180 // lowered to `return print(..)`, silently truncating the
5181 // function on Python alone — the same early-`return` family as
5182 // the statement `match`/`if` fixes above.
5183 let prev = std::mem::replace(&mut self.in_stmt_construct_arm, true);
5184 let r = self.emit_stmt_guard(let_pattern.as_deref(), condition, else_block);
5185 self.in_stmt_construct_arm = prev;
5186 r
5187 }
5188 NodeKind::Match { scrutinee, arms } => {
5189 // Statement position: a mid-block `match` is a Unit statement, so
5190 // each arm's tail expression is discarded (emitted as a bare
5191 // expression statement) rather than `return`ed. Without this, an
5192 // arm whose body is a bare expression (`Ok(m) => println(..)`)
5193 // emits `return println(..)`, aborting the enclosing function
5194 // after the matched arm — the chat-protocol truncation. The flag
5195 // is saved/restored and cleared inside any nested value context
5196 // (a nested fn/method body, a value-binding hoist), so it scopes
5197 // only to this match's own arm tails.
5198 let prev = std::mem::replace(&mut self.in_stmt_construct_arm, true);
5199 let r = self.emit_match(scrutinee, arms);
5200 self.in_stmt_construct_arm = prev;
5201 r
5202 }
5203 NodeKind::Block { stmts, tail } => {
5204 for s in stmts {
5205 self.emit_node(s)?;
5206 }
5207 if let Some(t) = tail {
5208 self.write_indent();
5209 self.emit_expr(t)?;
5210 self.buf.push('\n');
5211 }
5212 Ok(())
5213 }
5214 NodeKind::HandlingBlock { handlers, body } => {
5215 // handling block → handler variable bindings then body.
5216 // Each handling block gets a fresh numeric suffix so nested
5217 // blocks do not overwrite each other's handler variables —
5218 // Python has function scope, not block scope, for `=`, so
5219 // `__logger = X()` in an inner block would otherwise stomp
5220 // the outer binding permanently.
5221 let old_handler_vars = self.current_handler_vars.clone();
5222 self.handling_counter += 1;
5223 let suffix = format!("_h{}", self.handling_counter);
5224 for h in handlers {
5225 let effect_name = h
5226 .effect
5227 .segments
5228 .last()
5229 .map_or("effect", |s| s.name.as_str());
5230 let var_name = format!("__{}{suffix}", to_snake_case(effect_name));
5231 let ind = self.indent_str();
5232 let _ = write!(self.buf, "{ind}{var_name}: {effect_name} = ");
5233 self.emit_expr(&h.handler)?;
5234 self.buf.push('\n');
5235 self.current_handler_vars
5236 .insert(effect_name.to_string(), var_name);
5237 }
5238 if let NodeKind::Block { stmts, tail } = &body.kind {
5239 for s in stmts {
5240 self.emit_node(s)?;
5241 }
5242 if let Some(t) = tail {
5243 self.write_indent();
5244 self.emit_expr(t)?;
5245 self.buf.push('\n');
5246 }
5247 } else {
5248 self.emit_stmt(body)?;
5249 }
5250 self.current_handler_vars = old_handler_vars;
5251 Ok(())
5252 }
5253 NodeKind::Assign { op, target, value } => {
5254 let ind = self.indent_str();
5255 let _ = write!(self.buf, "{ind}");
5256 self.emit_expr(target)?;
5257 let op_str = match op {
5258 AssignOp::Assign => " = ",
5259 AssignOp::AddAssign => " += ",
5260 AssignOp::SubAssign => " -= ",
5261 AssignOp::MulAssign => " *= ",
5262 AssignOp::DivAssign => " /= ",
5263 AssignOp::RemAssign => " %= ",
5264 };
5265 self.buf.push_str(op_str);
5266 self.emit_expr(value)?;
5267 self.buf.push('\n');
5268 Ok(())
5269 }
5270 _ => {
5271 self.write_indent();
5272 self.emit_expr(node)?;
5273 self.buf.push('\n');
5274 Ok(())
5275 }
5276 }
5277 }
5278
5279 /// Emit a **statement-position** `if` (with its optional `else`/`else if`
5280 /// chain). `inline` is true for an `else if` continuation: the caller has
5281 /// already written this line's indentation plus the `el` prefix, so the
5282 /// emission starts at `if <cond>:` with no leading indent. (The previous
5283 /// code re-entered the generic statement emitter after writing `el`, which
5284 /// wrote its own indentation — `el if (…):`, a Python SyntaxError, for
5285 /// every mid-block `else if` chain. The tail-position twin
5286 /// [`Self::emit_tail_control_flow_inline`] already chained correctly.)
5287 ///
5288 /// The caller ([`Self::emit_stmt`]'s `If` arm) sets
5289 /// [`Self::in_stmt_construct_arm`] around the whole chain, so each branch
5290 /// body's bare-expression tail lowers to a bare statement, never a
5291 /// function-body `return` (Q-python-ifelse-truncation).
5292 fn emit_stmt_if(&mut self, node: &AIRNode, inline: bool) -> Result<(), CodegenError> {
5293 let NodeKind::If {
5294 let_pattern,
5295 condition,
5296 then_block,
5297 else_block,
5298 } = &node.kind
5299 else {
5300 // Defensive: only `If` nodes are routed here.
5301 return self.emit_stmt(node);
5302 };
5303 if let Some(pat) = let_pattern {
5304 // `if let` — bind first, then test. Never reached with `inline`
5305 // (an `else if let` continuation is emitted under a plain `else:`
5306 // below, because its binding statement needs its own line).
5307 let ind = self.indent_str();
5308 let binding = self.pattern_to_py_binding(pat);
5309 let _ = write!(self.buf, "{ind}{binding} = ");
5310 self.emit_expr(condition)?;
5311 self.buf.push('\n');
5312 self.writeln(&format!("if {binding} is not None:"));
5313 } else {
5314 if inline {
5315 self.buf.push_str("if ");
5316 } else {
5317 let ind = self.indent_str();
5318 let _ = write!(self.buf, "{ind}if ");
5319 }
5320 self.emit_expr(condition)?;
5321 self.buf.push_str(":\n");
5322 }
5323 self.indent += 1;
5324 self.emit_block_body(then_block)?;
5325 self.indent -= 1;
5326 if let Some(else_b) = else_block {
5327 if let NodeKind::If {
5328 let_pattern: nested_let,
5329 ..
5330 } = &else_b.kind
5331 {
5332 if nested_let.is_none() {
5333 // `else if` → `elif`: write the `el` prefix, then continue
5334 // on the same line.
5335 let ind = self.indent_str();
5336 let _ = write!(self.buf, "{ind}el");
5337 return self.emit_stmt_if(else_b, true);
5338 }
5339 // `else if let` needs its binding statement first — emit the
5340 // whole continuation indented under a plain `else:`.
5341 self.writeln("else:");
5342 self.indent += 1;
5343 let r = self.emit_stmt_if(else_b, false);
5344 self.indent -= 1;
5345 return r;
5346 }
5347 self.writeln("else:");
5348 self.indent += 1;
5349 self.emit_block_body(else_b)?;
5350 self.indent -= 1;
5351 }
5352 Ok(())
5353 }
5354
5355 /// Emit a **statement-position** `guard (cond) else { … }` /
5356 /// `guard (let PAT = EXPR) else { … }`. The caller ([`Self::emit_stmt`]'s
5357 /// `Guard` arm) sets [`Self::in_stmt_construct_arm`] around the call so a
5358 /// bare-expression tail in the `else` block lowers to a bare statement —
5359 /// see the rationale there; a spec-conforming diverging `else` (§8.4) is a
5360 /// statement tail and is emitted unchanged.
5361 fn emit_stmt_guard(
5362 &mut self,
5363 let_pattern: Option<&AIRNode>,
5364 condition: &AIRNode,
5365 else_block: &AIRNode,
5366 ) -> Result<(), CodegenError> {
5367 if let Some(pat) = let_pattern {
5368 // `guard (let PAT = EXPR) else { ELSE }` — a refutable
5369 // binding guard. Lower to a two-arm `match` so PAT's bindings
5370 // (e.g. `val` in `Ok(val)`) are extracted on success and stay
5371 // in scope after the guard (Python `match` bindings persist as
5372 // ordinary assignments); the `_` arm runs the diverging ELSE.
5373 let ind = self.indent_str();
5374 let _ = write!(self.buf, "{ind}match ");
5375 self.emit_expr(condition)?;
5376 self.buf.push_str(":\n");
5377 self.indent += 1;
5378 let ind = self.indent_str();
5379 let _ = write!(self.buf, "{ind}case ");
5380 self.emit_pattern(pat)?;
5381 self.buf.push_str(":\n");
5382 self.indent += 1;
5383 self.writeln("pass");
5384 self.indent -= 1;
5385 self.writeln("case _:");
5386 self.indent += 1;
5387 self.emit_block_body(else_block)?;
5388 self.indent -= 1;
5389 self.indent -= 1;
5390 return Ok(());
5391 }
5392 let ind = self.indent_str();
5393 let _ = write!(self.buf, "{ind}if not (");
5394 self.emit_expr(condition)?;
5395 self.buf.push_str("):\n");
5396 self.indent += 1;
5397 self.emit_block_body(else_block)?;
5398 self.indent -= 1;
5399 Ok(())
5400 }
5401
5402 // ── Expressions ─────────────────────────────────────────────────────────
5403
5404 fn emit_expr(&mut self, node: &AIRNode) -> Result<(), CodegenError> {
5405 match &node.kind {
5406 NodeKind::Literal { lit } => {
5407 match lit {
5408 Literal::Int(s) => self.buf.push_str(s),
5409 Literal::Float(s) => self.buf.push_str(s),
5410 Literal::Bool(b) => self.buf.push_str(if *b { "True" } else { "False" }),
5411 Literal::Char(s) => {
5412 self.buf.push('\'');
5413 self.buf.push_str(s);
5414 self.buf.push('\'');
5415 }
5416 Literal::String(s) => {
5417 self.buf.push('"');
5418 self.buf.push_str(&escape_py_string(s));
5419 self.buf.push('"');
5420 }
5421 Literal::Unit => self.buf.push_str("None"),
5422 }
5423 Ok(())
5424 }
5425 NodeKind::Identifier { name } => {
5426 // Bock's Optional `None` constructor must not collide with
5427 // Python's `None` keyword: it lowers to the `_bock_none`
5428 // singleton of the Optional runtime (see `OPTIONAL_RUNTIME_PY`).
5429 // Python's own `None` (Void/Unit) is emitted as a `Literal::Unit`,
5430 // not an identifier, so this rewrite is unambiguous.
5431 if name.name == "None" {
5432 self.buf.push_str("_bock_none");
5433 } else if let Some(variant) = crate::generator::ordering_variant(&name.name) {
5434 // Prelude `Ordering` variant → the Ordering-runtime singleton
5435 // (`_bock_less` / `_bock_equal` / `_bock_greater`). When the
5436 // `core.compare` enum decl is not among the reached modules,
5437 // the runtime stands in (mirrors `_bock_none`).
5438 self.buf.push_str(ordering_singleton_py(variant));
5439 } else if let Some(enum_name) = self
5440 .user_variant_for_name(&name.name)
5441 .map(|i| i.enum_name.clone())
5442 {
5443 // A unit-variant reference (`Empty`) → an instance of its
5444 // `@dataclass(frozen=True)` class: `Shape_Empty()`.
5445 let _ = write!(self.buf, "{enum_name}_{}()", name.name);
5446 } else if self.const_names.contains(&name.name) {
5447 // A module-scope `const` is emitted verbatim at its
5448 // declaration (see the `ConstDecl` arm); spell its use site
5449 // identically rather than through `identifier_to_py`.
5450 self.buf.push_str(&name.name);
5451 } else {
5452 // Resolve through the shadow-scope stack so a reference inside
5453 // a nested block reads the (renamed) shadowing binding while
5454 // code outside reads the original (see `ShadowScope`).
5455 let py = identifier_to_py(&name.name);
5456 self.buf.push_str(&self.resolve_shadow_name(&py));
5457 }
5458 Ok(())
5459 }
5460 NodeKind::BinaryOp { op, left, right } => {
5461 // Integer `/` and `%` (DQ23, §3.6). Python's native operators do
5462 // NOT match the contract: `//` *floors* (`-17 // 5 == -4`, the
5463 // ruling wants `-3`) and `%` follows floor division (`-17 % 5 == 3`,
5464 // the ruling wants `-2`); `int(a / b)` routes through lossy float
5465 // true-division and loses precision on large integers. Lower to an
5466 // integer-only IIFE that truncates toward zero (quotient magnitude
5467 // from `abs(a) // abs(b)`, sign from the operands) and gives a
5468 // dividend-sign remainder. The `//` / `%` inside still raise
5469 // `ZeroDivisionError` on a zero divisor — an abort — matching the
5470 // other targets.
5471 if matches!(op, BinOp::Div | BinOp::Rem) && crate::generator::is_int_arith(node) {
5472 let lam = if matches!(op, BinOp::Div) {
5473 "(lambda __a, __b: (abs(__a) // abs(__b)) * (1 if (__a < 0) == (__b < 0) else -1))("
5474 } else {
5475 "(lambda __a, __b: (abs(__a) % abs(__b)) * (1 if __a >= 0 else -1))("
5476 };
5477 self.buf.push_str(lam);
5478 self.emit_expr(left)?;
5479 self.buf.push_str(", ");
5480 self.emit_expr(right)?;
5481 self.buf.push(')');
5482 return Ok(());
5483 }
5484 // Ordering operators on a user `Comparable` type lower through the
5485 // type's `compare` (Python's `<` on two instances raises
5486 // `TypeError` unless they define `__lt__`). The returned `Ordering`
5487 // is one of the `_BockOrdering*` runtime singletons, tested with
5488 // `isinstance`: `a < b` ⇒ `isinstance(a.compare(b), …Less)`,
5489 // `a <= b` ⇒ `not isinstance(a.compare(b), …Greater)`, etc.
5490 if crate::generator::is_user_compare(node) {
5491 if let Some((tag, is_eq)) = crate::generator::user_compare_variant(*op) {
5492 let recv = self.expr_to_string(left)?;
5493 let other = self.expr_to_string(right)?;
5494 let class = ordering_class_py(tag);
5495 let neg = if is_eq { "" } else { "not " };
5496 let _ = write!(
5497 self.buf,
5498 "({neg}isinstance(({recv}).compare({other}), {class}))"
5499 );
5500 return Ok(());
5501 }
5502 }
5503 // DQ29 (§18.5): `==`/`!=` on a type with an explicit
5504 // `impl Equatable` dispatch through its `eq` method — Python's
5505 // native `==` is the dataclass-generated STRUCTURAL equality,
5506 // which would silently ignore the user's custom `eq`. The
5507 // structural/deep lanes stay native: dataclass `==`, `list`/
5508 // `dict`/`set`/`tuple` equality are already field-wise /
5509 // content-based (and `dict`/`set` order-independent).
5510 if matches!(op, BinOp::Eq | BinOp::Ne)
5511 && crate::generator::user_eq_kind(node) == Some("impl")
5512 {
5513 let recv = self.expr_to_string(left)?;
5514 let other = self.expr_to_string(right)?;
5515 let neg = if *op == BinOp::Ne { "not " } else { "" };
5516 let _ = write!(self.buf, "({neg}({recv}).eq({other}))");
5517 return Ok(());
5518 }
5519 self.buf.push('(');
5520 self.emit_expr(left)?;
5521 let op_str = match op {
5522 BinOp::Add => " + ",
5523 BinOp::Sub => " - ",
5524 BinOp::Mul => " * ",
5525 BinOp::Div => " / ",
5526 BinOp::Rem => " % ",
5527 BinOp::Pow => " ** ",
5528 BinOp::Eq => " == ",
5529 BinOp::Ne => " != ",
5530 BinOp::Lt => " < ",
5531 BinOp::Le => " <= ",
5532 BinOp::Gt => " > ",
5533 BinOp::Ge => " >= ",
5534 BinOp::And => " and ",
5535 BinOp::Or => " or ",
5536 BinOp::BitAnd => " & ",
5537 BinOp::BitOr => " | ",
5538 BinOp::BitXor => " ^ ",
5539 BinOp::Compose => " # compose ",
5540 BinOp::Is => " is ",
5541 };
5542 self.buf.push_str(op_str);
5543 self.emit_expr(right)?;
5544 self.buf.push(')');
5545 Ok(())
5546 }
5547 NodeKind::UnaryOp { op, operand } => {
5548 let op_str = match op {
5549 UnaryOp::Neg => "-",
5550 UnaryOp::Not => "not ",
5551 UnaryOp::BitNot => "~",
5552 };
5553 self.buf.push_str(op_str);
5554 self.emit_expr(operand)?;
5555 Ok(())
5556 }
5557 NodeKind::Call { callee, args, .. } => {
5558 if let Some(code) = self.map_prelude_call(callee, args)? {
5559 self.buf.push_str(&code);
5560 return Ok(());
5561 }
5562 // A call whose callee names a registered tuple variant is a
5563 // construction (`Rect(3.0, 4.0)` → `Shape_Rect(3.0, 4.0)`).
5564 // Handled here so the callee emits the bare class name, not the
5565 // unit-variant `Shape_Rect()` the `Identifier` arm would.
5566 if let NodeKind::Identifier { name } = &callee.kind {
5567 if let Some(enum_name) = self
5568 .user_variant_for_name(&name.name)
5569 .map(|i| i.enum_name.clone())
5570 {
5571 let _ = write!(self.buf, "{enum_name}_{}(", name.name);
5572 for (i, arg) in args.iter().enumerate() {
5573 if i > 0 {
5574 self.buf.push_str(", ");
5575 }
5576 self.emit_expr(&arg.value)?;
5577 }
5578 self.buf.push(')');
5579 return Ok(());
5580 }
5581 }
5582 if self.try_emit_time_assoc_call(callee, args)? {
5583 return Ok(());
5584 }
5585 if self.try_emit_time_desugared_method(node, callee, args)? {
5586 return Ok(());
5587 }
5588 if self.try_emit_concurrency_call(callee, args)? {
5589 return Ok(());
5590 }
5591 // Map/Set dispatch precedes the List recogniser so the
5592 // overlapping method names route by `recv_kind`, not by name.
5593 if self.try_emit_map_method(node, callee, args)? {
5594 return Ok(());
5595 }
5596 if self.try_emit_set_method(node, callee, args)? {
5597 return Ok(());
5598 }
5599 // String method dispatch runs *before* the List recogniser so the
5600 // overlapping `len`/`contains`/`is_empty` names route by the
5601 // checker's `recv_kind = "Primitive:String"`, not by name alone.
5602 if self.try_emit_string_method(node, callee, args)? {
5603 return Ok(());
5604 }
5605 // Numeric/Char/Bool primitive methods (`to_float`/`abs`/`sqrt`/…)
5606 // likewise route by the checker's `recv_kind = "Primitive:Int|…"`
5607 // before the generic fall-through, which would emit `n.to_float(n)`.
5608 if self.try_emit_numeric_method(node, callee, args)? {
5609 return Ok(());
5610 }
5611 if self.try_emit_list_mutating_method(node, callee, args)? {
5612 return Ok(());
5613 }
5614 if self.try_emit_list_inplace_mutator(node, callee, args)? {
5615 return Ok(());
5616 }
5617 if self.try_emit_list_method(node, callee, args)? {
5618 return Ok(());
5619 }
5620 if self.try_emit_list_functional_method(node, callee, args)? {
5621 return Ok(());
5622 }
5623 if self.try_emit_primitive_bridge(node, callee, args)? {
5624 return Ok(());
5625 }
5626 if self.try_emit_trait_bound_bridge(node, callee, args)? {
5627 return Ok(());
5628 }
5629 if self.try_emit_container_method(node, callee, args)? {
5630 return Ok(());
5631 }
5632 // Q-prim-assoc: a primitive associated-conversion call
5633 // (`Float.from(x)` / `Int.try_from(s)` / `String.from(c)`)
5634 // lowers to Python's native conversion. CRITICAL: `from` is a
5635 // Python keyword, so the static-member form below would emit
5636 // `Float.from_(...)` against an undefined `Float` — a hard error.
5637 if self.try_emit_primitive_conversion(node, callee, args)? {
5638 return Ok(());
5639 }
5640 // Associated-function call (`Type.method(args)` — stamped by the
5641 // lowerer, no `self` prepended) resolves to the `@staticmethod`
5642 // on the class. Emit `Type.method(args)` with the type name
5643 // preserved and the method name run through `py_method_name` (so a
5644 // keyword like `from` → `from_`, matching the `@staticmethod`
5645 // definition); the generic fall-through would snake-case the type
5646 // identifier into a non-existent value.
5647 if crate::generator::is_associated_call(node) {
5648 if let NodeKind::FieldAccess { object, field } = &callee.kind {
5649 if let NodeKind::Identifier { name: type_name } = &object.kind {
5650 let _ = write!(
5651 self.buf,
5652 "{}.{}",
5653 type_name.name,
5654 self.py_method_name(&field.name)
5655 );
5656 self.buf.push('(');
5657 for (i, arg) in args.iter().enumerate() {
5658 if i > 0 {
5659 self.buf.push_str(", ");
5660 }
5661 self.emit_expr(&arg.value)?;
5662 }
5663 self.buf.push(')');
5664 return Ok(());
5665 }
5666 }
5667 }
5668 // Desugared instance method call `Call(FieldAccess(recv, m),
5669 // [recv, ...rest])`: emit `recv.m(rest)` so the receiver binds
5670 // Python's `self` rather than being passed twice.
5671 if let Some((recv, method, rest)) =
5672 crate::generator::desugared_self_call(callee, args)
5673 {
5674 self.emit_expr(recv)?;
5675 let _ = write!(self.buf, ".{}", self.py_method_name(&method.name));
5676 self.buf.push('(');
5677 for (i, arg) in rest.iter().enumerate() {
5678 if i > 0 {
5679 self.buf.push_str(", ");
5680 }
5681 self.emit_expr(&arg.value)?;
5682 }
5683 self.buf.push(')');
5684 return Ok(());
5685 }
5686 // Rewrite bare effect operation calls: log(...) → handler.log(...)
5687 if let NodeKind::Identifier { name } = &callee.kind {
5688 if let Some(effect_name) = self.effect_ops.get(&name.name).cloned() {
5689 if let Some(handler_var) =
5690 self.current_handler_vars.get(&effect_name).cloned()
5691 {
5692 let _ =
5693 write!(self.buf, "{}.{}", handler_var, to_snake_case(&name.name));
5694 self.buf.push('(');
5695 for (i, arg) in args.iter().enumerate() {
5696 if i > 0 {
5697 self.buf.push_str(", ");
5698 }
5699 self.emit_expr(&arg.value)?;
5700 }
5701 self.buf.push(')');
5702 return Ok(());
5703 }
5704 }
5705 }
5706 // Pass handler args to effectful function calls.
5707 let effects_args = if let NodeKind::Identifier { name } = &callee.kind {
5708 self.build_effects_call_args_py(&name.name)
5709 } else {
5710 None
5711 };
5712 self.emit_callee(callee)?;
5713 self.buf.push('(');
5714 for (i, arg) in args.iter().enumerate() {
5715 if i > 0 {
5716 self.buf.push_str(", ");
5717 }
5718 self.emit_expr(&arg.value)?;
5719 }
5720 if let Some(ea) = effects_args {
5721 if !args.is_empty() {
5722 self.buf.push_str(", ");
5723 }
5724 self.buf.push_str(&ea);
5725 }
5726 self.buf.push(')');
5727 Ok(())
5728 }
5729 NodeKind::MethodCall {
5730 receiver,
5731 method,
5732 args,
5733 ..
5734 } => {
5735 if self.try_emit_time_method(receiver, &method.name, args)? {
5736 return Ok(());
5737 }
5738 self.emit_expr(receiver)?;
5739 let _ = write!(self.buf, ".{}", self.py_method_name(&method.name));
5740 self.buf.push('(');
5741 for (i, arg) in args.iter().enumerate() {
5742 if i > 0 {
5743 self.buf.push_str(", ");
5744 }
5745 self.emit_expr(&arg.value)?;
5746 }
5747 self.buf.push(')');
5748 Ok(())
5749 }
5750 NodeKind::FieldAccess { object, field } => {
5751 self.emit_expr(object)?;
5752 // `py_field_ident`: a keyword-named field (`pass`) reads as the
5753 // escaped attribute (`t.pass_`) — `t.pass` is a SyntaxError —
5754 // matching the escaped dataclass/`__init__` declaration.
5755 let _ = write!(self.buf, ".{}", py_field_ident(&field.name));
5756 Ok(())
5757 }
5758 NodeKind::Index { object, index } => {
5759 self.emit_expr(object)?;
5760 self.buf.push('[');
5761 self.emit_expr(index)?;
5762 self.buf.push(']');
5763 Ok(())
5764 }
5765 NodeKind::Lambda { params, body } => {
5766 // Python `lambda` params take no type annotations — see
5767 // `collect_param_strs_bare`.
5768 let param_strs = self.collect_param_strs_bare(params);
5769 let _ = write!(self.buf, "lambda {}: ", param_strs.join(", "));
5770 self.emit_expr(body)?;
5771 Ok(())
5772 }
5773 NodeKind::Pipe { left, right } => self.emit_pipe(left, right),
5774 NodeKind::Compose { left, right } => {
5775 // `f >> g` → `(lambda x: g(f(x)))`. The whole lambda is wrapped
5776 // so that a nested compose — emitted here as a `lambda x: ...`
5777 // for `left`/`right` — is itself parenthesized before the
5778 // `(x)` call is appended; otherwise Python binds the `(x)` to
5779 // the inner lambda's body rather than invoking it. (In practice
5780 // the AIR lowers `>>` to a `Lambda` before codegen, so this arm
5781 // is a defensive fall-through; `emit_callee` covers the lowered
5782 // form.)
5783 self.buf.push_str("(lambda x: ");
5784 self.emit_callee(right)?;
5785 self.buf.push('(');
5786 self.emit_callee(left)?;
5787 self.buf.push_str("(x)))");
5788 Ok(())
5789 }
5790 NodeKind::Await { expr } => {
5791 self.buf.push_str("(await ");
5792 self.emit_expr(expr)?;
5793 self.buf.push(')');
5794 Ok(())
5795 }
5796 NodeKind::Propagate { expr } => {
5797 // `expr?` → `_bock_try(expr)`: unwrap the `Ok`/`Some` payload, or
5798 // raise the `_BockPropagate` sentinel (carrying the `Err`/`None`)
5799 // that the enclosing function's `try/except` re-returns. The wrap
5800 // is installed by `emit_fn_body_with_propagate` for any function or
5801 // method whose body contains a `?` (see `body_contains_propagate`).
5802 self.buf.push_str("_bock_try(");
5803 self.emit_expr(expr)?;
5804 self.buf.push(')');
5805 Ok(())
5806 }
5807 NodeKind::Range { lo, hi, inclusive } => {
5808 self.buf.push_str("range(");
5809 self.emit_expr(lo)?;
5810 self.buf.push_str(", ");
5811 self.emit_expr(hi)?;
5812 if *inclusive {
5813 self.buf.push_str(" + 1");
5814 }
5815 self.buf.push(')');
5816 Ok(())
5817 }
5818 NodeKind::RecordConstruct {
5819 path,
5820 fields,
5821 spread,
5822 } => {
5823 // A struct-variant construction (`Circle { radius: 2.0 }`) → an
5824 // instance of the `{enum}_{variant}` dataclass, built with
5825 // keyword args (`Shape_Circle(radius=2.0)`). Plain records keep
5826 // their dotted path name.
5827 let type_name = if let Some(info) = self.user_variant_for_path(path) {
5828 let variant = path.segments.last().map_or("", |s| s.name.as_str());
5829 format!("{}_{variant}", info.enum_name)
5830 } else {
5831 path.segments
5832 .iter()
5833 .map(|s| s.name.as_str())
5834 .collect::<Vec<_>>()
5835 .join(".")
5836 };
5837 // `py_field_ident` throughout: keyword-named fields (`pass`)
5838 // construct through their escaped spelling — kwargs
5839 // (`Tally(pass_=7)`) and spread dict keys (`"pass_": 9`) must
5840 // match the escaped dataclass field, and `pass=7` is a
5841 // SyntaxError besides. A shorthand field's *value* is the
5842 // same-named value binding, whose spelling `py_value_ident`
5843 // escapes identically.
5844 if let Some(sp) = spread {
5845 // Spread: create dict, update, then construct
5846 self.buf.push_str(&format!("{type_name}(**{{**vars("));
5847 self.emit_expr(sp)?;
5848 self.buf.push_str("), ");
5849 for (i, f) in fields.iter().enumerate() {
5850 if i > 0 {
5851 self.buf.push_str(", ");
5852 }
5853 let _ = write!(self.buf, "\"{}\": ", py_field_ident(&f.name.name));
5854 if let Some(val) = &f.value {
5855 self.emit_expr(val)?;
5856 } else {
5857 self.buf.push_str(&py_value_ident(&f.name.name));
5858 }
5859 }
5860 self.buf.push_str("})");
5861 } else {
5862 self.buf.push_str(&type_name);
5863 self.buf.push('(');
5864 for (i, f) in fields.iter().enumerate() {
5865 if i > 0 {
5866 self.buf.push_str(", ");
5867 }
5868 let _ = write!(self.buf, "{}=", py_field_ident(&f.name.name));
5869 if let Some(val) = &f.value {
5870 self.emit_expr(val)?;
5871 } else {
5872 self.buf.push_str(&py_value_ident(&f.name.name));
5873 }
5874 }
5875 self.buf.push(')');
5876 }
5877 Ok(())
5878 }
5879 NodeKind::ListLiteral { elems } => {
5880 self.buf.push('[');
5881 for (i, e) in elems.iter().enumerate() {
5882 if i > 0 {
5883 self.buf.push_str(", ");
5884 }
5885 self.emit_expr(e)?;
5886 }
5887 self.buf.push(']');
5888 Ok(())
5889 }
5890 NodeKind::MapLiteral { entries } => {
5891 self.buf.push('{');
5892 for (i, entry) in entries.iter().enumerate() {
5893 if i > 0 {
5894 self.buf.push_str(", ");
5895 }
5896 self.emit_expr(&entry.key)?;
5897 self.buf.push_str(": ");
5898 self.emit_expr(&entry.value)?;
5899 }
5900 self.buf.push('}');
5901 Ok(())
5902 }
5903 NodeKind::SetLiteral { elems } => {
5904 if elems.is_empty() {
5905 self.buf.push_str("set()");
5906 } else {
5907 self.buf.push('{');
5908 for (i, e) in elems.iter().enumerate() {
5909 if i > 0 {
5910 self.buf.push_str(", ");
5911 }
5912 self.emit_expr(e)?;
5913 }
5914 self.buf.push('}');
5915 }
5916 Ok(())
5917 }
5918 NodeKind::TupleLiteral { elems } => {
5919 self.buf.push('(');
5920 for (i, e) in elems.iter().enumerate() {
5921 if i > 0 {
5922 self.buf.push_str(", ");
5923 }
5924 self.emit_expr(e)?;
5925 }
5926 if elems.len() == 1 {
5927 self.buf.push(',');
5928 }
5929 self.buf.push(')');
5930 Ok(())
5931 }
5932 NodeKind::Interpolation { parts } => {
5933 let has_newline = parts.iter().any(|p| {
5934 matches!(p,
5935 AirInterpolationPart::Literal(s) if s.contains('\n')
5936 )
5937 });
5938 if has_newline {
5939 self.buf.push_str("f\"\"\"");
5940 } else {
5941 self.buf.push_str("f\"");
5942 }
5943 for part in parts {
5944 match part {
5945 AirInterpolationPart::Literal(s) => {
5946 if has_newline {
5947 self.buf.push_str(&escape_fstring_triple(s));
5948 } else {
5949 self.buf.push_str(&escape_fstring(s));
5950 }
5951 }
5952 AirInterpolationPart::Expr(expr) => {
5953 self.buf.push('{');
5954 // A `Bool`-typed part must print the canonical
5955 // lowercase `true`/`false` (§3.5); a bare `f"{b}"`
5956 // would print Python's `True`/`False`. The checker
5957 // stamps such parts (`is_bool_stringify`); map them
5958 // through a lowercasing conditional expression.
5959 if crate::generator::is_bool_stringify(expr) {
5960 self.buf.push_str("'true' if (");
5961 self.emit_expr(expr)?;
5962 self.buf.push_str(") else 'false'");
5963 } else {
5964 // Q-displayable-interpolation-dispatch: render
5965 // through `_bock_str` so a user value with a
5966 // `Displayable` impl (its `to_string` method)
5967 // shows via that method, not the dataclass
5968 // `repr`. Primitives fall back to `str(x)`.
5969 self.needs_runtime_str = true;
5970 self.buf.push_str("_bock_str(");
5971 self.emit_expr(expr)?;
5972 self.buf.push(')');
5973 }
5974 self.buf.push('}');
5975 }
5976 }
5977 }
5978 if has_newline {
5979 self.buf.push_str("\"\"\"");
5980 } else {
5981 self.buf.push('"');
5982 }
5983 Ok(())
5984 }
5985 NodeKind::Placeholder => {
5986 self.buf.push('_');
5987 Ok(())
5988 }
5989 NodeKind::Unreachable => {
5990 self.buf.push_str("raise RuntimeError(\"unreachable\")");
5991 Ok(())
5992 }
5993 NodeKind::ResultConstruct { variant, value } => {
5994 // Construct the Result-runtime classes (`_BockOk`/`_BockErr`) —
5995 // the same shape the surface `Ok(..)`/`Err(..)` construction and
5996 // the `case _BockOk(..)`/`_BockErr(..)` match use. The old
5997 // dict-with-`value`/`error`-keys shape disagreed with the match
5998 // (which reads the runtime classes), so reconcile on the classes.
5999 let cls = match variant {
6000 ResultVariant::Ok => "_BockOk",
6001 ResultVariant::Err => "_BockErr",
6002 };
6003 let _ = write!(self.buf, "{cls}(");
6004 if let Some(v) = value {
6005 self.emit_expr(v)?;
6006 } else {
6007 self.buf.push_str("None");
6008 }
6009 self.buf.push(')');
6010 Ok(())
6011 }
6012 NodeKind::Assign { op, target, value } => {
6013 self.emit_expr(target)?;
6014 let op_str = match op {
6015 AssignOp::Assign => " = ",
6016 AssignOp::AddAssign => " += ",
6017 AssignOp::SubAssign => " -= ",
6018 AssignOp::MulAssign => " *= ",
6019 AssignOp::DivAssign => " /= ",
6020 AssignOp::RemAssign => " %= ",
6021 };
6022 self.buf.push_str(op_str);
6023 self.emit_expr(value)?;
6024 Ok(())
6025 }
6026 NodeKind::If {
6027 condition,
6028 then_block,
6029 else_block,
6030 ..
6031 } => {
6032 // Ternary for expression-position if.
6033 self.buf.push('(');
6034 self.emit_block_as_expr(then_block)?;
6035 self.buf.push_str(" if ");
6036 self.emit_expr(condition)?;
6037 self.buf.push_str(" else ");
6038 if let Some(eb) = else_block {
6039 self.emit_block_as_expr(eb)?;
6040 } else {
6041 self.buf.push_str("None");
6042 }
6043 self.buf.push(')');
6044 Ok(())
6045 }
6046 NodeKind::Block { stmts, tail } => {
6047 // Blocks in expression position. Python `lambda` bodies are
6048 // expression-only, so leading statements can't live inside an
6049 // IIFE the way they do in JS. When every leading statement is a
6050 // pure-expressible `let`/expression statement we fold them into
6051 // immediately-applied lambdas (`try_emit_block_stmts_as_expr`)
6052 // so their effects run and their bindings reach the tail.
6053 if stmts.is_empty() {
6054 if let Some(t) = tail {
6055 return self.emit_expr(t);
6056 }
6057 } else if self.try_emit_block_stmts_as_expr(stmts, tail.as_deref())? {
6058 return Ok(());
6059 }
6060 // Fallback for shapes the fold can't model (mutable `let`,
6061 // assignment, loops): wrap the tail alone (best effort).
6062 self.buf.push_str("(lambda: ");
6063 if let Some(t) = tail {
6064 self.emit_expr(t)?;
6065 } else {
6066 self.buf.push_str("None");
6067 }
6068 self.buf.push_str(")()");
6069 Ok(())
6070 }
6071 NodeKind::Match { scrutinee, arms } => {
6072 // Match in expression position: not directly supported in Python.
6073 // Emit as IIFE-like lambda with internal match.
6074 // For simplicity, try to emit as a series of ternary if-else.
6075 self.buf.push_str("(lambda __v: ");
6076 self.emit_match_expr(scrutinee, arms)?;
6077 self.buf.push_str(")(");
6078 self.emit_expr(scrutinee)?;
6079 self.buf.push(')');
6080 Ok(())
6081 }
6082 // Ownership nodes: erase in Python.
6083 NodeKind::Move { expr }
6084 | NodeKind::Borrow { expr }
6085 | NodeKind::MutableBorrow { expr } => self.emit_expr(expr),
6086 // Effect operation invocation.
6087 NodeKind::EffectOp {
6088 effect,
6089 operation,
6090 args,
6091 } => {
6092 let effect_name = effect.segments.last().map_or("effect", |s| s.name.as_str());
6093 let _ = write!(
6094 self.buf,
6095 "{}.{}",
6096 to_snake_case(effect_name),
6097 to_snake_case(&operation.name)
6098 );
6099 self.buf.push('(');
6100 for (i, arg) in args.iter().enumerate() {
6101 if i > 0 {
6102 self.buf.push_str(", ");
6103 }
6104 self.emit_expr(&arg.value)?;
6105 }
6106 self.buf.push(')');
6107 Ok(())
6108 }
6109 // Type expressions: erased in Python expression context.
6110 NodeKind::TypeNamed { .. }
6111 | NodeKind::TypeTuple { .. }
6112 | NodeKind::TypeFunction { .. }
6113 | NodeKind::TypeOptional { .. }
6114 | NodeKind::TypeSelf => {
6115 self.buf.push_str("# type");
6116 Ok(())
6117 }
6118 NodeKind::EffectRef { path } => {
6119 let name = path
6120 .segments
6121 .iter()
6122 .map(|s| s.name.as_str())
6123 .collect::<Vec<_>>()
6124 .join(".");
6125 self.buf.push_str(&name);
6126 Ok(())
6127 }
6128 NodeKind::Error => {
6129 self.buf.push_str("# error");
6130 Ok(())
6131 }
6132 _ => {
6133 self.buf.push_str("# unsupported");
6134 Ok(())
6135 }
6136 }
6137 }
6138
6139 // ── Match → match/case (Python 3.10+) ───────────────────────────────────
6140
6141 fn emit_match(&mut self, scrutinee: &AIRNode, arms: &[AIRNode]) -> Result<(), CodegenError> {
6142 let ind = self.indent_str();
6143 let _ = write!(self.buf, "{ind}match ");
6144 self.emit_expr(scrutinee)?;
6145 self.buf.push_str(":\n");
6146 self.indent += 1;
6147 for arm in arms {
6148 self.emit_match_arm(arm)?;
6149 }
6150 self.indent -= 1;
6151 Ok(())
6152 }
6153
6154 fn emit_match_arm(&mut self, arm: &AIRNode) -> Result<(), CodegenError> {
6155 if let NodeKind::MatchArm {
6156 pattern,
6157 guard,
6158 body,
6159 } = &arm.kind
6160 {
6161 let ind = self.indent_str();
6162 let _ = write!(self.buf, "{ind}case ");
6163 // A range pattern (`1..10 => …`) has no Python `case` literal form:
6164 // lower it to a capture-plus-guard `case __rv if lo <= __rv < hi:`.
6165 // The capture binds the whole scrutinee so the relational test can run
6166 // (Python `match`/`case` cannot reference the scrutinee name inside a
6167 // `case`). A user guard, if any, is AND-ed onto the range test.
6168 if let NodeKind::RangePat { lo, hi, inclusive } = &pattern.kind {
6169 let lo_s = range_bound_to_py(lo);
6170 let hi_s = range_bound_to_py(hi);
6171 let upper = if *inclusive { "<=" } else { "<" };
6172 let _ = write!(self.buf, "__rv if {lo_s} <= __rv {upper} {hi_s}");
6173 if let Some(g) = guard {
6174 self.buf.push_str(" and (");
6175 self.emit_expr(g)?;
6176 self.buf.push(')');
6177 }
6178 } else {
6179 self.emit_pattern(pattern)?;
6180 if let Some(g) = guard {
6181 self.buf.push_str(" if ");
6182 self.emit_expr(g)?;
6183 }
6184 }
6185 self.buf.push_str(":\n");
6186 self.indent += 1;
6187 self.emit_block_body(body)?;
6188 self.indent -= 1;
6189 }
6190 Ok(())
6191 }
6192
6193 fn emit_pattern(&mut self, pat: &AIRNode) -> Result<(), CodegenError> {
6194 match &pat.kind {
6195 NodeKind::WildcardPat => {
6196 self.buf.push('_');
6197 }
6198 NodeKind::BindPat { name, .. } => {
6199 self.buf.push_str(&py_value_ident(&name.name));
6200 }
6201 NodeKind::LiteralPat { lit } => match lit {
6202 Literal::Int(s) => self.buf.push_str(s),
6203 Literal::Float(s) => self.buf.push_str(s),
6204 Literal::Bool(b) => self.buf.push_str(if *b { "True" } else { "False" }),
6205 Literal::Char(s) => {
6206 self.buf.push('\'');
6207 self.buf.push_str(s);
6208 self.buf.push('\'');
6209 }
6210 Literal::String(s) => {
6211 self.buf.push('"');
6212 self.buf.push_str(&escape_py_string(s));
6213 self.buf.push('"');
6214 }
6215 Literal::Unit => self.buf.push_str("None"),
6216 },
6217 NodeKind::ConstructorPat { path, fields } => {
6218 // Optional `Some`/`None` patterns dispatch on the Optional
6219 // runtime classes (see `OPTIONAL_RUNTIME_PY`), not on a bare
6220 // `Some(...)` / `None()` class (the latter is undefined, and
6221 // `case None():` is a Python `SyntaxError`). `_BockSome` exposes
6222 // `__match_args__ = ('_0',)` so the payload binds positionally.
6223 let leaf = path.segments.last().map_or("", |s| s.name.as_str());
6224 match leaf {
6225 "Some" => {
6226 if let Some(f) = fields.first() {
6227 // Recurse so a *nested* payload pattern (`Some(Ok(v))`)
6228 // keeps its inner bindings, instead of flattening to a
6229 // bare name / `_` (which dropped `v`).
6230 let sub = self.pattern_to_py(f)?;
6231 let _ = write!(self.buf, "_BockSome({sub})");
6232 } else {
6233 self.buf.push_str("_BockSome(_)");
6234 }
6235 return Ok(());
6236 }
6237 "None" => {
6238 self.buf.push_str("_BockNone()");
6239 return Ok(());
6240 }
6241 // Result `Ok`/`Err` patterns dispatch on the Result runtime
6242 // classes (see `RESULT_RUNTIME_PY`), mirroring `Some`/`None`.
6243 // Both carry a single payload bound positionally via
6244 // `__match_args__ = ('_0',)`.
6245 "Ok" | "Err" => {
6246 let cls = if leaf == "Ok" { "_BockOk" } else { "_BockErr" };
6247 if let Some(f) = fields.first() {
6248 let sub = self.pattern_to_py(f)?;
6249 let _ = write!(self.buf, "{cls}({sub})");
6250 } else {
6251 let _ = write!(self.buf, "{cls}(_)");
6252 }
6253 return Ok(());
6254 }
6255 _ => {}
6256 }
6257 // Prelude `Ordering` variant pattern → its Ordering-runtime class
6258 // (`case _BockOrderingLess():`), matching the singleton the
6259 // construction/bridge side produces.
6260 if let Some(variant) = crate::generator::ordering_variant(leaf) {
6261 let _ = write!(self.buf, "{}()", ordering_class_py(variant));
6262 return Ok(());
6263 }
6264 let variant_name = if let Some(info) = self.user_variant_for_path(path) {
6265 let variant = path.segments.last().map_or("", |s| s.name.as_str());
6266 format!("{}_{variant}", info.enum_name)
6267 } else {
6268 path.segments
6269 .iter()
6270 .map(|s| s.name.as_str())
6271 .collect::<Vec<_>>()
6272 .join("_")
6273 };
6274 if fields.is_empty() {
6275 let _ = write!(self.buf, "{variant_name}()");
6276 } else {
6277 let mut field_pats: Vec<String> = Vec::with_capacity(fields.len());
6278 for (i, f) in fields.iter().enumerate() {
6279 // Recurse so a nested sub-pattern keeps its inner bindings.
6280 let sub = self.pattern_to_py(f)?;
6281 field_pats.push(format!("_{i}={sub}"));
6282 }
6283 let _ = write!(self.buf, "{variant_name}({})", field_pats.join(", "));
6284 }
6285 }
6286 NodeKind::RecordPat { path, fields, .. } => {
6287 let type_name = if let Some(info) = self.user_variant_for_path(path) {
6288 let variant = path.segments.last().map_or("", |s| s.name.as_str());
6289 format!("{}_{variant}", info.enum_name)
6290 } else {
6291 path.segments
6292 .iter()
6293 .map(|s| s.name.as_str())
6294 .collect::<Vec<_>>()
6295 .join("_")
6296 };
6297 let mut field_pats: Vec<String> = Vec::with_capacity(fields.len());
6298 for f in fields {
6299 // `py_field_ident`: a keyword-named field (`pass`)
6300 // destructures through its escaped spelling
6301 // (`case Tally(pass_=p)`), matching the escaped dataclass
6302 // field declaration.
6303 let field_name = py_field_ident(&f.name.name);
6304 if let Some(pat) = &f.pattern {
6305 // Recurse so a nested record/constructor/tuple sub-pattern
6306 // keeps its inner bindings.
6307 let sub = self.pattern_to_py(pat)?;
6308 field_pats.push(format!("{field_name}={sub}"));
6309 } else {
6310 // Shorthand `{ radius }` ≡ `{ radius: radius }`. Emit the
6311 // keyword form `radius=radius` so the bind is by field
6312 // name, not by `__match_args__` position (a dataclass's
6313 // positional order is field-decl order *plus* the trailing
6314 // `_tag`, so a bare positional sub-pattern would mis-bind
6315 // multi-field variants). For a keyword-named field both
6316 // sides escape identically (`pass_=pass_`): the bound
6317 // value identifier's references go through
6318 // `py_value_ident`, which produces the same spelling.
6319 field_pats.push(format!("{field_name}={field_name}"));
6320 }
6321 }
6322 let _ = write!(self.buf, "{type_name}({})", field_pats.join(", "));
6323 }
6324 NodeKind::TuplePat { elems } => {
6325 self.buf.push('(');
6326 for (i, e) in elems.iter().enumerate() {
6327 if i > 0 {
6328 self.buf.push_str(", ");
6329 }
6330 self.emit_pattern(e)?;
6331 }
6332 if elems.len() == 1 {
6333 self.buf.push(',');
6334 }
6335 self.buf.push(')');
6336 }
6337 // Python `match`/`case` supports native or-patterns: `case 1 | 2 | 3:`.
6338 // Without this, an `OrPat` fell through to the `_` catch-all, so
6339 // `1 | 2 | 3 => …` collapsed to `case _:` — shadowing every later arm
6340 // ("wildcard makes remaining patterns unreachable").
6341 NodeKind::OrPat { alternatives } => {
6342 for (i, alt) in alternatives.iter().enumerate() {
6343 if i > 0 {
6344 self.buf.push_str(" | ");
6345 }
6346 self.emit_pattern(alt)?;
6347 }
6348 }
6349 // Python `match`/`case` sequence patterns: `case []:`,
6350 // `case [only]:`, `case [first, *rest]:`. Without this, every list
6351 // pattern fell through to the `_` catch-all below, so `[]` and
6352 // `[first, ..rest]` both became `case _:` — the first shadowing the
6353 // rest ("wildcard makes remaining patterns unreachable"). The `*rest`
6354 // star-capture mirrors Bock's `..rest`; a `..` with no binding maps to
6355 // an anonymous `*_`.
6356 NodeKind::ListPat { elems, rest } => {
6357 self.buf.push('[');
6358 for (i, e) in elems.iter().enumerate() {
6359 if i > 0 {
6360 self.buf.push_str(", ");
6361 }
6362 self.emit_pattern(e)?;
6363 }
6364 if let Some(r) = rest {
6365 if !elems.is_empty() {
6366 self.buf.push_str(", ");
6367 }
6368 match &r.kind {
6369 NodeKind::BindPat { name, .. } => {
6370 let _ = write!(self.buf, "*{}", py_value_ident(&name.name));
6371 }
6372 // `..` rest with no binding (or a wildcard) captures and
6373 // discards the tail.
6374 _ => self.buf.push_str("*_"),
6375 }
6376 }
6377 self.buf.push(']');
6378 }
6379 _ => {
6380 self.buf.push('_');
6381 }
6382 }
6383 Ok(())
6384 }
6385
6386 /// Emit a `match` *expression* (each arm yields a value, no statement arm)
6387 /// as a nested Python conditional over the scrutinee, which the caller has
6388 /// bound to `__v` via the enclosing `(lambda __v: …)(<scrutinee>)`.
6389 ///
6390 /// Each non-final arm becomes `<body> if <test on __v> else (<rest>)`. The
6391 /// arm body may reference a pattern binding (the `x` in `Some(x)`, or a
6392 /// whole-scrutinee bind pattern); since a Python conditional can't introduce
6393 /// a binding, the body is wrapped in an immediately-applied
6394 /// `(lambda <bind>: <body>)(<value>)` so the name resolves. Patterns:
6395 ///
6396 /// - `Some(x)` → test `isinstance(__v, _BockSome)`, bind `x = __v._0`
6397 /// - `None` → test `isinstance(__v, _BockNone)`
6398 /// - literal → test `__v == <lit>`
6399 /// - wildcard / bind / final arm → the `else` (catch-all); a bind pattern
6400 /// binds the whole scrutinee
6401 ///
6402 /// This replaces an earlier stub that emitted a hardcoded `… if False else …`
6403 /// chain (which always selected the *last* arm and never bound the payload),
6404 /// mis-compiling every expression-position `match` whose arms were not all
6405 /// `return`s — e.g. `let r = match o { Some(x) => x + 1; None => 0 }`.
6406 fn emit_match_expr(
6407 &mut self,
6408 _scrutinee: &AIRNode,
6409 arms: &[AIRNode],
6410 ) -> Result<(), CodegenError> {
6411 self.emit_match_expr_from(arms, 0)
6412 }
6413
6414 /// Tail of [`Self::emit_match_expr`]: emit the conditional for `arms[idx..]`.
6415 fn emit_match_expr_from(&mut self, arms: &[AIRNode], idx: usize) -> Result<(), CodegenError> {
6416 let Some(NodeKind::MatchArm { pattern, body, .. }) = arms.get(idx).map(|a| &a.kind) else {
6417 // No arm at this index: Bock matches are exhaustive, so this is
6418 // unreachable, but emit a defined value to keep the expression valid.
6419 self.buf.push_str("None");
6420 return Ok(());
6421 };
6422 let is_last = idx + 1 >= arms.len();
6423 let is_catch_all = matches!(
6424 pattern.kind,
6425 NodeKind::WildcardPat | NodeKind::BindPat { .. }
6426 );
6427 // The final arm, or any catch-all, is the unconditional `else` value.
6428 if is_last || is_catch_all {
6429 return self.emit_arm_value(pattern, body, /*whole_scrutinee_bind=*/ true);
6430 }
6431 // Otherwise: `<value> if <test> else (<rest>)`.
6432 self.emit_arm_value(pattern, body, /*whole_scrutinee_bind=*/ false)?;
6433 self.buf.push_str(" if ");
6434 self.emit_match_expr_test(pattern)?;
6435 self.buf.push_str(" else (");
6436 self.emit_match_expr_from(arms, idx + 1)?;
6437 self.buf.push(')');
6438 Ok(())
6439 }
6440
6441 /// Emit the boolean test (over the bound `__v`) that selects `pattern`.
6442 fn emit_match_expr_test(&mut self, pattern: &AIRNode) -> Result<(), CodegenError> {
6443 match &pattern.kind {
6444 NodeKind::ConstructorPat { path, .. } => {
6445 let leaf = path.segments.last().map_or("", |s| s.name.as_str());
6446 let cls: String = match leaf {
6447 "Some" => "_BockSome".to_string(),
6448 "None" => "_BockNone".to_string(),
6449 // Result `Ok`/`Err` test against the Result-runtime classes,
6450 // mirroring `Some`/`None`.
6451 "Ok" => "_BockOk".to_string(),
6452 "Err" => "_BockErr".to_string(),
6453 other => {
6454 // Prelude `Ordering` variants test against the
6455 // Ordering-runtime class so an expression-position match
6456 // (`lambda __v: isinstance(__v, _BockOrderingLess) …`)
6457 // recognises the singleton the bridge/construction
6458 // produces.
6459 if let Some(v) = crate::generator::ordering_variant(other) {
6460 ordering_class_py(v).to_string()
6461 } else if let Some(info) = self.user_variant_for_path(path) {
6462 // A user-enum variant tests against its dataclass
6463 // `{enum}_{variant}` — the same class the statement
6464 // `emit_pattern` and the construction side produce.
6465 // Without this the test used the bare variant leaf
6466 // (`isinstance(__v, Red)`), an undefined name.
6467 format!("{}_{other}", info.enum_name)
6468 } else {
6469 other.to_string()
6470 }
6471 }
6472 };
6473 let _ = write!(self.buf, "isinstance(__v, {cls})");
6474 }
6475 NodeKind::LiteralPat { .. } => {
6476 self.buf.push_str("__v == ");
6477 self.emit_pattern(pattern)?;
6478 }
6479 NodeKind::ListPat { elems, rest } => {
6480 // `[a, b]` requires a list of exactly len(elems); `[a, ..rest]`
6481 // requires at least len(elems). Element literal sub-patterns add
6482 // positional `__v[i] == <lit>` tests; bind/wildcard elements add
6483 // none. Mirrors the js/ts/go list test.
6484 let n = elems.len();
6485 let len_test = if rest.is_some() {
6486 format!("isinstance(__v, list) and len(__v) >= {n}")
6487 } else {
6488 format!("isinstance(__v, list) and len(__v) == {n}")
6489 };
6490 self.buf.push_str(&len_test);
6491 for (i, e) in elems.iter().enumerate() {
6492 if let NodeKind::LiteralPat { .. } = &e.kind {
6493 self.buf.push_str(&format!(" and __v[{i}] == "));
6494 self.emit_pattern(e)?;
6495 }
6496 }
6497 }
6498 NodeKind::RangePat { lo, hi, inclusive } => {
6499 // `lo..hi` → `lo <= __v < hi`; `lo..=hi` → `lo <= __v <= hi`.
6500 let lo_s = range_bound_to_py(lo);
6501 let hi_s = range_bound_to_py(hi);
6502 let upper = if *inclusive { "<=" } else { "<" };
6503 let _ = write!(self.buf, "{lo_s} <= __v {upper} {hi_s}");
6504 }
6505 // Catch-alls never produce a test (handled as the `else`).
6506 _ => self.buf.push_str("True"),
6507 }
6508 Ok(())
6509 }
6510
6511 /// Emit one arm's value, binding any pattern variable via an applied lambda
6512 /// so it resolves inside the conditional. `whole_scrutinee_bind` allows a
6513 /// bind pattern in `else` position to capture the whole scrutinee (`__v`).
6514 fn emit_arm_value(
6515 &mut self,
6516 pattern: &AIRNode,
6517 body: &AIRNode,
6518 whole_scrutinee_bind: bool,
6519 ) -> Result<(), CodegenError> {
6520 match &pattern.kind {
6521 // `Some(x)` / `Ok(x)` / `Err(e)` bind the payload `__v._0`.
6522 NodeKind::ConstructorPat { path, fields }
6523 if path
6524 .segments
6525 .last()
6526 .is_some_and(|s| matches!(s.name.as_str(), "Some" | "Ok" | "Err")) =>
6527 {
6528 if let Some(f) = fields.first() {
6529 let name = self.pattern_to_binding_name(f);
6530 if name != "_" {
6531 let _ = write!(self.buf, "(lambda {name}: ");
6532 self.emit_block_as_expr(body)?;
6533 self.buf.push_str(")(__v._0)");
6534 return Ok(());
6535 }
6536 }
6537 self.emit_block_as_expr(body)
6538 }
6539 // A bind pattern (`x => …`) captures the whole scrutinee.
6540 NodeKind::BindPat { name, .. } if whole_scrutinee_bind => {
6541 let bind = py_value_ident(&name.name);
6542 let _ = write!(self.buf, "(lambda {bind}: ");
6543 self.emit_block_as_expr(body)?;
6544 self.buf.push_str(")(__v)");
6545 Ok(())
6546 }
6547 // A list pattern binds its elements positionally (`__v[i]`) and a
6548 // `..rest` to the tail slice (`__v[n:]`) via an applied lambda, so the
6549 // names resolve inside the conditional. Wildcard/literal elements bind
6550 // nothing. Without this the `first`/`rest` in `[first, ..rest] => …`
6551 // were undefined (a `NameError` at runtime).
6552 NodeKind::ListPat { elems, rest } => {
6553 let mut params: Vec<String> = Vec::new();
6554 let mut argvals: Vec<String> = Vec::new();
6555 for (i, e) in elems.iter().enumerate() {
6556 if let NodeKind::BindPat { name, .. } = &e.kind {
6557 params.push(py_value_ident(&name.name));
6558 argvals.push(format!("__v[{i}]"));
6559 }
6560 }
6561 if let Some(r) = rest {
6562 if let NodeKind::BindPat { name, .. } = &r.kind {
6563 params.push(py_value_ident(&name.name));
6564 argvals.push(format!("__v[{}:]", elems.len()));
6565 }
6566 }
6567 if params.is_empty() {
6568 return self.emit_block_as_expr(body);
6569 }
6570 let _ = write!(self.buf, "(lambda {}: ", params.join(", "));
6571 self.emit_block_as_expr(body)?;
6572 let _ = write!(self.buf, ")({})", argvals.join(", "));
6573 Ok(())
6574 }
6575 _ => self.emit_block_as_expr(body),
6576 }
6577 }
6578
6579 // ── Pipe operator ───────────────────────────────────────────────────────
6580
6581 /// Emit an expression in callee position, parenthesizing it when its
6582 /// surface syntax would otherwise swallow the trailing argument list.
6583 ///
6584 /// A bare Python `lambda` is the case that matters: `lambda x: body`
6585 /// followed by `(arg)` parses as `lambda x: (body(arg))` — the call binds
6586 /// to the body, never invoking the lambda. Wrapping it as `(lambda x:
6587 /// body)(arg)` makes the call apply to the lambda itself. This shows up
6588 /// whenever the AIR compose desugar (`f >> g` → `(__compose_x) =>
6589 /// g(f(__compose_x))`) nests: chained `>>` lowers the inner compose to a
6590 /// `Lambda`, which then appears as the callee `f` inside `f(__compose_x)`.
6591 fn emit_callee(&mut self, callee: &AIRNode) -> Result<(), CodegenError> {
6592 if matches!(callee.kind, NodeKind::Lambda { .. }) {
6593 self.buf.push('(');
6594 self.emit_expr(callee)?;
6595 self.buf.push(')');
6596 Ok(())
6597 } else {
6598 self.emit_expr(callee)
6599 }
6600 }
6601
6602 fn emit_pipe(&mut self, left: &AIRNode, right: &AIRNode) -> Result<(), CodegenError> {
6603 if let NodeKind::Call { callee, args, .. } = &right.kind {
6604 let has_placeholder = args
6605 .iter()
6606 .any(|a| matches!(a.value.kind, NodeKind::Placeholder));
6607 if has_placeholder {
6608 self.emit_callee(callee)?;
6609 self.buf.push('(');
6610 for (i, arg) in args.iter().enumerate() {
6611 if i > 0 {
6612 self.buf.push_str(", ");
6613 }
6614 if matches!(arg.value.kind, NodeKind::Placeholder) {
6615 self.emit_expr(left)?;
6616 } else {
6617 self.emit_expr(&arg.value)?;
6618 }
6619 }
6620 self.buf.push(')');
6621 return Ok(());
6622 }
6623 }
6624 self.emit_callee(right)?;
6625 self.buf.push('(');
6626 self.emit_expr(left)?;
6627 self.buf.push(')');
6628 Ok(())
6629 }
6630
6631 // ── Type emission ───────────────────────────────────────────────────────
6632
6633 fn type_to_py(&self, node: &AIRNode) -> String {
6634 match &node.kind {
6635 NodeKind::TypeNamed { path, args } => {
6636 let name = path
6637 .segments
6638 .iter()
6639 .map(|s| s.name.as_str())
6640 .collect::<Vec<_>>()
6641 .join(".");
6642 // `Result[T, E]` lowers to the tagged Result-runtime classes, not
6643 // a subscripted generic — the value is `_BockOk(...)` /
6644 // `_BockErr(...)`, so the annotation is the union `_BockOk |
6645 // _BockErr` with no `[T, E]` (which would be a Python error on a
6646 // union). Mirrors the `TypeOptional` arm below.
6647 if name == "Result" {
6648 return "_BockOk | _BockErr".to_string();
6649 }
6650 let py_name = self.map_type_name(&name);
6651 if args.is_empty() {
6652 py_name
6653 } else {
6654 let arg_strs: Vec<String> = args.iter().map(|a| self.type_to_py(a)).collect();
6655 format!("{py_name}[{}]", arg_strs.join(", "))
6656 }
6657 }
6658 NodeKind::TypeTuple { elems } => {
6659 let elem_strs: Vec<String> = elems.iter().map(|e| self.type_to_py(e)).collect();
6660 format!("tuple[{}]", elem_strs.join(", "))
6661 }
6662 NodeKind::TypeFunction { params, ret, .. } => {
6663 self.needs_typing_callable.set(true);
6664 let param_strs: Vec<String> = params.iter().map(|p| self.type_to_py(p)).collect();
6665 format!(
6666 "Callable[[{}], {}]",
6667 param_strs.join(", "),
6668 self.type_to_py(ret)
6669 )
6670 }
6671 NodeKind::TypeOptional { inner } => {
6672 // `T?` lowers to the tagged Optional runtime, not `T | None`:
6673 // the value is `_BockSome(...)` / `_bock_none`, so the annotation
6674 // must describe those classes for type and value to agree (mirrors
6675 // Go's `__bockOption` and TS's `BockOption<T>`). The element type
6676 // `T` is preserved as a comment for readability; Python does not
6677 // enforce annotations at runtime.
6678 let _ = inner;
6679 "_BockSome | _BockNone".to_string()
6680 }
6681 NodeKind::TypeSelf => {
6682 self.needs_typing_self.set(true);
6683 "Self".into()
6684 }
6685 _ => {
6686 self.needs_typing_any.set(true);
6687 "Any".into()
6688 }
6689 }
6690 }
6691
6692 fn map_type_name(&self, name: &str) -> String {
6693 match name {
6694 "Int" => "int".into(),
6695 "Float" => "float".into(),
6696 "Bool" => "bool".into(),
6697 "String" => "str".into(),
6698 "Void" | "Unit" => "None".into(),
6699 "List" => "list".into(),
6700 "Map" => "dict".into(),
6701 "Set" => "set".into(),
6702 "Any" => {
6703 self.needs_typing_any.set(true);
6704 "Any".into()
6705 }
6706 "Never" => {
6707 self.needs_typing_never.set(true);
6708 "Never".into()
6709 }
6710 other => other.into(),
6711 }
6712 }
6713
6714 fn ast_type_to_py(&self, ty: &TypeExpr) -> String {
6715 match ty {
6716 TypeExpr::Named { path, args, .. } => {
6717 let name = path
6718 .segments
6719 .iter()
6720 .map(|s| s.name.as_str())
6721 .collect::<Vec<_>>()
6722 .join(".");
6723 // See the `Result` case in `type_to_py`: lowers to the tagged
6724 // Result-runtime union, no subscript.
6725 if name == "Result" {
6726 return "_BockOk | _BockErr".to_string();
6727 }
6728 let py_name = self.map_type_name(&name);
6729 if args.is_empty() {
6730 py_name
6731 } else {
6732 let arg_strs: Vec<String> =
6733 args.iter().map(|a| self.ast_type_to_py(a)).collect();
6734 format!("{py_name}[{}]", arg_strs.join(", "))
6735 }
6736 }
6737 TypeExpr::Tuple { elems, .. } => {
6738 let elem_strs: Vec<String> = elems.iter().map(|e| self.ast_type_to_py(e)).collect();
6739 format!("tuple[{}]", elem_strs.join(", "))
6740 }
6741 TypeExpr::Function { params, ret, .. } => {
6742 self.needs_typing_callable.set(true);
6743 let param_strs: Vec<String> =
6744 params.iter().map(|p| self.ast_type_to_py(p)).collect();
6745 format!(
6746 "Callable[[{}], {}]",
6747 param_strs.join(", "),
6748 self.ast_type_to_py(ret)
6749 )
6750 }
6751 TypeExpr::Optional { inner, .. } => {
6752 // See the `TypeOptional` arm of `type_to_py`: the tagged Optional
6753 // runtime classes must match the emitted tagged value.
6754 let _ = inner;
6755 "_BockSome | _BockNone".to_string()
6756 }
6757 TypeExpr::SelfType { .. } => {
6758 self.needs_typing_self.set(true);
6759 "Self".into()
6760 }
6761 }
6762 }
6763
6764 // ── Helpers ─────────────────────────────────────────────────────────────
6765
6766 /// Scan a sequence of block statements and return the set of bound names
6767 /// that are later `await`ed as bare identifiers within the same block.
6768 /// The caller wraps those LetBindings' Call values in `asyncio.create_task`.
6769 ///
6770 /// Only direct `let name = call(...)` bindings qualify. Non-call RHS are
6771 /// skipped (not awaitable work we can parallelise). The binding must be
6772 /// awaited in the same flat block — nested scopes are ignored because we
6773 /// can't prove the binding is still live once control leaves the block.
6774 fn collect_task_bindings(stmts: &[AIRNode]) -> std::collections::HashSet<String> {
6775 let mut awaited: std::collections::HashSet<String> = std::collections::HashSet::new();
6776 for s in stmts {
6777 Self::collect_awaited_identifiers(s, &mut awaited);
6778 }
6779 let mut out = std::collections::HashSet::new();
6780 for s in stmts {
6781 if let NodeKind::LetBinding { pattern, value, .. } = &s.kind {
6782 if let NodeKind::BindPat { name, .. } = &pattern.kind {
6783 let py_name = py_value_ident(&name.name);
6784 if matches!(&value.kind, NodeKind::Call { .. }) && awaited.contains(&py_name) {
6785 out.insert(py_name);
6786 }
6787 }
6788 }
6789 }
6790 out
6791 }
6792
6793 /// Walk an AIR subtree and record every `await name` where `name` is a
6794 /// bare identifier. Nested function / lambda bodies are not descended —
6795 /// an inner closure awaiting the name doesn't imply the outer block
6796 /// wants a task.
6797 fn collect_awaited_identifiers(node: &AIRNode, out: &mut std::collections::HashSet<String>) {
6798 match &node.kind {
6799 NodeKind::Await { expr } => {
6800 if let NodeKind::Identifier { name } = &expr.kind {
6801 out.insert(py_value_ident(&name.name));
6802 }
6803 Self::collect_awaited_identifiers(expr, out);
6804 }
6805 NodeKind::Lambda { .. } | NodeKind::FnDecl { .. } => {
6806 // Don't cross function boundaries.
6807 }
6808 NodeKind::Block { stmts, tail } => {
6809 for s in stmts {
6810 Self::collect_awaited_identifiers(s, out);
6811 }
6812 if let Some(t) = tail {
6813 Self::collect_awaited_identifiers(t, out);
6814 }
6815 }
6816 NodeKind::LetBinding { value, .. } => {
6817 Self::collect_awaited_identifiers(value, out);
6818 }
6819 NodeKind::Call { callee, args, .. } => {
6820 Self::collect_awaited_identifiers(callee, out);
6821 for a in args {
6822 Self::collect_awaited_identifiers(&a.value, out);
6823 }
6824 }
6825 NodeKind::MethodCall { receiver, args, .. } => {
6826 Self::collect_awaited_identifiers(receiver, out);
6827 for a in args {
6828 Self::collect_awaited_identifiers(&a.value, out);
6829 }
6830 }
6831 NodeKind::BinaryOp { left, right, .. } => {
6832 Self::collect_awaited_identifiers(left, out);
6833 Self::collect_awaited_identifiers(right, out);
6834 }
6835 NodeKind::UnaryOp { operand, .. } => {
6836 Self::collect_awaited_identifiers(operand, out);
6837 }
6838 NodeKind::If {
6839 condition,
6840 then_block,
6841 else_block,
6842 ..
6843 } => {
6844 Self::collect_awaited_identifiers(condition, out);
6845 Self::collect_awaited_identifiers(then_block, out);
6846 if let Some(e) = else_block {
6847 Self::collect_awaited_identifiers(e, out);
6848 }
6849 }
6850 NodeKind::While { condition, body } => {
6851 Self::collect_awaited_identifiers(condition, out);
6852 Self::collect_awaited_identifiers(body, out);
6853 }
6854 NodeKind::For { iterable, body, .. } => {
6855 Self::collect_awaited_identifiers(iterable, out);
6856 Self::collect_awaited_identifiers(body, out);
6857 }
6858 NodeKind::Return { value: Some(v) } | NodeKind::Break { value: Some(v) } => {
6859 Self::collect_awaited_identifiers(v, out);
6860 }
6861 NodeKind::Assign { value, .. } => {
6862 Self::collect_awaited_identifiers(value, out);
6863 }
6864 NodeKind::TupleLiteral { elems } | NodeKind::ListLiteral { elems } => {
6865 for e in elems {
6866 Self::collect_awaited_identifiers(e, out);
6867 }
6868 }
6869 _ => {}
6870 }
6871 }
6872
6873 /// Push a fresh lexical-block frame for nested-block `let`-shadow renaming,
6874 /// seeded with any names queued in [`Self::pending_scope_seed`] (a function's
6875 /// parameters, so they share the body frame). The seed is drained.
6876 fn enter_shadow_scope(&mut self) {
6877 let mut frame = ShadowScope::default();
6878 for n in self.pending_scope_seed.drain(..) {
6879 frame.bound.insert(n);
6880 }
6881 self.shadow_scopes.push(frame);
6882 }
6883
6884 /// Pop the innermost lexical-block frame pushed by [`Self::enter_shadow_scope`].
6885 fn leave_shadow_scope(&mut self) {
6886 self.shadow_scopes.pop();
6887 }
6888
6889 /// Whether `py_name` is bound in any *enclosing* frame (every frame except
6890 /// the innermost), i.e. binding it again in the current block shadows an
6891 /// outer binding and — on Python's function-scoped `=` — would stomp it.
6892 fn shadowed_in_outer_scope(&self, py_name: &str) -> bool {
6893 let n = self.shadow_scopes.len();
6894 if n < 2 {
6895 return false;
6896 }
6897 self.shadow_scopes[..n - 1]
6898 .iter()
6899 .any(|s| s.bound.contains(py_name) || s.renames.contains_key(py_name))
6900 }
6901
6902 /// Resolve a Python identifier through the shadow-scope stack: the innermost
6903 /// frame that renamed `py_name` wins, else the name is unchanged. Used at
6904 /// every identifier *use* site so a reference inside a shadowing block reads
6905 /// the alias while code outside reads the original.
6906 fn resolve_shadow_name(&self, py_name: &str) -> String {
6907 for s in self.shadow_scopes.iter().rev() {
6908 if let Some(alias) = s.renames.get(py_name) {
6909 return alias.clone();
6910 }
6911 // A frame that *binds* the name directly (without a rename) stops the
6912 // search: an inner block's same-name binding is the live one there.
6913 if s.bound.contains(py_name) {
6914 return py_name.to_string();
6915 }
6916 }
6917 py_name.to_string()
6918 }
6919
6920 /// Plan a `let`-binding's LHS Python name without yet activating it in the
6921 /// scope frame. A `let`'s RHS reads the *prior* binding (`let y = y + 10`
6922 /// reads the outer `y`), so the rename must take effect only *after* the RHS
6923 /// is emitted — [`Self::commit_shadow_let`] does that. Returns the LHS name to
6924 /// emit (a fresh alias when the binding shadows an enclosing frame, else the
6925 /// name unchanged) paired with the rename to commit (`Some((orig, alias))`
6926 /// only for a fresh shadow; `None` for a same-block rebind or first binding).
6927 fn plan_shadow_let(&mut self, py_name: &str) -> (String, Option<(String, String)>) {
6928 // No active frame (defensive) → emit verbatim, nothing to commit.
6929 if self.shadow_scopes.is_empty() {
6930 return (py_name.to_string(), None);
6931 }
6932 // Same-block re-bind: an existing alias or direct binding here is reused
6933 // (a plain Python rebind), with nothing new to commit.
6934 if let Some(cur) = self.shadow_scopes.last() {
6935 if let Some(alias) = cur.renames.get(py_name) {
6936 return (alias.clone(), None);
6937 }
6938 if cur.bound.contains(py_name) {
6939 return (py_name.to_string(), None);
6940 }
6941 }
6942 if self.shadowed_in_outer_scope(py_name) {
6943 self.shadow_counter += 1;
6944 let alias = format!("{py_name}__s{}", self.shadow_counter);
6945 return (alias.clone(), Some((py_name.to_string(), alias)));
6946 }
6947 // A `let` whose name collides with a Python builtin the codegen *itself*
6948 // emits as a call (`list(map.keys())`, `set(...)`, `map(...)`) must be
6949 // renamed even when it shadows nothing — binding `list = [...]` rebinds
6950 // the global `list` for the rest of the function scope, so a later
6951 // codegen-emitted `list(...)` would raise `TypeError: 'list' object is
6952 // not callable`. The rename flows to references via the same
6953 // `renames` map `resolve_shadow_name` consults, so use sites stay in sync.
6954 if is_shadow_sensitive_py_builtin(py_name) {
6955 self.shadow_counter += 1;
6956 let alias = format!("{py_name}__b{}", self.shadow_counter);
6957 return (alias.clone(), Some((py_name.to_string(), alias)));
6958 }
6959 (py_name.to_string(), None)
6960 }
6961
6962 /// Activate a planned `let` binding in the current frame, after its RHS has
6963 /// been emitted. `pending` is the rename returned by [`Self::plan_shadow_let`]
6964 /// (`Some` only for a fresh shadow); `py_name` is the original name (used to
6965 /// record a non-shadowing first binding so later same-block rebinds and
6966 /// nested shadows resolve correctly).
6967 fn commit_shadow_let(&mut self, py_name: &str, pending: Option<(String, String)>) {
6968 let Some(cur) = self.shadow_scopes.last_mut() else {
6969 return;
6970 };
6971 if let Some((orig, alias)) = pending {
6972 cur.renames.insert(orig, alias.clone());
6973 cur.bound.insert(alias);
6974 } else {
6975 cur.bound.insert(py_name.to_string());
6976 }
6977 }
6978
6979 /// Emit a **function/method body**, wrapping it in the `?`-propagate
6980 /// envelope when the body contains a `?` operator. `expr?` lowers to
6981 /// `_bock_try(expr)`, which raises `_BockPropagate` on an `Err`/`None`; the
6982 /// envelope catches that and re-returns the carried value, giving Rust-`?`
6983 /// early-return semantics:
6984 ///
6985 /// ```python
6986 /// try:
6987 /// <body>
6988 /// except _BockPropagate as __bock_p:
6989 /// return __bock_p.value
6990 /// ```
6991 ///
6992 /// A body with no `?` is emitted unchanged (no envelope, no runtime cost).
6993 fn emit_fn_body(&mut self, body: &AIRNode) -> Result<(), CodegenError> {
6994 if body_contains_propagate(body) {
6995 self.writeln("try:");
6996 self.indent += 1;
6997 self.emit_block_body(body)?;
6998 self.indent -= 1;
6999 self.writeln("except _BockPropagate as __bock_p:");
7000 self.indent += 1;
7001 self.writeln("return __bock_p.value");
7002 self.indent -= 1;
7003 Ok(())
7004 } else {
7005 self.emit_block_body(body)
7006 }
7007 }
7008
7009 /// Emit a block (or bare-body) in statement/`return` position, opening a
7010 /// fresh shadow-scope frame so a nested-block `let` that shadows an enclosing
7011 /// binding is renamed rather than stomping it (see [`ShadowScope`]).
7012 fn emit_block_body(&mut self, node: &AIRNode) -> Result<(), CodegenError> {
7013 self.enter_shadow_scope();
7014 let r = self.emit_block_body_inner(node);
7015 self.leave_shadow_scope();
7016 r
7017 }
7018
7019 /// Emit the **body of a `for`/`while`/`loop`** — statement position, so its
7020 /// tail expression is *discarded* (a Bock loop evaluates to Unit). Sets
7021 /// [`Self::in_loop_body_tail`] for the body's duration so
7022 /// [`Self::emit_block_body_inner`] emits the tail as a bare expression
7023 /// statement (`<value>`) instead of a function-body `return <value>` — a
7024 /// `return` inside a loop aborts the enclosing function after one iteration
7025 /// (the fizzbuzz / inventory-system one-line truncation). The flag is
7026 /// saved/restored so it never leaks past the loop, and any nested value
7027 /// context (a value-binding hoist, a value-`if`/`match` arm) clears it — see
7028 /// [`Self::emit_block_body_inner`]. A `break v` value flows through the
7029 /// separate `loop_value_targets` stack, not this flag.
7030 fn emit_loop_body(&mut self, node: &AIRNode) -> Result<(), CodegenError> {
7031 let prev = std::mem::replace(&mut self.in_loop_body_tail, true);
7032 let r = self.emit_block_body(node);
7033 self.in_loop_body_tail = prev;
7034 r
7035 }
7036
7037 fn emit_block_body_inner(&mut self, node: &AIRNode) -> Result<(), CodegenError> {
7038 if let NodeKind::Block { stmts, tail } = &node.kind {
7039 if stmts.is_empty() && tail.is_none() {
7040 self.writeln("pass");
7041 return Ok(());
7042 }
7043 // Concurrent-pattern detection: names bound in this block whose
7044 // Call RHS should be scheduled as tasks because the same name is
7045 // later `await`ed in the same block. Python coroutines don't run
7046 // until awaited, so without this, independent async calls
7047 // serialise — wrapping with `asyncio.create_task` preserves the
7048 // author's concurrent intent (JS/TS get this for free because
7049 // Promises are eager).
7050 let task_bindings = Self::collect_task_bindings(stmts);
7051 let prev = std::mem::replace(&mut self.task_bound_names, task_bindings);
7052 for s in stmts {
7053 self.emit_node(s)?;
7054 }
7055 self.task_bound_names = prev;
7056 if let Some(t) = tail {
7057 // A statement tail (`break`/`continue`/`return`/assignment) is
7058 // emitted as a statement, never wrapped in `return`.
7059 if crate::generator::node_is_statement(t) {
7060 self.emit_node(t)?;
7061 return Ok(());
7062 }
7063 // A bare `loop`/`while`/`for` in tail position yields no value (it
7064 // exits only via `break`/`return`, and a value-`loop` is rewritten
7065 // by `emit_value_binding`, not here). Emit it as a Python loop
7066 // statement — never `return <loop>`, which `emit_expr` would lower
7067 // to the `# unsupported` fallthrough, silently discarding the whole
7068 // loop body (the guessing-game `play()` tail-`loop` shape).
7069 if matches!(
7070 t.kind,
7071 NodeKind::Loop { .. } | NodeKind::While { .. } | NodeKind::For { .. }
7072 ) {
7073 self.emit_node(t)?;
7074 return Ok(());
7075 }
7076 // A diverging `raise` expression (`todo()` / `unreachable()`)
7077 // yields no value: emit it bare, never `return raise …` (a
7078 // `SyntaxError`).
7079 if is_raise_expr(t) {
7080 self.write_indent();
7081 self.emit_expr(t)?;
7082 self.buf.push('\n');
7083 return Ok(());
7084 }
7085 // A value-`if`/`match` with a diverging `raise` branch can't be a
7086 // ternary (`return raise … if … else …`). Hoist it to a
7087 // statement-form `if`/`match` whose branches `return`/`raise`.
7088 if control_flow_has_raise_branch(t) {
7089 return self.emit_tail_control_flow(t);
7090 }
7091 // A `match` with statement arms yields no value: emit a Python
7092 // `match`/`case` statement, not a `return (lambda ...)`. A value
7093 // match whose arms need the structural if-chain (guards,
7094 // or/tuple/record/range/list patterns, or a nested constructor)
7095 // is *also* routed here: the `(lambda __v: …)` conditional chain
7096 // cannot test or bind those — it dropped guards, tested record /
7097 // tuple / or arms as `if True`, and emitted the arm body with the
7098 // pattern binding free (`(lambda __v: f"x={x}")(p)` → `NameError`).
7099 // The statement-form `emit_pattern` binds and tests every pattern
7100 // kind correctly (each expression arm body becomes `return <v>`).
7101 if let NodeKind::Match { scrutinee, arms } = &t.kind {
7102 if crate::generator::match_has_statement_arm(arms)
7103 || match_value_needs_stmt_form(arms)
7104 {
7105 self.emit_match(scrutinee, arms)?;
7106 return Ok(());
7107 }
7108 }
7109 // A value-position `if` whose branch block carries statements (a
7110 // `let` binding) can't be a ternary: the ternary emits only each
7111 // branch's tail, dropping the `let` (a later reference then
7112 // `NameError`s — the microservice `handle_delete_user` case). Emit
7113 // it as statement-form `if`/`elif`/`else`, each branch recursing
7114 // through `emit_block_body` so the binding is kept and the tail
7115 // `return`ed.
7116 if if_value_needs_stmt_form(t) {
7117 return self.emit_tail_control_flow(t);
7118 }
7119 // Plain value-expression tail. In a loop body this is statement
7120 // position — the value is discarded — so emit a bare expression
7121 // statement, not `return <value>` (a `return` in a loop aborts
7122 // the function after one iteration: the fizzbuzz / inventory
7123 // truncation). Elsewhere this is the function-body tail: `return`.
7124 self.emit_tail_value_or_discard(t)?;
7125 }
7126 } else if crate::generator::node_is_statement(node) {
7127 // A bare statement body (`break`/`continue`/`return`/assignment).
7128 self.emit_node(node)?;
7129 } else if matches!(
7130 node.kind,
7131 NodeKind::Loop { .. } | NodeKind::While { .. } | NodeKind::For { .. }
7132 ) {
7133 // A bare `loop`/`while`/`for` body yields no value — emit the loop
7134 // statement, never `return <loop>` (see the tail-position note above).
7135 self.emit_node(node)?;
7136 } else if is_raise_expr(node) {
7137 self.write_indent();
7138 self.emit_expr(node)?;
7139 self.buf.push('\n');
7140 } else if control_flow_has_raise_branch(node) {
7141 return self.emit_tail_control_flow(node);
7142 } else if let NodeKind::Match { scrutinee, arms } = &node.kind {
7143 // See the tail-position note above: a value match needing the
7144 // structural if-chain (guards, or/tuple/record/range/list, nested
7145 // constructor) is lowered to statement-form `match`/`case` so every
7146 // pattern binds and tests correctly, instead of a `(lambda __v: …)`
7147 // chain that drops guards and leaves pattern bindings free.
7148 if crate::generator::match_has_statement_arm(arms) || match_value_needs_stmt_form(arms)
7149 {
7150 self.emit_match(scrutinee, arms)?;
7151 } else {
7152 self.emit_tail_value_or_discard(node)?;
7153 }
7154 } else if if_value_needs_stmt_form(node) {
7155 // See the tail-position note above: a value `if` whose branch block
7156 // carries a `let` can't be a ternary (the binding would be dropped) —
7157 // emit statement-form `if`/`elif`/`else`.
7158 return self.emit_tail_control_flow(node);
7159 } else {
7160 // Single expression as body.
7161 self.emit_tail_value_or_discard(node)?;
7162 }
7163 Ok(())
7164 }
7165
7166 /// Emit a block/body **tail value expression** in the correct position.
7167 ///
7168 /// In a function/method body (the default) the tail is the body's value, so
7169 /// it is `return <value>`. Inside the body of a `for`/`while`/`loop`
7170 /// ([`Self::in_loop_body_tail`] set by [`Self::emit_loop_body`]) the tail is
7171 /// *statement* position — a Bock loop evaluates to Unit, so the value is
7172 /// discarded — and a `return` would abort the enclosing function after the
7173 /// first iteration (the fizzbuzz one-line / inventory single-product
7174 /// truncation). There it is emitted as a bare expression statement. The
7175 /// arm/branch body of a statement-position `match` or `if`/`else`
7176 /// ([`Self::in_stmt_construct_arm`], set by [`Self::emit_stmt`]'s `Match`
7177 /// and `If` arms) is discarded for the same reason — a mid-block
7178 /// `match`/`if` is a Unit statement, and a `return` there aborts the
7179 /// enclosing function after the matched arm / taken branch (the
7180 /// chat-protocol truncation and its if/else sibling,
7181 /// Q-python-ifelse-truncation).
7182 fn emit_tail_value_or_discard(&mut self, node: &AIRNode) -> Result<(), CodegenError> {
7183 let ind = self.indent_str();
7184 if self.in_loop_body_tail || self.in_stmt_construct_arm {
7185 self.buf.push_str(&ind);
7186 } else {
7187 let _ = write!(self.buf, "{ind}return ");
7188 }
7189 self.emit_expr(node)?;
7190 self.buf.push('\n');
7191 Ok(())
7192 }
7193
7194 /// Emit a value-position `if`/`match` that carries a diverging `raise`
7195 /// branch (`todo()` / `unreachable()`) in **tail/return** position as
7196 /// statement-form Python: each branch/arm recurses through
7197 /// [`Self::emit_block_body`], so a non-diverging branch `return`s its value
7198 /// while the diverging branch `raise`s. This replaces the invalid ternary
7199 /// (`return raise … if … else …`).
7200 fn emit_tail_control_flow(&mut self, node: &AIRNode) -> Result<(), CodegenError> {
7201 match &node.kind {
7202 NodeKind::If {
7203 condition,
7204 then_block,
7205 else_block,
7206 ..
7207 } => {
7208 let ind = self.indent_str();
7209 let _ = write!(self.buf, "{ind}if ");
7210 self.emit_expr(condition)?;
7211 self.buf.push_str(":\n");
7212 self.indent += 1;
7213 self.emit_block_body(then_block)?;
7214 self.indent -= 1;
7215 if let Some(eb) = else_block {
7216 if matches!(eb.kind, NodeKind::If { .. }) {
7217 let ind = self.indent_str();
7218 let _ = write!(self.buf, "{ind}el");
7219 // Chain `elif` by re-emitting the nested `if` inline.
7220 return self.emit_tail_control_flow_inline(eb);
7221 }
7222 self.writeln("else:");
7223 self.indent += 1;
7224 self.emit_block_body(eb)?;
7225 self.indent -= 1;
7226 }
7227 Ok(())
7228 }
7229 NodeKind::Match { scrutinee, arms } => self.emit_match(scrutinee, arms),
7230 // Not a control-flow node — fall back to a plain tail value (or a
7231 // bare statement inside a loop body; see `emit_tail_value_or_discard`).
7232 _ => self.emit_tail_value_or_discard(node),
7233 }
7234 }
7235
7236 /// `elif`-chaining tail for [`Self::emit_tail_control_flow`]: the caller has
7237 /// already written the `el` prefix, so emit `if <cond>: … (elif/else)`.
7238 fn emit_tail_control_flow_inline(&mut self, node: &AIRNode) -> Result<(), CodegenError> {
7239 let NodeKind::If {
7240 condition,
7241 then_block,
7242 else_block,
7243 ..
7244 } = &node.kind
7245 else {
7246 return self.emit_tail_control_flow(node);
7247 };
7248 self.buf.push_str("if ");
7249 self.emit_expr(condition)?;
7250 self.buf.push_str(":\n");
7251 self.indent += 1;
7252 self.emit_block_body(then_block)?;
7253 self.indent -= 1;
7254 if let Some(eb) = else_block {
7255 if matches!(eb.kind, NodeKind::If { .. }) {
7256 let ind = self.indent_str();
7257 let _ = write!(self.buf, "{ind}el");
7258 return self.emit_tail_control_flow_inline(eb);
7259 }
7260 self.writeln("else:");
7261 self.indent += 1;
7262 self.emit_block_body(eb)?;
7263 self.indent -= 1;
7264 }
7265 Ok(())
7266 }
7267
7268 /// Emit a block (or bare body) in statement position, **assigning** its
7269 /// value to `target` instead of `return`ing it — the value-producing twin of
7270 /// [`Self::emit_block_body`]. Used by [`Self::emit_value_binding`] to hoist
7271 /// an expression-position control-flow construct into Python statements.
7272 ///
7273 /// A *diverging* body (one ending in `return`/`break`/`continue`, or a
7274 /// statement-only block) is emitted as-is with **no** assignment: control
7275 /// leaves the construct before the binding is read, exactly as in Bock where
7276 /// such an arm has type `Never` and unifies with the binding's type.
7277 fn emit_block_body_assigning(
7278 &mut self,
7279 target: &str,
7280 node: &AIRNode,
7281 ) -> Result<(), CodegenError> {
7282 // A value-binding RHS is a *value* context: its tail is assigned to
7283 // `target`, never discarded. Clear any active discard flag (a loop-body
7284 // tail set by an enclosing `emit_loop_body`, or a statement-`match`/`if`
7285 // arm set by `emit_stmt`) so it doesn't leak into this value position; a
7286 // nested loop/statement-construct inside the RHS re-sets the relevant
7287 // flag. Restored after.
7288 let prev_discard = std::mem::replace(&mut self.in_loop_body_tail, false);
7289 let prev_match = std::mem::replace(&mut self.in_stmt_construct_arm, false);
7290 self.enter_shadow_scope();
7291 let r = self.emit_block_body_assigning_inner(target, node);
7292 self.leave_shadow_scope();
7293 self.in_loop_body_tail = prev_discard;
7294 self.in_stmt_construct_arm = prev_match;
7295 r
7296 }
7297
7298 fn emit_block_body_assigning_inner(
7299 &mut self,
7300 target: &str,
7301 node: &AIRNode,
7302 ) -> Result<(), CodegenError> {
7303 if let NodeKind::Block { stmts, tail } = &node.kind {
7304 let task_bindings = Self::collect_task_bindings(stmts);
7305 let prev = std::mem::replace(&mut self.task_bound_names, task_bindings);
7306 for s in stmts {
7307 self.emit_node(s)?;
7308 }
7309 self.task_bound_names = prev;
7310 match tail {
7311 None => {
7312 // No tail value. If the block had no statements either, keep
7313 // the suite non-empty.
7314 if stmts.is_empty() {
7315 self.writeln("pass");
7316 }
7317 }
7318 Some(t) if crate::generator::node_is_statement(t) => {
7319 // Diverging / statement tail: emit as a statement (it leaves
7320 // the construct; nothing is assigned).
7321 self.emit_node(t)?;
7322 }
7323 Some(t) => self.emit_value_assign(target, t)?,
7324 }
7325 } else if crate::generator::node_is_statement(node) {
7326 self.emit_node(node)?;
7327 } else {
7328 self.emit_value_assign(target, node)?;
7329 }
7330 Ok(())
7331 }
7332
7333 /// Emit `<target> = <expr>` for a value expression, recursing through nested
7334 /// control-flow that itself needs statement form (so an arm whose value is
7335 /// another `match`/`loop`/`if`-statement assigns the same target).
7336 fn emit_value_assign(&mut self, target: &str, expr: &AIRNode) -> Result<(), CodegenError> {
7337 if value_needs_stmt_form(expr) {
7338 return self.emit_value_binding(target, expr);
7339 }
7340 // A diverging `raise` (`todo()`/`unreachable()`) cannot be assigned;
7341 // emit it bare (the assignment target is never reached).
7342 if is_raise_expr(expr) {
7343 self.write_indent();
7344 self.emit_expr(expr)?;
7345 self.buf.push('\n');
7346 return Ok(());
7347 }
7348 let ind = self.indent_str();
7349 let _ = write!(self.buf, "{ind}{target} = ");
7350 self.emit_expr(expr)?;
7351 self.buf.push('\n');
7352 Ok(())
7353 }
7354
7355 /// Lower an **expression-position control-flow** value (`match` with
7356 /// statement/diverging arms, a value-`loop`, or a statement-form `if`) bound
7357 /// to `target` into Python statements that assign `target`.
7358 ///
7359 /// Python has no statement-admitting expression form, so
7360 /// `let r = loop { … break v }` / `let l = match n { _ => { return … } }`
7361 /// cannot be emitted as a ternary/IIFE. Callers ([`Self::emit_stmt`]'s
7362 /// `LetBinding` arm) first declare `target = None` so it is always bound,
7363 /// then call this to fill it in via real `if`/`while`/`match` statements.
7364 fn emit_value_binding(&mut self, target: &str, value: &AIRNode) -> Result<(), CodegenError> {
7365 match &value.kind {
7366 NodeKind::Block { .. } => self.emit_block_body_assigning(target, value),
7367 NodeKind::Match { scrutinee, arms } => {
7368 self.emit_match_assigning(target, scrutinee, arms)
7369 }
7370 NodeKind::If {
7371 condition,
7372 then_block,
7373 else_block,
7374 ..
7375 } => {
7376 let ind = self.indent_str();
7377 let _ = write!(self.buf, "{ind}if ");
7378 self.emit_expr(condition)?;
7379 self.buf.push_str(":\n");
7380 self.indent += 1;
7381 self.emit_block_body_assigning(target, then_block)?;
7382 self.indent -= 1;
7383 if let Some(eb) = else_block {
7384 if matches!(eb.kind, NodeKind::If { .. }) {
7385 let ind = self.indent_str();
7386 let _ = write!(self.buf, "{ind}el");
7387 // Re-enter via `emit_value_binding` to chain `elif`.
7388 self.emit_value_binding_if_chain(target, eb)?;
7389 } else {
7390 self.writeln("else:");
7391 self.indent += 1;
7392 self.emit_block_body_assigning(target, eb)?;
7393 self.indent -= 1;
7394 }
7395 }
7396 Ok(())
7397 }
7398 NodeKind::Loop { body } => {
7399 self.writeln("while True:");
7400 self.indent += 1;
7401 self.loop_value_targets.push(Some(target.to_string()));
7402 // The loop's value arrives via `break v` (recorded in
7403 // `loop_value_targets`), not the body's tail; the body is
7404 // statement position, so a tail expr is discarded.
7405 self.emit_loop_body(body)?;
7406 self.loop_value_targets.pop();
7407 self.indent -= 1;
7408 Ok(())
7409 }
7410 NodeKind::While { condition, body } => {
7411 let ind = self.indent_str();
7412 let _ = write!(self.buf, "{ind}while ");
7413 self.emit_expr(condition)?;
7414 self.buf.push_str(":\n");
7415 self.indent += 1;
7416 self.loop_value_targets.push(Some(target.to_string()));
7417 // The loop's value arrives via `break v` (recorded in
7418 // `loop_value_targets`), not the body's tail; the body is
7419 // statement position, so a tail expr is discarded.
7420 self.emit_loop_body(body)?;
7421 self.loop_value_targets.pop();
7422 self.indent -= 1;
7423 Ok(())
7424 }
7425 // Not a hoisted construct: plain assignment.
7426 _ => self.emit_value_assign(target, value),
7427 }
7428 }
7429
7430 /// Helper for [`Self::emit_value_binding`]'s `elif` chaining: emit the
7431 /// keyword tail of an `if` (`if <cond>: … elif …`) for an `else if`. The
7432 /// caller has already written the `el` prefix.
7433 fn emit_value_binding_if_chain(
7434 &mut self,
7435 target: &str,
7436 node: &AIRNode,
7437 ) -> Result<(), CodegenError> {
7438 let NodeKind::If {
7439 condition,
7440 then_block,
7441 else_block,
7442 ..
7443 } = &node.kind
7444 else {
7445 return self.emit_value_binding(target, node);
7446 };
7447 self.buf.push_str("if ");
7448 self.emit_expr(condition)?;
7449 self.buf.push_str(":\n");
7450 self.indent += 1;
7451 self.emit_block_body_assigning(target, then_block)?;
7452 self.indent -= 1;
7453 if let Some(eb) = else_block {
7454 if matches!(eb.kind, NodeKind::If { .. }) {
7455 let ind = self.indent_str();
7456 let _ = write!(self.buf, "{ind}el");
7457 self.emit_value_binding_if_chain(target, eb)?;
7458 } else {
7459 self.writeln("else:");
7460 self.indent += 1;
7461 self.emit_block_body_assigning(target, eb)?;
7462 self.indent -= 1;
7463 }
7464 }
7465 Ok(())
7466 }
7467
7468 /// Emit a `match` (statement form) whose arms **assign** `target` rather than
7469 /// `return`. Mirrors [`Self::emit_match`] but uses
7470 /// [`Self::emit_block_body_assigning`] for arm bodies.
7471 fn emit_match_assigning(
7472 &mut self,
7473 target: &str,
7474 scrutinee: &AIRNode,
7475 arms: &[AIRNode],
7476 ) -> Result<(), CodegenError> {
7477 let ind = self.indent_str();
7478 let _ = write!(self.buf, "{ind}match ");
7479 self.emit_expr(scrutinee)?;
7480 self.buf.push_str(":\n");
7481 self.indent += 1;
7482 for arm in arms {
7483 if let NodeKind::MatchArm {
7484 pattern,
7485 guard,
7486 body,
7487 } = &arm.kind
7488 {
7489 let ind = self.indent_str();
7490 let _ = write!(self.buf, "{ind}case ");
7491 self.emit_pattern(pattern)?;
7492 if let Some(g) = guard {
7493 self.buf.push_str(" if ");
7494 self.emit_expr(g)?;
7495 }
7496 self.buf.push_str(":\n");
7497 self.indent += 1;
7498 self.emit_block_body_assigning(target, body)?;
7499 self.indent -= 1;
7500 }
7501 }
7502 self.indent -= 1;
7503 Ok(())
7504 }
7505
7506 fn emit_block_as_expr(&mut self, node: &AIRNode) -> Result<(), CodegenError> {
7507 if let NodeKind::Block { stmts, tail } = &node.kind {
7508 if stmts.is_empty() {
7509 if let Some(t) = tail {
7510 return self.emit_expr(t);
7511 }
7512 } else if self.try_emit_block_stmts_as_expr(stmts, tail.as_deref())? {
7513 // A block with leading statements *and* a tail value — e.g. a
7514 // value-position `match` arm `Ok(sum) => { let s = …; … }`. The
7515 // leading `let`s / side-effecting expression statements are
7516 // folded into immediately-applied lambdas so they actually run
7517 // and their bindings are in scope for the tail. See
7518 // `try_emit_block_stmts_as_expr`.
7519 return Ok(());
7520 }
7521 }
7522 self.emit_expr(node)
7523 }
7524
7525 /// Emit a block's leading statements + tail as a single Python expression by
7526 /// folding each statement into an immediately-applied lambda, preserving
7527 /// both the statement's effect and any binding it introduces:
7528 ///
7529 /// ```text
7530 /// { let x = V; REST } → (lambda x: <REST>)(V)
7531 /// { side_effect(); REST } → (lambda _: <REST>)(side_effect())
7532 /// ```
7533 ///
7534 /// Python `lambda` bodies are expression-only, so a block with statements in
7535 /// expression position (a value-position `match`/`if` arm) otherwise loses
7536 /// its leading statements — the old "best effort" emitted `(lambda: <tail>)()`
7537 /// and dropped them, leaving later references unbound (the calculator
7538 /// `let step2 = …` bug) or skipping a side effect (microservice's dropped
7539 /// `log`).
7540 ///
7541 /// Returns `Ok(true)` when the whole block was emitted this way. Returns
7542 /// `Ok(false)` — emitting nothing — when a leading statement can't be
7543 /// expressed as a pure expression (mutable `let`, assignment, loop, …); the
7544 /// caller then falls back to the prior best-effort path. The conservative
7545 /// gate keeps this from emitting broken code for shapes it can't model.
7546 fn try_emit_block_stmts_as_expr(
7547 &mut self,
7548 stmts: &[AIRNode],
7549 tail: Option<&AIRNode>,
7550 ) -> Result<bool, CodegenError> {
7551 // Every leading statement must be expressible as a value-producing
7552 // expression: an immutable simple `let`, or a plain expression
7553 // statement. Anything else (mutable/destructuring `let`, assignment,
7554 // loop, `return`, nested block) bails so the caller can fall back.
7555 for s in stmts {
7556 match &s.kind {
7557 NodeKind::LetBinding {
7558 is_mut, pattern, ..
7559 } if *is_mut || Self::simple_bind_name(pattern).is_none() => {
7560 return Ok(false);
7561 }
7562 NodeKind::LetBinding { .. } => {}
7563 NodeKind::Assign { .. }
7564 | NodeKind::While { .. }
7565 | NodeKind::For { .. }
7566 | NodeKind::Loop { .. }
7567 | NodeKind::Return { .. }
7568 | NodeKind::Break { .. }
7569 | NodeKind::Continue
7570 | NodeKind::Block { .. } => return Ok(false),
7571 _ => {}
7572 }
7573 }
7574 self.emit_block_stmt_chain(stmts, tail)?;
7575 Ok(true)
7576 }
7577
7578 /// Recursively emit the `(lambda …: …)(…)` chain validated by
7579 /// [`Self::try_emit_block_stmts_as_expr`].
7580 fn emit_block_stmt_chain(
7581 &mut self,
7582 stmts: &[AIRNode],
7583 tail: Option<&AIRNode>,
7584 ) -> Result<(), CodegenError> {
7585 let Some((first, rest)) = stmts.split_first() else {
7586 // No more leading statements — emit the tail value (or `None`).
7587 return match tail {
7588 Some(t) => self.emit_expr(t),
7589 None => {
7590 self.buf.push_str("None");
7591 Ok(())
7592 }
7593 };
7594 };
7595 match &first.kind {
7596 NodeKind::LetBinding { pattern, value, .. } => {
7597 let name = Self::simple_bind_name(pattern).unwrap_or_else(|| "_".to_string());
7598 let _ = write!(self.buf, "(lambda {name}: ");
7599 self.emit_block_stmt_chain(rest, tail)?;
7600 self.buf.push_str(")(");
7601 self.emit_expr(value)?;
7602 self.buf.push(')');
7603 Ok(())
7604 }
7605 // A bare expression statement: bind its value to a throwaway
7606 // parameter so the effect runs, then continue with the rest.
7607 _ => {
7608 self.buf.push_str("(lambda _: ");
7609 self.emit_block_stmt_chain(rest, tail)?;
7610 self.buf.push_str(")(");
7611 self.emit_expr(first)?;
7612 self.buf.push(')');
7613 Ok(())
7614 }
7615 }
7616 }
7617
7618 /// The single Python value-name a *simple* `let` pattern binds, if any: only
7619 /// a `BindPat` qualifies (tuple/record patterns bind several names or use
7620 /// Python-side destructuring that shadow-renaming does not rewrite). Used to
7621 /// gate nested-block `let`-shadow renaming to the simple, common case.
7622 fn simple_bind_name(pat: &AIRNode) -> Option<String> {
7623 match &pat.kind {
7624 NodeKind::BindPat { name, .. } => Some(py_value_ident(&name.name)),
7625 _ => None,
7626 }
7627 }
7628
7629 fn pattern_to_binding_name(&self, pat: &AIRNode) -> String {
7630 match &pat.kind {
7631 NodeKind::BindPat { name, .. } => py_value_ident(&name.name),
7632 NodeKind::WildcardPat => "_".into(),
7633 NodeKind::TuplePat { elems } => {
7634 format!(
7635 "({})",
7636 elems
7637 .iter()
7638 .map(|e| self.pattern_to_binding_name(e))
7639 .collect::<Vec<_>>()
7640 .join(", ")
7641 )
7642 }
7643 NodeKind::RecordPat { fields, .. } => {
7644 // Python doesn't have destructuring; use first field name or
7645 // underscore (keyword-escaped like every value binding).
7646 fields
7647 .first()
7648 .map(|f| py_value_ident(&f.name.name))
7649 .unwrap_or_else(|| "_".into())
7650 }
7651 _ => "_".into(),
7652 }
7653 }
7654
7655 fn pattern_to_py_binding(&self, pat: &AIRNode) -> String {
7656 self.pattern_to_binding_name(pat)
7657 }
7658
7659 fn type_expr_to_string(&self, node: &AIRNode) -> String {
7660 match &node.kind {
7661 NodeKind::TypeNamed { path, .. } => path
7662 .segments
7663 .iter()
7664 .map(|s| s.name.as_str())
7665 .collect::<Vec<_>>()
7666 .join("."),
7667 NodeKind::Identifier { name } => name.name.clone(),
7668 _ => "Unknown".into(),
7669 }
7670 }
7671}
7672
7673// ─── Utility functions ───────────────────────────────────────────────────────
7674
7675/// Convert a `PascalCase` or `camelCase` name to `snake_case`.
7676/// Extract the unqualified type name from an `impl` target AIR node.
7677/// Returns `None` for types that aren't simple named references
7678/// (tuples, function types, etc.).
7679fn ast_type_name(node: &AIRNode) -> Option<String> {
7680 if let NodeKind::TypeNamed { path, .. } = &node.kind {
7681 path.segments.last().map(|s| s.name.clone())
7682 } else {
7683 None
7684 }
7685}
7686
7687/// Compute a stable emission order over a module's top-level `items` such that a
7688/// type declaration (`trait` / `record` / `class`) that becomes a Python base
7689/// class of another (its supertype) is always emitted *before* the subtype.
7690///
7691/// Python evaluates a `class Sub(Base):` statement's base list eagerly, so
7692/// `Base` must already be a bound name when `Sub` is defined; emitting them in
7693/// source order risks `NameError` when a trait is declared after the type that
7694/// impls it, or when an inlined-impl record precedes its trait
7695/// (Q-py-impl-before-trait).
7696///
7697/// The reorder is a **stable topological sort**: items are emitted in original
7698/// order except that any type decl is delayed until all the type decls it
7699/// depends on (its declared `base`, declared `traits`, and the trait paths of
7700/// the impl blocks targeting it in `impls_by_target`) have been emitted. Only
7701/// dependencies on types *declared in this same module* create edges; references
7702/// to imported/prelude bases never block (they resolve via imports). A
7703/// dependency cycle (which Python could not represent anyway) degrades
7704/// gracefully to source order for the involved nodes rather than dropping them.
7705fn type_decl_emission_order(
7706 items: &[AIRNode],
7707 impls_by_target: &HashMap<String, Vec<AIRNode>>,
7708) -> Vec<usize> {
7709 use std::collections::HashMap as Map;
7710
7711 // name → index for every type decl declared in this module. Effects are
7712 // included because the Python backend also emits an `effect` as an ABC class
7713 // that an `impl Effect for T` makes a *base* of `T` (`class StubChannel(
7714 // Channel)`), so an effect declared after its impl is the same base-ordering
7715 // hazard as a trait.
7716 let mut decl_index: Map<String, usize> = Map::new();
7717 for (i, item) in items.iter().enumerate() {
7718 match &item.kind {
7719 NodeKind::TraitDecl { name, .. }
7720 | NodeKind::RecordDecl { name, .. }
7721 | NodeKind::ClassDecl { name, .. }
7722 | NodeKind::EffectDecl { name, .. } => {
7723 decl_index.entry(name.name.clone()).or_insert(i);
7724 }
7725 _ => {}
7726 }
7727 }
7728
7729 // deps[i] = set of item indices that item i must follow (its in-module
7730 // base/trait supertypes). Non-type items have no deps.
7731 let mut deps: Vec<Vec<usize>> = vec![Vec::new(); items.len()];
7732 let add_dep = |deps: &mut Vec<Vec<usize>>, i: usize, dep_name: &str| {
7733 if let Some(&j) = decl_index.get(dep_name) {
7734 if j != i && !deps[i].contains(&j) {
7735 deps[i].push(j);
7736 }
7737 }
7738 };
7739 for (i, item) in items.iter().enumerate() {
7740 let (name, declared_base, declared_traits) = match &item.kind {
7741 NodeKind::ClassDecl {
7742 name, base, traits, ..
7743 } => (Some(name), base.as_ref(), traits.as_slice()),
7744 NodeKind::RecordDecl { name, .. } | NodeKind::TraitDecl { name, .. } => {
7745 (Some(name), None, [].as_slice())
7746 }
7747 _ => (None, None, [].as_slice()),
7748 };
7749 let Some(name) = name else { continue };
7750 if let Some(b) = declared_base {
7751 if let Some(seg) = b.segments.last() {
7752 add_dep(&mut deps, i, &seg.name);
7753 }
7754 }
7755 for tp in declared_traits {
7756 if let Some(seg) = tp.segments.last() {
7757 add_dep(&mut deps, i, &seg.name);
7758 }
7759 }
7760 // Trait paths from the impl blocks that fold into this type's body.
7761 if let Some(impls) = impls_by_target.get(&name.name) {
7762 for im in impls {
7763 if let NodeKind::ImplBlock {
7764 trait_path: Some(tp),
7765 ..
7766 } = &im.kind
7767 {
7768 if let Some(seg) = tp.segments.last() {
7769 add_dep(&mut deps, i, &seg.name);
7770 }
7771 }
7772 }
7773 }
7774 }
7775
7776 // Stable topological emit: repeatedly take the earliest not-yet-emitted item
7777 // whose deps are all emitted. If none qualifies (a cycle), take the earliest
7778 // remaining item to make progress (graceful degradation).
7779 let n = items.len();
7780 let mut emitted = vec![false; n];
7781 let mut order = Vec::with_capacity(n);
7782 for _ in 0..n {
7783 let mut pick = None;
7784 for i in 0..n {
7785 if emitted[i] {
7786 continue;
7787 }
7788 if deps[i].iter().all(|&d| emitted[d]) {
7789 pick = Some(i);
7790 break;
7791 }
7792 }
7793 let i = pick.unwrap_or_else(|| (0..n).find(|&i| !emitted[i]).unwrap_or(0));
7794 emitted[i] = true;
7795 order.push(i);
7796 }
7797 order
7798}
7799
7800/// Emit a Bock identifier as a Python identifier. PascalCase names are
7801/// preserved — they denote classes, ABC traits, or enum variant constructors,
7802/// all of which stay PascalCase in Python by convention.
7803fn identifier_to_py(s: &str) -> String {
7804 if s.chars().next().is_some_and(char::is_uppercase) {
7805 s.to_string()
7806 } else {
7807 py_value_ident(s)
7808 }
7809}
7810
7811/// Convert a Bock *value* identifier (a param, local binding, or free-function
7812/// name) to its Python form: `snake_case`, then escaped against the Python
7813/// keyword set so a binding named e.g. `def` emits `def_` rather than the
7814/// illegal bare keyword. Apply at every value declaration and reference site so
7815/// the escaped name is used uniformly; member/method names use bare
7816/// [`to_snake_case`]. See [`crate::generator::escape_target_keyword`].
7817fn py_value_ident(name: &str) -> String {
7818 crate::generator::escape_target_keyword(
7819 &to_snake_case(name),
7820 crate::generator::KeywordTarget::Python,
7821 )
7822}
7823
7824/// The Python spelling of a record / class / enum-struct-variant **field**
7825/// name: `snake_case`, then escaped against the Python keyword set — the same
7826/// policy as value identifiers ([`py_value_ident`]), extended to field
7827/// position (Q-python-keyword-record-fields). A Bock field named `pass` emits
7828/// as `pass_`; left verbatim it is a `SyntaxError` in the dataclass
7829/// declaration (`pass: int`), the constructor keyword args (`Tally(pass=7)`),
7830/// the attribute access (`t.pass`), and the match pattern
7831/// (`case Tally(pass=p)`). Applied identically at every field position —
7832/// dataclass / `__init__` declaration, constructor kwargs (plain and spread
7833/// dict keys), field access, and record-pattern destructuring — so the escaped
7834/// spelling always agrees. A record-pattern *shorthand* (`{ pass }`) binds a
7835/// value identifier of the same Bock name, which [`py_value_ident`] escapes to
7836/// the identical spelling at every reference site.
7837fn py_field_ident(name: &str) -> String {
7838 py_value_ident(name)
7839}
7840
7841/// True when `name` (an already snake_cased Python value identifier) collides
7842/// with a built-in that the Python backend *emits as a call* in generated code —
7843/// the collection constructors and functional combinators a list/set/map literal
7844/// or method lowers to (`list(...)`, `set(...)`, `dict(...)`, `map(...)`,
7845/// `filter(...)`, `sorted(...)`, `enumerate(...)`, `range(...)`, `len(...)`,
7846/// `tuple(...)`, `frozenset(...)`, `zip(...)`, `iter(...)`, `print(...)`).
7847///
7848/// A local `let list = [...]` rebinds these names for the rest of the
7849/// (function-scoped) Python frame, so a subsequent codegen-emitted `list(...)`
7850/// would raise `TypeError: 'list' object is not callable`. [`PyEmitCtx::
7851/// plan_shadow_let`] renames such bindings to a fresh alias so the builtin stays
7852/// callable; references resolve through the same alias map. The set is limited to
7853/// builtins the codegen actually emits (not every Python builtin) so unrelated
7854/// names are never mangled. Bock keywords (`type`, etc.) are handled separately
7855/// by [`crate::generator::escape_target_keyword`].
7856fn is_shadow_sensitive_py_builtin(name: &str) -> bool {
7857 matches!(
7858 name,
7859 "list"
7860 | "set"
7861 | "dict"
7862 | "map"
7863 | "filter"
7864 | "sorted"
7865 | "enumerate"
7866 | "range"
7867 | "len"
7868 | "tuple"
7869 | "frozenset"
7870 | "zip"
7871 | "iter"
7872 | "next"
7873 | "print"
7874 | "str"
7875 | "int"
7876 | "float"
7877 | "bool"
7878 | "abs"
7879 | "min"
7880 | "max"
7881 | "sum"
7882 | "round"
7883 )
7884}
7885
7886/// Render a `RangePat` bound (`lo`/`hi`) as a Python expression. Range bounds
7887/// are literals (`1..10`) or a const identifier (`MIN..MAX`); anything else
7888/// falls back to `0` for an unrecognised node. Mirrors `range_bound_to_js`.
7889fn range_bound_to_py(node: &AIRNode) -> String {
7890 let lit = match &node.kind {
7891 NodeKind::LiteralPat { lit } | NodeKind::Literal { lit } => Some(lit),
7892 NodeKind::Identifier { name } => return py_value_ident(&name.name),
7893 _ => None,
7894 };
7895 match lit {
7896 Some(Literal::Int(s)) | Some(Literal::Float(s)) => s.clone(),
7897 Some(Literal::Bool(b)) => if *b { "True" } else { "False" }.to_string(),
7898 Some(Literal::Char(s)) => format!("'{s}'"),
7899 Some(Literal::String(s)) => format!("\"{}\"", escape_py_string(s)),
7900 Some(Literal::Unit) | None => "0".to_string(),
7901 }
7902}
7903
7904/// Returns true if `name` is the identifier of a Duration or Instant instance
7905/// method. Used to recognise `d.as_millis()` / `i.elapsed()` calls during codegen.
7906fn is_time_method_name(name: &str) -> bool {
7907 matches!(
7908 name,
7909 "as_nanos"
7910 | "as_millis"
7911 | "as_seconds"
7912 | "is_zero"
7913 | "is_negative"
7914 | "abs"
7915 | "elapsed"
7916 | "duration_since"
7917 )
7918}
7919
7920/// Walk a `@test` body and record the Optional/Result runtime tag classes its
7921/// predicate assertions (`to_be_some`/`to_be_none`/`to_be_ok`/`to_be_err`)
7922/// reference, so the Python test file imports exactly those from
7923/// `_bock_runtime` (which only defines the runtimes the program uses).
7924fn collect_runtime_tag_imports(node: &AIRNode, out: &mut std::collections::BTreeSet<&'static str>) {
7925 if let Some((assertion, _actual, _expected)) = crate::generator::classify_assertion(node) {
7926 use crate::generator::TestAssertion as T;
7927 match assertion {
7928 T::BeSome => {
7929 out.insert("_BockSome");
7930 }
7931 T::BeNone => {
7932 out.insert("_BockNone");
7933 }
7934 T::BeOk => {
7935 out.insert("_BockOk");
7936 }
7937 T::BeErr => {
7938 out.insert("_BockErr");
7939 }
7940 _ => {}
7941 }
7942 }
7943 if let NodeKind::Block { stmts, tail } = &node.kind {
7944 for s in stmts {
7945 collect_runtime_tag_imports(s, out);
7946 }
7947 if let Some(t) = tail {
7948 collect_runtime_tag_imports(t, out);
7949 }
7950 }
7951}
7952
7953fn to_snake_case(s: &str) -> String {
7954 // If already snake_case or a single word, return as-is
7955 if s.is_empty() || s == "_" {
7956 return s.to_string();
7957 }
7958 // Don't convert if it's already snake_case (contains underscores but no uppercase)
7959 if s.contains('_') && !s.chars().any(|c| c.is_uppercase()) {
7960 return s.to_string();
7961 }
7962 // Don't convert simple lowercase words or all-uppercase words
7963 if !s.chars().any(|c| c.is_uppercase()) {
7964 return s.to_string();
7965 }
7966 // Special case: single char
7967 if s.len() == 1 {
7968 return s.to_lowercase();
7969 }
7970
7971 let mut result = String::with_capacity(s.len() + 4);
7972 let chars: Vec<char> = s.chars().collect();
7973
7974 for (i, &ch) in chars.iter().enumerate() {
7975 if ch.is_uppercase() {
7976 let prev_is_upper = i > 0 && chars[i - 1].is_uppercase();
7977 let prev_is_underscore = i > 0 && chars[i - 1] == '_';
7978 let next_is_lower = i + 1 < chars.len() && chars[i + 1].is_lowercase();
7979 if i > 0 && !prev_is_underscore && (!prev_is_upper || next_is_lower) {
7980 result.push('_');
7981 }
7982 result.push(
7983 ch.to_lowercase()
7984 .next()
7985 .expect("lowercase yields at least one char"),
7986 );
7987 } else {
7988 result.push(ch);
7989 }
7990 }
7991 result
7992}
7993
7994/// Escape special characters in a Python string literal.
7995fn escape_py_string(s: &str) -> String {
7996 let mut out = String::with_capacity(s.len());
7997 for ch in s.chars() {
7998 match ch {
7999 '"' => out.push_str("\\\""),
8000 '\\' => out.push_str("\\\\"),
8001 '\n' => out.push_str("\\n"),
8002 '\r' => out.push_str("\\r"),
8003 '\t' => out.push_str("\\t"),
8004 _ => out.push(ch),
8005 }
8006 }
8007 out
8008}
8009
8010/// Escape special characters in a Python f-string.
8011fn escape_fstring(s: &str) -> String {
8012 let mut out = String::with_capacity(s.len());
8013 for ch in s.chars() {
8014 match ch {
8015 '"' => out.push_str("\\\""),
8016 '\\' => out.push_str("\\\\"),
8017 '\n' => out.push_str("\\n"),
8018 '\r' => out.push_str("\\r"),
8019 '\t' => out.push_str("\\t"),
8020 '{' => out.push_str("{{"),
8021 '}' => out.push_str("}}"),
8022 _ => out.push(ch),
8023 }
8024 }
8025 out
8026}
8027
8028/// Escape special characters in a triple-quoted Python f-string.
8029/// Newlines pass through literally; quotes don't need escaping.
8030fn escape_fstring_triple(s: &str) -> String {
8031 let mut out = String::with_capacity(s.len());
8032 for ch in s.chars() {
8033 match ch {
8034 '\\' => out.push_str("\\\\"),
8035 '{' => out.push_str("{{"),
8036 '}' => out.push_str("}}"),
8037 _ => out.push(ch),
8038 }
8039 }
8040 out
8041}
8042
8043// ─── Tests ───────────────────────────────────────────────────────────────────
8044
8045#[cfg(test)]
8046mod tests {
8047 use super::*;
8048 use bock_air::{AirArg, AirRecordField, AirRecordPatternField};
8049 use bock_ast::{Ident, TypePath};
8050 use bock_errors::{FileId, Span};
8051
8052 fn span() -> Span {
8053 Span {
8054 file: FileId(0),
8055 start: 0,
8056 end: 0,
8057 }
8058 }
8059
8060 fn ident(name: &str) -> Ident {
8061 Ident {
8062 name: name.to_string(),
8063 span: span(),
8064 }
8065 }
8066
8067 fn type_path(segments: &[&str]) -> TypePath {
8068 TypePath {
8069 segments: segments.iter().map(|s| ident(s)).collect(),
8070 span: span(),
8071 }
8072 }
8073
8074 fn node(id: u32, kind: NodeKind) -> AIRNode {
8075 AIRNode::new(id, span(), kind)
8076 }
8077
8078 fn int_lit(id: u32, val: &str) -> AIRNode {
8079 node(
8080 id,
8081 NodeKind::Literal {
8082 lit: Literal::Int(val.into()),
8083 },
8084 )
8085 }
8086
8087 fn str_lit(id: u32, val: &str) -> AIRNode {
8088 node(
8089 id,
8090 NodeKind::Literal {
8091 lit: Literal::String(val.into()),
8092 },
8093 )
8094 }
8095
8096 fn bool_lit(id: u32, val: bool) -> AIRNode {
8097 node(
8098 id,
8099 NodeKind::Literal {
8100 lit: Literal::Bool(val),
8101 },
8102 )
8103 }
8104
8105 fn id_node(id: u32, name: &str) -> AIRNode {
8106 node(id, NodeKind::Identifier { name: ident(name) })
8107 }
8108
8109 fn bind_pat(id: u32, name: &str) -> AIRNode {
8110 node(
8111 id,
8112 NodeKind::BindPat {
8113 name: ident(name),
8114 is_mut: false,
8115 },
8116 )
8117 }
8118
8119 fn param_node(id: u32, name: &str) -> AIRNode {
8120 node(
8121 id,
8122 NodeKind::Param {
8123 pattern: Box::new(bind_pat(id + 100, name)),
8124 ty: None,
8125 default: None,
8126 },
8127 )
8128 }
8129
8130 fn typed_param_node(id: u32, name: &str, ty_name: &str) -> AIRNode {
8131 node(
8132 id,
8133 NodeKind::Param {
8134 pattern: Box::new(bind_pat(id + 100, name)),
8135 ty: Some(Box::new(node(
8136 id + 200,
8137 NodeKind::TypeNamed {
8138 path: type_path(&[ty_name]),
8139 args: vec![],
8140 },
8141 ))),
8142 default: None,
8143 },
8144 )
8145 }
8146
8147 fn block(id: u32, stmts: Vec<AIRNode>, tail: Option<AIRNode>) -> AIRNode {
8148 node(
8149 id,
8150 NodeKind::Block {
8151 stmts,
8152 tail: tail.map(Box::new),
8153 },
8154 )
8155 }
8156
8157 fn module(imports: Vec<AIRNode>, items: Vec<AIRNode>) -> AIRNode {
8158 node(
8159 0,
8160 NodeKind::Module {
8161 path: None,
8162 annotations: vec![],
8163 imports,
8164 items,
8165 },
8166 )
8167 }
8168
8169 fn gen(module: &AIRNode) -> String {
8170 let gen = PyGenerator::new();
8171 let result = gen.generate_module(module).unwrap();
8172 result.files[0].content.clone()
8173 }
8174
8175 // ── Basic tests ─────────────────────────────────────────────────────────
8176
8177 #[test]
8178 fn implements_code_generator_trait() {
8179 let gen = PyGenerator::new();
8180 assert_eq!(gen.target().id, "python");
8181 }
8182
8183 #[test]
8184 fn empty_module() {
8185 let m = module(vec![], vec![]);
8186 let out = gen(&m);
8187 assert_eq!(out, "");
8188 }
8189
8190 /// A module node with a declared dotted `path` (e.g. `core.option`), used
8191 /// by the per-module emission tests where the file layout and import path
8192 /// are keyed on the declared module-path.
8193 fn module_with_path(path: &[&str], imports: Vec<AIRNode>, items: Vec<AIRNode>) -> AIRNode {
8194 node(
8195 0,
8196 NodeKind::Module {
8197 path: Some(bock_ast::ModulePath {
8198 segments: path.iter().map(|s| ident(s)).collect(),
8199 span: span(),
8200 }),
8201 annotations: vec![],
8202 imports,
8203 items,
8204 },
8205 )
8206 }
8207
8208 /// An `import <path>.{ name }` AIR node (a `Named` single-item import).
8209 fn import_named(id: u32, path: &[&str], name: &str) -> AIRNode {
8210 node(
8211 id,
8212 NodeKind::ImportDecl {
8213 path: bock_ast::ModulePath {
8214 segments: path.iter().map(|s| ident(s)).collect(),
8215 span: span(),
8216 },
8217 items: bock_ast::ImportItems::Named(vec![bock_ast::ImportedName {
8218 span: span(),
8219 name: ident(name),
8220 alias: None,
8221 }]),
8222 },
8223 )
8224 }
8225
8226 /// A bare `fn <name>() -> <ret? expr>` declaration with the given visibility
8227 /// and a single tail expression as its body.
8228 fn fn_decl_tail(id: u32, vis: Visibility, name: &str, tail: AIRNode) -> AIRNode {
8229 node(
8230 id,
8231 NodeKind::FnDecl {
8232 annotations: vec![],
8233 visibility: vis,
8234 is_async: false,
8235 name: ident(name),
8236 generic_params: vec![],
8237 params: vec![],
8238 return_type: None,
8239 effect_clause: vec![],
8240 where_clause: vec![],
8241 body: Box::new(block(id + 1, vec![], Some(tail))),
8242 },
8243 )
8244 }
8245
8246 #[test]
8247 fn per_module_emits_native_import_tree() {
8248 // entry `module main` uses `mathutil.add_one`; `module mathutil` exports
8249 // a `public fn add_one`. Per-module emission must produce TWO files —
8250 // `main.py` (with a real `from mathutil import add_one`) and a separate
8251 // `mathutil.py` — not a single collapsed file.
8252 let call = node(
8253 10,
8254 NodeKind::Call {
8255 callee: Box::new(id_node(11, "add_one")),
8256 args: vec![AirArg {
8257 label: None,
8258 value: int_lit(12, "6"),
8259 }],
8260 type_args: vec![],
8261 },
8262 );
8263 let main_mod = module_with_path(
8264 &["main"],
8265 vec![import_named(5, &["mathutil"], "add_one")],
8266 vec![fn_decl_tail(1, Visibility::Private, "main", call)],
8267 );
8268 let mathutil_mod = module_with_path(
8269 &["mathutil"],
8270 vec![],
8271 vec![fn_decl_tail(
8272 20,
8273 Visibility::Public,
8274 "add_one",
8275 int_lit(22, "7"),
8276 )],
8277 );
8278
8279 let gen = PyGenerator::new();
8280 let main_path = std::path::Path::new("src/main.bock");
8281 let util_path = std::path::Path::new("src/mathutil.bock");
8282 let out = gen
8283 .generate_project(&[(&main_mod, main_path), (&mathutil_mod, util_path)])
8284 .unwrap();
8285
8286 // Two module files (no shared runtime needed here).
8287 let by_name = |p: &str| out.files.iter().find(|f| f.path == std::path::Path::new(p));
8288 let main_file = by_name("main.py").expect("main.py emitted");
8289 let util_file = by_name("mathutil.py").expect("mathutil.py emitted");
8290 assert!(
8291 main_file.content.contains("from mathutil import add_one"),
8292 "main.py must import from the sibling module; got:\n{}",
8293 main_file.content
8294 );
8295 assert!(
8296 main_file.content.contains("if __name__ == \"__main__\":"),
8297 "main.py must carry the entry invocation; got:\n{}",
8298 main_file.content
8299 );
8300 assert!(
8301 util_file.content.contains("def add_one():"),
8302 "mathutil.py must carry the exported fn; got:\n{}",
8303 util_file.content
8304 );
8305 // The bundling no-op import comment must NOT appear (real import only).
8306 assert!(
8307 !main_file.content.contains("# import"),
8308 "per-module path emits a real import, not a comment"
8309 );
8310 }
8311
8312 #[test]
8313 fn per_module_shares_optional_runtime() {
8314 // Two modules both referencing `None` must share ONE `_bock_runtime.py`
8315 // (so `_bock_none` is the same object across files) and import it.
8316 let main_mod = module_with_path(
8317 &["main"],
8318 vec![import_named(5, &["other"], "thing")],
8319 vec![fn_decl_tail(
8320 1,
8321 Visibility::Private,
8322 "main",
8323 id_node(12, "None"),
8324 )],
8325 );
8326 let other_mod = module_with_path(
8327 &["other"],
8328 vec![],
8329 vec![fn_decl_tail(
8330 20,
8331 Visibility::Public,
8332 "thing",
8333 id_node(22, "None"),
8334 )],
8335 );
8336 let gen = PyGenerator::new();
8337 let out = gen
8338 .generate_project(&[
8339 (&main_mod, std::path::Path::new("src/main.bock")),
8340 (&other_mod, std::path::Path::new("src/other.bock")),
8341 ])
8342 .unwrap();
8343 let runtime = out
8344 .files
8345 .iter()
8346 .find(|f| f.path == std::path::Path::new("_bock_runtime.py"))
8347 .expect("_bock_runtime.py emitted once");
8348 assert!(runtime.content.contains("class _BockNone:"), "got runtime");
8349 assert!(
8350 runtime.content.contains("__all__") && runtime.content.contains("\"_bock_none\""),
8351 "runtime must export underscore names via __all__; got:\n{}",
8352 runtime.content
8353 );
8354 // Every consuming module imports the shared runtime.
8355 let importers = out
8356 .files
8357 .iter()
8358 .filter(|f| f.content.contains("from _bock_runtime import *"))
8359 .count();
8360 assert_eq!(importers, 2, "both modules import the shared runtime");
8361 }
8362
8363 #[test]
8364 fn simple_function() {
8365 let body = block(2, vec![], Some(int_lit(3, "42")));
8366 let f = node(
8367 1,
8368 NodeKind::FnDecl {
8369 annotations: vec![],
8370 visibility: Visibility::Private,
8371 is_async: false,
8372 name: ident("answer"),
8373 generic_params: vec![],
8374 params: vec![],
8375 return_type: None,
8376 effect_clause: vec![],
8377 where_clause: vec![],
8378 body: Box::new(body),
8379 },
8380 );
8381 let out = gen(&module(vec![], vec![f]));
8382 assert!(out.contains("def answer():"), "got: {out}");
8383 assert!(out.contains("return 42"), "got: {out}");
8384 }
8385
8386 #[test]
8387 fn function_with_params_and_type_hints() {
8388 let body = block(
8389 5,
8390 vec![],
8391 Some(node(
8392 6,
8393 NodeKind::BinaryOp {
8394 op: BinOp::Add,
8395 left: Box::new(id_node(7, "a")),
8396 right: Box::new(id_node(8, "b")),
8397 },
8398 )),
8399 );
8400 let f = node(
8401 1,
8402 NodeKind::FnDecl {
8403 annotations: vec![],
8404 visibility: Visibility::Public,
8405 is_async: false,
8406 name: ident("add"),
8407 generic_params: vec![],
8408 params: vec![
8409 typed_param_node(2, "a", "Int"),
8410 typed_param_node(3, "b", "Int"),
8411 ],
8412 return_type: Some(Box::new(node(
8413 4,
8414 NodeKind::TypeNamed {
8415 path: type_path(&["Int"]),
8416 args: vec![],
8417 },
8418 ))),
8419 effect_clause: vec![],
8420 where_clause: vec![],
8421 body: Box::new(body),
8422 },
8423 );
8424 let out = gen(&module(vec![], vec![f]));
8425 assert!(
8426 out.contains("def add(a: int, b: int) -> int:"),
8427 "got: {out}"
8428 );
8429 assert!(out.contains("(a + b)"), "got: {out}");
8430 }
8431
8432 #[test]
8433 fn async_function() {
8434 let body = block(
8435 3,
8436 vec![],
8437 Some(node(
8438 4,
8439 NodeKind::Await {
8440 expr: Box::new(node(
8441 5,
8442 NodeKind::Call {
8443 callee: Box::new(id_node(6, "fetch")),
8444 args: vec![AirArg {
8445 label: None,
8446 value: str_lit(7, "https://example.com"),
8447 }],
8448 type_args: vec![],
8449 },
8450 )),
8451 },
8452 )),
8453 );
8454 let f = node(
8455 1,
8456 NodeKind::FnDecl {
8457 annotations: vec![],
8458 visibility: Visibility::Private,
8459 is_async: true,
8460 name: ident("fetchData"),
8461 generic_params: vec![],
8462 params: vec![],
8463 return_type: None,
8464 effect_clause: vec![],
8465 where_clause: vec![],
8466 body: Box::new(body),
8467 },
8468 );
8469 let out = gen(&module(vec![], vec![f]));
8470 assert!(out.contains("async def fetch_data():"), "got: {out}");
8471 assert!(out.contains("await fetch"), "got: {out}");
8472 }
8473
8474 #[test]
8475 fn effects_as_keyword_args() {
8476 let body = block(
8477 3,
8478 vec![node(
8479 4,
8480 NodeKind::LetBinding {
8481 is_mut: false,
8482 pattern: Box::new(bind_pat(5, "msg")),
8483 ty: None,
8484 value: Box::new(str_lit(6, "hello")),
8485 },
8486 )],
8487 Some(node(
8488 7,
8489 NodeKind::EffectOp {
8490 effect: type_path(&["Log"]),
8491 operation: ident("info"),
8492 args: vec![AirArg {
8493 label: None,
8494 value: id_node(8, "msg"),
8495 }],
8496 },
8497 )),
8498 );
8499 let f = node(
8500 1,
8501 NodeKind::FnDecl {
8502 annotations: vec![],
8503 visibility: Visibility::Private,
8504 is_async: false,
8505 name: ident("process"),
8506 generic_params: vec![],
8507 params: vec![param_node(2, "data")],
8508 return_type: None,
8509 effect_clause: vec![type_path(&["Log"]), type_path(&["Clock"])],
8510 where_clause: vec![],
8511 body: Box::new(body),
8512 },
8513 );
8514 let out = gen(&module(vec![], vec![f]));
8515 assert!(
8516 out.contains("def process(data, *, log: Log, clock: Clock):"),
8517 "got: {out}"
8518 );
8519 assert!(out.contains("log.info(msg)"), "got: {out}");
8520 }
8521
8522 /// Q-clock-handler-routing: inside a `with Clock` function (where the
8523 /// `clock` handler is in scope), the §18.3.1 time builtins must route
8524 /// through the handler — `Instant.now()` → `clock.now_monotonic()`,
8525 /// `sleep(d)` → `clock.sleep(d)`, `start.elapsed()` → `clock.now_monotonic()
8526 /// - start` — NOT the inlined host primitives (`time.monotonic_ns()` /
8527 /// `asyncio.sleep`).
8528 #[test]
8529 fn clock_time_ops_route_through_handler() {
8530 let out = gen(&module(vec![], vec![clock_timed_fn()]));
8531 assert!(out.contains("clock.now_monotonic()"), "got: {out}");
8532 assert!(out.contains("clock.sleep("), "got: {out}");
8533 assert!(
8534 !out.contains("time.monotonic_ns()"),
8535 "host clock primitive leaked past the handler: {out}"
8536 );
8537 assert!(
8538 !out.contains("asyncio.sleep("),
8539 "host sleep primitive leaked past the handler: {out}"
8540 );
8541 }
8542
8543 /// Builds `fn timed() with Clock { let start = Instant.now(); sleep(
8544 /// Duration.millis(1)); let d = start.elapsed() }`. The `with Clock` clause
8545 /// puts the `clock` handler in scope so the time builtins route through it.
8546 fn clock_timed_fn() -> AIRNode {
8547 let instant_now = node(
8548 40,
8549 NodeKind::Call {
8550 callee: Box::new(node(
8551 41,
8552 NodeKind::FieldAccess {
8553 object: Box::new(id_node(42, "Instant")),
8554 field: ident("now"),
8555 },
8556 )),
8557 args: vec![],
8558 type_args: vec![],
8559 },
8560 );
8561 let duration_millis = node(
8562 50,
8563 NodeKind::Call {
8564 callee: Box::new(node(
8565 51,
8566 NodeKind::FieldAccess {
8567 object: Box::new(id_node(52, "Duration")),
8568 field: ident("millis"),
8569 },
8570 )),
8571 args: vec![AirArg {
8572 label: None,
8573 value: int_lit(53, "1"),
8574 }],
8575 type_args: vec![],
8576 },
8577 );
8578 let sleep_call = node(
8579 60,
8580 NodeKind::Call {
8581 callee: Box::new(id_node(61, "sleep")),
8582 args: vec![AirArg {
8583 label: None,
8584 value: duration_millis,
8585 }],
8586 type_args: vec![],
8587 },
8588 );
8589 let elapsed_call = node(
8590 70,
8591 NodeKind::MethodCall {
8592 receiver: Box::new(id_node(71, "start")),
8593 method: ident("elapsed"),
8594 type_args: vec![],
8595 args: vec![],
8596 },
8597 );
8598 let body = block(
8599 30,
8600 vec![
8601 node(
8602 31,
8603 NodeKind::LetBinding {
8604 is_mut: false,
8605 pattern: Box::new(bind_pat(32, "start")),
8606 ty: None,
8607 value: Box::new(instant_now),
8608 },
8609 ),
8610 sleep_call,
8611 node(
8612 33,
8613 NodeKind::LetBinding {
8614 is_mut: false,
8615 pattern: Box::new(bind_pat(34, "d")),
8616 ty: None,
8617 value: Box::new(elapsed_call),
8618 },
8619 ),
8620 ],
8621 None,
8622 );
8623 node(
8624 1,
8625 NodeKind::FnDecl {
8626 annotations: vec![],
8627 visibility: Visibility::Private,
8628 is_async: false,
8629 name: ident("timed"),
8630 generic_params: vec![],
8631 params: vec![],
8632 return_type: None,
8633 effect_clause: vec![type_path(&["Clock"])],
8634 where_clause: vec![],
8635 body: Box::new(body),
8636 },
8637 )
8638 }
8639
8640 #[test]
8641 fn record_to_dataclass() {
8642 let rec = node(
8643 1,
8644 NodeKind::RecordDecl {
8645 annotations: vec![],
8646 visibility: Visibility::Public,
8647 name: ident("Point"),
8648 generic_params: vec![],
8649 fields: vec![
8650 bock_ast::RecordDeclField {
8651 id: 0,
8652 span: span(),
8653 name: ident("x"),
8654 ty: bock_ast::TypeExpr::Named {
8655 id: 0,
8656 span: span(),
8657 path: type_path(&["Float"]),
8658 args: vec![],
8659 },
8660 default: None,
8661 },
8662 bock_ast::RecordDeclField {
8663 id: 0,
8664 span: span(),
8665 name: ident("y"),
8666 ty: bock_ast::TypeExpr::Named {
8667 id: 0,
8668 span: span(),
8669 path: type_path(&["Float"]),
8670 args: vec![],
8671 },
8672 default: None,
8673 },
8674 ],
8675 },
8676 );
8677 let out = gen(&module(vec![], vec![rec]));
8678 assert!(
8679 out.contains("from dataclasses import dataclass"),
8680 "got: {out}"
8681 );
8682 assert!(out.contains("@dataclass"), "got: {out}");
8683 assert!(out.contains("class Point:"), "got: {out}");
8684 assert!(out.contains("x: float"), "got: {out}");
8685 assert!(out.contains("y: float"), "got: {out}");
8686 }
8687
8688 /// Builds `print("<s>")` as a bare call node.
8689 fn print_call(id: u32, s: &str) -> AIRNode {
8690 node(
8691 id,
8692 NodeKind::Call {
8693 callee: Box::new(id_node(id + 1, "print")),
8694 args: vec![AirArg {
8695 label: None,
8696 value: str_lit(id + 2, s),
8697 }],
8698 type_args: vec![],
8699 },
8700 )
8701 }
8702
8703 /// A mid-block (statement-position) `if`/`else if`/`else` whose branches
8704 /// are bare `print` expressions must lower each branch tail to a *bare
8705 /// statement*, never a function-body `return`, and chain `else if` as
8706 /// `elif` (Q-python-ifelse-truncation). Before the fix: each branch
8707 /// emitted `return print(..)` — aborting the function after the taken
8708 /// branch so the trailing statement never ran — and the chain emitted
8709 /// `el if (..):`, a SyntaxError, because the `el` prefix was followed
8710 /// by a fully-indented statement `if`. Sibling of the statement-`match`
8711 /// fix (#259).
8712 #[test]
8713 fn stmt_position_if_else_discards_branch_tails_and_chains_elif() {
8714 // if c1 { print("a") } else if c2 { print("b") } else { print("c") }
8715 // print("after")
8716 let chain = node(
8717 10,
8718 NodeKind::If {
8719 let_pattern: None,
8720 condition: Box::new(id_node(11, "c1")),
8721 then_block: Box::new(block(12, vec![], Some(print_call(13, "a")))),
8722 else_block: Some(Box::new(node(
8723 20,
8724 NodeKind::If {
8725 let_pattern: None,
8726 condition: Box::new(id_node(21, "c2")),
8727 then_block: Box::new(block(22, vec![], Some(print_call(23, "b")))),
8728 else_block: Some(Box::new(block(30, vec![], Some(print_call(31, "c"))))),
8729 },
8730 ))),
8731 },
8732 );
8733 let f = node(
8734 1,
8735 NodeKind::FnDecl {
8736 annotations: vec![],
8737 visibility: Visibility::Private,
8738 is_async: false,
8739 name: ident("report"),
8740 generic_params: vec![],
8741 params: vec![param_node(2, "c1"), param_node(3, "c2")],
8742 return_type: None,
8743 effect_clause: vec![],
8744 where_clause: vec![],
8745 body: Box::new(block(4, vec![chain, print_call(40, "after")], None)),
8746 },
8747 );
8748 let out = gen(&module(vec![], vec![f]));
8749 assert!(
8750 !out.contains("return print("),
8751 "statement-if branch tails must be discarded, not returned: {out}"
8752 );
8753 assert!(
8754 out.contains("elif c2:"),
8755 "`else if` must chain as a single `elif`: {out}"
8756 );
8757 assert!(
8758 !out.contains("el "),
8759 "the `el` prefix must not be followed by an indented statement: {out}"
8760 );
8761 assert!(
8762 out.contains("print(\"after\""),
8763 "the statement after the chain must still be emitted: {out}"
8764 );
8765 }
8766
8767 /// A `guard` whose `else` block does **not** diverge (spec §8.4 requires
8768 /// divergence, but the checker currently accepts this — surfaced as OPEN)
8769 /// must still not `return` out of the enclosing function: every other
8770 /// backend and the interpreter fall through to the statements after the
8771 /// guard. A spec-conforming diverging `else` is a statement tail and is
8772 /// unaffected by this. Same early-`return` family as the statement
8773 /// `match`/`if` truncations.
8774 #[test]
8775 fn stmt_guard_nondiverging_else_discards_tail() {
8776 let guard = node(
8777 10,
8778 NodeKind::Guard {
8779 let_pattern: None,
8780 condition: Box::new(id_node(11, "ok")),
8781 else_block: Box::new(block(12, vec![], Some(print_call(13, "warn")))),
8782 },
8783 );
8784 let f = node(
8785 1,
8786 NodeKind::FnDecl {
8787 annotations: vec![],
8788 visibility: Visibility::Private,
8789 is_async: false,
8790 name: ident("report"),
8791 generic_params: vec![],
8792 params: vec![param_node(2, "ok")],
8793 return_type: None,
8794 effect_clause: vec![],
8795 where_clause: vec![],
8796 body: Box::new(block(4, vec![guard, print_call(40, "after")], None)),
8797 },
8798 );
8799 let out = gen(&module(vec![], vec![f]));
8800 assert!(
8801 !out.contains("return print("),
8802 "a non-diverging guard else must fall through, not return: {out}"
8803 );
8804 assert!(
8805 out.contains("print(\"after\""),
8806 "the statement after the guard must still be emitted: {out}"
8807 );
8808 }
8809
8810 /// Record / enum-struct-variant fields named after Python keywords must be
8811 /// escaped (`pass` → `pass_`, `lambda` → `lambda_`) at every field
8812 /// position with one agreed spelling: the dataclass declaration, the
8813 /// constructor keyword args, attribute access, and record-pattern
8814 /// destructuring (Q-python-keyword-record-fields; extends #162's value-
8815 /// identifier escaping to field position). Unescaped, the dataclass
8816 /// declaration `pass: int` is a SyntaxError before `main` even runs.
8817 #[test]
8818 fn keyword_record_fields_escaped_at_every_site() {
8819 let int_ty = || bock_ast::TypeExpr::Named {
8820 id: 0,
8821 span: span(),
8822 path: type_path(&["Int"]),
8823 args: vec![],
8824 };
8825 let rec = node(
8826 1,
8827 NodeKind::RecordDecl {
8828 annotations: vec![],
8829 visibility: Visibility::Public,
8830 name: ident("Tally"),
8831 generic_params: vec![],
8832 fields: vec![bock_ast::RecordDeclField {
8833 id: 0,
8834 span: span(),
8835 name: ident("pass"),
8836 ty: int_ty(),
8837 default: None,
8838 }],
8839 },
8840 );
8841 let enum_decl = node(
8842 2,
8843 NodeKind::EnumDecl {
8844 annotations: vec![],
8845 visibility: Visibility::Public,
8846 name: ident("Gate"),
8847 generic_params: vec![],
8848 variants: vec![node(
8849 3,
8850 NodeKind::EnumVariant {
8851 name: ident("Open"),
8852 payload: EnumVariantPayload::Struct(vec![bock_ast::RecordDeclField {
8853 id: 0,
8854 span: span(),
8855 name: ident("lambda"),
8856 ty: int_ty(),
8857 default: None,
8858 }]),
8859 },
8860 )],
8861 },
8862 );
8863 // let t = Tally { pass: 7 }
8864 let let_t = node(
8865 10,
8866 NodeKind::LetBinding {
8867 is_mut: false,
8868 pattern: Box::new(bind_pat(11, "t")),
8869 ty: None,
8870 value: Box::new(node(
8871 12,
8872 NodeKind::RecordConstruct {
8873 path: type_path(&["Tally"]),
8874 fields: vec![AirRecordField {
8875 name: ident("pass"),
8876 value: Some(Box::new(int_lit(13, "7"))),
8877 }],
8878 spread: None,
8879 },
8880 )),
8881 },
8882 );
8883 // print(t.pass)
8884 let access = node(
8885 20,
8886 NodeKind::Call {
8887 callee: Box::new(id_node(21, "print")),
8888 args: vec![AirArg {
8889 label: None,
8890 value: node(
8891 22,
8892 NodeKind::FieldAccess {
8893 object: Box::new(id_node(23, "t")),
8894 field: ident("pass"),
8895 },
8896 ),
8897 }],
8898 type_args: vec![],
8899 },
8900 );
8901 // match t { Tally { pass: p } => print("x") }
8902 let m = node(
8903 30,
8904 NodeKind::Match {
8905 scrutinee: Box::new(id_node(31, "t")),
8906 arms: vec![node(
8907 32,
8908 NodeKind::MatchArm {
8909 pattern: Box::new(node(
8910 33,
8911 NodeKind::RecordPat {
8912 path: type_path(&["Tally"]),
8913 fields: vec![AirRecordPatternField {
8914 name: ident("pass"),
8915 pattern: Some(Box::new(bind_pat(34, "p"))),
8916 }],
8917 rest: false,
8918 },
8919 )),
8920 guard: None,
8921 body: Box::new(block(35, vec![], Some(print_call(36, "x")))),
8922 },
8923 )],
8924 },
8925 );
8926 // let g = Open { lambda: 3 }
8927 let let_g = node(
8928 40,
8929 NodeKind::LetBinding {
8930 is_mut: false,
8931 pattern: Box::new(bind_pat(41, "g")),
8932 ty: None,
8933 value: Box::new(node(
8934 42,
8935 NodeKind::RecordConstruct {
8936 path: type_path(&["Open"]),
8937 fields: vec![AirRecordField {
8938 name: ident("lambda"),
8939 value: Some(Box::new(int_lit(43, "3"))),
8940 }],
8941 spread: None,
8942 },
8943 )),
8944 },
8945 );
8946 let f = node(
8947 5,
8948 NodeKind::FnDecl {
8949 annotations: vec![],
8950 visibility: Visibility::Private,
8951 is_async: false,
8952 name: ident("main"),
8953 generic_params: vec![],
8954 params: vec![],
8955 return_type: None,
8956 effect_clause: vec![],
8957 where_clause: vec![],
8958 body: Box::new(block(6, vec![let_t, access, m, let_g], None)),
8959 },
8960 );
8961 let out = gen(&module(vec![], vec![rec, enum_decl, f]));
8962 assert!(
8963 out.contains("pass_: int"),
8964 "dataclass field must be keyword-escaped: {out}"
8965 );
8966 assert!(
8967 out.contains("Tally(pass_=7)"),
8968 "constructor kwargs must be keyword-escaped: {out}"
8969 );
8970 assert!(
8971 out.contains("t.pass_"),
8972 "field access must be keyword-escaped: {out}"
8973 );
8974 assert!(
8975 out.contains("Tally(pass_=p)"),
8976 "record-pattern destructuring must be keyword-escaped: {out}"
8977 );
8978 assert!(
8979 out.contains("lambda_: int"),
8980 "enum struct-variant field must be keyword-escaped: {out}"
8981 );
8982 assert!(
8983 out.contains("Gate_Open(lambda_=3)"),
8984 "variant constructor kwargs must be keyword-escaped: {out}"
8985 );
8986 assert!(
8987 !out.contains("pass:") && !out.contains("pass=") && !out.contains("lambda:"),
8988 "no field position may emit the unescaped keyword: {out}"
8989 );
8990 }
8991
8992 #[test]
8993 fn enum_to_dataclass_variants() {
8994 let enum_decl = node(
8995 1,
8996 NodeKind::EnumDecl {
8997 annotations: vec![],
8998 visibility: Visibility::Public,
8999 name: ident("Shape"),
9000 generic_params: vec![],
9001 variants: vec![
9002 node(
9003 2,
9004 NodeKind::EnumVariant {
9005 name: ident("Circle"),
9006 payload: EnumVariantPayload::Struct(vec![bock_ast::RecordDeclField {
9007 id: 0,
9008 span: span(),
9009 name: ident("radius"),
9010 ty: bock_ast::TypeExpr::Named {
9011 id: 0,
9012 span: span(),
9013 path: type_path(&["Float"]),
9014 args: vec![],
9015 },
9016 default: None,
9017 }]),
9018 },
9019 ),
9020 node(
9021 3,
9022 NodeKind::EnumVariant {
9023 name: ident("None"),
9024 payload: EnumVariantPayload::Unit,
9025 },
9026 ),
9027 ],
9028 },
9029 );
9030 let out = gen(&module(vec![], vec![enum_decl]));
9031 assert!(
9032 out.contains("from dataclasses import dataclass"),
9033 "got: {out}"
9034 );
9035 assert!(out.contains("@dataclass"), "got: {out}");
9036 assert!(out.contains("class Shape_Circle:"), "got: {out}");
9037 assert!(out.contains("radius: float"), "got: {out}");
9038 assert!(out.contains("_tag: str = \"Circle\""), "got: {out}");
9039 assert!(out.contains("class Shape_None:"), "got: {out}");
9040 assert!(out.contains("_tag: str = \"None\""), "got: {out}");
9041 }
9042
9043 #[test]
9044 fn match_to_match_case() {
9045 let scrutinee = id_node(10, "shape");
9046 let arms = vec![
9047 node(
9048 11,
9049 NodeKind::MatchArm {
9050 pattern: Box::new(node(
9051 12,
9052 NodeKind::ConstructorPat {
9053 path: type_path(&["Shape", "Circle"]),
9054 fields: vec![bind_pat(13, "r")],
9055 },
9056 )),
9057 guard: None,
9058 body: Box::new(block(
9059 14,
9060 vec![],
9061 Some(node(
9062 15,
9063 NodeKind::BinaryOp {
9064 op: BinOp::Mul,
9065 left: Box::new(id_node(16, "r")),
9066 right: Box::new(id_node(17, "r")),
9067 },
9068 )),
9069 )),
9070 },
9071 ),
9072 node(
9073 18,
9074 NodeKind::MatchArm {
9075 pattern: Box::new(node(19, NodeKind::WildcardPat)),
9076 guard: None,
9077 body: Box::new(block(20, vec![], Some(int_lit(21, "0")))),
9078 },
9079 ),
9080 ];
9081 let match_stmt = node(
9082 9,
9083 NodeKind::Match {
9084 scrutinee: Box::new(scrutinee),
9085 arms,
9086 },
9087 );
9088 let f = node(
9089 1,
9090 NodeKind::FnDecl {
9091 annotations: vec![],
9092 visibility: Visibility::Private,
9093 is_async: false,
9094 name: ident("area"),
9095 generic_params: vec![],
9096 params: vec![param_node(2, "shape")],
9097 return_type: None,
9098 effect_clause: vec![],
9099 where_clause: vec![],
9100 body: Box::new(block(3, vec![match_stmt], None)),
9101 },
9102 );
9103 let out = gen(&module(vec![], vec![f]));
9104 assert!(out.contains("match shape:"), "got: {out}");
9105 assert!(out.contains("case Shape_Circle(_0=r):"), "got: {out}");
9106 assert!(out.contains("case _:"), "got: {out}");
9107 }
9108
9109 /// An EXPRESSION-position user-enum `match` (a `match` consumed as a value,
9110 /// here bound into a `let`) lowers to a `(lambda __v: …)(scrutinee)` whose
9111 /// per-arm test is `isinstance(__v, <cls>)`. The variant class must be
9112 /// resolved through the registry to its `{enum}_{variant}` dataclass
9113 /// (`isinstance(__v, Light_Red)`), NOT the bare variant leaf name
9114 /// (`isinstance(__v, Red)`, an undefined name → `NameError`). Mirrors the
9115 /// statement-position `emit_pattern` resolution (Q-match-exprpos P4).
9116 #[test]
9117 fn expr_position_user_enum_match_test_resolves_variant_class() {
9118 let enum_decl = node(
9119 1,
9120 NodeKind::EnumDecl {
9121 annotations: vec![],
9122 visibility: Visibility::Public,
9123 name: ident("Light"),
9124 generic_params: vec![],
9125 variants: vec![
9126 node(
9127 2,
9128 NodeKind::EnumVariant {
9129 name: ident("Red"),
9130 payload: EnumVariantPayload::Unit,
9131 },
9132 ),
9133 node(
9134 3,
9135 NodeKind::EnumVariant {
9136 name: ident("Green"),
9137 payload: EnumVariantPayload::Unit,
9138 },
9139 ),
9140 ],
9141 },
9142 );
9143 // let n: Int = match l { Red => 1; _ => 0 } (a value-position match)
9144 let red_arm = node(
9145 20,
9146 NodeKind::MatchArm {
9147 pattern: Box::new(node(
9148 21,
9149 NodeKind::ConstructorPat {
9150 path: type_path(&["Red"]),
9151 fields: vec![],
9152 },
9153 )),
9154 guard: None,
9155 body: Box::new(block(22, vec![], Some(int_lit(23, "1")))),
9156 },
9157 );
9158 let default_arm = node(
9159 30,
9160 NodeKind::MatchArm {
9161 pattern: Box::new(node(31, NodeKind::WildcardPat)),
9162 guard: None,
9163 body: Box::new(block(32, vec![], Some(int_lit(33, "0")))),
9164 },
9165 );
9166 let m = node(
9167 40,
9168 NodeKind::Match {
9169 scrutinee: Box::new(id_node(41, "l")),
9170 arms: vec![red_arm, default_arm],
9171 },
9172 );
9173 let let_n = node(
9174 50,
9175 NodeKind::LetBinding {
9176 is_mut: false,
9177 pattern: Box::new(bind_pat(51, "n")),
9178 ty: Some(Box::new(node(
9179 52,
9180 NodeKind::TypeNamed {
9181 path: type_path(&["Int"]),
9182 args: vec![],
9183 },
9184 ))),
9185 value: Box::new(m),
9186 },
9187 );
9188 let f = node(
9189 5,
9190 NodeKind::FnDecl {
9191 annotations: vec![],
9192 visibility: Visibility::Private,
9193 is_async: false,
9194 name: ident("rank"),
9195 generic_params: vec![],
9196 params: vec![param_node(6, "l")],
9197 return_type: None,
9198 effect_clause: vec![],
9199 where_clause: vec![],
9200 body: Box::new(block(7, vec![let_n], None)),
9201 },
9202 );
9203 let out = gen(&module(vec![], vec![enum_decl, f]));
9204 assert!(
9205 out.contains("isinstance(__v, Light_Red)"),
9206 "expr-position variant test must resolve to the dataclass Light_Red, got: {out}"
9207 );
9208 assert!(
9209 !out.contains("isinstance(__v, Red)"),
9210 "must not test against the bare variant leaf name (undefined), got: {out}"
9211 );
9212 }
9213
9214 #[test]
9215 fn ownership_erased() {
9216 let move_expr = node(
9217 1,
9218 NodeKind::Move {
9219 expr: Box::new(id_node(2, "x")),
9220 },
9221 );
9222 let borrow_expr = node(
9223 3,
9224 NodeKind::Borrow {
9225 expr: Box::new(id_node(4, "y")),
9226 },
9227 );
9228 let mut_borrow_expr = node(
9229 5,
9230 NodeKind::MutableBorrow {
9231 expr: Box::new(id_node(6, "z")),
9232 },
9233 );
9234 let body = block(
9235 7,
9236 vec![
9237 node(
9238 8,
9239 NodeKind::LetBinding {
9240 is_mut: false,
9241 pattern: Box::new(bind_pat(9, "a")),
9242 ty: None,
9243 value: Box::new(move_expr),
9244 },
9245 ),
9246 node(
9247 10,
9248 NodeKind::LetBinding {
9249 is_mut: false,
9250 pattern: Box::new(bind_pat(11, "b")),
9251 ty: None,
9252 value: Box::new(borrow_expr),
9253 },
9254 ),
9255 node(
9256 12,
9257 NodeKind::LetBinding {
9258 is_mut: false,
9259 pattern: Box::new(bind_pat(13, "c")),
9260 ty: None,
9261 value: Box::new(mut_borrow_expr),
9262 },
9263 ),
9264 ],
9265 None,
9266 );
9267 let f = node(
9268 0,
9269 NodeKind::FnDecl {
9270 annotations: vec![],
9271 visibility: Visibility::Private,
9272 is_async: false,
9273 name: ident("test"),
9274 generic_params: vec![],
9275 params: vec![],
9276 return_type: None,
9277 effect_clause: vec![],
9278 where_clause: vec![],
9279 body: Box::new(body),
9280 },
9281 );
9282 let out = gen(&module(vec![], vec![f]));
9283 assert!(out.contains("a = x"), "got: {out}");
9284 assert!(out.contains("b = y"), "got: {out}");
9285 assert!(out.contains("c = z"), "got: {out}");
9286 }
9287
9288 #[test]
9289 fn string_interpolation_fstring() {
9290 let interp = node(
9291 1,
9292 NodeKind::Interpolation {
9293 parts: vec![
9294 AirInterpolationPart::Literal("Hello, ".into()),
9295 AirInterpolationPart::Expr(Box::new(id_node(2, "name"))),
9296 AirInterpolationPart::Literal("!".into()),
9297 ],
9298 },
9299 );
9300 let binding = node(
9301 3,
9302 NodeKind::LetBinding {
9303 is_mut: false,
9304 pattern: Box::new(bind_pat(4, "msg")),
9305 ty: None,
9306 value: Box::new(interp),
9307 },
9308 );
9309 let f = node(
9310 0,
9311 NodeKind::FnDecl {
9312 annotations: vec![],
9313 visibility: Visibility::Private,
9314 is_async: false,
9315 name: ident("greet"),
9316 generic_params: vec![],
9317 params: vec![param_node(5, "name")],
9318 return_type: None,
9319 effect_clause: vec![],
9320 where_clause: vec![],
9321 body: Box::new(block(6, vec![binding], Some(id_node(7, "msg")))),
9322 },
9323 );
9324 let out = gen(&module(vec![], vec![f]));
9325 // `${name}` renders through `_bock_str`
9326 // (Q-displayable-interpolation-dispatch).
9327 assert!(out.contains("f\"Hello, {_bock_str(name)}!\""), "got: {out}");
9328 }
9329
9330 #[test]
9331 fn multiline_interpolation_uses_triple_quoted_fstring() {
9332 let interp = node(
9333 1,
9334 NodeKind::Interpolation {
9335 parts: vec![
9336 AirInterpolationPart::Literal("=== ".into()),
9337 AirInterpolationPart::Expr(Box::new(id_node(2, "title"))),
9338 AirInterpolationPart::Literal(" ===\n".into()),
9339 AirInterpolationPart::Expr(Box::new(id_node(3, "msg"))),
9340 AirInterpolationPart::Literal("\n================".into()),
9341 ],
9342 },
9343 );
9344 let binding = node(
9345 4,
9346 NodeKind::LetBinding {
9347 is_mut: false,
9348 pattern: Box::new(bind_pat(5, "result")),
9349 ty: None,
9350 value: Box::new(interp),
9351 },
9352 );
9353 let f = node(
9354 0,
9355 NodeKind::FnDecl {
9356 annotations: vec![],
9357 visibility: Visibility::Private,
9358 is_async: false,
9359 name: ident("banner"),
9360 generic_params: vec![],
9361 params: vec![param_node(6, "title"), param_node(7, "msg")],
9362 return_type: None,
9363 effect_clause: vec![],
9364 where_clause: vec![],
9365 body: Box::new(block(8, vec![binding], Some(id_node(9, "result")))),
9366 },
9367 );
9368 let out = gen(&module(vec![], vec![f]));
9369 // Each `${expr}` part renders through `_bock_str` so a user value with a
9370 // `Displayable` impl dispatches through its `to_string`
9371 // (Q-displayable-interpolation-dispatch); primitives fall back to
9372 // `str(x)`.
9373 assert!(
9374 out.contains(
9375 "f\"\"\"=== {_bock_str(title)} ===\n{_bock_str(msg)}\n================\"\"\""
9376 ),
9377 "got: {out}"
9378 );
9379 // Single-line interpolation should still use regular f-string
9380 assert!(
9381 !out.contains("f\"Hello"),
9382 "single-line should not appear: {out}"
9383 );
9384 }
9385
9386 #[test]
9387 fn single_line_interpolation_still_uses_regular_fstring() {
9388 let interp = node(
9389 1,
9390 NodeKind::Interpolation {
9391 parts: vec![
9392 AirInterpolationPart::Literal("Hi ".into()),
9393 AirInterpolationPart::Expr(Box::new(id_node(2, "name"))),
9394 ],
9395 },
9396 );
9397 let binding = node(
9398 3,
9399 NodeKind::LetBinding {
9400 is_mut: false,
9401 pattern: Box::new(bind_pat(4, "greeting")),
9402 ty: None,
9403 value: Box::new(interp),
9404 },
9405 );
9406 let f = node(
9407 0,
9408 NodeKind::FnDecl {
9409 annotations: vec![],
9410 visibility: Visibility::Private,
9411 is_async: false,
9412 name: ident("greet"),
9413 generic_params: vec![],
9414 params: vec![param_node(5, "name")],
9415 return_type: None,
9416 effect_clause: vec![],
9417 where_clause: vec![],
9418 body: Box::new(block(6, vec![binding], Some(id_node(7, "greeting")))),
9419 },
9420 );
9421 let out = gen(&module(vec![], vec![f]));
9422 // `${name}` renders through `_bock_str`
9423 // (Q-displayable-interpolation-dispatch).
9424 assert!(out.contains("f\"Hi {_bock_str(name)}\""), "got: {out}");
9425 assert!(
9426 !out.contains("f\"\"\""),
9427 "should not use triple quotes: {out}"
9428 );
9429 }
9430
9431 #[test]
9432 fn list_map_set_literals() {
9433 let list = node(
9434 1,
9435 NodeKind::ListLiteral {
9436 elems: vec![int_lit(2, "1"), int_lit(3, "2"), int_lit(4, "3")],
9437 },
9438 );
9439 let map = node(
9440 5,
9441 NodeKind::MapLiteral {
9442 entries: vec![bock_air::AirMapEntry {
9443 key: str_lit(6, "a"),
9444 value: int_lit(7, "1"),
9445 }],
9446 },
9447 );
9448 let set = node(
9449 8,
9450 NodeKind::SetLiteral {
9451 elems: vec![int_lit(9, "1"), int_lit(10, "2")],
9452 },
9453 );
9454 let body = block(
9455 11,
9456 vec![
9457 node(
9458 12,
9459 NodeKind::LetBinding {
9460 is_mut: false,
9461 pattern: Box::new(bind_pat(13, "xs")),
9462 ty: None,
9463 value: Box::new(list),
9464 },
9465 ),
9466 node(
9467 14,
9468 NodeKind::LetBinding {
9469 is_mut: false,
9470 pattern: Box::new(bind_pat(15, "m")),
9471 ty: None,
9472 value: Box::new(map),
9473 },
9474 ),
9475 node(
9476 16,
9477 NodeKind::LetBinding {
9478 is_mut: false,
9479 pattern: Box::new(bind_pat(17, "s")),
9480 ty: None,
9481 value: Box::new(set),
9482 },
9483 ),
9484 ],
9485 None,
9486 );
9487 let f = node(
9488 0,
9489 NodeKind::FnDecl {
9490 annotations: vec![],
9491 visibility: Visibility::Private,
9492 is_async: false,
9493 name: ident("collections"),
9494 generic_params: vec![],
9495 params: vec![],
9496 return_type: None,
9497 effect_clause: vec![],
9498 where_clause: vec![],
9499 body: Box::new(body),
9500 },
9501 );
9502 let out = gen(&module(vec![], vec![f]));
9503 assert!(out.contains("[1, 2, 3]"), "got: {out}");
9504 assert!(out.contains("{\"a\": 1}"), "got: {out}");
9505 assert!(out.contains("{1, 2}"), "got: {out}");
9506 }
9507
9508 #[test]
9509 fn record_construction() {
9510 let rec = node(
9511 1,
9512 NodeKind::RecordConstruct {
9513 path: type_path(&["User"]),
9514 fields: vec![
9515 AirRecordField {
9516 name: ident("name"),
9517 value: Some(Box::new(str_lit(2, "Alice"))),
9518 },
9519 AirRecordField {
9520 name: ident("age"),
9521 value: Some(Box::new(int_lit(3, "30"))),
9522 },
9523 ],
9524 spread: None,
9525 },
9526 );
9527 let binding = node(
9528 4,
9529 NodeKind::LetBinding {
9530 is_mut: false,
9531 pattern: Box::new(bind_pat(5, "user")),
9532 ty: None,
9533 value: Box::new(rec),
9534 },
9535 );
9536 let f = node(
9537 0,
9538 NodeKind::FnDecl {
9539 annotations: vec![],
9540 visibility: Visibility::Private,
9541 is_async: false,
9542 name: ident("test"),
9543 generic_params: vec![],
9544 params: vec![],
9545 return_type: None,
9546 effect_clause: vec![],
9547 where_clause: vec![],
9548 body: Box::new(block(6, vec![binding], None)),
9549 },
9550 );
9551 let out = gen(&module(vec![], vec![f]));
9552 assert!(out.contains("User(name=\"Alice\", age=30)"), "got: {out}");
9553 }
9554
9555 #[test]
9556 fn control_flow() {
9557 let if_stmt = node(
9558 1,
9559 NodeKind::If {
9560 let_pattern: None,
9561 condition: Box::new(bool_lit(2, true)),
9562 then_block: Box::new(block(3, vec![], Some(int_lit(4, "1")))),
9563 else_block: Some(Box::new(block(5, vec![], Some(int_lit(6, "2"))))),
9564 },
9565 );
9566 let for_stmt = node(
9567 7,
9568 NodeKind::For {
9569 pattern: Box::new(bind_pat(8, "x")),
9570 iterable: Box::new(id_node(9, "items")),
9571 body: Box::new(block(10, vec![], None)),
9572 },
9573 );
9574 let while_stmt = node(
9575 11,
9576 NodeKind::While {
9577 condition: Box::new(bool_lit(12, true)),
9578 body: Box::new(block(
9579 13,
9580 vec![node(14, NodeKind::Break { value: None })],
9581 None,
9582 )),
9583 },
9584 );
9585 let body = block(15, vec![if_stmt, for_stmt, while_stmt], None);
9586 let f = node(
9587 0,
9588 NodeKind::FnDecl {
9589 annotations: vec![],
9590 visibility: Visibility::Private,
9591 is_async: false,
9592 name: ident("flow"),
9593 generic_params: vec![],
9594 params: vec![param_node(16, "items")],
9595 return_type: None,
9596 effect_clause: vec![],
9597 where_clause: vec![],
9598 body: Box::new(body),
9599 },
9600 );
9601 let out = gen(&module(vec![], vec![f]));
9602 assert!(out.contains("if True:"), "got: {out}");
9603 assert!(out.contains("else:"), "got: {out}");
9604 assert!(out.contains("for x in items:"), "got: {out}");
9605 assert!(out.contains("while True:"), "got: {out}");
9606 assert!(out.contains("break"), "got: {out}");
9607 }
9608
9609 // ── Statement-position loop tails must NOT be `return`ed ─────────────────
9610 //
9611 // A loop body's final expression is a *statement* in Bock — the loop
9612 // evaluates to Unit, the body's value is discarded. The Python backend's
9613 // shared `emit_block_body` had emitted a tail expression as a function-body
9614 // `return`, so e.g. `for i in 1..=3 { println(i) }` lowered to
9615 // `for i in …: return print(i)` — the `return` exits `main` on the FIRST
9616 // iteration (the loop runs once, then the function returns). fizzbuzz
9617 // printed one line, inventory-system listed one product. These tests pin
9618 // the bare-statement discard for each loop kind.
9619
9620 /// Build a `println(<arg>)` call node.
9621 fn py_println_call(id: u32, arg: AIRNode) -> AIRNode {
9622 node(
9623 id,
9624 NodeKind::Call {
9625 callee: Box::new(id_node(id + 1, "println")),
9626 args: vec![AirArg {
9627 label: None,
9628 value: arg,
9629 }],
9630 type_args: vec![],
9631 },
9632 )
9633 }
9634
9635 #[test]
9636 fn py_for_loop_body_tail_call_is_statement_not_returned() {
9637 // fn main() { for i in 1..=3 { println(i) } }
9638 let range = node(
9639 20,
9640 NodeKind::Range {
9641 lo: Box::new(int_lit(21, "1")),
9642 hi: Box::new(int_lit(22, "3")),
9643 inclusive: true,
9644 },
9645 );
9646 let loop_body = block(30, vec![], Some(py_println_call(31, id_node(33, "i"))));
9647 let for_loop = node(
9648 10,
9649 NodeKind::For {
9650 pattern: Box::new(bind_pat(11, "i")),
9651 iterable: Box::new(range),
9652 body: Box::new(loop_body),
9653 },
9654 );
9655 let f = fn_decl_body(0, "main", block(2, vec![for_loop], None));
9656 let out = gen(&module(vec![], vec![f]));
9657 assert!(
9658 !out.contains("return print(i)"),
9659 "for-loop body tail must be a bare statement, not `return` (would abort \
9660 main after one iteration); got:\n{out}"
9661 );
9662 assert!(
9663 out.contains("print(i)"),
9664 "for-loop body tail call must still be emitted; got:\n{out}"
9665 );
9666 }
9667
9668 #[test]
9669 fn py_while_loop_body_tail_call_is_statement_not_returned() {
9670 // fn main() { while cond { println(i) } }
9671 let loop_body = block(30, vec![], Some(py_println_call(31, id_node(33, "i"))));
9672 let while_loop = node(
9673 10,
9674 NodeKind::While {
9675 condition: Box::new(bool_lit(12, true)),
9676 body: Box::new(loop_body),
9677 },
9678 );
9679 let f = fn_decl_body(0, "main", block(2, vec![while_loop], None));
9680 let out = gen(&module(vec![], vec![f]));
9681 assert!(
9682 !out.contains("return print(i)"),
9683 "while-loop body tail must be a bare statement, not `return`; got:\n{out}"
9684 );
9685 assert!(
9686 out.contains("print(i)"),
9687 "while-loop body tail call must still be emitted; got:\n{out}"
9688 );
9689 }
9690
9691 #[test]
9692 fn py_infinite_loop_body_tail_call_is_statement_not_returned() {
9693 // fn main() { loop { println(i) } }
9694 let loop_body = block(30, vec![], Some(py_println_call(31, id_node(33, "i"))));
9695 let inf_loop = node(
9696 10,
9697 NodeKind::Loop {
9698 body: Box::new(loop_body),
9699 },
9700 );
9701 let f = fn_decl_body(0, "main", block(2, vec![inf_loop], None));
9702 let out = gen(&module(vec![], vec![f]));
9703 assert!(
9704 !out.contains("return print(i)"),
9705 "loop body tail must be a bare statement, not `return`; got:\n{out}"
9706 );
9707 assert!(
9708 out.contains("print(i)"),
9709 "loop body tail call must still be emitted; got:\n{out}"
9710 );
9711 }
9712
9713 #[test]
9714 fn py_function_body_tail_call_still_returned() {
9715 // Guard against over-correction: a *function* body tail must still
9716 // `return` its value (this is NOT statement position).
9717 // fn answer() { 42 }
9718 let f = fn_decl_body(0, "answer", block(2, vec![], Some(int_lit(3, "42"))));
9719 let out = gen(&module(vec![], vec![f]));
9720 assert!(
9721 out.contains("return 42"),
9722 "function-body tail must still be returned; got:\n{out}"
9723 );
9724 }
9725
9726 #[test]
9727 fn lambda_and_pipe() {
9728 let lambda = node(
9729 1,
9730 NodeKind::Lambda {
9731 params: vec![param_node(2, "x")],
9732 body: Box::new(node(
9733 3,
9734 NodeKind::BinaryOp {
9735 op: BinOp::Mul,
9736 left: Box::new(id_node(4, "x")),
9737 right: Box::new(int_lit(5, "2")),
9738 },
9739 )),
9740 },
9741 );
9742 let pipe = node(
9743 6,
9744 NodeKind::Pipe {
9745 left: Box::new(int_lit(7, "5")),
9746 right: Box::new(id_node(8, "double")),
9747 },
9748 );
9749 let body = block(
9750 9,
9751 vec![
9752 node(
9753 10,
9754 NodeKind::LetBinding {
9755 is_mut: false,
9756 pattern: Box::new(bind_pat(11, "double")),
9757 ty: None,
9758 value: Box::new(lambda),
9759 },
9760 ),
9761 node(
9762 12,
9763 NodeKind::LetBinding {
9764 is_mut: false,
9765 pattern: Box::new(bind_pat(13, "result")),
9766 ty: None,
9767 value: Box::new(pipe),
9768 },
9769 ),
9770 ],
9771 None,
9772 );
9773 let f = node(
9774 0,
9775 NodeKind::FnDecl {
9776 annotations: vec![],
9777 visibility: Visibility::Private,
9778 is_async: false,
9779 name: ident("test"),
9780 generic_params: vec![],
9781 params: vec![],
9782 return_type: None,
9783 effect_clause: vec![],
9784 where_clause: vec![],
9785 body: Box::new(body),
9786 },
9787 );
9788 let out = gen(&module(vec![], vec![f]));
9789 assert!(out.contains("lambda x: (x * 2)"), "got: {out}");
9790 assert!(out.contains("double(5)"), "got: {out}");
9791 }
9792
9793 /// A `Call` whose callee is a `Lambda` must parenthesize the lambda so the
9794 /// trailing argument list invokes the lambda, not its body. Without the
9795 /// grouping, `lambda x: x(42)` parses as `lambda x: (x(42))` — the `(42)`
9796 /// binds to the body, never calling the lambda.
9797 ///
9798 /// This is the shape the AIR compose desugar (`f >> g` →
9799 /// `(__compose_x) => g(f(__compose_x))`) produces for chained `>>`: the
9800 /// inner compose lowers to a `Lambda`, which then appears as the callee
9801 /// `f` in the outer `f(__compose_x)`. See examples/real-world/data-pipeline
9802 /// (`normalize >> compute_summary >> format_summary`).
9803 #[test]
9804 fn py_call_with_lambda_callee_parenthesizes() {
9805 // (lambda x: x)(42)
9806 let lambda = node(
9807 1,
9808 NodeKind::Lambda {
9809 params: vec![param_node(2, "x")],
9810 body: Box::new(id_node(3, "x")),
9811 },
9812 );
9813 let call = node(
9814 4,
9815 NodeKind::Call {
9816 callee: Box::new(lambda),
9817 args: vec![AirArg {
9818 label: None,
9819 value: int_lit(5, "42"),
9820 }],
9821 type_args: vec![],
9822 },
9823 );
9824 let f = fn_decl_tail(0, Visibility::Private, "test", call);
9825 let out = gen(&module(vec![], vec![f]));
9826 assert!(
9827 out.contains("(lambda x: x)(42)"),
9828 "lambda callee must be parenthesized so it is invoked; got: {out}"
9829 );
9830 }
9831
9832 #[test]
9833 fn const_declaration() {
9834 let c = node(
9835 1,
9836 NodeKind::ConstDecl {
9837 annotations: vec![],
9838 visibility: Visibility::Public,
9839 name: ident("PI"),
9840 ty: Box::new(node(
9841 2,
9842 NodeKind::TypeNamed {
9843 path: type_path(&["Float"]),
9844 args: vec![],
9845 },
9846 )),
9847 value: Box::new(node(
9848 3,
9849 NodeKind::Literal {
9850 lit: Literal::Float("3.14159".into()),
9851 },
9852 )),
9853 },
9854 );
9855 let out = gen(&module(vec![], vec![c]));
9856 // PI should become snake case, but since it's all-caps we leave it
9857 assert!(out.contains("= 3.14159"), "got: {out}");
9858 assert!(out.contains(": float"), "got: {out}");
9859 }
9860
9861 #[test]
9862 fn class_declaration() {
9863 let method_body = block(10, vec![], Some(id_node(11, "undefined")));
9864 let method = node(
9865 5,
9866 NodeKind::FnDecl {
9867 annotations: vec![],
9868 visibility: Visibility::Public,
9869 is_async: false,
9870 name: ident("greet"),
9871 generic_params: vec![],
9872 // Instance method leads with `self` (real lowering); a no-`self`
9873 // method is an associated `@staticmethod`.
9874 params: vec![param_node(6, "self")],
9875 return_type: None,
9876 effect_clause: vec![],
9877 where_clause: vec![],
9878 body: Box::new(method_body),
9879 },
9880 );
9881 let cls = node(
9882 1,
9883 NodeKind::ClassDecl {
9884 annotations: vec![],
9885 visibility: Visibility::Public,
9886 name: ident("Person"),
9887 generic_params: vec![],
9888 base: None,
9889 traits: vec![],
9890 fields: vec![bock_ast::RecordDeclField {
9891 id: 0,
9892 span: span(),
9893 name: ident("name"),
9894 ty: bock_ast::TypeExpr::Named {
9895 id: 0,
9896 span: span(),
9897 path: type_path(&["String"]),
9898 args: vec![],
9899 },
9900 default: None,
9901 }],
9902 methods: vec![method],
9903 },
9904 );
9905 let out = gen(&module(vec![], vec![cls]));
9906 assert!(out.contains("class Person:"), "got: {out}");
9907 assert!(out.contains("def __init__(self, name: str):"), "got: {out}");
9908 assert!(out.contains("self.name = name"), "got: {out}");
9909 assert!(out.contains("def greet(self):"), "got: {out}");
9910 }
9911
9912 /// A self-method `fn <name>(self) -> String { "<lit>" }`, for class/impl
9913 /// fixtures.
9914 fn self_method_returning(id: u32, name: &str, lit: &str) -> AIRNode {
9915 node(
9916 id,
9917 NodeKind::FnDecl {
9918 annotations: vec![],
9919 visibility: Visibility::Public,
9920 is_async: false,
9921 name: ident(name),
9922 generic_params: vec![],
9923 params: vec![param_node(id + 1, "self")],
9924 return_type: Some(Box::new(node(
9925 id + 2,
9926 NodeKind::TypeNamed {
9927 path: type_path(&["String"]),
9928 args: vec![],
9929 },
9930 ))),
9931 effect_clause: vec![],
9932 where_clause: vec![],
9933 body: Box::new(block(id + 3, vec![], Some(str_lit(id + 4, lit)))),
9934 },
9935 )
9936 }
9937
9938 /// An `impl <trait?> for <target>` block carrying `methods`.
9939 fn impl_block_node(
9940 id: u32,
9941 target: &str,
9942 trait_name: Option<&str>,
9943 methods: Vec<AIRNode>,
9944 ) -> AIRNode {
9945 node(
9946 id,
9947 NodeKind::ImplBlock {
9948 annotations: vec![],
9949 target: Box::new(node(
9950 id + 1,
9951 NodeKind::TypeNamed {
9952 path: type_path(&[target]),
9953 args: vec![],
9954 },
9955 )),
9956 trait_path: trait_name.map(|t| type_path(&[t])),
9957 trait_args: vec![],
9958 generic_params: vec![],
9959 where_clause: vec![],
9960 methods,
9961 },
9962 )
9963 }
9964
9965 /// A `trait <name> { fn <m>(self) -> String }` declaration (ABC stub).
9966 fn trait_node(id: u32, name: &str, method: &str) -> AIRNode {
9967 node(
9968 id,
9969 NodeKind::TraitDecl {
9970 annotations: vec![],
9971 visibility: Visibility::Public,
9972 is_platform: false,
9973 name: ident(name),
9974 generic_params: vec![],
9975 associated_types: vec![],
9976 methods: vec![self_method_returning(id + 50, method, "")],
9977 },
9978 )
9979 }
9980
9981 /// A `class <name> { <field>: String }` with no inline methods.
9982 fn class_with_field(id: u32, name: &str, field: &str) -> AIRNode {
9983 node(
9984 id,
9985 NodeKind::ClassDecl {
9986 annotations: vec![],
9987 visibility: Visibility::Public,
9988 name: ident(name),
9989 generic_params: vec![],
9990 base: None,
9991 traits: vec![],
9992 fields: vec![named_field(field, "String")],
9993 methods: vec![],
9994 },
9995 )
9996 }
9997
9998 /// Q-class-codegen (py): a `class T` with an inherent `impl T` and a trait
9999 /// `impl Trait for T` must route BOTH impls' methods into the class body —
10000 /// the same path records already use — and subclass the trait ABC. Before
10001 /// the fix the Python backend emitted `class T:` with only `__init__`,
10002 /// silently DROPPING every impl/trait method.
10003 #[test]
10004 fn py_class_attaches_inherent_and_trait_impl_methods() {
10005 let cls = class_with_field(1, "Widget", "name");
10006 let inherent = impl_block_node(
10007 10,
10008 "Widget",
10009 None,
10010 vec![self_method_returning(11, "describe", "a widget")],
10011 );
10012 let trait_decl = trait_node(20, "Render", "render");
10013 let trait_impl = impl_block_node(
10014 30,
10015 "Widget",
10016 Some("Render"),
10017 vec![self_method_returning(31, "render", "<widget/>")],
10018 );
10019 let out = gen(&module(vec![], vec![trait_decl, cls, inherent, trait_impl]));
10020 // The inherent-impl method is attached to the class body.
10021 assert!(
10022 out.contains("def describe(self)"),
10023 "inherent-impl method must be attached to the class, got:\n{out}"
10024 );
10025 // The trait-impl method is attached to the class body.
10026 assert!(
10027 out.contains("def render(self)"),
10028 "trait-impl method must be attached to the class, got:\n{out}"
10029 );
10030 // The class subclasses the trait ABC for real dispatch.
10031 assert!(
10032 out.contains("class Widget(Render):"),
10033 "class must subclass the implemented trait, got:\n{out}"
10034 );
10035 // No orphan module-level `# impl` functions left behind.
10036 assert!(
10037 !out.contains("\ndef describe("),
10038 "impl method must not leak as a module-level function, got:\n{out}"
10039 );
10040 }
10041
10042 /// Q-class-codegen behavioral check: a generated class actually dispatches
10043 /// its inherent and trait methods at runtime.
10044 #[test]
10045 fn py_class_methods_dispatch_at_runtime() {
10046 if !has_python3() {
10047 return;
10048 }
10049 let cls = class_with_field(1, "Widget", "name");
10050 let inherent = impl_block_node(
10051 10,
10052 "Widget",
10053 None,
10054 vec![self_method_returning(11, "describe", "a widget")],
10055 );
10056 let trait_decl = trait_node(20, "Render", "render");
10057 let trait_impl = impl_block_node(
10058 30,
10059 "Widget",
10060 Some("Render"),
10061 vec![self_method_returning(31, "render", "<widget/>")],
10062 );
10063 let out = gen(&module(vec![], vec![trait_decl, cls, inherent, trait_impl]));
10064 let program =
10065 format!("{out}\nw = Widget(name=\"x\")\nprint(w.describe())\nprint(w.render())\n");
10066 let got = run_py(&program);
10067 assert_eq!(got, "a widget\n<widget/>", "got:\n{got}\nfrom:\n{out}");
10068 }
10069
10070 /// Q-py-impl-before-trait (ordering): a class/record that subclasses a trait
10071 /// ABC must be emitted AFTER the trait is defined. Here the trait `Render`
10072 /// is declared textually AFTER the record `Widget` that impls it; naive
10073 /// source-order emission produced `class Widget(Render):` before `Render`
10074 /// existed → `NameError`. The fix topologically orders type decls so a base
10075 /// precedes every subclass.
10076 #[test]
10077 fn py_trait_emitted_before_subclassing_record() {
10078 // record Widget { name: String } — declared FIRST.
10079 let rec = node(
10080 1,
10081 NodeKind::RecordDecl {
10082 annotations: vec![],
10083 visibility: Visibility::Public,
10084 name: ident("Widget"),
10085 generic_params: vec![],
10086 fields: vec![named_field("name", "String")],
10087 },
10088 );
10089 let trait_impl = impl_block_node(
10090 10,
10091 "Widget",
10092 Some("Render"),
10093 vec![self_method_returning(11, "render", "<w/>")],
10094 );
10095 // trait Render — declared LAST, after the record + its impl.
10096 let trait_decl = trait_node(20, "Render", "render");
10097 let out = gen(&module(vec![], vec![rec, trait_impl, trait_decl]));
10098 let widget_pos = out
10099 .find("class Widget(Render):")
10100 .unwrap_or_else(|| panic!("expected `class Widget(Render):`, got:\n{out}"));
10101 let render_pos = out
10102 .find("class Render:")
10103 .unwrap_or_else(|| panic!("expected `class Render:`, got:\n{out}"));
10104 assert!(
10105 render_pos < widget_pos,
10106 "trait ABC `Render` must be emitted before subclass `Widget`, got:\n{out}"
10107 );
10108 // And it must actually import/parse + run without NameError.
10109 if has_python3() {
10110 assert!(
10111 check_py_syntax(&out),
10112 "ordered output must parse, got:\n{out}"
10113 );
10114 let program = format!("{out}\nw = Widget(name=\"x\")\nprint(w.render())\n");
10115 assert_eq!(run_py(&program), "<w/>", "got from:\n{out}");
10116 }
10117 }
10118
10119 /// Q-class-codegen recursion guard: when an inherent `impl T { fn render }`
10120 /// and a trait `impl Trait for T { fn render }` share a method name, Python's
10121 /// single per-class namespace must keep exactly ONE `def render` — the
10122 /// inherent (concrete) one. Emitting both made the delegating trait body
10123 /// (`self.render()`) overwrite and call itself → `RecursionError`
10124 /// (react-components' `Button`).
10125 #[test]
10126 fn py_inherent_method_wins_over_colliding_trait_method() {
10127 let cls = class_with_field(1, "Button", "label");
10128 // inherent: concrete body.
10129 let inherent = impl_block_node(
10130 10,
10131 "Button",
10132 None,
10133 vec![self_method_returning(11, "render", "<button/>")],
10134 );
10135 let trait_decl = trait_node(20, "Component", "render");
10136 // trait impl: delegates to the inherent method (`self.render()`).
10137 let trait_render = node(
10138 31,
10139 NodeKind::FnDecl {
10140 annotations: vec![],
10141 visibility: Visibility::Public,
10142 is_async: false,
10143 name: ident("render"),
10144 generic_params: vec![],
10145 params: vec![param_node(32, "self")],
10146 return_type: Some(Box::new(node(
10147 33,
10148 NodeKind::TypeNamed {
10149 path: type_path(&["String"]),
10150 args: vec![],
10151 },
10152 ))),
10153 effect_clause: vec![],
10154 where_clause: vec![],
10155 body: Box::new(block(
10156 34,
10157 vec![],
10158 Some(node(
10159 35,
10160 NodeKind::Call {
10161 callee: Box::new(node(
10162 36,
10163 NodeKind::FieldAccess {
10164 object: Box::new(id_node(37, "self")),
10165 field: ident("render"),
10166 },
10167 )),
10168 type_args: vec![],
10169 args: vec![],
10170 },
10171 )),
10172 )),
10173 },
10174 );
10175 let trait_impl = impl_block_node(30, "Button", Some("Component"), vec![trait_render]);
10176 let out = gen(&module(vec![], vec![trait_decl, cls, inherent, trait_impl]));
10177 // Exactly one `def render` in the Button class — count occurrences.
10178 let count = out.matches("def render(self)").count();
10179 assert_eq!(
10180 count,
10181 2, // one in the `Component` ABC stub, one in `Button`
10182 "expected one `render` in Button + one in the ABC, got {count}:\n{out}"
10183 );
10184 // The kept Button method is the inherent (concrete) one, not the
10185 // self-delegating trait one.
10186 assert!(
10187 out.contains("return \"<button/>\""),
10188 "Button.render must be the concrete inherent body, got:\n{out}"
10189 );
10190 if has_python3() {
10191 let program = format!("{out}\nb = Button(label=\"x\")\nprint(b.render())\n");
10192 assert_eq!(run_py(&program), "<button/>", "got from:\n{out}");
10193 }
10194 }
10195
10196 /// A class declared with an inline `base` (another class) must be emitted
10197 /// after that base class, even when source order puts the subclass first.
10198 #[test]
10199 fn py_base_class_emitted_before_subclass() {
10200 // class Sub (base = Base) — declared FIRST.
10201 let sub = node(
10202 1,
10203 NodeKind::ClassDecl {
10204 annotations: vec![],
10205 visibility: Visibility::Public,
10206 name: ident("Sub"),
10207 generic_params: vec![],
10208 base: Some(type_path(&["Base"])),
10209 traits: vec![],
10210 fields: vec![named_field("name", "String")],
10211 methods: vec![],
10212 },
10213 );
10214 // class Base — declared LAST.
10215 let base = class_with_field(10, "Base", "id");
10216 let out = gen(&module(vec![], vec![sub, base]));
10217 let base_pos = out
10218 .find("class Base:")
10219 .unwrap_or_else(|| panic!("expected `class Base:`, got:\n{out}"));
10220 let sub_pos = out
10221 .find("class Sub(Base):")
10222 .unwrap_or_else(|| panic!("expected `class Sub(Base):`, got:\n{out}"));
10223 assert!(
10224 base_pos < sub_pos,
10225 "base class must be emitted before subclass, got:\n{out}"
10226 );
10227 }
10228
10229 /// An `effect` is emitted as an `(ABC)` base class that an `impl Effect for
10230 /// T` makes a base of `T` (`class StubChannel(Channel):`). An effect declared
10231 /// AFTER its impl must still be emitted before the implementing record, else
10232 /// the base list `(Channel)` raises `NameError` (chat-protocol's `Channel`).
10233 #[test]
10234 fn py_effect_emitted_before_subclassing_record() {
10235 // record StubChannel {} — declared FIRST, impls the effect.
10236 let rec = node(
10237 1,
10238 NodeKind::RecordDecl {
10239 annotations: vec![],
10240 visibility: Visibility::Public,
10241 name: ident("StubChannel"),
10242 generic_params: vec![],
10243 fields: vec![named_field("tag", "String")],
10244 },
10245 );
10246 let chan_impl = impl_block_node(
10247 10,
10248 "StubChannel",
10249 Some("Channel"),
10250 vec![self_method_returning(11, "send", "sent")],
10251 );
10252 // effect Channel { fn send(self) -> String } — declared LAST.
10253 let effect = node(
10254 20,
10255 NodeKind::EffectDecl {
10256 annotations: vec![],
10257 visibility: Visibility::Public,
10258 name: ident("Channel"),
10259 generic_params: vec![],
10260 components: vec![],
10261 operations: vec![self_method_returning(21, "send", "")],
10262 },
10263 );
10264 let out = gen(&module(vec![], vec![rec, chan_impl, effect]));
10265 let chan_pos = out
10266 .find("class Channel(ABC):")
10267 .unwrap_or_else(|| panic!("expected `class Channel(ABC):`, got:\n{out}"));
10268 let stub_pos = out
10269 .find("class StubChannel(Channel):")
10270 .unwrap_or_else(|| panic!("expected `class StubChannel(Channel):`, got:\n{out}"));
10271 assert!(
10272 chan_pos < stub_pos,
10273 "effect ABC `Channel` must precede the record that impls it, got:\n{out}"
10274 );
10275 }
10276
10277 #[test]
10278 fn boolean_operators() {
10279 let expr = node(
10280 1,
10281 NodeKind::BinaryOp {
10282 op: BinOp::And,
10283 left: Box::new(bool_lit(2, true)),
10284 right: Box::new(bool_lit(3, false)),
10285 },
10286 );
10287 let not_expr = node(
10288 4,
10289 NodeKind::UnaryOp {
10290 op: UnaryOp::Not,
10291 operand: Box::new(bool_lit(5, true)),
10292 },
10293 );
10294 let body = block(
10295 6,
10296 vec![
10297 node(
10298 7,
10299 NodeKind::LetBinding {
10300 is_mut: false,
10301 pattern: Box::new(bind_pat(8, "a")),
10302 ty: None,
10303 value: Box::new(expr),
10304 },
10305 ),
10306 node(
10307 9,
10308 NodeKind::LetBinding {
10309 is_mut: false,
10310 pattern: Box::new(bind_pat(10, "b")),
10311 ty: None,
10312 value: Box::new(not_expr),
10313 },
10314 ),
10315 ],
10316 None,
10317 );
10318 let f = node(
10319 0,
10320 NodeKind::FnDecl {
10321 annotations: vec![],
10322 visibility: Visibility::Private,
10323 is_async: false,
10324 name: ident("test"),
10325 generic_params: vec![],
10326 params: vec![],
10327 return_type: None,
10328 effect_clause: vec![],
10329 where_clause: vec![],
10330 body: Box::new(body),
10331 },
10332 );
10333 let out = gen(&module(vec![], vec![f]));
10334 assert!(out.contains("(True and False)"), "got: {out}");
10335 assert!(out.contains("not True"), "got: {out}");
10336 }
10337
10338 #[test]
10339 fn result_construct() {
10340 let ok = node(
10341 1,
10342 NodeKind::ResultConstruct {
10343 variant: ResultVariant::Ok,
10344 value: Some(Box::new(int_lit(2, "42"))),
10345 },
10346 );
10347 let err = node(
10348 3,
10349 NodeKind::ResultConstruct {
10350 variant: ResultVariant::Err,
10351 value: Some(Box::new(str_lit(4, "failed"))),
10352 },
10353 );
10354 let body = block(
10355 5,
10356 vec![
10357 node(
10358 6,
10359 NodeKind::LetBinding {
10360 is_mut: false,
10361 pattern: Box::new(bind_pat(7, "good")),
10362 ty: None,
10363 value: Box::new(ok),
10364 },
10365 ),
10366 node(
10367 8,
10368 NodeKind::LetBinding {
10369 is_mut: false,
10370 pattern: Box::new(bind_pat(9, "bad")),
10371 ty: None,
10372 value: Box::new(err),
10373 },
10374 ),
10375 ],
10376 None,
10377 );
10378 let f = node(
10379 0,
10380 NodeKind::FnDecl {
10381 annotations: vec![],
10382 visibility: Visibility::Private,
10383 is_async: false,
10384 name: ident("test"),
10385 generic_params: vec![],
10386 params: vec![],
10387 return_type: None,
10388 effect_clause: vec![],
10389 where_clause: vec![],
10390 body: Box::new(body),
10391 },
10392 );
10393 let out = gen(&module(vec![], vec![f]));
10394 // Reconciled on the `_BockOk`/`_BockErr` runtime classes the `Result`
10395 // match reads (the old dict-with-`value`/`error`-keys shape disagreed).
10396 assert!(out.contains("_BockOk(42)"), "got: {out}");
10397 assert!(out.contains("_BockErr(\"failed\")"), "got: {out}");
10398 }
10399
10400 /// A `let`-binding node (immutable, simple bind pattern).
10401 fn let_node(id: u32, name: &str, value: AIRNode) -> AIRNode {
10402 node(
10403 id,
10404 NodeKind::LetBinding {
10405 is_mut: false,
10406 pattern: Box::new(bind_pat(id + 1, name)),
10407 ty: None,
10408 value: Box::new(value),
10409 },
10410 )
10411 }
10412
10413 /// `expr?` — a `Propagate` over `expr`.
10414 fn propagate(id: u32, expr: AIRNode) -> AIRNode {
10415 node(
10416 id,
10417 NodeKind::Propagate {
10418 expr: Box::new(expr),
10419 },
10420 )
10421 }
10422
10423 /// `fn() -> Result[..]` whose body is `let v = inner?` then a tail `Ok(v)`.
10424 /// Exercises the `?` lowering: `_bock_try(..)` + the try/except envelope.
10425 #[test]
10426 fn propagate_unwraps_and_wraps_body() {
10427 let inner_call = node(
10428 10,
10429 NodeKind::Call {
10430 callee: Box::new(id_node(11, "fallible")),
10431 args: vec![],
10432 type_args: vec![],
10433 },
10434 );
10435 let body = block(
10436 2,
10437 vec![let_node(3, "v", propagate(4, inner_call))],
10438 Some(node(
10439 6,
10440 NodeKind::ResultConstruct {
10441 variant: ResultVariant::Ok,
10442 value: Some(Box::new(id_node(7, "v"))),
10443 },
10444 )),
10445 );
10446 let f = fn_decl_body(1, "do_it", body);
10447 let out = gen(&module(vec![], vec![f]));
10448 // `?` lowers to the unwrap helper, not a bare passthrough.
10449 assert!(out.contains("_bock_try(fallible())"), "got: {out}");
10450 // The function body is wrapped in the propagate envelope.
10451 assert!(out.contains("try:"), "got: {out}");
10452 assert!(
10453 out.contains("except _BockPropagate as __bock_p:"),
10454 "got: {out}"
10455 );
10456 assert!(out.contains("return __bock_p.value"), "got: {out}");
10457 // The propagate runtime prelude is emitted.
10458 assert!(out.contains("def _bock_try(v):"), "got: {out}");
10459 }
10460
10461 /// A function with no `?` must NOT gain the try/except envelope or the
10462 /// propagate runtime (no needless cost / behavioural change).
10463 #[test]
10464 fn no_propagate_no_envelope() {
10465 let body = block(2, vec![], Some(int_lit(3, "1")));
10466 let f = fn_decl_body(1, "plain", body);
10467 let out = gen(&module(vec![], vec![f]));
10468 assert!(!out.contains("_bock_try"), "got: {out}");
10469 assert!(!out.contains("_BockPropagate"), "got: {out}");
10470 }
10471
10472 /// `fn() { let y = 1; let z = { let y = y + 10; y * 2 }; y + z }` — the inner
10473 /// block's `let y` shadows the outer `y` and must be renamed so the outer `y`
10474 /// is untouched (Python has no block scope for `=`).
10475 #[test]
10476 fn nested_block_let_shadow_is_renamed() {
10477 let add = |id, l: AIRNode, r: AIRNode| {
10478 node(
10479 id,
10480 NodeKind::BinaryOp {
10481 op: BinOp::Add,
10482 left: Box::new(l),
10483 right: Box::new(r),
10484 },
10485 )
10486 };
10487 let mul = |id, l: AIRNode, r: AIRNode| {
10488 node(
10489 id,
10490 NodeKind::BinaryOp {
10491 op: BinOp::Mul,
10492 left: Box::new(l),
10493 right: Box::new(r),
10494 },
10495 )
10496 };
10497 // inner block: { let y = y + 10; y * 2 }
10498 let inner_block = block(
10499 20,
10500 vec![let_node(
10501 21,
10502 "y",
10503 add(22, id_node(23, "y"), int_lit(24, "10")),
10504 )],
10505 Some(mul(25, id_node(26, "y"), int_lit(27, "2"))),
10506 );
10507 let body = block(
10508 2,
10509 vec![
10510 let_node(3, "y", int_lit(4, "1")),
10511 let_node(5, "z", inner_block),
10512 ],
10513 Some(add(8, id_node(9, "y"), id_node(10, "z"))),
10514 );
10515 let f = fn_decl_body(1, "nested", body);
10516 let out = gen(&module(vec![], vec![f]));
10517 // The inner `let y` is renamed; the outer `y = 1` and the final `y + z`
10518 // read the original `y`.
10519 assert!(out.contains("y__s"), "expected a shadow alias, got: {out}");
10520 assert!(out.contains("y = 1"), "got: {out}");
10521 // The final tail uses the *un*-aliased outer `y` (it appears as `(y + z)`
10522 // — the alias name is `y__s1`, which `(y + z)` does not contain).
10523 assert!(out.contains("return (y + z)"), "got: {out}");
10524 }
10525
10526 /// A same-block re-bind (`let acc = …; let acc = acc + 1`) is a plain Python
10527 /// rebind — no alias, no duplicate.
10528 #[test]
10529 fn same_block_rebind_is_not_renamed() {
10530 let add = |id, l: AIRNode, r: AIRNode| {
10531 node(
10532 id,
10533 NodeKind::BinaryOp {
10534 op: BinOp::Add,
10535 left: Box::new(l),
10536 right: Box::new(r),
10537 },
10538 )
10539 };
10540 let body = block(
10541 2,
10542 vec![
10543 let_node(3, "acc", int_lit(4, "1")),
10544 let_node(5, "acc", add(6, id_node(7, "acc"), int_lit(8, "2"))),
10545 ],
10546 Some(id_node(9, "acc")),
10547 );
10548 let f = fn_decl_body(1, "rebind", body);
10549 let out = gen(&module(vec![], vec![f]));
10550 assert!(!out.contains("acc__s"), "must not rename, got: {out}");
10551 assert!(out.contains("acc = 1"), "got: {out}");
10552 assert!(out.contains("acc = (acc + 2)"), "got: {out}");
10553 }
10554
10555 /// A Void function whose tail is a bare `loop { break }` must emit a
10556 /// `while True:` statement, never `return # unsupported`.
10557 #[test]
10558 fn tail_loop_emits_while_not_unsupported() {
10559 let loop_body = block(10, vec![node(11, NodeKind::Break { value: None })], None);
10560 let tail_loop = node(
10561 5,
10562 NodeKind::Loop {
10563 body: Box::new(loop_body),
10564 },
10565 );
10566 let body = block(2, vec![], Some(tail_loop));
10567 let f = fn_decl_body(1, "spin", body);
10568 let out = gen(&module(vec![], vec![f]));
10569 assert!(out.contains("while True:"), "got: {out}");
10570 assert!(!out.contains("# unsupported"), "got: {out}");
10571 assert!(!out.contains("return # unsupported"), "got: {out}");
10572 }
10573
10574 /// Helper: a private `fn <name>()` with an explicit body block.
10575 fn fn_decl_body(id: u32, name: &str, body: AIRNode) -> AIRNode {
10576 node(
10577 id,
10578 NodeKind::FnDecl {
10579 annotations: vec![],
10580 visibility: Visibility::Private,
10581 is_async: false,
10582 name: ident(name),
10583 generic_params: vec![],
10584 params: vec![],
10585 return_type: None,
10586 effect_clause: vec![],
10587 where_clause: vec![],
10588 body: Box::new(body),
10589 },
10590 )
10591 }
10592
10593 #[test]
10594 fn to_snake_case_conversions() {
10595 assert_eq!(to_snake_case("fetchData"), "fetch_data");
10596 assert_eq!(to_snake_case("MyClass"), "my_class");
10597 assert_eq!(to_snake_case("already_snake"), "already_snake");
10598 assert_eq!(to_snake_case("simple"), "simple");
10599 assert_eq!(to_snake_case("HTMLParser"), "html_parser");
10600 assert_eq!(to_snake_case("x"), "x");
10601 assert_eq!(to_snake_case("_"), "_");
10602 }
10603
10604 // ── End-to-end tests (python3 --check + python3 execution) ──────────────
10605
10606 fn has_python3() -> bool {
10607 std::process::Command::new("which")
10608 .arg("python3")
10609 .output()
10610 .map(|o| o.status.success())
10611 .unwrap_or(false)
10612 }
10613
10614 /// Run generated Python through `python3 -m py_compile` for syntax validation.
10615 fn check_py_syntax(code: &str) -> bool {
10616 use std::sync::atomic::{AtomicU64, Ordering};
10617 static SEQ: AtomicU64 = AtomicU64::new(0);
10618 let dir = std::env::temp_dir();
10619 // Unique filename per call: `cargo test` runs these checks on parallel
10620 // threads, so a shared fixed path races — one test's `py_compile` reads or
10621 // removes the file another test just wrote, yielding spurious "must parse"
10622 // failures (this flaked every CI lane except ubuntu-stable once the new
10623 // value-position match tests added more concurrent callers).
10624 let path = dir.join(format!(
10625 "bock_test_output_{}_{}.py",
10626 std::process::id(),
10627 SEQ.fetch_add(1, Ordering::Relaxed)
10628 ));
10629 std::fs::write(&path, code).expect("failed to write temp file");
10630 let result = std::process::Command::new("python3")
10631 .arg("-m")
10632 .arg("py_compile")
10633 .arg(&path)
10634 .output()
10635 .expect("failed to spawn python3");
10636 let _ = std::fs::remove_file(&path);
10637 result.status.success()
10638 }
10639
10640 /// Run generated Python with `python3` and capture stdout.
10641 fn run_py(code: &str) -> String {
10642 let output = std::process::Command::new("python3")
10643 .arg("-c")
10644 .arg(code)
10645 .output()
10646 .expect("failed to run python3");
10647 // Normalize CRLF→LF: on Windows python writes `\r\n` line endings, which
10648 // would fail exact-match assertions against `\n`-terminated expectations.
10649 String::from_utf8(output.stdout)
10650 .unwrap()
10651 .replace("\r\n", "\n")
10652 .trim()
10653 .to_string()
10654 }
10655
10656 #[test]
10657 #[ignore]
10658 fn e2e_hello_world() {
10659 if !has_python3() {
10660 return;
10661 }
10662 let body = block(
10663 2,
10664 vec![],
10665 Some(node(
10666 3,
10667 NodeKind::Call {
10668 callee: Box::new(id_node(4, "print")),
10669 args: vec![AirArg {
10670 label: None,
10671 value: str_lit(5, "Hello, World!"),
10672 }],
10673 type_args: vec![],
10674 },
10675 )),
10676 );
10677 let f = node(
10678 1,
10679 NodeKind::FnDecl {
10680 annotations: vec![],
10681 visibility: Visibility::Private,
10682 is_async: false,
10683 name: ident("main"),
10684 generic_params: vec![],
10685 params: vec![],
10686 return_type: None,
10687 effect_clause: vec![],
10688 where_clause: vec![],
10689 body: Box::new(body),
10690 },
10691 );
10692 let code = gen(&module(vec![], vec![f]));
10693 let full = format!("{code}\nmain()\n");
10694 assert!(
10695 check_py_syntax(&full),
10696 "Python syntax check failed:\n{full}"
10697 );
10698 assert_eq!(run_py(&full), "Hello, World!");
10699 }
10700
10701 #[test]
10702 #[ignore]
10703 fn e2e_arithmetic() {
10704 if !has_python3() {
10705 return;
10706 }
10707 let body = block(
10708 2,
10709 vec![],
10710 Some(node(
10711 3,
10712 NodeKind::BinaryOp {
10713 op: BinOp::Add,
10714 left: Box::new(int_lit(4, "10")),
10715 right: Box::new(int_lit(5, "32")),
10716 },
10717 )),
10718 );
10719 let f = node(
10720 1,
10721 NodeKind::FnDecl {
10722 annotations: vec![],
10723 visibility: Visibility::Private,
10724 is_async: false,
10725 name: ident("calc"),
10726 generic_params: vec![],
10727 params: vec![],
10728 return_type: None,
10729 effect_clause: vec![],
10730 where_clause: vec![],
10731 body: Box::new(body),
10732 },
10733 );
10734 let code = gen(&module(vec![], vec![f]));
10735 let full = format!("{code}\nprint(calc())\n");
10736 assert!(
10737 check_py_syntax(&full),
10738 "Python syntax check failed:\n{full}"
10739 );
10740 assert_eq!(run_py(&full), "42");
10741 }
10742
10743 #[test]
10744 #[ignore]
10745 fn e2e_if_else() {
10746 if !has_python3() {
10747 return;
10748 }
10749 let if_stmt = node(
10750 3,
10751 NodeKind::If {
10752 let_pattern: None,
10753 condition: Box::new(node(
10754 4,
10755 NodeKind::BinaryOp {
10756 op: BinOp::Gt,
10757 left: Box::new(id_node(5, "x")),
10758 right: Box::new(int_lit(6, "0")),
10759 },
10760 )),
10761 then_block: Box::new(block(
10762 7,
10763 vec![node(
10764 8,
10765 NodeKind::Return {
10766 value: Some(Box::new(str_lit(9, "positive"))),
10767 },
10768 )],
10769 None,
10770 )),
10771 else_block: Some(Box::new(block(
10772 10,
10773 vec![node(
10774 11,
10775 NodeKind::Return {
10776 value: Some(Box::new(str_lit(12, "non-positive"))),
10777 },
10778 )],
10779 None,
10780 ))),
10781 },
10782 );
10783 let body = block(2, vec![if_stmt], None);
10784 let f = node(
10785 1,
10786 NodeKind::FnDecl {
10787 annotations: vec![],
10788 visibility: Visibility::Private,
10789 is_async: false,
10790 name: ident("classify"),
10791 generic_params: vec![],
10792 params: vec![param_node(13, "x")],
10793 return_type: None,
10794 effect_clause: vec![],
10795 where_clause: vec![],
10796 body: Box::new(body),
10797 },
10798 );
10799 let code = gen(&module(vec![], vec![f]));
10800 let full = format!("{code}\nprint(classify(5))\nprint(classify(-1))\n");
10801 assert!(
10802 check_py_syntax(&full),
10803 "Python syntax check failed:\n{full}"
10804 );
10805 let output = run_py(&full);
10806 assert!(output.contains("positive"), "got: {output}");
10807 assert!(output.contains("non-positive"), "got: {output}");
10808 }
10809
10810 #[test]
10811 #[ignore]
10812 fn e2e_for_loop() {
10813 if !has_python3() {
10814 return;
10815 }
10816 let body = block(
10817 2,
10818 vec![
10819 node(
10820 3,
10821 NodeKind::LetBinding {
10822 is_mut: true,
10823 pattern: Box::new(bind_pat(4, "sum")),
10824 ty: None,
10825 value: Box::new(int_lit(5, "0")),
10826 },
10827 ),
10828 node(
10829 6,
10830 NodeKind::For {
10831 pattern: Box::new(bind_pat(7, "x")),
10832 iterable: Box::new(node(
10833 8,
10834 NodeKind::ListLiteral {
10835 elems: vec![int_lit(9, "1"), int_lit(10, "2"), int_lit(11, "3")],
10836 },
10837 )),
10838 body: Box::new(block(
10839 12,
10840 vec![node(
10841 13,
10842 NodeKind::Assign {
10843 op: AssignOp::AddAssign,
10844 target: Box::new(id_node(14, "sum")),
10845 value: Box::new(id_node(15, "x")),
10846 },
10847 )],
10848 None,
10849 )),
10850 },
10851 ),
10852 ],
10853 Some(id_node(16, "sum")),
10854 );
10855 let f = node(
10856 1,
10857 NodeKind::FnDecl {
10858 annotations: vec![],
10859 visibility: Visibility::Private,
10860 is_async: false,
10861 name: ident("total"),
10862 generic_params: vec![],
10863 params: vec![],
10864 return_type: None,
10865 effect_clause: vec![],
10866 where_clause: vec![],
10867 body: Box::new(body),
10868 },
10869 );
10870 let code = gen(&module(vec![], vec![f]));
10871 let full = format!("{code}\nprint(total())\n");
10872 assert!(
10873 check_py_syntax(&full),
10874 "Python syntax check failed:\n{full}"
10875 );
10876 assert_eq!(run_py(&full), "6");
10877 }
10878
10879 #[test]
10880 #[ignore]
10881 fn e2e_dataclass() {
10882 if !has_python3() {
10883 return;
10884 }
10885 let rec = node(
10886 1,
10887 NodeKind::RecordDecl {
10888 annotations: vec![],
10889 visibility: Visibility::Public,
10890 name: ident("Point"),
10891 generic_params: vec![],
10892 fields: vec![
10893 bock_ast::RecordDeclField {
10894 id: 0,
10895 span: span(),
10896 name: ident("x"),
10897 ty: bock_ast::TypeExpr::Named {
10898 id: 0,
10899 span: span(),
10900 path: type_path(&["Float"]),
10901 args: vec![],
10902 },
10903 default: None,
10904 },
10905 bock_ast::RecordDeclField {
10906 id: 0,
10907 span: span(),
10908 name: ident("y"),
10909 ty: bock_ast::TypeExpr::Named {
10910 id: 0,
10911 span: span(),
10912 path: type_path(&["Float"]),
10913 args: vec![],
10914 },
10915 default: None,
10916 },
10917 ],
10918 },
10919 );
10920 let code = gen(&module(vec![], vec![rec]));
10921 let full = format!("{code}\np = Point(x=1.0, y=2.0)\nprint(f\"{{p.x}}, {{p.y}}\")\n");
10922 assert!(
10923 check_py_syntax(&full),
10924 "Python syntax check failed:\n{full}"
10925 );
10926 let output = run_py(&full);
10927 assert!(output.contains("1.0, 2.0"), "got: {output}");
10928 }
10929
10930 #[test]
10931 #[ignore]
10932 fn e2e_match_statement() {
10933 if !has_python3() {
10934 return;
10935 }
10936 // match on literal values
10937 let scrutinee = id_node(10, "x");
10938 let arms = vec![
10939 node(
10940 11,
10941 NodeKind::MatchArm {
10942 pattern: Box::new(node(
10943 12,
10944 NodeKind::LiteralPat {
10945 lit: Literal::Int("1".into()),
10946 },
10947 )),
10948 guard: None,
10949 body: Box::new(block(
10950 13,
10951 vec![node(
10952 14,
10953 NodeKind::Return {
10954 value: Some(Box::new(str_lit(15, "one"))),
10955 },
10956 )],
10957 None,
10958 )),
10959 },
10960 ),
10961 node(
10962 16,
10963 NodeKind::MatchArm {
10964 pattern: Box::new(node(17, NodeKind::WildcardPat)),
10965 guard: None,
10966 body: Box::new(block(
10967 18,
10968 vec![node(
10969 19,
10970 NodeKind::Return {
10971 value: Some(Box::new(str_lit(20, "other"))),
10972 },
10973 )],
10974 None,
10975 )),
10976 },
10977 ),
10978 ];
10979 let match_stmt = node(
10980 9,
10981 NodeKind::Match {
10982 scrutinee: Box::new(scrutinee),
10983 arms,
10984 },
10985 );
10986 let f = node(
10987 1,
10988 NodeKind::FnDecl {
10989 annotations: vec![],
10990 visibility: Visibility::Private,
10991 is_async: false,
10992 name: ident("describe"),
10993 generic_params: vec![],
10994 params: vec![param_node(2, "x")],
10995 return_type: None,
10996 effect_clause: vec![],
10997 where_clause: vec![],
10998 body: Box::new(block(3, vec![match_stmt], None)),
10999 },
11000 );
11001 let code = gen(&module(vec![], vec![f]));
11002 let full = format!("{code}\nprint(describe(1))\nprint(describe(99))\n");
11003 assert!(
11004 check_py_syntax(&full),
11005 "Python syntax check failed:\n{full}"
11006 );
11007 let output = run_py(&full);
11008 assert!(output.contains("one"), "got: {output}");
11009 assert!(output.contains("other"), "got: {output}");
11010 }
11011
11012 // ── Prelude function mapping tests ──────────────────────────────────────
11013
11014 /// Helper: generate Python for a module with a `main` function containing a single call.
11015 fn gen_prelude_call(func_name: &str, arg: AIRNode) -> String {
11016 let call = node(
11017 10,
11018 NodeKind::Call {
11019 callee: Box::new(id_node(11, func_name)),
11020 args: vec![AirArg {
11021 label: None,
11022 value: arg,
11023 }],
11024 type_args: vec![],
11025 },
11026 );
11027 let body = block(2, vec![call], None);
11028 let f = node(
11029 1,
11030 NodeKind::FnDecl {
11031 name: ident("main"),
11032 params: vec![],
11033 return_type: None,
11034 body: Box::new(body),
11035 generic_params: vec![],
11036 visibility: Visibility::Private,
11037 annotations: vec![],
11038 effect_clause: vec![],
11039 where_clause: vec![],
11040 is_async: false,
11041 },
11042 );
11043 gen(&module(vec![], vec![f]))
11044 }
11045
11046 /// Helper: generate Python for a nullary prelude call (no args).
11047 fn gen_prelude_call_no_args(func_name: &str) -> String {
11048 let call = node(
11049 10,
11050 NodeKind::Call {
11051 callee: Box::new(id_node(11, func_name)),
11052 args: vec![],
11053 type_args: vec![],
11054 },
11055 );
11056 let body = block(2, vec![call], None);
11057 let f = node(
11058 1,
11059 NodeKind::FnDecl {
11060 name: ident("main"),
11061 params: vec![],
11062 return_type: None,
11063 body: Box::new(body),
11064 generic_params: vec![],
11065 visibility: Visibility::Private,
11066 annotations: vec![],
11067 effect_clause: vec![],
11068 where_clause: vec![],
11069 is_async: false,
11070 },
11071 );
11072 gen(&module(vec![], vec![f]))
11073 }
11074
11075 #[test]
11076 fn prelude_println_maps_to_print() {
11077 let out = gen_prelude_call("println", str_lit(12, "hello"));
11078 assert!(
11079 out.contains("print("),
11080 "println should map to print, got: {out}"
11081 );
11082 assert!(
11083 !out.contains("println("),
11084 "should not emit bare println(, got: {out}"
11085 );
11086 }
11087
11088 #[test]
11089 fn prelude_print_maps_to_print_no_newline() {
11090 let out = gen_prelude_call("print", str_lit(12, "hello"));
11091 assert!(
11092 out.contains("print(") && out.contains("end=\"\""),
11093 "print should map to print with end=\"\", got: {out}"
11094 );
11095 }
11096
11097 #[test]
11098 fn prelude_debug_maps_to_repr() {
11099 let out = gen_prelude_call("debug", str_lit(12, "val"));
11100 assert!(
11101 out.contains("print(repr("),
11102 "debug should map to print(repr(...)), got: {out}"
11103 );
11104 }
11105
11106 #[test]
11107 fn prelude_assert_maps_to_assert() {
11108 let out = gen_prelude_call("assert", bool_lit(12, true));
11109 assert!(
11110 out.contains("assert "),
11111 "assert should map to Python assert, got: {out}"
11112 );
11113 assert!(
11114 !out.contains("assert("),
11115 "should not emit assert as function call, got: {out}"
11116 );
11117 }
11118
11119 #[test]
11120 fn prelude_todo_maps_to_not_implemented_error() {
11121 let out = gen_prelude_call_no_args("todo");
11122 assert!(
11123 out.contains("raise NotImplementedError()"),
11124 "todo should map to raise NotImplementedError, got: {out}"
11125 );
11126 }
11127
11128 #[test]
11129 fn prelude_unreachable_maps_to_runtime_error() {
11130 let out = gen_prelude_call_no_args("unreachable");
11131 assert!(
11132 out.contains("raise RuntimeError(\"unreachable\")"),
11133 "unreachable should map to raise RuntimeError, got: {out}"
11134 );
11135 }
11136
11137 #[test]
11138 fn non_prelude_call_passes_through() {
11139 let out = gen_prelude_call("my_custom_func", str_lit(12, "arg"));
11140 assert!(
11141 out.contains("my_custom_func("),
11142 "non-prelude call should use snake_case, got: {out}"
11143 );
11144 }
11145
11146 // ── Effect declaration tests ────────────────────────────────────────────
11147
11148 /// Build a `MatchArm` with a wildcard pattern and the given body.
11149 fn wildcard_arm(id: u32, body: AIRNode) -> AIRNode {
11150 node(
11151 id,
11152 NodeKind::MatchArm {
11153 pattern: Box::new(node(id + 100, NodeKind::WildcardPat)),
11154 guard: None,
11155 body: Box::new(body),
11156 },
11157 )
11158 }
11159
11160 /// A `Block` with the given leading statements and a string tail value.
11161 fn block_with_tail(id: u32, stmts: Vec<AIRNode>, tail: &str) -> AIRNode {
11162 node(
11163 id,
11164 NodeKind::Block {
11165 stmts,
11166 tail: Some(Box::new(str_lit(id + 1, tail))),
11167 },
11168 )
11169 }
11170
11171 #[test]
11172 fn valpos_arm_with_loop_leading_stmt_needs_stmt_form() {
11173 // `_ => { for _ in xs { f() } "v" }` — the leading `for` loop has no
11174 // Python expression form, so the lambda chain would drop it. The arm
11175 // must be routed to statement form.
11176 let loop_stmt = node(
11177 10,
11178 NodeKind::For {
11179 pattern: Box::new(node(11, NodeKind::WildcardPat)),
11180 iterable: Box::new(id_node(12, "xs")),
11181 body: Box::new(node(
11182 13,
11183 NodeKind::Block {
11184 stmts: vec![],
11185 tail: None,
11186 },
11187 )),
11188 },
11189 );
11190 let arms = vec![wildcard_arm(1, block_with_tail(20, vec![loop_stmt], "v"))];
11191 assert!(
11192 match_arm_drops_leading_stmts(&arms),
11193 "a value-tail arm with a leading `for` loop must route to statement form"
11194 );
11195 assert!(match_value_needs_stmt_form(&arms));
11196 assert!(value_needs_stmt_form(&node(
11197 30,
11198 NodeKind::Match {
11199 scrutinee: Box::new(id_node(31, "j")),
11200 arms,
11201 }
11202 )));
11203 }
11204
11205 #[test]
11206 fn valpos_arm_with_assign_leading_stmt_needs_stmt_form() {
11207 // `_ => { x = 1; "v" }` — a leading assignment is not lambda-expressible.
11208 let assign = node(
11209 10,
11210 NodeKind::Assign {
11211 target: Box::new(id_node(11, "x")),
11212 op: AssignOp::Assign,
11213 value: Box::new(int_lit(12, "1")),
11214 },
11215 );
11216 let arms = vec![wildcard_arm(1, block_with_tail(20, vec![assign], "v"))];
11217 assert!(match_arm_drops_leading_stmts(&arms));
11218 }
11219
11220 #[test]
11221 fn valpos_arm_with_mut_let_leading_stmt_needs_stmt_form() {
11222 // `_ => { let mut x = 1; "v" }` — a mutable `let` cannot be a lambda
11223 // parameter, so the chain would drop it.
11224 let mut_let = node(
11225 10,
11226 NodeKind::LetBinding {
11227 is_mut: true,
11228 pattern: Box::new(node(
11229 11,
11230 NodeKind::BindPat {
11231 name: ident("x"),
11232 is_mut: true,
11233 },
11234 )),
11235 ty: None,
11236 value: Box::new(int_lit(12, "1")),
11237 },
11238 );
11239 let arms = vec![wildcard_arm(1, block_with_tail(20, vec![mut_let], "v"))];
11240 assert!(match_arm_drops_leading_stmts(&arms));
11241 }
11242
11243 #[test]
11244 fn valpos_arm_with_simple_let_stays_on_lambda_chain() {
11245 // `_ => { let x = 1; "v" }` — a simple immutable `let` folds into a
11246 // `lambda x:` parameter, so the chain handles it and we must NOT force
11247 // statement form (that would regress the proven lambda-chain path).
11248 let simple_let = node(
11249 10,
11250 NodeKind::LetBinding {
11251 is_mut: false,
11252 pattern: Box::new(node(
11253 11,
11254 NodeKind::BindPat {
11255 name: ident("x"),
11256 is_mut: false,
11257 },
11258 )),
11259 ty: None,
11260 value: Box::new(int_lit(12, "1")),
11261 },
11262 );
11263 let arms = vec![wildcard_arm(1, block_with_tail(20, vec![simple_let], "v"))];
11264 assert!(
11265 !match_arm_drops_leading_stmts(&arms),
11266 "a simple immutable `let` is lambda-expressible — keep it on the chain"
11267 );
11268 }
11269
11270 #[test]
11271 fn valpos_arm_with_bare_call_stays_on_lambda_chain() {
11272 // `_ => { f(); "v" }` — a bare expression statement folds into a
11273 // `lambda _:` and stays on the chain.
11274 let call = node(
11275 10,
11276 NodeKind::Call {
11277 callee: Box::new(id_node(11, "f")),
11278 args: vec![],
11279 type_args: vec![],
11280 },
11281 );
11282 let arms = vec![wildcard_arm(1, block_with_tail(20, vec![call], "v"))];
11283 assert!(!match_arm_drops_leading_stmts(&arms));
11284 }
11285
11286 #[test]
11287 fn valpos_arm_tail_only_block_stays_on_lambda_chain() {
11288 // `_ => { "v" }` — no leading statements, nothing to drop.
11289 let arms = vec![wildcard_arm(1, block_with_tail(20, vec![], "v"))];
11290 assert!(!match_arm_drops_leading_stmts(&arms));
11291 }
11292
11293 #[test]
11294 fn effect_decl_becomes_abc() {
11295 let effect = node(
11296 1,
11297 NodeKind::EffectDecl {
11298 annotations: vec![],
11299 visibility: Visibility::Public,
11300 name: ident("Logger"),
11301 generic_params: vec![],
11302 components: vec![],
11303 operations: vec![
11304 node(
11305 2,
11306 NodeKind::FnDecl {
11307 annotations: vec![],
11308 visibility: Visibility::Public,
11309 is_async: false,
11310 name: ident("log"),
11311 generic_params: vec![],
11312 params: vec![
11313 typed_param_node(3, "level", "String"),
11314 typed_param_node(4, "msg", "String"),
11315 ],
11316 return_type: Some(Box::new(node(
11317 5,
11318 NodeKind::TypeNamed {
11319 path: type_path(&["Void"]),
11320 args: vec![],
11321 },
11322 ))),
11323 effect_clause: vec![],
11324 where_clause: vec![],
11325 body: Box::new(block(6, vec![], None)),
11326 },
11327 ),
11328 node(
11329 7,
11330 NodeKind::FnDecl {
11331 annotations: vec![],
11332 visibility: Visibility::Public,
11333 is_async: false,
11334 name: ident("flush"),
11335 generic_params: vec![],
11336 params: vec![],
11337 return_type: Some(Box::new(node(
11338 8,
11339 NodeKind::TypeNamed {
11340 path: type_path(&["Void"]),
11341 args: vec![],
11342 },
11343 ))),
11344 effect_clause: vec![],
11345 where_clause: vec![],
11346 body: Box::new(block(9, vec![], None)),
11347 },
11348 ),
11349 ],
11350 },
11351 );
11352 let out = gen(&module(vec![], vec![effect]));
11353 assert!(
11354 out.contains("from abc import ABC, abstractmethod"),
11355 "got: {out}"
11356 );
11357 assert!(out.contains("class Logger(ABC):"), "got: {out}");
11358 assert!(out.contains("@abstractmethod"), "got: {out}");
11359 assert!(
11360 out.contains("def log(self, level: str, msg: str) -> None:"),
11361 "got: {out}"
11362 );
11363 assert!(out.contains("def flush(self) -> None:"), "got: {out}");
11364 assert!(out.contains(" ..."), "got: {out}");
11365 }
11366
11367 #[test]
11368 fn effect_decl_empty_operations() {
11369 let effect = node(
11370 1,
11371 NodeKind::EffectDecl {
11372 annotations: vec![],
11373 visibility: Visibility::Public,
11374 name: ident("Empty"),
11375 generic_params: vec![],
11376 components: vec![],
11377 operations: vec![],
11378 },
11379 );
11380 let out = gen(&module(vec![], vec![effect]));
11381 assert!(out.contains("class Empty(ABC):"), "got: {out}");
11382 assert!(out.contains(" pass"), "got: {out}");
11383 }
11384
11385 #[test]
11386 fn handling_block_passes_handlers_to_effectful_call() {
11387 use bock_air::AirHandlerPair;
11388
11389 let effect_decl = node(
11390 1,
11391 NodeKind::EffectDecl {
11392 annotations: vec![],
11393 visibility: Visibility::Public,
11394 name: ident("Logger"),
11395 generic_params: vec![],
11396 components: vec![],
11397 operations: vec![node(
11398 2,
11399 NodeKind::FnDecl {
11400 annotations: vec![],
11401 visibility: Visibility::Public,
11402 is_async: false,
11403 name: ident("log"),
11404 generic_params: vec![],
11405 params: vec![typed_param_node(3, "msg", "String")],
11406 return_type: None,
11407 effect_clause: vec![],
11408 where_clause: vec![],
11409 body: Box::new(block(4, vec![], None)),
11410 },
11411 )],
11412 },
11413 );
11414
11415 let inner_fn = node(
11416 10,
11417 NodeKind::FnDecl {
11418 annotations: vec![],
11419 visibility: Visibility::Private,
11420 is_async: false,
11421 name: ident("inner"),
11422 generic_params: vec![],
11423 params: vec![],
11424 return_type: None,
11425 effect_clause: vec![type_path(&["Logger"])],
11426 where_clause: vec![],
11427 body: Box::new(block(12, vec![], Some(str_lit(13, "hello")))),
11428 },
11429 );
11430
11431 let call_inner = node(
11432 20,
11433 NodeKind::Call {
11434 callee: Box::new(id_node(21, "inner")),
11435 args: vec![],
11436 type_args: vec![],
11437 },
11438 );
11439 let handling = node(
11440 30,
11441 NodeKind::HandlingBlock {
11442 handlers: vec![AirHandlerPair {
11443 effect: type_path(&["Logger"]),
11444 handler: Box::new(node(
11445 31,
11446 NodeKind::Call {
11447 callee: Box::new(id_node(32, "StdoutLogger")),
11448 args: vec![],
11449 type_args: vec![],
11450 },
11451 )),
11452 }],
11453 body: Box::new(block(33, vec![], Some(call_inner))),
11454 },
11455 );
11456 let main_fn = node(
11457 40,
11458 NodeKind::FnDecl {
11459 annotations: vec![],
11460 visibility: Visibility::Private,
11461 is_async: false,
11462 name: ident("main"),
11463 generic_params: vec![],
11464 params: vec![],
11465 return_type: None,
11466 effect_clause: vec![],
11467 where_clause: vec![],
11468 body: Box::new(block(41, vec![handling], None)),
11469 },
11470 );
11471
11472 let out = gen(&module(vec![], vec![effect_decl, inner_fn, main_fn]));
11473 // Python: inner(logger=__logger_h<N>) — handling blocks get a fresh
11474 // numeric suffix so nested blocks don't shadow each other.
11475 assert!(
11476 out.contains("inner(logger=__logger_h"),
11477 "handling block should pass handler to effectful call, got: {out}"
11478 );
11479 // Handler constructors are PascalCase in Python — they name a class.
11480 assert!(
11481 out.contains(": Logger = StdoutLogger()"),
11482 "handling block should instantiate handler, got: {out}"
11483 );
11484 }
11485
11486 // ── Async / concurrent patterns ────────────────────────────────────────
11487
11488 #[test]
11489 fn async_function_imports_asyncio() {
11490 let body = block(3, vec![], None);
11491 let f = node(
11492 1,
11493 NodeKind::FnDecl {
11494 annotations: vec![],
11495 visibility: Visibility::Private,
11496 is_async: true,
11497 name: ident("tick"),
11498 generic_params: vec![],
11499 params: vec![],
11500 return_type: None,
11501 effect_clause: vec![],
11502 where_clause: vec![],
11503 body: Box::new(body),
11504 },
11505 );
11506 let out = gen(&module(vec![], vec![f]));
11507 assert!(out.contains("import asyncio"), "got: {out}");
11508 assert!(out.contains("async def tick():"), "got: {out}");
11509 }
11510
11511 #[test]
11512 fn sync_module_has_no_asyncio_import() {
11513 let body = block(3, vec![], Some(int_lit(4, "1")));
11514 let f = node(
11515 1,
11516 NodeKind::FnDecl {
11517 annotations: vec![],
11518 visibility: Visibility::Private,
11519 is_async: false,
11520 name: ident("one"),
11521 generic_params: vec![],
11522 params: vec![],
11523 return_type: None,
11524 effect_clause: vec![],
11525 where_clause: vec![],
11526 body: Box::new(body),
11527 },
11528 );
11529 let out = gen(&module(vec![], vec![f]));
11530 assert!(!out.contains("import asyncio"), "got: {out}");
11531 }
11532
11533 #[test]
11534 fn entry_invocation_async_main_python() {
11535 let inv = PyGenerator::new().entry_invocation(true).unwrap();
11536 assert!(inv.contains("asyncio.run(main())"), "got: {inv}");
11537 }
11538
11539 #[test]
11540 fn entry_invocation_sync_main_python() {
11541 let inv = PyGenerator::new().entry_invocation(false).unwrap();
11542 assert_eq!(inv, "if __name__ == \"__main__\":\n main()\n");
11543 }
11544
11545 #[test]
11546 fn generate_project_async_main_uses_asyncio_run() {
11547 let main_fn = node(
11548 1,
11549 NodeKind::FnDecl {
11550 annotations: vec![],
11551 visibility: Visibility::Private,
11552 is_async: true,
11553 name: ident("main"),
11554 generic_params: vec![],
11555 params: vec![],
11556 return_type: None,
11557 effect_clause: vec![],
11558 where_clause: vec![],
11559 body: Box::new(block(2, vec![], None)),
11560 },
11561 );
11562 let m = module(vec![], vec![main_fn]);
11563 let gen = PyGenerator::new();
11564 let src_path = std::path::Path::new("src/main.bock");
11565 let out = gen.generate_project(&[(&m, src_path)]).unwrap();
11566 let src = &out.files[0].content;
11567 assert_eq!(out.files[0].path, std::path::PathBuf::from("main.py"));
11568 assert!(src.contains("import asyncio"), "got: {src}");
11569 assert!(src.contains("async def main():"), "got: {src}");
11570 assert!(src.contains("asyncio.run(main())"), "got: {src}");
11571 }
11572
11573 #[test]
11574 fn concurrent_pattern_wraps_in_create_task() {
11575 // Block:
11576 // let a = task1()
11577 // let b = task2()
11578 // let ra = await a
11579 // let rb = await b
11580 // return ra
11581 let call_task = |id: u32, name: &str| {
11582 node(
11583 id,
11584 NodeKind::Call {
11585 callee: Box::new(id_node(id + 1, name)),
11586 args: vec![],
11587 type_args: vec![],
11588 },
11589 )
11590 };
11591 let let_stmt = |id: u32, name: &str, val: AIRNode| {
11592 node(
11593 id,
11594 NodeKind::LetBinding {
11595 is_mut: false,
11596 pattern: Box::new(bind_pat(id + 1, name)),
11597 ty: None,
11598 value: Box::new(val),
11599 },
11600 )
11601 };
11602 let await_id = |id: u32, name: &str| {
11603 node(
11604 id,
11605 NodeKind::Await {
11606 expr: Box::new(id_node(id + 1, name)),
11607 },
11608 )
11609 };
11610
11611 let body = block(
11612 10,
11613 vec![
11614 let_stmt(20, "a", call_task(21, "task1")),
11615 let_stmt(30, "b", call_task(31, "task2")),
11616 let_stmt(40, "ra", await_id(41, "a")),
11617 let_stmt(50, "rb", await_id(51, "b")),
11618 ],
11619 Some(id_node(60, "ra")),
11620 );
11621 let f = node(
11622 1,
11623 NodeKind::FnDecl {
11624 annotations: vec![],
11625 visibility: Visibility::Private,
11626 is_async: true,
11627 name: ident("run"),
11628 generic_params: vec![],
11629 params: vec![],
11630 return_type: None,
11631 effect_clause: vec![],
11632 where_clause: vec![],
11633 body: Box::new(body),
11634 },
11635 );
11636 let out = gen(&module(vec![], vec![f]));
11637 assert!(
11638 out.contains("a = asyncio.create_task(task1())"),
11639 "task1 should be scheduled as a task, got: {out}"
11640 );
11641 assert!(
11642 out.contains("b = asyncio.create_task(task2())"),
11643 "task2 should be scheduled as a task, got: {out}"
11644 );
11645 assert!(out.contains("ra = (await a)"), "got: {out}");
11646 assert!(out.contains("rb = (await b)"), "got: {out}");
11647 }
11648
11649 #[test]
11650 fn sequential_await_no_task_wrapping() {
11651 // `let a = await task1()` directly awaits — no task wrap needed.
11652 let await_call = node(
11653 20,
11654 NodeKind::Await {
11655 expr: Box::new(node(
11656 21,
11657 NodeKind::Call {
11658 callee: Box::new(id_node(22, "task1")),
11659 args: vec![],
11660 type_args: vec![],
11661 },
11662 )),
11663 },
11664 );
11665 let let_stmt = node(
11666 10,
11667 NodeKind::LetBinding {
11668 is_mut: false,
11669 pattern: Box::new(bind_pat(11, "a")),
11670 ty: None,
11671 value: Box::new(await_call),
11672 },
11673 );
11674 let body = block(30, vec![let_stmt], Some(id_node(40, "a")));
11675 let f = node(
11676 1,
11677 NodeKind::FnDecl {
11678 annotations: vec![],
11679 visibility: Visibility::Private,
11680 is_async: true,
11681 name: ident("run"),
11682 generic_params: vec![],
11683 params: vec![],
11684 return_type: None,
11685 effect_clause: vec![],
11686 where_clause: vec![],
11687 body: Box::new(body),
11688 },
11689 );
11690 let out = gen(&module(vec![], vec![f]));
11691 assert!(
11692 !out.contains("create_task"),
11693 "sequential await should not wrap in create_task, got: {out}"
11694 );
11695 assert!(out.contains("a = (await task1())"), "got: {out}");
11696 }
11697
11698 #[test]
11699 fn non_call_rhs_not_wrapped_in_task() {
11700 // `let a = 42 ; ... await a` — RHS is not a Call, so we can't wrap.
11701 let let_stmt = node(
11702 10,
11703 NodeKind::LetBinding {
11704 is_mut: false,
11705 pattern: Box::new(bind_pat(11, "a")),
11706 ty: None,
11707 value: Box::new(int_lit(12, "42")),
11708 },
11709 );
11710 let await_a = node(
11711 20,
11712 NodeKind::Await {
11713 expr: Box::new(id_node(21, "a")),
11714 },
11715 );
11716 let body = block(30, vec![let_stmt], Some(await_a));
11717 let f = node(
11718 1,
11719 NodeKind::FnDecl {
11720 annotations: vec![],
11721 visibility: Visibility::Private,
11722 is_async: true,
11723 name: ident("run"),
11724 generic_params: vec![],
11725 params: vec![],
11726 return_type: None,
11727 effect_clause: vec![],
11728 where_clause: vec![],
11729 body: Box::new(body),
11730 },
11731 );
11732 let out = gen(&module(vec![], vec![f]));
11733 assert!(!out.contains("create_task"), "got: {out}");
11734 assert!(out.contains("a = 42"), "got: {out}");
11735 }
11736
11737 /// Q-py-optional: the Python Optional runtime is emitted when a module uses
11738 /// Optional/`Some`/`None`; `Some(x)` and `None` lower to the tagged runtime
11739 /// values (`_BockSome(...)` / `_bock_none`), and an Optional `match` lowers
11740 /// to structural arms (`case _BockSome(x):` / `case _BockNone():`) — not the
11741 /// old bare `Some`/`None` (undefined) and `case None():` (a SyntaxError).
11742 #[test]
11743 fn optional_runtime_construct_and_match() {
11744 // fn describe(o: Int?) -> Int {
11745 // match o { Some(x) => return x; None => return Some(0); } (Some forces construction)
11746 // }
11747 let opt_int_ty = node(
11748 200,
11749 NodeKind::TypeOptional {
11750 inner: Box::new(node(
11751 201,
11752 NodeKind::TypeNamed {
11753 path: type_path(&["Int"]),
11754 args: vec![],
11755 },
11756 )),
11757 },
11758 );
11759 let o_param = node(
11760 30,
11761 NodeKind::Param {
11762 pattern: Box::new(bind_pat(31, "o")),
11763 ty: Some(Box::new(opt_int_ty)),
11764 default: None,
11765 },
11766 );
11767 // Construct Some(1) and None in the body so the prelude + constructors
11768 // are exercised.
11769 let some_call = node(
11770 70,
11771 NodeKind::Call {
11772 callee: Box::new(id_node(71, "Some")),
11773 args: vec![AirArg {
11774 label: None,
11775 value: int_lit(72, "1"),
11776 }],
11777 type_args: vec![],
11778 },
11779 );
11780 let none_ref = id_node(73, "None");
11781 let some_arm = node(
11782 40,
11783 NodeKind::MatchArm {
11784 pattern: Box::new(node(
11785 41,
11786 NodeKind::ConstructorPat {
11787 path: type_path(&["Some"]),
11788 fields: vec![bind_pat(42, "x")],
11789 },
11790 )),
11791 guard: None,
11792 body: Box::new(block(
11793 43,
11794 vec![node(
11795 44,
11796 NodeKind::Return {
11797 value: Some(Box::new(id_node(45, "x"))),
11798 },
11799 )],
11800 None,
11801 )),
11802 },
11803 );
11804 let none_arm = node(
11805 50,
11806 NodeKind::MatchArm {
11807 pattern: Box::new(node(
11808 51,
11809 NodeKind::ConstructorPat {
11810 path: type_path(&["None"]),
11811 fields: vec![],
11812 },
11813 )),
11814 guard: None,
11815 body: Box::new(block(
11816 52,
11817 vec![node(
11818 53,
11819 NodeKind::Return {
11820 value: Some(Box::new(int_lit(54, "0"))),
11821 },
11822 )],
11823 None,
11824 )),
11825 },
11826 );
11827 let match_stmt = node(
11828 60,
11829 NodeKind::Match {
11830 scrutinee: Box::new(id_node(61, "o")),
11831 arms: vec![some_arm, none_arm],
11832 },
11833 );
11834 let f = node(
11835 1,
11836 NodeKind::FnDecl {
11837 annotations: vec![],
11838 visibility: Visibility::Private,
11839 is_async: false,
11840 name: ident("describe"),
11841 generic_params: vec![],
11842 params: vec![o_param],
11843 return_type: Some(Box::new(node(
11844 2,
11845 NodeKind::TypeNamed {
11846 path: type_path(&["Int"]),
11847 args: vec![],
11848 },
11849 ))),
11850 effect_clause: vec![],
11851 where_clause: vec![],
11852 body: Box::new(block(
11853 3,
11854 vec![
11855 node(
11856 80,
11857 NodeKind::LetBinding {
11858 is_mut: false,
11859 pattern: Box::new(bind_pat(81, "a")),
11860 ty: None,
11861 value: Box::new(some_call),
11862 },
11863 ),
11864 node(
11865 82,
11866 NodeKind::LetBinding {
11867 is_mut: false,
11868 pattern: Box::new(bind_pat(83, "b")),
11869 ty: None,
11870 value: Box::new(none_ref),
11871 },
11872 ),
11873 match_stmt,
11874 ],
11875 None,
11876 )),
11877 },
11878 );
11879 let out = gen(&module(vec![], vec![f]));
11880 // Runtime prelude is present.
11881 assert!(out.contains("class _BockSome:"), "got: {out}");
11882 assert!(out.contains("class _BockNone:"), "got: {out}");
11883 assert!(out.contains("_bock_none = _BockNone()"), "got: {out}");
11884 // Constructors lower to the runtime values, not bare `Some`/`None`.
11885 assert!(out.contains("_BockSome(1)"), "got: {out}");
11886 assert!(out.contains("b = _bock_none"), "got: {out}");
11887 // Match arms are structural, not `Some(...)` / `case None():`.
11888 assert!(out.contains("case _BockSome(x):"), "got: {out}");
11889 assert!(out.contains("case _BockNone():"), "got: {out}");
11890 assert!(!out.contains("case None()"), "got: {out}");
11891 }
11892
11893 /// An Optional `match` in *expression* position (value of a `let`) with
11894 /// *non-`return`* arms must lower to a real conditional over the bound
11895 /// scrutinee that tests the tag and binds the payload — NOT the old stub
11896 /// `(lambda __v: <some> if False else <none>)` (which always selected the
11897 /// last arm and never bound the payload). Regression-locking the Python
11898 /// expression-position Optional-match defect.
11899 #[test]
11900 fn optional_match_in_expression_position_binds_payload() {
11901 // fn pick(o: Int?) -> Int { let r = match o { Some(x) => x + 1; None => 0 }; return r }
11902 let opt_int_ty = node(
11903 200,
11904 NodeKind::TypeOptional {
11905 inner: Box::new(node(
11906 201,
11907 NodeKind::TypeNamed {
11908 path: type_path(&["Int"]),
11909 args: vec![],
11910 },
11911 )),
11912 },
11913 );
11914 let o_param = node(
11915 30,
11916 NodeKind::Param {
11917 pattern: Box::new(bind_pat(31, "o")),
11918 ty: Some(Box::new(opt_int_ty)),
11919 default: None,
11920 },
11921 );
11922 // Some(x) => x + 1 (a value, not a `return`).
11923 let some_arm = node(
11924 40,
11925 NodeKind::MatchArm {
11926 pattern: Box::new(node(
11927 41,
11928 NodeKind::ConstructorPat {
11929 path: type_path(&["Some"]),
11930 fields: vec![bind_pat(42, "x")],
11931 },
11932 )),
11933 guard: None,
11934 body: Box::new(block(
11935 43,
11936 vec![],
11937 Some(node(
11938 44,
11939 NodeKind::BinaryOp {
11940 op: BinOp::Add,
11941 left: Box::new(id_node(45, "x")),
11942 right: Box::new(int_lit(46, "1")),
11943 },
11944 )),
11945 )),
11946 },
11947 );
11948 // None => 0
11949 let none_arm = node(
11950 50,
11951 NodeKind::MatchArm {
11952 pattern: Box::new(node(
11953 51,
11954 NodeKind::ConstructorPat {
11955 path: type_path(&["None"]),
11956 fields: vec![],
11957 },
11958 )),
11959 guard: None,
11960 body: Box::new(block(52, vec![], Some(int_lit(53, "0")))),
11961 },
11962 );
11963 let match_expr = node(
11964 60,
11965 NodeKind::Match {
11966 scrutinee: Box::new(id_node(61, "o")),
11967 arms: vec![some_arm, none_arm],
11968 },
11969 );
11970 // let r = <match_expr> (match appears in expression position).
11971 let let_r = node(
11972 70,
11973 NodeKind::LetBinding {
11974 is_mut: false,
11975 pattern: Box::new(bind_pat(71, "r")),
11976 ty: None,
11977 value: Box::new(match_expr),
11978 },
11979 );
11980 let f = node(
11981 1,
11982 NodeKind::FnDecl {
11983 annotations: vec![],
11984 visibility: Visibility::Private,
11985 is_async: false,
11986 name: ident("pick"),
11987 generic_params: vec![],
11988 params: vec![o_param],
11989 return_type: Some(Box::new(node(
11990 2,
11991 NodeKind::TypeNamed {
11992 path: type_path(&["Int"]),
11993 args: vec![],
11994 },
11995 ))),
11996 effect_clause: vec![],
11997 where_clause: vec![],
11998 body: Box::new(block(
11999 3,
12000 vec![
12001 let_r,
12002 node(
12003 80,
12004 NodeKind::Return {
12005 value: Some(Box::new(id_node(81, "r"))),
12006 },
12007 ),
12008 ],
12009 None,
12010 )),
12011 },
12012 );
12013 let out = gen(&module(vec![], vec![f]));
12014 // No hardcoded `if False` stub.
12015 assert!(
12016 !out.contains("if False"),
12017 "expression-position match must not emit the `if False` stub, got: {out}"
12018 );
12019 // Tests the tag and binds the payload via an applied lambda.
12020 assert!(
12021 out.contains("isinstance(__v, _BockSome)"),
12022 "expected a tag test, got: {out}"
12023 );
12024 assert!(
12025 out.contains("(lambda x:") && out.contains(")(__v._0)"),
12026 "expected the Some payload bound from __v._0, got: {out}"
12027 );
12028 }
12029
12030 // ── Lambdas, typing imports, and generics (DV12 + lambda fix) ─────────────
12031
12032 /// A `GenericParam` named `name` with optional single trait `bound`.
12033 fn generic_param(name: &str, bound: Option<&str>) -> bock_ast::GenericParam {
12034 bock_ast::GenericParam {
12035 id: 0,
12036 span: span(),
12037 name: ident(name),
12038 bounds: bound.map(|b| vec![type_path(&[b])]).unwrap_or_default(),
12039 }
12040 }
12041
12042 /// A record field whose declared type is the bare named type `ty_name`
12043 /// (e.g. a type parameter `T`).
12044 fn named_field(field: &str, ty_name: &str) -> bock_ast::RecordDeclField {
12045 bock_ast::RecordDeclField {
12046 id: 0,
12047 span: span(),
12048 name: ident(field),
12049 ty: bock_ast::TypeExpr::Named {
12050 id: 0,
12051 span: span(),
12052 path: type_path(&[ty_name]),
12053 args: vec![],
12054 },
12055 default: None,
12056 }
12057 }
12058
12059 #[test]
12060 fn lambda_params_have_no_type_hints() {
12061 // `(x: Int) => x + 1` must emit `lambda x: …`, never `lambda x: int: …`
12062 // (the latter is a Python `SyntaxError` — the bug this fix closes).
12063 let lambda = node(
12064 1,
12065 NodeKind::Lambda {
12066 params: vec![typed_param_node(2, "x", "Int")],
12067 body: Box::new(node(
12068 3,
12069 NodeKind::BinaryOp {
12070 op: BinOp::Add,
12071 left: Box::new(id_node(4, "x")),
12072 right: Box::new(int_lit(5, "1")),
12073 },
12074 )),
12075 },
12076 );
12077 let body = block(
12078 6,
12079 vec![node(
12080 7,
12081 NodeKind::LetBinding {
12082 is_mut: false,
12083 pattern: Box::new(bind_pat(8, "inc")),
12084 ty: None,
12085 value: Box::new(lambda),
12086 },
12087 )],
12088 None,
12089 );
12090 let f = node(
12091 9,
12092 NodeKind::FnDecl {
12093 annotations: vec![],
12094 visibility: Visibility::Private,
12095 is_async: false,
12096 name: ident("run"),
12097 generic_params: vec![],
12098 params: vec![],
12099 return_type: None,
12100 effect_clause: vec![],
12101 where_clause: vec![],
12102 body: Box::new(body),
12103 },
12104 );
12105 let out = gen(&module(vec![], vec![f]));
12106 assert!(
12107 out.contains("lambda x: "),
12108 "lambda must emit a bare param list, got: {out}"
12109 );
12110 assert!(
12111 !out.contains("lambda x: int"),
12112 "lambda param must NOT carry a type hint (SyntaxError), got: {out}"
12113 );
12114 }
12115
12116 #[test]
12117 fn fn_type_param_emits_callable_import() {
12118 // A parameter of function type lowers to `Callable[[int], int]`, which
12119 // must be imported from `typing` or it raises `NameError`.
12120 let f_param = node(
12121 2,
12122 NodeKind::Param {
12123 pattern: Box::new(bind_pat(3, "f")),
12124 ty: Some(Box::new(node(
12125 4,
12126 NodeKind::TypeFunction {
12127 params: vec![node(
12128 5,
12129 NodeKind::TypeNamed {
12130 path: type_path(&["Int"]),
12131 args: vec![],
12132 },
12133 )],
12134 ret: Box::new(node(
12135 6,
12136 NodeKind::TypeNamed {
12137 path: type_path(&["Int"]),
12138 args: vec![],
12139 },
12140 )),
12141 effects: vec![],
12142 },
12143 ))),
12144 default: None,
12145 },
12146 );
12147 let body = block(7, vec![], Some(int_lit(8, "0")));
12148 let f = node(
12149 1,
12150 NodeKind::FnDecl {
12151 annotations: vec![],
12152 visibility: Visibility::Private,
12153 is_async: false,
12154 name: ident("apply"),
12155 generic_params: vec![],
12156 params: vec![f_param],
12157 return_type: None,
12158 effect_clause: vec![],
12159 where_clause: vec![],
12160 body: Box::new(body),
12161 },
12162 );
12163 let out = gen(&module(vec![], vec![f]));
12164 assert!(
12165 out.contains("from typing import Callable"),
12166 "Callable annotation needs its typing import, got: {out}"
12167 );
12168 assert!(
12169 out.contains("f: Callable[[int], int]"),
12170 "expected the Callable annotation, got: {out}"
12171 );
12172 }
12173
12174 #[test]
12175 fn generic_record_emits_typevar_and_generic() {
12176 // `record Box[T] { value: T }` must declare `T = TypeVar("T")`, list
12177 // `Generic[T]` in the class bases, and import both from `typing`, or
12178 // the field annotation `value: T` raises `NameError` at class-eval time.
12179 let rec = node(
12180 1,
12181 NodeKind::RecordDecl {
12182 annotations: vec![],
12183 visibility: Visibility::Public,
12184 name: ident("Box"),
12185 generic_params: vec![generic_param("T", None)],
12186 fields: vec![named_field("value", "T")],
12187 },
12188 );
12189 let out = gen(&module(vec![], vec![rec]));
12190 assert!(
12191 out.contains("from typing import Generic, TypeVar"),
12192 "expected merged typing import, got: {out}"
12193 );
12194 assert!(
12195 out.contains("T = TypeVar(\"T\")"),
12196 "expected a TypeVar declaration, got: {out}"
12197 );
12198 assert!(
12199 out.contains("class Box(Generic[T]):"),
12200 "expected Generic[T] in the class bases, got: {out}"
12201 );
12202 assert!(out.contains("value: T"), "got: {out}");
12203 }
12204
12205 #[test]
12206 fn bounded_type_param_emits_typevar_bound() {
12207 // `fn describe[T: Named](x: T) -> ...` → `T = TypeVar("T", bound=Named)`.
12208 let body = block(3, vec![], Some(int_lit(4, "0")));
12209 let f = node(
12210 1,
12211 NodeKind::FnDecl {
12212 annotations: vec![],
12213 visibility: Visibility::Private,
12214 is_async: false,
12215 name: ident("describe"),
12216 generic_params: vec![generic_param("T", Some("Named"))],
12217 params: vec![typed_param_node(2, "x", "T")],
12218 return_type: None,
12219 effect_clause: vec![],
12220 where_clause: vec![],
12221 body: Box::new(body),
12222 },
12223 );
12224 let out = gen(&module(vec![], vec![f]));
12225 assert!(
12226 out.contains("T = TypeVar(\"T\", bound=Named)"),
12227 "expected a bounded TypeVar, got: {out}"
12228 );
12229 }
12230
12231 #[test]
12232 fn shared_type_param_typevar_is_deduped() {
12233 // Two generic records sharing the parameter name `T` must declare
12234 // `T = TypeVar("T")` exactly once in the file.
12235 let box_a = node(
12236 1,
12237 NodeKind::RecordDecl {
12238 annotations: vec![],
12239 visibility: Visibility::Public,
12240 name: ident("Boxed"),
12241 generic_params: vec![generic_param("T", None)],
12242 fields: vec![named_field("value", "T")],
12243 },
12244 );
12245 let box_b = node(
12246 2,
12247 NodeKind::RecordDecl {
12248 annotations: vec![],
12249 visibility: Visibility::Public,
12250 name: ident("Wrapped"),
12251 generic_params: vec![generic_param("T", None)],
12252 fields: vec![named_field("inner", "T")],
12253 },
12254 );
12255 let out = gen(&module(vec![], vec![box_a, box_b]));
12256 let typevar_count = out.matches("T = TypeVar(\"T\")").count();
12257 assert_eq!(
12258 typevar_count, 1,
12259 "shared type param T must be declared exactly once, got {typevar_count} in: {out}"
12260 );
12261 }
12262
12263 #[test]
12264 fn method_colliding_with_field_is_disambiguated() {
12265 // record SimpleError { message: String }
12266 let record_decl = node(
12267 1,
12268 NodeKind::RecordDecl {
12269 annotations: vec![],
12270 visibility: Visibility::Public,
12271 name: ident("SimpleError"),
12272 generic_params: vec![],
12273 fields: vec![named_field("message", "String")],
12274 },
12275 );
12276 // impl Error for SimpleError { fn message(self) -> String { self.message } }
12277 let method = node(
12278 10,
12279 NodeKind::FnDecl {
12280 annotations: vec![],
12281 visibility: Visibility::Public,
12282 is_async: false,
12283 name: ident("message"),
12284 generic_params: vec![],
12285 params: vec![param_node(11, "self")],
12286 return_type: None,
12287 effect_clause: vec![],
12288 where_clause: vec![],
12289 body: Box::new(block(
12290 12,
12291 vec![],
12292 Some(node(
12293 13,
12294 NodeKind::FieldAccess {
12295 object: Box::new(id_node(14, "self")),
12296 field: ident("message"),
12297 },
12298 )),
12299 )),
12300 },
12301 );
12302 let impl_block = node(
12303 20,
12304 NodeKind::ImplBlock {
12305 annotations: vec![],
12306 target: Box::new(node(
12307 21,
12308 NodeKind::TypeNamed {
12309 path: type_path(&["SimpleError"]),
12310 args: vec![],
12311 },
12312 )),
12313 trait_path: Some(type_path(&["Error"])),
12314 trait_args: vec![],
12315 generic_params: vec![],
12316 where_clause: vec![],
12317 methods: vec![method],
12318 },
12319 );
12320 // fn read(e: SimpleError) -> String { e.message() }
12321 let read_fn = node(
12322 30,
12323 NodeKind::FnDecl {
12324 annotations: vec![],
12325 visibility: Visibility::Public,
12326 is_async: false,
12327 name: ident("read"),
12328 generic_params: vec![],
12329 params: vec![typed_param_node(31, "e", "SimpleError")],
12330 return_type: None,
12331 effect_clause: vec![],
12332 where_clause: vec![],
12333 body: Box::new(block(
12334 32,
12335 vec![],
12336 Some(node(
12337 33,
12338 NodeKind::Call {
12339 callee: Box::new(node(
12340 34,
12341 NodeKind::FieldAccess {
12342 // The lowerer reuses the *same* receiver node
12343 // in both the field-access object and the
12344 // self arg; `desugared_self_call` keys on the
12345 // shared NodeId, so the test must too.
12346 object: Box::new(id_node(35, "e")),
12347 field: ident("message"),
12348 },
12349 )),
12350 type_args: vec![],
12351 args: vec![AirArg {
12352 label: None,
12353 value: id_node(35, "e"),
12354 }],
12355 },
12356 )),
12357 )),
12358 },
12359 );
12360 let out = gen(&module(vec![], vec![record_decl, impl_block, read_fn]));
12361 // The dataclass field stays `message`.
12362 assert!(
12363 out.contains("message: str"),
12364 "dataclass field should remain `message: str`, got: {out}"
12365 );
12366 // The inlined method and the call site are renamed to `message_method`
12367 // so the dataclass field no longer overwrites the method attribute.
12368 assert!(
12369 out.contains("def message_method(self)"),
12370 "method should be `def message_method`, got: {out}"
12371 );
12372 assert!(
12373 out.contains(".message_method()"),
12374 "call site should be `.message_method()`, got: {out}"
12375 );
12376 // The method body still reads the field via `self.message`.
12377 assert!(
12378 out.contains("return self.message"),
12379 "method body should read the field `self.message`, got: {out}"
12380 );
12381 // No bare `def message(self)` that the field would clobber.
12382 assert!(
12383 !out.contains("def message(self)"),
12384 "must NOT emit a `def message(self)` clobbered by the field, got: {out}"
12385 );
12386 }
12387
12388 // ── Python-specific control-flow / import lowering ──────────────────────
12389
12390 fn call_no_args(id: u32, name: &str) -> AIRNode {
12391 node(
12392 id,
12393 NodeKind::Call {
12394 callee: Box::new(id_node(id + 1, name)),
12395 args: vec![],
12396 type_args: vec![],
12397 },
12398 )
12399 }
12400
12401 /// `todo()` / `unreachable()` are diverging `raise` expressions; an arbitrary
12402 /// call or the `Unreachable` node is not (the latter *prints* a `raise` but
12403 /// is the dedicated node, recognised separately).
12404 #[test]
12405 fn is_raise_expr_recognises_todo_and_unreachable() {
12406 assert!(is_raise_expr(&call_no_args(1, "todo")));
12407 assert!(is_raise_expr(&call_no_args(3, "unreachable")));
12408 assert!(is_raise_expr(&node(5, NodeKind::Unreachable)));
12409 assert!(!is_raise_expr(&call_no_args(6, "compute")));
12410 assert!(!is_raise_expr(&int_lit(8, "1")));
12411 }
12412
12413 /// A `let x = todo()` body emits a bare `raise`, never `x = raise …` (a
12414 /// `SyntaxError`), and a `todo()` function tail emits a bare `raise`, never
12415 /// `return raise …`.
12416 #[test]
12417 fn todo_in_return_and_let_position_emits_bare_raise() {
12418 // Tail position: `fn f() { todo() }`.
12419 let f = fn_decl_tail(1, Visibility::Private, "f", call_no_args(10, "todo"));
12420 let out = gen(&module(vec![], vec![f]));
12421 assert!(
12422 out.contains("raise NotImplementedError()"),
12423 "expected a `raise`, got: {out}"
12424 );
12425 assert!(
12426 !out.contains("return raise"),
12427 "must NOT emit `return raise …`, got: {out}"
12428 );
12429 }
12430
12431 /// An expression-position `loop` bound into a `let`, yielding via `break v`,
12432 /// is hoisted to a `while True:` whose `break v` becomes `<target> = v` then
12433 /// `break` — never the invalid expression `let x = <loop>`.
12434 #[test]
12435 fn value_loop_break_hoists_to_while_assign() {
12436 // `fn f() { let r = loop { break 5 } r }`
12437 let break_node = node(
12438 30,
12439 NodeKind::Break {
12440 value: Some(Box::new(int_lit(31, "5"))),
12441 },
12442 );
12443 let loop_node = node(
12444 20,
12445 NodeKind::Loop {
12446 body: Box::new(block(21, vec![], Some(break_node))),
12447 },
12448 );
12449 let let_r = node(
12450 10,
12451 NodeKind::LetBinding {
12452 is_mut: false,
12453 pattern: Box::new(bind_pat(11, "r")),
12454 ty: None,
12455 value: Box::new(loop_node),
12456 },
12457 );
12458 let body = block(2, vec![let_r], Some(id_node(40, "r")));
12459 let f = node(
12460 1,
12461 NodeKind::FnDecl {
12462 annotations: vec![],
12463 visibility: Visibility::Private,
12464 is_async: false,
12465 name: ident("f"),
12466 generic_params: vec![],
12467 params: vec![],
12468 return_type: None,
12469 effect_clause: vec![],
12470 where_clause: vec![],
12471 body: Box::new(body),
12472 },
12473 );
12474 let out = gen(&module(vec![], vec![f]));
12475 assert!(
12476 out.contains("while True:"),
12477 "value-loop should hoist to `while True:`, got: {out}"
12478 );
12479 // The shared AIR value-CF hoist introduces a `__bock_cf_N` temp: the
12480 // `break 5` assigns it (`__bock_cf_0 = 5`) then `break`, and `r` reads it.
12481 assert!(
12482 out.contains("__bock_cf_0 = 5"),
12483 "break value should assign the hoisted temp `__bock_cf_0 = 5`, got: {out}"
12484 );
12485 assert!(
12486 out.contains("r = __bock_cf_0"),
12487 "the let should read the hoisted temp `r = __bock_cf_0`, got: {out}"
12488 );
12489 assert!(
12490 out.contains("break"),
12491 "the loop should still `break`, got: {out}"
12492 );
12493 assert!(
12494 !out.contains("# unsupported"),
12495 "must NOT emit `# unsupported`, got: {out}"
12496 );
12497 }
12498
12499 /// A record field declaration whose name matches a sibling-module public
12500 /// function must NOT be counted as a reference: the field-label occurrence is
12501 /// subtracted, so the implicit-import scan does not pull in
12502 /// `from <sibling> import <name>` (which closes a Python import cycle).
12503 #[test]
12504 fn field_label_does_not_trigger_implicit_import() {
12505 // `module models` declares `record Summary { total: Int }`. `total` is a
12506 // FIELD name; it must not match a sibling `fn total`.
12507 let summary = node(
12508 5,
12509 NodeKind::RecordDecl {
12510 annotations: vec![],
12511 visibility: Visibility::Public,
12512 name: ident("Summary"),
12513 generic_params: vec![],
12514 fields: vec![bock_ast::RecordDeclField {
12515 id: 6,
12516 span: span(),
12517 name: ident("total"),
12518 ty: bock_ast::TypeExpr::Named {
12519 id: 7,
12520 span: span(),
12521 path: type_path(&["Int"]),
12522 args: vec![],
12523 },
12524 default: None,
12525 }],
12526 },
12527 );
12528 let models = module_with_path(&["models"], vec![], vec![summary]);
12529 // Public-symbol map says `total` is declared by `service`.
12530 let mut public_symbols = HashMap::new();
12531 public_symbols.insert("total".to_string(), "service".to_string());
12532 let imports = implicit_imports_for(&models, &public_symbols, "models");
12533 assert!(
12534 imports.is_empty(),
12535 "a field named `total` must not implicit-import `service.total`, got: {imports:?}"
12536 );
12537 }
12538
12539 /// `fn f() { let x = if (c) { 1 } else { return 0 } x }` — value-position
12540 /// `if` with a diverging else. The shared value-CF hoist pre-binds a temp
12541 /// and lowers the `if` to statements, never `# unsupported` or an invalid
12542 /// `lambda` capturing the `return`.
12543 fn diverging_value_if_fn() -> AIRNode {
12544 let then_b = block(2, vec![], Some(int_lit(3, "1")));
12545 let ret = node(
12546 5,
12547 NodeKind::Return {
12548 value: Some(Box::new(int_lit(6, "0"))),
12549 },
12550 );
12551 let else_b = block(4, vec![], Some(ret));
12552 let if_node = node(
12553 1,
12554 NodeKind::If {
12555 let_pattern: None,
12556 condition: Box::new(id_node(7, "c")),
12557 then_block: Box::new(then_b),
12558 else_block: Some(Box::new(else_b)),
12559 },
12560 );
12561 let let_x = node(
12562 10,
12563 NodeKind::LetBinding {
12564 is_mut: false,
12565 pattern: Box::new(bind_pat(11, "x")),
12566 ty: None,
12567 value: Box::new(if_node),
12568 },
12569 );
12570 let body = block(20, vec![let_x], Some(id_node(21, "x")));
12571 let f = node(
12572 30,
12573 NodeKind::FnDecl {
12574 annotations: vec![],
12575 visibility: Visibility::Private,
12576 is_async: false,
12577 name: ident("f"),
12578 generic_params: vec![],
12579 params: vec![],
12580 return_type: None,
12581 effect_clause: vec![],
12582 where_clause: vec![],
12583 body: Box::new(body),
12584 },
12585 );
12586 module(vec![], vec![f])
12587 }
12588
12589 #[test]
12590 fn diverging_value_if_hoists_to_stmt_form_no_unsupported() {
12591 let out = gen(&diverging_value_if_fn());
12592 assert!(
12593 !out.contains("# unsupported"),
12594 "diverging value-if must not emit `# unsupported`, got: {out}"
12595 );
12596 assert!(
12597 out.contains("__bock_cf_0 = 1"),
12598 "value arm must assign the temp, got: {out}"
12599 );
12600 assert!(
12601 out.contains("return 0"),
12602 "diverging arm must keep its return, got: {out}"
12603 );
12604 }
12605
12606 // ── Value-position match: plain-record / tuple / guard / or / nested ────────
12607 //
12608 // These cover the value-position (`match` consumed as a value / function
12609 // tail) lowering for the pattern kinds that the legacy `(lambda __v: …)`
12610 // conditional chain could not bind: a bare-bind record field
12611 // (`Point { x, .. } => "x=${x}"` — Q-plainrecord-valpos-match, py half), a
12612 // tuple destructure, a guard arm (`n if (n < 0) => …`), an or-pattern, and a
12613 // nested constructor (`Some(Ok(n)) => …`). The chain emitted the body lambda
12614 // with the binding free (`(lambda __v: f"x={x}")(p)` → `NameError: name 'x'`),
12615 // dropped the guard entirely, and tested or/record/tuple arms as `if True`
12616 // (collapsing every later arm). The fix routes a value-position match needing
12617 // the if-chain to the statement-form `match`/`case` machinery, which binds
12618 // and tests every pattern kind correctly.
12619
12620 /// Build a single-arg `fn <name>(p) -> ...` whose body is a value-position
12621 /// (tail) `match p { <arms> }`. The arms are expression-bodied.
12622 fn match_fn(name: &str, arms: Vec<AIRNode>) -> AIRNode {
12623 let match_node = node(
12624 900,
12625 NodeKind::Match {
12626 scrutinee: Box::new(id_node(901, "p")),
12627 arms,
12628 },
12629 );
12630 let f = node(
12631 910,
12632 NodeKind::FnDecl {
12633 annotations: vec![],
12634 visibility: Visibility::Public,
12635 is_async: false,
12636 name: ident(name),
12637 generic_params: vec![],
12638 params: vec![param_node(911, "p")],
12639 return_type: None,
12640 effect_clause: vec![],
12641 where_clause: vec![],
12642 body: Box::new(block(912, vec![], Some(match_node))),
12643 },
12644 );
12645 module(vec![], vec![f])
12646 }
12647
12648 fn record_pat_field(_id: u32, name: &str, pat: Option<AIRNode>) -> AirRecordPatternField {
12649 AirRecordPatternField {
12650 name: ident(name),
12651 pattern: pat.map(Box::new),
12652 }
12653 }
12654
12655 /// `Point { x, .. } => "x=${x}"` — a bare-bind record field in value
12656 /// position. Must bind `x` from the scrutinee, never emit a free `x`.
12657 #[test]
12658 fn py_plainrecord_match_binds_field_in_value_position() {
12659 let arm = node(
12660 100,
12661 NodeKind::MatchArm {
12662 pattern: Box::new(node(
12663 101,
12664 NodeKind::RecordPat {
12665 path: type_path(&["Point"]),
12666 fields: vec![record_pat_field(102, "x", None)],
12667 rest: true,
12668 },
12669 )),
12670 guard: None,
12671 body: Box::new(block(103, vec![], Some(id_node(104, "x")))),
12672 },
12673 );
12674 let out = gen(&match_fn("get_x", vec![arm]));
12675 // The field bind must be introduced (statement-form `case Point(x=x):`),
12676 // not left free inside a `(lambda __v: … x …)` chain.
12677 assert!(
12678 out.contains("match p:") && out.contains("case Point(x=x):"),
12679 "plain-record value match must bind the field via case Point(x=x), got:\n{out}"
12680 );
12681 assert!(
12682 !out.contains("(lambda __v: x)"),
12683 "must not emit the field name free inside a value lambda, got:\n{out}"
12684 );
12685 // Inject a real `Point` dataclass (after the leading `from __future__`
12686 // line, which must stay first) so `case Point(x=x):` has a class to bind.
12687 let stubbed = out.replacen(
12688 "from __future__ import annotations\n",
12689 "from __future__ import annotations\nfrom dataclasses import dataclass as _dc\n@_dc\nclass Point:\n x: int = 0\n",
12690 1,
12691 );
12692 assert!(
12693 !has_python3() || check_py_syntax(&stubbed),
12694 "generated python must parse, got:\n{stubbed}"
12695 );
12696 }
12697
12698 /// The predicate that drives the fix: a user-enum constructor pattern that
12699 /// binds a payload (`Circle(r)`) needs statement form, but the runtime
12700 /// `Some`/`None`/`Ok`/`Err` shapes (handled by the value chain) do not.
12701 #[test]
12702 fn arm_constructor_binds_payload_distinguishes_user_from_runtime() {
12703 let user_bind = node(
12704 1,
12705 NodeKind::ConstructorPat {
12706 path: type_path(&["Circle"]),
12707 fields: vec![bind_pat(2, "r")],
12708 },
12709 );
12710 assert!(
12711 arm_constructor_binds_payload(&user_bind),
12712 "user-enum constructor binding a payload must route to statement form"
12713 );
12714 let some_bind = node(
12715 3,
12716 NodeKind::ConstructorPat {
12717 path: type_path(&["Some"]),
12718 fields: vec![bind_pat(4, "x")],
12719 },
12720 );
12721 assert!(
12722 !arm_constructor_binds_payload(&some_bind),
12723 "Some(x) is bound by the value chain, stays off statement form"
12724 );
12725 let user_unit = node(
12726 5,
12727 NodeKind::ConstructorPat {
12728 path: type_path(&["Red"]),
12729 fields: vec![],
12730 },
12731 );
12732 assert!(
12733 !arm_constructor_binds_payload(&user_unit),
12734 "a payload-less user variant binds nothing, stays off statement form"
12735 );
12736 }
12737
12738 /// Q-py-letexpr-match-namerror: a user-enum constructor-payload bind in a
12739 /// LET-EXPRESSION-position match (`let a = match s { Circle(r) => r … }`)
12740 /// must route to the statement-form `match`/`case` so `r` is bound — never
12741 /// the `(lambda __v: r …)(s)` chain that leaves `r` free (`NameError`).
12742 #[test]
12743 fn py_letexpr_constructor_payload_routes_to_statement_form() {
12744 let circle = node(
12745 100,
12746 NodeKind::MatchArm {
12747 pattern: Box::new(node(
12748 101,
12749 NodeKind::ConstructorPat {
12750 path: type_path(&["Circle"]),
12751 fields: vec![bind_pat(102, "r")],
12752 },
12753 )),
12754 guard: None,
12755 body: Box::new(block(103, vec![], Some(id_node(104, "r")))),
12756 },
12757 );
12758 let other = node(
12759 110,
12760 NodeKind::MatchArm {
12761 pattern: Box::new(node(111, NodeKind::WildcardPat)),
12762 guard: None,
12763 body: Box::new(block(112, vec![], Some(int_lit(113, "0")))),
12764 },
12765 );
12766 let match_node = node(
12767 120,
12768 NodeKind::Match {
12769 scrutinee: Box::new(id_node(121, "s")),
12770 arms: vec![circle, other],
12771 },
12772 );
12773 // `let a = match (s) { … }`; tail reads `a`.
12774 let let_a = node(
12775 130,
12776 NodeKind::LetBinding {
12777 is_mut: false,
12778 pattern: Box::new(bind_pat(131, "a")),
12779 ty: None,
12780 value: Box::new(match_node),
12781 },
12782 );
12783 let f = node(
12784 140,
12785 NodeKind::FnDecl {
12786 annotations: vec![],
12787 visibility: Visibility::Public,
12788 is_async: false,
12789 name: ident("size"),
12790 generic_params: vec![],
12791 params: vec![param_node(141, "s")],
12792 return_type: None,
12793 effect_clause: vec![],
12794 where_clause: vec![],
12795 body: Box::new(block(142, vec![let_a], Some(id_node(143, "a")))),
12796 },
12797 );
12798 let out = gen(&module(vec![], vec![f]));
12799 assert!(
12800 out.contains("match s:") && out.contains("case Circle(_0=r):"),
12801 "let-expr constructor-payload match must bind r via statement form, got:\n{out}"
12802 );
12803 assert!(
12804 !out.contains("lambda __v"),
12805 "must not lower the payload-binding match to a value lambda, got:\n{out}"
12806 );
12807 }
12808
12809 /// `n if (n < 0) => "neg" _ => "nonneg"` — a guard arm in value position.
12810 /// The guard test must survive (the legacy chain dropped it, so every input
12811 /// took the first arm).
12812 #[test]
12813 fn py_matcharm_guard_value_position_keeps_guard() {
12814 let guarded = node(
12815 200,
12816 NodeKind::MatchArm {
12817 pattern: Box::new(bind_pat(201, "n")),
12818 guard: Some(Box::new(node(
12819 202,
12820 NodeKind::BinaryOp {
12821 op: BinOp::Lt,
12822 left: Box::new(id_node(203, "n")),
12823 right: Box::new(int_lit(204, "0")),
12824 },
12825 ))),
12826 body: Box::new(block(205, vec![], Some(str_lit(206, "neg")))),
12827 },
12828 );
12829 let default = node(
12830 210,
12831 NodeKind::MatchArm {
12832 pattern: Box::new(node(211, NodeKind::WildcardPat)),
12833 guard: None,
12834 body: Box::new(block(212, vec![], Some(str_lit(213, "nonneg")))),
12835 },
12836 );
12837 let out = gen(&match_fn("classify", vec![guarded, default]));
12838 assert!(
12839 out.contains("match p:") && out.contains("case n if (n < 0):"),
12840 "guard arm in value position must keep its guard test, got:\n{out}"
12841 );
12842 assert!(
12843 !has_python3() || check_py_syntax(&out),
12844 "generated python must parse, got:\n{out}"
12845 );
12846 }
12847
12848 /// `(0, _) => "zero" (n, s) => "${n}: ${s}"` — tuple patterns in value
12849 /// position must bind `n`/`s` and test the literal element.
12850 #[test]
12851 fn py_tuple_match_value_position_binds_and_tests() {
12852 let zero_arm = node(
12853 300,
12854 NodeKind::MatchArm {
12855 pattern: Box::new(node(
12856 301,
12857 NodeKind::TuplePat {
12858 elems: vec![
12859 node(
12860 302,
12861 NodeKind::LiteralPat {
12862 lit: Literal::Int("0".into()),
12863 },
12864 ),
12865 node(303, NodeKind::WildcardPat),
12866 ],
12867 },
12868 )),
12869 guard: None,
12870 body: Box::new(block(304, vec![], Some(str_lit(305, "zero")))),
12871 },
12872 );
12873 let bind_arm = node(
12874 310,
12875 NodeKind::MatchArm {
12876 pattern: Box::new(node(
12877 311,
12878 NodeKind::TuplePat {
12879 elems: vec![bind_pat(312, "n"), bind_pat(313, "s")],
12880 },
12881 )),
12882 guard: None,
12883 body: Box::new(block(314, vec![], Some(id_node(315, "n")))),
12884 },
12885 );
12886 let out = gen(&match_fn("describe", vec![zero_arm, bind_arm]));
12887 assert!(
12888 out.contains("match p:")
12889 && out.contains("case (0, _):")
12890 && out.contains("case (n, s):"),
12891 "tuple value match must test the literal and bind elements, got:\n{out}"
12892 );
12893 assert!(
12894 !has_python3() || check_py_syntax(&out),
12895 "generated python must parse, got:\n{out}"
12896 );
12897 }
12898
12899 /// `Some(Ok(n)) => "${n}" …` — a nested constructor in value position must
12900 /// test the inner `Ok` and bind `n`, not collapse to `isinstance(__v, …)`
12901 /// with `n` free.
12902 #[test]
12903 fn py_nested_constructor_match_value_position_binds_inner() {
12904 let some_ok = node(
12905 400,
12906 NodeKind::MatchArm {
12907 pattern: Box::new(node(
12908 401,
12909 NodeKind::ConstructorPat {
12910 path: type_path(&["Some"]),
12911 fields: vec![node(
12912 402,
12913 NodeKind::ConstructorPat {
12914 path: type_path(&["Ok"]),
12915 fields: vec![bind_pat(403, "n")],
12916 },
12917 )],
12918 },
12919 )),
12920 guard: None,
12921 body: Box::new(block(404, vec![], Some(id_node(405, "n")))),
12922 },
12923 );
12924 let none_arm = node(
12925 410,
12926 NodeKind::MatchArm {
12927 pattern: Box::new(node(
12928 411,
12929 NodeKind::ConstructorPat {
12930 path: type_path(&["None"]),
12931 fields: vec![],
12932 },
12933 )),
12934 guard: None,
12935 body: Box::new(block(412, vec![], Some(str_lit(413, "none")))),
12936 },
12937 );
12938 let out = gen(&match_fn("nested", vec![some_ok, none_arm]));
12939 assert!(
12940 out.contains("match p:") && out.contains("case _BockSome(_BockOk(n)):"),
12941 "nested constructor value match must test+bind the inner Ok, got:\n{out}"
12942 );
12943 assert!(
12944 !has_python3() || check_py_syntax(&out),
12945 "generated python must parse, got:\n{out}"
12946 );
12947 }
12948
12949 /// Q-py-valpos-stmt-arms: a value-position `match` arm whose body is a
12950 /// *block with leading statements* must run those statements and keep their
12951 /// bindings in scope for the tail. The calculator's chained-computation arm
12952 /// `Ok(sum) => { let step2 = …; <inner match> }` previously emitted
12953 /// `(lambda: <tail>)()`, dropping the `let step2` and leaving `step2`
12954 /// unbound at runtime. The fix folds the `let` into an immediately-applied
12955 /// lambda: `(lambda step2: <tail>)(<value>)`.
12956 #[test]
12957 fn py_valpos_match_arm_block_keeps_leading_let() {
12958 // Ok(n) => { let doubled = (n + n); doubled }
12959 let let_stmt = node(
12960 500,
12961 NodeKind::LetBinding {
12962 is_mut: false,
12963 pattern: Box::new(bind_pat(501, "doubled")),
12964 ty: None,
12965 value: Box::new(node(
12966 502,
12967 NodeKind::BinaryOp {
12968 op: BinOp::Add,
12969 left: Box::new(id_node(503, "n")),
12970 right: Box::new(id_node(504, "n")),
12971 },
12972 )),
12973 },
12974 );
12975 let ok_arm = node(
12976 510,
12977 NodeKind::MatchArm {
12978 pattern: Box::new(node(
12979 511,
12980 NodeKind::ConstructorPat {
12981 path: type_path(&["Ok"]),
12982 fields: vec![bind_pat(512, "n")],
12983 },
12984 )),
12985 guard: None,
12986 body: Box::new(block(513, vec![let_stmt], Some(id_node(514, "doubled")))),
12987 },
12988 );
12989 let err_arm = node(
12990 520,
12991 NodeKind::MatchArm {
12992 pattern: Box::new(node(
12993 521,
12994 NodeKind::ConstructorPat {
12995 path: type_path(&["Err"]),
12996 fields: vec![bind_pat(522, "e")],
12997 },
12998 )),
12999 guard: None,
13000 body: Box::new(block(523, vec![], Some(id_node(524, "e")))),
13001 },
13002 );
13003 let out = gen(&match_fn("keep_let", vec![ok_arm, err_arm]));
13004 assert!(
13005 out.contains("lambda doubled:"),
13006 "value-position match-arm block must keep its `let doubled` binding, got:\n{out}"
13007 );
13008 assert!(
13009 !out.contains("(lambda: "),
13010 "must not emit the statement-dropping `(lambda: <tail>)()` form, got:\n{out}"
13011 );
13012 assert!(
13013 !has_python3() || check_py_syntax(&out),
13014 "generated python must parse, got:\n{out}"
13015 );
13016 }
13017}