From 84c487f428131f9448645598ee7a5475e55c8d39 Mon Sep 17 00:00:00 2001 From: simontreanor Date: Mon, 28 Sep 2026 03:08:35 +0100 Subject: [PATCH] lowering: native ladders reach async return! matches return! (match ...) in an async block lowers through lower_async_bang_return, which built its own match and so kept every one of them in native output. It now tries the native ladder first (arm bodies returned awaited), Option and Result shapes included since that path never tries their ladder. The self tail call loop rewrite still fires on the cookbook agent. On a real 11-module program the native output now has no match statement at all, and its recorded game replays print the same in both modes. --- DESIGN.md | 5 ++++- src/lowering/mod.rs | 49 +++++++++++++++++++++++++++++++++++---------- tests/compile.rs | 21 +++++++++++++++++++ 3 files changed, 63 insertions(+), 12 deletions(-) diff --git a/DESIGN.md b/DESIGN.md index a1e0156..1ce0779 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -424,7 +424,10 @@ its own copy of the body, a nested or-pattern that binds no names (`Some (1 | 2) joined with `or` (one that binds names keeps `match`), and a list pattern becomes a length test (`len(xs) == n`, or `>=` with a rest), positional reads with the suffix counted from the end (`xs[-1]`), and the rest as a slice (`xs[1:len(xs) - 1]`). Active patterns already lower to their -recognizer and an `isinstance` ladder in both modes. +recognizer and an `isinstance` ladder in both modes. The `return!` of a `match` in an `async { }` +block, which lowers through its own path so a self tail call stays visible (§5.4), takes the ladder +too, `Option`/`Result` shapes included; on the dogfooded Scrabble game (11 modules) native output +has no `match` statement left, and every recorded replay prints the same in both modes. The ladder is also faster on CPython by itself, since a class pattern goes through `__match_args__` and positional binding at run time: on `bench/expr_eval` (ADT allocation and matching) the output diff --git a/src/lowering/mod.rs b/src/lowering/mod.rs index 408367d..fdb7bfe 100644 --- a/src/lowering/mod.rs +++ b/src/lowering/mod.rs @@ -1973,7 +1973,9 @@ impl Lowerer { return Ok(stmts); } // An Option/Result match is an `isinstance` ladder (§5.5). - if let Some(stmts) = self.try_lower_native_match(scrutinee, arms, locals, None)? { + if let Some(stmts) = + self.try_lower_native_match(scrutinee, arms, locals, NativeArmBody::Return)? + { return Ok(stmts); } if let Some(stmts) = self.try_lower_ladder_match(scrutinee, arms, locals, None)? { @@ -2177,9 +2179,12 @@ impl Lowerer { // temps exactly as before. let counter = self.tmp_counter; let tmp = self.fresh_tmp(); - if let Some(stmts) = - self.try_lower_native_match(scrutinee, arms, locals, Some(&tmp))? - { + if let Some(stmts) = self.try_lower_native_match( + scrutinee, + arms, + locals, + NativeArmBody::Assign(&tmp), + )? { return Ok((stmts, PyExpr::Name(tmp))); } if let Some(stmts) = @@ -5029,6 +5034,14 @@ impl Lowerer { Ok(stmts) } ExprKind::Match { scrutinee, arms } if !self.match_uses_ap(arms) => { + if let Some(stmts) = self.try_lower_native_match( + scrutinee, + arms, + locals, + NativeArmBody::AwaitReturn, + )? { + return Ok(stmts); + } let (mut stmts, subject) = self.lower_value(scrutinee, locals)?; let mut cases = Vec::new(); for (i, arm) in arms.iter().enumerate() { @@ -5526,7 +5539,7 @@ impl Lowerer { scrutinee: &Expr, arms: &[crate::parser::ast::MatchArm], locals: &HashSet, - assign_to: Option<&str>, + mode: NativeArmBody<'_>, ) -> Result>, LowerError> { if !self.native || arms.is_empty() { return Ok(None); @@ -5545,11 +5558,12 @@ impl Lowerer { { return Ok(None); } - // The Option/Result ladder already has the best form for its shape. - if ladder_shape(arms).is_some() { + // The Option/Result ladder already has the best form for its shape where + // it is tried; an `async { }` block's `return!` never tries it. + if ladder_shape(arms).is_some() && !matches!(mode, NativeArmBody::AwaitReturn) { return Ok(None); } - if assign_to.is_some() && arms.iter().any(|a| a.guard.is_some()) { + if matches!(mode, NativeArmBody::Assign(_)) && arms.iter().any(|a| a.guard.is_some()) { return Ok(None); } let (mut stmts, subject) = self.lower_value(scrutinee, locals)?; @@ -5577,9 +5591,12 @@ impl Lowerer { let mut binds = Vec::new(); self.native_pattern(&alt, PyExpr::Name(subject.clone()), &mut conds, &mut binds); let guard = self.lower_guard(&arm.guard, &scope.locals)?; - let body = match assign_to { - None => self.lower_return(&arm.body, &scope.locals)?, - Some(tmp) => { + let body = match mode { + NativeArmBody::Return => self.lower_return(&arm.body, &scope.locals)?, + NativeArmBody::AwaitReturn => { + self.lower_async_bang_return(&arm.body, &scope.locals)? + } + NativeArmBody::Assign(tmp) => { let (arm_stmts, arm_val) = self.lower_value(&arm.body, &scope.locals)?; with_assign(arm_stmts, tmp, arm_val) } @@ -10717,6 +10734,16 @@ impl LadderFamily { } } +/// How [`Lowerer::try_lower_native_match`] lowers each arm's body: returned +/// (tail position), assigned to a temp (value position), or, in an `async { }` +/// block's `return!`, returned awaited (`lower_async_bang_return`). +#[derive(Clone, Copy)] +enum NativeArmBody<'a> { + Return, + Assign(&'a str), + AwaitReturn, +} + /// One arm of a ladder-shaped match. struct LadderArm<'a> { arm: &'a crate::parser::ast::MatchArm, diff --git a/tests/compile.rs b/tests/compile.rs index e996fb2..8be66e2 100644 --- a/tests/compile.rs +++ b/tests/compile.rs @@ -4016,6 +4016,27 @@ fn native_annotates_top_level_functions_it_can_spell() { assert_eq!(out, ["4.0", "4", "['A']", "Some(1)", "2"]); } +#[test] +fn native_ladders_reach_async_return_bang_matches() { + // `return! (match …)` in an `async { }` block lowers through its own path, + // which native mode now takes too, the Option/Result shape included. + let src = "extern runAsync: Async a -> a = asyncio.run\n\ + let check n = if n > 0 then Ok n else Error \"negative\"\n\ + let describe n =\n \ + async {\n \ + return! (match check n:\n \ + case Ok v: async { return f\"ok {v}\" }\n \ + case Error why: async { return why })\n \ + }\n\ + print (runAsync (describe 3))\n\ + print (runAsync (describe (0 - 1)))"; + let Some((py, out)) = run_native(src) else { + return; + }; + assert!(!py.contains("match "), "{py}"); + assert_eq!(out, ["ok 3", "negative"]); +} + #[test] fn native_leaves_active_patterns_to_their_own_lowering() { // A total active pattern already lowers to its recognizer plus an