Skip to content

Commit 7cecb94

Browse files
committed
perf(python): xs[a:b] = xs[c:d:-1] reverses in place, and slice assignment copies its source only when it aliases the target
The frontend lowers the reversed self-slice to one call that normalises both ranges and swaps in place when they name the same elements, taking the slice first otherwise. assign_slice compares the two storages and copies only the list assigned from itself. git-bug: 8db796da28a7bb96bca3865e11f325e52d9b961fc263a44c1852a6d3defdc55f
1 parent ecf911d commit 7cecb94

4 files changed

Lines changed: 267 additions & 3 deletions

File tree

crates/zyntax_builtins/src/lists.rs

Lines changed: 115 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -552,17 +552,129 @@ fn kind_declarations(k: &KindOps) -> Vec<Decl> {
552552
ret(out.e()),
553553
],
554554
));
555-
// Replace selected elements in the same header, including when the source aliases it.
556-
let replacement = local("replacement", k.list.clone());
555+
// Replace selected elements in the same header. The source is read
556+
// as it is, unless it is the target's own storage, which is copied
557+
// first so the elements moved are the ones the slice named.
558+
let replacement = borrowed("replacement", k.list.clone());
557559
let selected = local("selected", i64());
558560
let width = local("width", i64());
559561
let common = local("common", i64());
562+
let data = |xs: &Local| fld(xs.e(), "data", i64());
563+
let assign_from = |source: Expr, step: Expr| {
564+
expr(call(
565+
&name("assign_slice_from"),
566+
vec![source, xs.e(), start.e(), stop.e(), step, mask.e()],
567+
unit(),
568+
))
569+
};
560570
d.push(define(
561571
&name("assign_slice"),
562572
&[&ys, &xs, &start, &stop, &step, &mask],
563573
unit(),
564574
vec![
565-
replacement.decl(call(&name("copy"), vec![ys.e()], k.list.clone())),
575+
when(
576+
eq(data(&ys), data(&xs)),
577+
vec![
578+
replacement.decl(call(&name("copy"), vec![ys.e()], k.list.clone())),
579+
assign_from(replacement.e(), step.e()),
580+
ret_void(),
581+
],
582+
),
583+
assign_from(ys.e(), step.e()),
584+
ret_void(),
585+
],
586+
));
587+
// xs[start:stop] = xs[rstart:rstop:-1]. The two ranges naming the
588+
// same elements is a reversal in place; anything else is the slice
589+
// taken first and assigned as any other source.
590+
let rstart = local("rstart", i64());
591+
let rstop = local("rstop", i64());
592+
let rmask = local("rmask", i64());
593+
let rlo = local("rlo", i64());
594+
let rhi = local("rhi", i64());
595+
let taken = local("taken", k.list.clone());
596+
d.push(define(
597+
&name("assign_reversed_slice"),
598+
&[&xs, &start, &stop, &mask, &rstart, &rstop, &rmask],
599+
unit(),
600+
vec![
601+
n.decl(len(xs.e())),
602+
lo.decl(int(0)),
603+
hi.decl(n.e()),
604+
when(
605+
ne(bitand(mask.e(), int(1)), int(0)),
606+
vec![lo.set(call(
607+
"zb_slice_bound",
608+
vec![start.e(), n.e(), int(1)],
609+
i64(),
610+
))],
611+
),
612+
when(
613+
ne(bitand(mask.e(), int(2)), int(0)),
614+
vec![hi.set(call("zb_slice_bound", vec![stop.e(), n.e(), int(1)], i64()))],
615+
),
616+
when(lt(hi.e(), lo.e()), vec![hi.set(lo.e())]),
617+
rlo.decl(sub(n.e(), int(1))),
618+
rhi.decl(int(-1)),
619+
when(
620+
ne(bitand(rmask.e(), int(1)), int(0)),
621+
vec![rlo.set(call(
622+
"zb_slice_bound",
623+
vec![rstart.e(), n.e(), int(-1)],
624+
i64(),
625+
))],
626+
),
627+
when(
628+
ne(bitand(rmask.e(), int(2)), int(0)),
629+
vec![rhi.set(call(
630+
"zb_slice_bound",
631+
vec![rstop.e(), n.e(), int(-1)],
632+
i64(),
633+
))],
634+
),
635+
if_(
636+
and(
637+
eq(rlo.e(), sub(hi.e(), int(1))),
638+
eq(rhi.e(), sub(lo.e(), int(1))),
639+
),
640+
vec![
641+
i.decl(lo.e()),
642+
j.decl(sub(hi.e(), int(1))),
643+
while_(
644+
lt(i.e(), j.e()),
645+
vec![
646+
a.decl(el(&xs, i.e())),
647+
b.decl(el(&xs, j.e())),
648+
set_idx(xs.e(), i.e(), b.e()),
649+
set_idx(xs.e(), j.e(), a.e()),
650+
i.add_assign(int(1)),
651+
j.set(sub(j.e(), int(1))),
652+
],
653+
),
654+
],
655+
vec![
656+
taken.decl(call(
657+
&name("slice"),
658+
vec![
659+
xs.e(),
660+
rstart.e(),
661+
rstop.e(),
662+
int(-1),
663+
bitor(rmask.e(), int(4)),
664+
],
665+
k.list.clone(),
666+
)),
667+
assign_from(taken.e(), int(0)),
668+
],
669+
),
670+
ret_void(),
671+
],
672+
));
673+
d.push(define(
674+
&name("assign_slice_from"),
675+
&[&replacement, &xs, &start, &stop, &step, &mask],
676+
unit(),
677+
vec![
566678
n.decl(len(xs.e())),
567679
st.decl(int(1)),
568680
when(ne(bitand(mask.e(), int(4)), int(0)), vec![st.set(step.e())]),
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
prefix [2, 1, 0, 3, 4, 5]
2+
k [4, 3, 0, 1, 2, 5]
3+
middle [4, 3, 2, 1, 0, 5]
4+
whole [5, 0, 1, 2, 3, 4]
5+
same length elsewhere [4, 3, 1, 2, 3, 4]
6+
longer [4, 3, 2, 1, 1, 2, 3, 4]
7+
shorter [4, 1, 2, 3, 4]
8+
inverted target [4, 1, 2, 4, 3, 3, 4]
9+
negative [4, 1, 2, 4, 4, 3, 3]
10+
out of range [3, 3, 4, 4, 2, 1, 4]
11+
floats [2.5, 1.5, 3.5]
12+
strings ['a', 'd', 'c', 'b']
13+
mixed [None, 2.5, 'x', 1]
14+
empty [4, 1, 2, 4, 4, 3, 3, 3, 3, 4, 4, 2, 1, 4]
15+
itself [0, 0, 1, 2]
16+
own slice [0, 1, 0, 1, 2]
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
# Every shape of xs[a:b] = xs[c:d:-1]: coinciding ranges, ranges of
2+
# the same length elsewhere, different lengths, empty, out of range,
3+
# negative bounds, and a second name for the same list.
4+
5+
def show(label, xs):
6+
print(label, xs)
7+
8+
xs = [0, 1, 2, 3, 4, 5]
9+
xs[:3] = xs[2::-1]
10+
show("prefix", xs)
11+
k = 4
12+
xs[:k + 1] = xs[k::-1]
13+
show("k", xs)
14+
xs[2:5] = xs[4:1:-1]
15+
show("middle", xs)
16+
xs[:] = xs[::-1]
17+
show("whole", xs)
18+
xs[0:2] = xs[5:3:-1]
19+
show("same length elsewhere", xs)
20+
xs[1:2] = xs[4:1:-1]
21+
show("longer", xs)
22+
xs[1:4] = xs[0:0:-1]
23+
show("shorter", xs)
24+
xs[3:1] = xs[5:2:-1]
25+
show("inverted target", xs)
26+
xs[-3:] = xs[-1:-4:-1]
27+
show("negative", xs)
28+
xs[:100] = xs[100::-1]
29+
show("out of range", xs)
30+
ys = [1.5, 2.5, 3.5]
31+
ys[:2] = ys[1::-1]
32+
show("floats", ys)
33+
ws = ["a", "b", "c", "d"]
34+
ws[1:] = ws[:0:-1]
35+
show("strings", ws)
36+
zs = [1, "x", 2.5, None]
37+
zs[:] = zs[::-1]
38+
show("mixed", zs)
39+
n = 0
40+
xs[:n] = xs[n - 1::-1]
41+
show("empty", xs)
42+
xs = [0, 1, 2]
43+
xs[1:3] = xs
44+
show("itself", xs)
45+
xs[0:1] = xs[1:3]
46+
show("own slice", xs)

crates/zyntax_python/src/lower.rs

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2009,6 +2009,11 @@ impl<'m> Lowerer<'m> {
20092009
push(out, TypedStatement::Expression(Box::new(v.node)));
20102010
}
20112011
py::Stmt::Assign(a) => {
2012+
if a.targets.len() == 1
2013+
&& self.reversed_slice_assign(&a.targets[0], &a.value, span, out)?
2014+
{
2015+
return Ok(());
2016+
}
20122017
// `a = b = v` evaluates `v` once and binds each target
20132018
// to it, left to right.
20142019
let value = self.expr(&a.value)?;
@@ -2273,6 +2278,91 @@ impl<'m> Lowerer<'m> {
22732278
Ok(())
22742279
}
22752280

2281+
/// `xs[a:b] = xs[c:d:-1]` over one list name, with bounds that
2282+
/// cannot rebind the name: lowered as one call that reverses the
2283+
/// elements in place when the two ranges name the same ones, so the
2284+
/// flip of a permutation copies nothing. Anything else is left to
2285+
/// the general slice assignment; says whether it was taken.
2286+
fn reversed_slice_assign(
2287+
&mut self,
2288+
target: &py::Expr,
2289+
value: &py::Expr,
2290+
span: Span,
2291+
out: &mut Vec<Stmt>,
2292+
) -> Result<bool> {
2293+
let (py::Expr::Subscript(t), py::Expr::Subscript(v)) = (target, value) else {
2294+
return Ok(false);
2295+
};
2296+
let (py::Expr::Name(tn), py::Expr::Name(vn)) = (&*t.value, &*v.value) else {
2297+
return Ok(false);
2298+
};
2299+
let (py::Expr::Slice(ts), py::Expr::Slice(vs)) = (&*t.slice, &*v.slice) else {
2300+
return Ok(false);
2301+
};
2302+
let minus_one = |e: &Option<Box<py::Expr>>| {
2303+
matches!(e.as_deref(), Some(py::Expr::UnaryOp(u))
2304+
if u.op == py::UnaryOp::USub
2305+
&& matches!(&*u.operand, py::Expr::NumberLiteral(n)
2306+
if matches!(&n.value, py::Number::Int(i) if i.as_u64() == Some(1))))
2307+
};
2308+
fn pure_bound(e: &Option<Box<py::Expr>>) -> bool {
2309+
fn pure(e: &py::Expr) -> bool {
2310+
match e {
2311+
py::Expr::Name(_) | py::Expr::NumberLiteral(_) => true,
2312+
py::Expr::BinOp(b) => pure(&b.left) && pure(&b.right),
2313+
py::Expr::UnaryOp(u) => pure(&u.operand),
2314+
_ => false,
2315+
}
2316+
}
2317+
e.as_deref().is_none_or(pure)
2318+
}
2319+
if tn.id != vn.id
2320+
|| ts.step.is_some()
2321+
|| !minus_one(&vs.step)
2322+
|| !pure_bound(&ts.lower)
2323+
|| !pure_bound(&ts.upper)
2324+
|| !pure_bound(&vs.lower)
2325+
|| !pure_bound(&vs.upper)
2326+
{
2327+
return Ok(false);
2328+
}
2329+
let seq = self.expr(&t.value)?;
2330+
let Ty::List(e) = seq.ty else {
2331+
return Ok(false);
2332+
};
2333+
let bounds = |this: &mut Self,
2334+
lower: &Option<Box<py::Expr>>,
2335+
upper: &Option<Box<py::Expr>>|
2336+
-> Result<(Node, Node, Node)> {
2337+
let mut mask = 0;
2338+
let mut bound = |this: &mut Self, e: &Option<Box<py::Expr>>, bit: i64| match e {
2339+
Some(e) => {
2340+
mask |= bit;
2341+
this.expr_as(e, Ty::Int)
2342+
}
2343+
None => Ok(int_lit(0, span)),
2344+
};
2345+
let lo = bound(this, lower, 1)?;
2346+
let hi = bound(this, upper, 2)?;
2347+
Ok((lo, hi, int_lit(mask, span)))
2348+
};
2349+
let (start, stop, mask) = bounds(self, &ts.lower, &ts.upper)?;
2350+
let (rstart, rstop, rmask) = bounds(self, &vs.lower, &vs.upper)?;
2351+
let call = call(
2352+
&list_fn("assign_reversed_slice", e),
2353+
vec![seq.node, start, stop, mask, rstart, rstop, rmask],
2354+
Ty::None,
2355+
span,
2356+
);
2357+
out.push(TypedNode::new(
2358+
TypedStatement::Expression(Box::new(call)),
2359+
Type::Unknown,
2360+
span,
2361+
));
2362+
out.push(self.pending_check(span));
2363+
Ok(true)
2364+
}
2365+
22762366
/// `target = value`: a `let` the first time a name is seen in this
22772367
/// function, an assignment afterwards; the value converted to the
22782368
/// type inference gave the name.

0 commit comments

Comments
 (0)