Skip to content

Commit 66b02b7

Browse files
committed
lowering: accept a destructuring element parameter in the fold-loop pass
A folder that names its element by destructuring (`fun m (p, l) -> Map.add p l m`) was rejected by the in-place fold pass and fell back to `_pf_fold`, which copies the accumulator every step and goes quadratic. Only the accumulator parameter is substituted by name; the element parameter is the loop target, so any irrefutable pattern works there. `PyStmt::For` now takes a `PyForTarget` (a name or a nested tuple of names), so a tuple element emits Python's own `for (p, l) in steps:` header. Any other irrefutable shape binds a temp and unpacks on the first body line via the existing `unpack_into_as`. The pattern's bound names feed the P8 collision check as body binders. The accumulator parameter keeps its rules.
1 parent 697a7b5 commit 66b02b7

7 files changed

Lines changed: 302 additions & 46 deletions

File tree

INTERNALS.md

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -77,15 +77,21 @@ accumulator). The pass (`src/lowering/fold_loop.rs`, hooked in `lower_applicatio
7777
`PYFUN_NO_FOLD_OPT` is the kill switch) recognizes the common case and rewrites
7878
`functools.reduce(f, xs, acc)` into a `for`-loop over a **mutable** accumulator, turning the
7979
copy-returning ops into in-place mutations (`Map.add``m[k]=v`, `List.concat``.append`/`.extend`,
80-
`Set.add``.add`), collapsing the build to linear. This adds two Python-IR nodes: `PyStmt::For` and
81-
`PyStmt::SubscriptAssign`.
80+
`Set.add``.add`), collapsing the build to linear. This adds two Python-IR nodes: `PyStmt::For`
81+
(whose target is a `PyForTarget`: a name or a nested tuple of names) and `PyStmt::SubscriptAssign`.
8282

8383
**Soundness is what the pass must protect.** The rewrite is observable only through a *retained reference* to a
8484
mutated container, so the pass is a set of conservative **syntactic** proof obligations on the AST,
8585
checked with no side effects on the lowerer (a rejected fold falls through to the byte-identical
8686
`_pf_fold` lowering). A fold qualifies only when: it is a fully-applied `Seq.fold`/`List.fold`
8787
(exactly 3 args); the folder is a 2-ary lambda literal or a **top-level** named `let` (an inlinable
88-
body, not a `mut`/extern/imported member/parameter); the accumulator is a fresh literal collection or
88+
body, not a `mut`/extern/imported member/parameter); the accumulator parameter is a plain name (the
89+
pass substitutes it by name), while the element parameter may be any irrefutable pattern, since it is
90+
only ever the loop target: a name, a wildcard, or a tuple of those becomes Python's own
91+
`for (p, l) in steps:` header (`PyForTarget`), and any other shape binds a temp and unpacks on the
92+
first line of the body, so `fun m (p, l) -> Map.add p l m` stays linear like its `fst`/`snd`
93+
spelling and the pattern's bound names count as body binders for the P8 collision check; the
94+
accumulator is a fresh literal collection or
8995
a flat tuple of them (a `Var` init is rejected — it may be read after the fold); every slot is
9096
threaded **position-preservingly** (no swap, no duplication, no cross-slot storage, no closure
9197
capture, no escape to a user function — retention is unknowable, so reject); reads of a slot use a

src/lowering/decode_spec.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -392,7 +392,7 @@ impl Lowerer {
392392
args: vec![decoded],
393393
}));
394394
out.push(PyStmt::For {
395-
target: item,
395+
target: item.into(),
396396
iter: in_name(),
397397
body,
398398
});

src/lowering/fold_loop.rs

Lines changed: 99 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,9 @@
2727
use std::collections::HashSet;
2828

2929
use 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 {
8080
pub(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+
}

src/lowering/mod.rs

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -4795,7 +4795,7 @@ fn list_prelude(used: &BTreeSet<&'static str>) -> Vec<PyStmt> {
47954795
value,
47964796
};
47974797
let for_ = |target: &str, iter: PyExpr, body: Vec<PyStmt>| PyStmt::For {
4798-
target: target.to_string(),
4798+
target: target.into(),
47994799
iter,
48004800
body,
48014801
};
@@ -4894,7 +4894,7 @@ fn list_prelude(used: &BTreeSet<&'static str>) -> Vec<PyStmt> {
48944894
value: PyExpr::List(vec![]),
48954895
},
48964896
PyStmt::For {
4897-
target: "x".to_string(),
4897+
target: "x".into(),
48984898
iter: name("xs"),
48994899
body: vec![PyStmt::Expr(PyExpr::Call {
49004900
func: Box::new(PyExpr::Attribute {
@@ -5742,7 +5742,7 @@ fn collection_prelude(used: &BTreeSet<&'static str>) -> Vec<PyStmt> {
57425742
value: PyExpr::List(vec![]),
57435743
},
57445744
PyStmt::For {
5745-
target: "x".to_string(),
5745+
target: "x".into(),
57465746
iter: name("xs"),
57475747
body: vec![
57485748
PyStmt::Assign {
@@ -6024,7 +6024,7 @@ fn collection_prelude(used: &BTreeSet<&'static str>) -> Vec<PyStmt> {
60246024
&["f", "xs"],
60256025
vec![
60266026
PyStmt::For {
6027-
target: "p".to_string(),
6027+
target: "p".into(),
60286028
iter: call("enumerate", vec![name("xs")]),
60296029
body: vec![PyStmt::If {
60306030
test: PyExpr::Call {
@@ -6241,7 +6241,7 @@ fn collection_prelude(used: &BTreeSet<&'static str>) -> Vec<PyStmt> {
62416241
value: call("set", vec![]),
62426242
},
62436243
PyStmt::For {
6244-
target: "x".to_string(),
6244+
target: "x".into(),
62456245
iter: name("xs"),
62466246
body: vec![PyStmt::If {
62476247
test: PyExpr::Not(Box::new(binop(
@@ -6373,7 +6373,7 @@ fn collection_prelude(used: &BTreeSet<&'static str>) -> Vec<PyStmt> {
63736373
&["f", "xs"],
63746374
vec![
63756375
PyStmt::For {
6376-
target: "x".to_string(),
6376+
target: "x".into(),
63776377
iter: name("xs"),
63786378
body: vec![PyStmt::Expr(PyExpr::Call {
63796379
func: Box::new(name("f")),
@@ -6426,7 +6426,7 @@ fn collection_prelude(used: &BTreeSet<&'static str>) -> Vec<PyStmt> {
64266426
value: call("set", vec![]),
64276427
},
64286428
PyStmt::For {
6429-
target: "x".to_string(),
6429+
target: "x".into(),
64306430
iter: name("s"),
64316431
body: vec![PyStmt::If {
64326432
test: PyExpr::Call {
@@ -6489,7 +6489,7 @@ fn collection_prelude(used: &BTreeSet<&'static str>) -> Vec<PyStmt> {
64896489
value: call("dict", vec![]),
64906490
},
64916491
PyStmt::For {
6492-
target: "kv".to_string(),
6492+
target: "kv".into(),
64936493
iter: method(name("m"), "items", vec![]),
64946494
body: vec![PyStmt::SubscriptAssign {
64956495
obj: name("out"),
@@ -6525,7 +6525,7 @@ fn collection_prelude(used: &BTreeSet<&'static str>) -> Vec<PyStmt> {
65256525
value: call("dict", vec![]),
65266526
},
65276527
PyStmt::For {
6528-
target: "kv".to_string(),
6528+
target: "kv".into(),
65296529
iter: method(name("m"), "items", vec![]),
65306530
body: vec![PyStmt::If {
65316531
test: PyExpr::Call {
@@ -6564,7 +6564,7 @@ fn collection_prelude(used: &BTreeSet<&'static str>) -> Vec<PyStmt> {
65646564
&["f", "acc", "m"],
65656565
vec![
65666566
PyStmt::For {
6567-
target: "kv".to_string(),
6567+
target: "kv".into(),
65686568
iter: method(name("m"), "items", vec![]),
65696569
body: vec![PyStmt::Assign {
65706570
target: "acc".to_string(),
@@ -6593,7 +6593,7 @@ fn collection_prelude(used: &BTreeSet<&'static str>) -> Vec<PyStmt> {
65936593
&["f", "m"],
65946594
vec![
65956595
PyStmt::For {
6596-
target: "kv".to_string(),
6596+
target: "kv".into(),
65976597
iter: method(name("m"), "items", vec![]),
65986598
body: vec![PyStmt::If {
65996599
test: PyExpr::Call {
@@ -6621,7 +6621,7 @@ fn collection_prelude(used: &BTreeSet<&'static str>) -> Vec<PyStmt> {
66216621
&["f", "m"],
66226622
vec![
66236623
PyStmt::For {
6624-
target: "kv".to_string(),
6624+
target: "kv".into(),
66256625
iter: method(name("m"), "items", vec![]),
66266626
body: vec![PyStmt::If {
66276627
test: PyExpr::Not(Box::new(PyExpr::Call {
@@ -6658,7 +6658,7 @@ fn collection_prelude(used: &BTreeSet<&'static str>) -> Vec<PyStmt> {
66586658
value: call("dict", vec![]),
66596659
},
66606660
PyStmt::For {
6661-
target: "kv".to_string(),
6661+
target: "kv".into(),
66626662
iter: method(name("m"), "items", vec![]),
66636663
body: vec![PyStmt::If {
66646664
test: PyExpr::Call {
@@ -6937,7 +6937,7 @@ fn collection_prelude(used: &BTreeSet<&'static str>) -> Vec<PyStmt> {
69376937
value: call("set", vec![]),
69386938
},
69396939
PyStmt::For {
6940-
target: "x".to_string(),
6940+
target: "x".into(),
69416941
iter: name("xs"),
69426942
body: vec![
69436943
PyStmt::Assign {
@@ -7013,7 +7013,7 @@ fn collection_prelude(used: &BTreeSet<&'static str>) -> Vec<PyStmt> {
70137013
value: call("None_", vec![]),
70147014
},
70157015
PyStmt::For {
7016-
target: "x".to_string(),
7016+
target: "x".into(),
70177017
iter: name("xs"),
70187018
body: vec![PyStmt::Assign {
70197019
target: "out".to_string(),
@@ -7082,7 +7082,7 @@ fn collection_prelude(used: &BTreeSet<&'static str>) -> Vec<PyStmt> {
70827082
&["f", "s"],
70837083
vec![
70847084
PyStmt::For {
7085-
target: "x".to_string(),
7085+
target: "x".into(),
70867086
iter: name("s"),
70877087
body: vec![PyStmt::Expr(PyExpr::Call {
70887088
func: Box::new(name("f")),
@@ -7097,7 +7097,7 @@ fn collection_prelude(used: &BTreeSet<&'static str>) -> Vec<PyStmt> {
70977097
&["f", "m"],
70987098
vec![
70997099
PyStmt::For {
7100-
target: "kv".to_string(),
7100+
target: "kv".into(),
71017101
iter: method(name("m"), "items", vec![]),
71027102
body: vec![PyStmt::Expr(PyExpr::Call {
71037103
func: Box::new(name("f")),

src/lowering/self_tail_call.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,7 @@ fn binds(stmts: &[PyStmt], name: &str) -> bool {
191191
stmts.iter().any(|s| match s {
192192
PyStmt::Assign { target, .. } => target == name,
193193
PyStmt::UnpackAssign { targets, .. } => targets.iter().any(|t| t == name),
194-
PyStmt::For { target, body, .. } => target == name || binds(body, name),
194+
PyStmt::For { target, body, .. } => target.binds(name) || binds(body, name),
195195
PyStmt::FuncDef { name: n, .. } => n == name,
196196
PyStmt::ClassDef { name: n, .. } => n == name,
197197
PyStmt::If { body, orelse, .. } => binds(body, name) || binds(orelse, name),
@@ -252,7 +252,7 @@ fn collect_bound(stmts: &[PyStmt], out: &mut HashSet<String>) {
252252
}
253253
PyStmt::UnpackAssign { targets, .. } => out.extend(targets.iter().cloned()),
254254
PyStmt::For { target, body, .. } => {
255-
out.insert(target.clone());
255+
out.extend(target.names());
256256
collect_bound(body, out);
257257
}
258258
PyStmt::If { body, orelse, .. } => {

0 commit comments

Comments
 (0)