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