2727use std:: collections:: HashSet ;
2828
2929use crate :: parser:: ast:: { BlockStmt , CeItem , Expr , ExprKind , InterpPart , Param , Pattern } ;
30- use crate :: python_emitter:: { PyCase , PyExpr , PyStmt } ;
30+ use crate :: python_emitter:: { PyCase , PyExpr , PyForTarget , PyStmt } ;
3131
32- use super :: { LowerError , Lowered , Lowerer , py_value_name} ;
32+ use super :: { LowerError , Lowered , Lowerer , py_value_name, unpack_into_as } ;
3333
3434/// One recognized in-place update at a fold tail leaf, with the (still-unlowered)
3535/// argument expressions the mutation needs.
@@ -80,8 +80,9 @@ enum InitKind {
8080pub ( super ) struct FoldPlan < ' a > {
8181 /// A single-slot accumulator (vs a flat tuple of slots).
8282 single : bool ,
83- /// The folder's element parameter — becomes the loop variable.
84- elem_param : String ,
83+ /// The folder's element parameter (an irrefutable pattern) — becomes the
84+ /// loop target: a name, or a tuple Python unpacks itself (`for (p, l) in …`).
85+ elem : & ' a Pattern ,
8586 /// The accumulator slot local names (length 1 for a single accumulator).
8687 slots : Vec < String > ,
8788 /// The folder's accumulator parameter (the destructure scrutinee / single slot).
@@ -167,19 +168,23 @@ impl Lowerer {
167168 #[ allow( clippy:: too_many_arguments) ]
168169 fn plan_fold < ' a > (
169170 & self ,
170- params : & [ Param ] ,
171+ params : & ' a [ Param ] ,
171172 body : & ' a Expr ,
172173 init : & ' a Expr ,
173174 xs : & ' a Expr ,
174175 same_frame : bool ,
175176 locals : & HashSet < String > ,
176177 enclosing : & HashSet < String > ,
177178 ) -> Option < FoldPlan < ' a > > {
178- // A destructuring folder parameter (`fun (a, b) c -> …`) has no single
179- // name to substitute, and the pass rewrites by name — reject and fall
180- // through to the byte-identical `_pf_fold` lowering.
179+ // A destructuring ACCUMULATOR parameter (`fun (a, b) c -> …`) has no
180+ // single name to substitute, and the pass rewrites the accumulator by
181+ // name — reject and fall through to the byte-identical `_pf_fold`
182+ // lowering. The ELEMENT parameter is only ever the loop target, so any
183+ // irrefutable pattern is fine there: its bound names are body binders
184+ // for the P8 check, exactly where the non-loop lowering puts them.
181185 let acc_param = params[ 0 ] . name ( ) ?. to_string ( ) ;
182- let elem_param = params[ 1 ] . name ( ) ?. to_string ( ) ;
186+ let elem = & params[ 1 ] . pattern ;
187+ let elem_names: Vec < String > = elem. bound_names ( ) ;
183188
184189 // P3 + P4: classify the accumulator shape. Each slot's init must be a
185190 // fresh literal (`Map.empty`/`Set.empty`/a list literal) or a bare `Var`
@@ -218,7 +223,7 @@ impl Lowerer {
218223 // enclosing local (inlining would clobber it).
219224 let mut introduced = binders;
220225 introduced. extend ( slots. iter ( ) . cloned ( ) ) ;
221- introduced. insert ( elem_param . clone ( ) ) ;
226+ introduced. extend ( elem_names . iter ( ) . cloned ( ) ) ;
222227 if !introduced. is_disjoint ( enclosing) {
223228 return None ;
224229 }
@@ -229,7 +234,7 @@ impl Lowerer {
229234 // the call site's own frame by construction, so the check is skipped.
230235 if !same_frame {
231236 let mut bound: HashSet < String > = sensitive. clone ( ) ;
232- bound. insert ( elem_param . clone ( ) ) ;
237+ bound. extend ( elem_names . iter ( ) . cloned ( ) ) ;
233238 let mut free = HashSet :: new ( ) ;
234239 collect_free ( body, & bound, & mut free) ;
235240 if !free. is_disjoint ( enclosing) {
@@ -271,14 +276,14 @@ impl Lowerer {
271276 // `def`, so `lower_var` rerouting is identical); a lambda or local folder
272277 // additionally sees the call site's locals (where its free vars resolve).
273278 let mut base_locals: HashSet < String > = sensitive. clone ( ) ;
274- base_locals. insert ( elem_param . clone ( ) ) ;
279+ base_locals. extend ( elem_names . iter ( ) . cloned ( ) ) ;
275280 if same_frame {
276281 base_locals. extend ( locals. iter ( ) . cloned ( ) ) ;
277282 }
278283
279284 Some ( FoldPlan {
280285 single,
281- elem_param ,
286+ elem ,
282287 slots,
283288 acc_param,
284289 inits,
@@ -371,19 +376,27 @@ impl Lowerer {
371376 // The collection, evaluated once after the inits (P10), under site locals.
372377 let ( xs_stmts, iter) = self . lower_value ( plan. xs , & plan. site_locals ) ?;
373378 stmts. extend ( xs_stmts) ;
379+ // The loop target: a name or a tuple of names Python unpacks itself;
380+ // any other irrefutable shape binds a temp and unpacks on the first
381+ // line of the body, as a destructuring parameter does in a `def`.
382+ let ( target, mut body) = match loop_target ( plan. elem ) {
383+ Some ( target) => ( target, Vec :: new ( ) ) ,
384+ None => {
385+ let tmp = self . fresh_tmp ( ) ;
386+ let mut unpack = Vec :: new ( ) ;
387+ unpack_into_as ( plan. elem , & tmp, & tmp, & |n| py_value_name ( n) , & mut unpack) ;
388+ ( PyForTarget :: Name ( tmp) , unpack)
389+ }
390+ } ;
374391 // The inlined folder body as the loop body.
375- let body = self . lower_fold_step (
392+ body. extend ( self . lower_fold_step (
376393 plan. step_body ,
377394 & plan. base_locals ,
378395 & plan. slots ,
379396 & plan. acc_param ,
380397 plan. single ,
381- ) ?;
382- stmts. push ( PyStmt :: For {
383- target : py_value_name ( & plan. elem_param ) ,
384- iter,
385- body,
386- } ) ;
398+ ) ?) ;
399+ stmts. push ( PyStmt :: For { target, iter, body } ) ;
387400 // The result: the mutated accumulator(s) — a fresh container graph (P11).
388401 let value = if plan. single {
389402 PyExpr :: Name ( py_value_name ( & plan. slots [ 0 ] ) )
@@ -1298,3 +1311,69 @@ pub(super) fn collect_free(e: &Expr, bound: &HashSet<String>, out: &mut HashSet<
12981311 | ExprKind :: OpFunc ( _) => { }
12991312 }
13001313}
1314+
1315+ /// The `for`-loop target for an element pattern, when Python can unpack it
1316+ /// itself: a name, a wildcard (`_`), or a tuple of those, nested. Anything
1317+ /// else needs a temp and an explicit unpack.
1318+ fn loop_target ( pattern : & Pattern ) -> Option < PyForTarget > {
1319+ match pattern {
1320+ Pattern :: Var { name, .. } => Some ( PyForTarget :: Name ( py_value_name ( name) ) ) ,
1321+ Pattern :: Wildcard => Some ( PyForTarget :: Name ( "_" . to_string ( ) ) ) ,
1322+ Pattern :: Tuple { elems } => elems
1323+ . iter ( )
1324+ . map ( loop_target)
1325+ . collect :: < Option < Vec < _ > > > ( )
1326+ . map ( PyForTarget :: Tuple ) ,
1327+ _ => None ,
1328+ }
1329+ }
1330+
1331+ #[ cfg( test) ]
1332+ mod tests {
1333+ use super :: * ;
1334+ use crate :: parser:: ast:: NodeSpan ;
1335+
1336+ fn var ( name : & str ) -> Pattern {
1337+ Pattern :: Var {
1338+ name : name. to_string ( ) ,
1339+ span : NodeSpan :: new ( crate :: lexer:: Span :: new ( 0 , 0 ) ) ,
1340+ }
1341+ }
1342+
1343+ #[ test]
1344+ fn loop_target_covers_names_wildcards_and_nested_tuples ( ) {
1345+ assert_eq ! ( loop_target( & var( "x" ) ) , Some ( PyForTarget :: Name ( "x" . into( ) ) ) ) ;
1346+ assert_eq ! (
1347+ loop_target( & Pattern :: Wildcard ) ,
1348+ Some ( PyForTarget :: Name ( "_" . into( ) ) )
1349+ ) ;
1350+ let nested = Pattern :: Tuple {
1351+ elems : vec ! [
1352+ var( "a" ) ,
1353+ Pattern :: Tuple {
1354+ elems: vec![ var( "b" ) , Pattern :: Wildcard ] ,
1355+ } ,
1356+ ] ,
1357+ } ;
1358+ assert_eq ! (
1359+ loop_target( & nested) ,
1360+ Some ( PyForTarget :: Tuple ( vec![
1361+ PyForTarget :: Name ( "a" . into( ) ) ,
1362+ PyForTarget :: Tuple ( vec![
1363+ PyForTarget :: Name ( "b" . into( ) ) ,
1364+ PyForTarget :: Name ( "_" . into( ) ) ,
1365+ ] ) ,
1366+ ] ) )
1367+ ) ;
1368+ }
1369+
1370+ #[ test]
1371+ fn loop_target_refuses_a_shape_python_cannot_unpack_in_the_header ( ) {
1372+ // A literal element (not a parameter shape the parser admits today, but
1373+ // the emit path must still fall back to a temp + explicit unpack).
1374+ let shape = Pattern :: Tuple {
1375+ elems : vec ! [ var( "a" ) , Pattern :: Int ( 0 ) ] ,
1376+ } ;
1377+ assert_eq ! ( loop_target( & shape) , None ) ;
1378+ }
1379+ }
0 commit comments