Skip to content

Commit b52fff8

Browse files
committed
a receiver block binds self and it separately
a trailing lambda block bound to a receiver callback now binds the receiver as a parameter of its own — spelled `self` in the body, alongside the members it already saw unqualified — and `it` is the callback argument after the receiver. the block was previously called with more arguments than it declared, which raised a TypeError at runtime a callback that takes more arguments than the block binds is now rejected with the new `trailing-lambda-parameters` lint rather than crashing at runtime the `local` / `once` modifier a callback declares is read off the parameter `it` actually binds, so it skips the receiver too
1 parent efc7189 commit b52fff8

23 files changed

Lines changed: 769 additions & 405 deletions

‎crates/by_transforms/src/transforms/ast_driver.rs‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -640,9 +640,9 @@ pub(crate) fn run_against_source<'a>(
640640
&implementation_block_pass,
641641
&conversion_pass,
642642
// implicit receivers: `x.fn()` → `fn(x)` for a receiver callable in
643-
// scope, and a trailing lambda block's unqualified receiver members →
644-
// `it.<name>`. same shape as the extension rewrite above, which wins
645-
// when both could apply
643+
// scope, and a trailing lambda block's `self` / unqualified receiver
644+
// members → its receiver parameter. same shape as the extension rewrite
645+
// above, which wins when both could apply
646646
&implicit_receiver_pass,
647647
&dynamic_keyword_pass,
648648
// import-only companion to the ty-side implicit `Character` resolution;

‎crates/by_transforms/src/transforms/implicit_receiver.rs‎

Lines changed: 69 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -9,21 +9,23 @@
99
//! in scope rather than to a member of `x`. An unapplied `x.fn` becomes a
1010
//! `functools.partial`, matching how a bound method would have carried the
1111
//! receiver
12-
//! - a bare name in a trailing lambda block that resolves to a member of the
13-
//! block's receiver → `it.<name>`, where `it` is the block's implicit
14-
//! parameter (the receiver itself)
12+
//! - a bare name in a trailing lambda block that resolves through the block's
13+
//! receiver → the block's receiver parameter, either on its own (`self`) or
14+
//! as the object a member is read off (`<receiver>.<name>`)
1515
//!
1616
//! Both are narrow edits nested inside the trailing-lambda template's `Src`
1717
//! spans, so they compose with the block lowering.
1818
//!
1919
//! [`callable`]: super::callable
2020
21-
use ruff_python_ast::visitor::{Visitor, walk_expr, walk_stmt};
22-
use ruff_python_ast::{Expr, ExprAttribute, ExprContext, Stmt};
21+
use ruff_python_ast::visitor::{Visitor, walk_expr};
22+
use ruff_python_ast::{Expr, ExprAttribute, Stmt};
2323
use ruff_text_size::{Ranged, TextRange};
24+
use ty_python_semantic::ImplicitReceiverReference;
2425

2526
use super::ast_driver::{Fragment, PassContext, TypeAwarePass};
2627
use super::extension::{arguments_span, spine_has_optional};
28+
use super::trailing_lambda::RECEIVER_PARAMETER;
2729
use crate::type_info::TypeInfo;
2830

2931
struct ImplicitReceiverLower<'a> {
@@ -33,62 +35,10 @@ struct ImplicitReceiverLower<'a> {
3335
/// attribute ranges already rewritten as part of an enclosing call, so the
3436
/// bare-access arm doesn't rewrite them a second time
3537
handled: Vec<TextRange>,
36-
/// the enclosing trailing lambda blocks, innermost last, and whether each
37-
/// rebinds `it` — the name the receiver members are read from
38-
blocks: Vec<Block>,
3938
needs_functools: bool,
4039
}
4140

42-
/// An enclosing trailing lambda block, for the `it`-rebinding check
43-
struct Block {
44-
rebinds_it: bool,
45-
reported: bool,
46-
}
47-
48-
/// Whether a block assigns `it` anywhere in its body. The lowering reads the
49-
/// receiver's members off that parameter, so a rebinding would silently redirect
50-
/// them to the new value. Nested scopes count too: this only decides whether to
51-
/// reject, and over-rejecting is the safe direction.
52-
fn rebinds_it(body: &[Stmt]) -> bool {
53-
struct RebindsIt {
54-
found: bool,
55-
}
56-
57-
impl<'ast> Visitor<'ast> for RebindsIt {
58-
fn visit_expr(&mut self, expr: &'ast Expr) {
59-
if let Expr::Name(name) = expr
60-
&& name.id.as_str() == "it"
61-
&& matches!(name.ctx, ExprContext::Store | ExprContext::Del)
62-
{
63-
self.found = true;
64-
}
65-
walk_expr(self, expr);
66-
}
67-
}
68-
69-
let mut visitor = RebindsIt { found: false };
70-
for stmt in body {
71-
visitor.visit_stmt(stmt);
72-
}
73-
visitor.found
74-
}
75-
7641
impl<'ast> Visitor<'ast> for ImplicitReceiverLower<'_> {
77-
fn visit_stmt(&mut self, stmt: &'ast Stmt) {
78-
if let Stmt::FunctionDef(function) = stmt
79-
&& function.is_trailing_lambda
80-
{
81-
self.blocks.push(Block {
82-
rebinds_it: rebinds_it(&function.body),
83-
reported: false,
84-
});
85-
walk_stmt(self, stmt);
86-
self.blocks.pop();
87-
return;
88-
}
89-
walk_stmt(self, stmt);
90-
}
91-
9242
fn visit_expr(&mut self, expr: &'ast Expr) {
9343
match expr {
9444
// `x.fn(a)` → `fn(x, a)`
@@ -127,15 +77,20 @@ impl<'ast> Visitor<'ast> for ImplicitReceiverLower<'_> {
12777
}
12878
}
12979
}
130-
// a receiver member used unqualified inside a trailing lambda block
80+
// the receiver of a trailing lambda block, or one of its members,
81+
// used unqualified inside the block
13182
Expr::Name(name) => {
132-
if name.ctx.is_load() && self.types.is_implicit_receiver_name(name) {
133-
if self.block_rebinds_it() {
134-
self.report_rebound_it();
135-
} else {
136-
self.edits
137-
.push((name.range(), vec![Fragment::Lit(format!("it.{}", name.id))]));
138-
}
83+
if name.ctx.is_load()
84+
&& let Some(reference) = self.types.implicit_receiver_name(name)
85+
{
86+
let lowered = match reference {
87+
ImplicitReceiverReference::Receiver => RECEIVER_PARAMETER.to_owned(),
88+
ImplicitReceiverReference::Member => {
89+
format!("{RECEIVER_PARAMETER}.{}", name.id)
90+
}
91+
};
92+
self.edits
93+
.push((name.range(), vec![Fragment::Lit(lowered)]));
13994
}
14095
}
14196
_ => {}
@@ -159,28 +114,6 @@ impl ImplicitReceiverLower<'_> {
159114
fragments.push(Fragment::Lit(")".to_owned()));
160115
self.edits.push((call.range(), fragments));
161116
}
162-
163-
/// whether the innermost enclosing block rebinds `it`, which the member
164-
/// rewrite reads the receiver from
165-
fn block_rebinds_it(&self) -> bool {
166-
self.blocks.last().is_some_and(|block| block.rebinds_it)
167-
}
168-
169-
/// report the rebinding once per block, however many members it uses
170-
fn report_rebound_it(&mut self) {
171-
let Some(block) = self.blocks.last_mut() else {
172-
return;
173-
};
174-
if block.reported {
175-
return;
176-
}
177-
block.reported = true;
178-
self.errors.push(
179-
"a trailing lambda block that rebinds `it` cannot use its receiver's members \
180-
unqualified yet — the lowering reads them from `it`"
181-
.to_owned(),
182-
);
183-
}
184117
}
185118

186119
/// whether the access is a link of a `?.` chain, which the receiver rewrite
@@ -202,7 +135,6 @@ impl TypeAwarePass for ImplicitReceiverPass {
202135
edits: Vec::new(),
203136
handled: Vec::new(),
204137
errors: Vec::new(),
205-
blocks: Vec::new(),
206138
needs_functools: false,
207139
};
208140
for stmt in stmts {
@@ -270,15 +202,51 @@ mod tests {
270202
}
271203

272204
#[test]
273-
fn trailing_block_members_bind_to_it() {
205+
fn trailing_block_members_bind_to_the_receiver_parameter() {
274206
let out = check(indoc! {"
275207
def f(fn: int.() -> None) -> None:
276208
fn(1)
277209
278210
f:
279211
print(imag)
280212
"});
281-
assert!(out.contains("print(it.imag)"), "got:\n{out}");
213+
assert!(out.contains("print(_by_self.imag)"), "got:\n{out}");
214+
assert!(
215+
out.contains("def _trailing_lambda_0(_by_self=None, it=None):"),
216+
"got:\n{out}"
217+
);
218+
}
219+
220+
#[test]
221+
fn a_trailing_block_spells_its_receiver_self() {
222+
let out = check(indoc! {"
223+
def f(fn: str.(int) -> None) -> None:
224+
fn(\"a\", 1)
225+
226+
f:
227+
print(upper(), it, self)
228+
"});
229+
assert!(
230+
out.contains("print(_by_self.upper(), it, _by_self)"),
231+
"got:\n{out}"
232+
);
233+
}
234+
235+
#[test]
236+
fn an_enclosing_self_keeps_its_meaning() {
237+
// the receiver is the *last* fallback, so a method's own `self` still
238+
// wins — and the block's receiver parameter is a name the source cannot
239+
// spell, so it cannot be shadowed either
240+
let out = check(indoc! {"
241+
def f(fn: str.() -> None) -> None:
242+
fn(\"a\")
243+
244+
class C:
245+
def m(self) -> None:
246+
f:
247+
print(self, upper())
248+
"});
249+
assert!(out.contains("print(self, _by_self.upper())"), "got:\n{out}");
282250
}
283251

284252
#[test]
@@ -295,20 +263,18 @@ mod tests {
295263
}
296264

297265
#[test]
298-
fn rebinding_it_is_rejected() {
299-
let error = transpile(
300-
indoc! {"
301-
def f(fn: str.() -> None) -> None:
302-
fn(\"abc\")
266+
fn a_block_local_shadows_the_receiver_spelling() {
267+
// a block that binds `self` itself keeps that binding — the receiver's
268+
// members are read off the block's own parameter, so they are unaffected
269+
let out = check(indoc! {"
270+
def f(fn: str.() -> None) -> None:
271+
fn(\"abc\")
303272
304-
f:
305-
it = 5
306-
print(upper())
307-
"},
308-
&Config::test_default(),
309-
)
310-
.expect_err("a block that rebinds `it` should be rejected");
311-
assert!(error.contains("rebinds `it`"), "got:\n{error}");
273+
f:
274+
self = 5
275+
print(self, upper())
276+
"});
277+
assert!(out.contains("print(self, _by_self.upper())"), "got:\n{out}");
312278
}
313279

314280
#[test]

‎crates/by_transforms/src/transforms/trailing_lambda.rs‎

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,13 @@ use super::ast_driver::{Fragment, PassContext, TypeAwarePass};
3737
use super::source_util::line_indent;
3838
use crate::type_info::{CaptureKind, TypeInfo};
3939

40+
/// The parameter a block binds its callback's implicit receiver to. The body
41+
/// spells it `self`, which [`implicit_receiver`] rewrites to this name — a name
42+
/// the source cannot produce, so a method's own `self` is never shadowed
43+
///
44+
/// [`implicit_receiver`]: super::implicit_receiver
45+
pub(crate) const RECEIVER_PARAMETER: &str = "_by_self";
46+
4047
struct TrailingLambdaLower<'a, 'src> {
4148
source: &'src str,
4249
types: &'a dyn TypeInfo,
@@ -247,9 +254,18 @@ impl TrailingLambdaLower<'_, '_> {
247254
// surviving bindings to pre-init before it
248255
let (declarations, preinits) = self.capture_declarations(function);
249256

250-
// `it` defaults to `None` so a callback whose type takes no argument
251-
// (`() -> None`, invoked as `fn()`) can still call the block, which
252-
// always declares the single implicit `it` parameter
257+
// a receiver callback is called with its receiver first, so the block
258+
// declares a parameter for it ahead of `it`. both default to `None` so a
259+
// callback whose type takes fewer arguments (`() -> None`, invoked as
260+
// `fn()`) can still call the block, which always declares them
261+
let parameters = if self
262+
.types
263+
.trailing_lambda_callback_has_receiver(signature_callee)
264+
{
265+
format!("{RECEIVER_PARAMETER}=None, it=None")
266+
} else {
267+
"it=None".to_owned()
268+
};
253269
let mut fragments = Vec::new();
254270
if !returns.is_empty() {
255271
fragments.push(Fragment::Lit(format!("{ret_cell} = []\n{indent}")));
@@ -259,7 +275,7 @@ impl TrailingLambdaLower<'_, '_> {
259275
for preinit in &preinits {
260276
fragments.push(Fragment::Lit(format!("{preinit} = None\n{indent}")));
261277
}
262-
fragments.push(Fragment::Lit(format!("def {name}(it=None):")));
278+
fragments.push(Fragment::Lit(format!("def {name}({parameters}):")));
263279
// write-through declarations so block assignments update enclosing
264280
// bindings; spliced ahead of the suite so they precede every use
265281
if let Some(declarations) = declarations {

‎crates/by_transforms/src/type_info.rs‎

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ use ty_python_core::{global_scope, place_table, semantic_index};
88
use ty_python_semantic::types::{
99
DynamicType, KnownClass, KnownInstanceType, Type, UnpackedKwargs, character,
1010
};
11-
use ty_python_semantic::{HasType, SemanticModel};
11+
use ty_python_semantic::{HasType, ImplicitReceiverReference, SemanticModel};
1212

1313
/// How the postfix `^` / `!` operators test the "absent" arm of an operand's
1414
/// wrapped type. `T?` lowers to `T | None`, so its absent arm is `None`; a
@@ -170,9 +170,10 @@ pub(crate) trait TypeInfo {
170170
/// rather than a member of `x`. lowered to `fn(x)`
171171
fn is_implicit_receiver_attribute(&self, attribute: &ruff_python_ast::ExprAttribute) -> bool;
172172

173-
/// whether `name` is a member of the enclosing trailing lambda block's
174-
/// receiver, used unqualified. lowered to `it.<name>`
175-
fn is_implicit_receiver_name(&self, name: &ExprName) -> bool;
173+
/// how `name` resolves through the enclosing trailing lambda block's
174+
/// receiver: `self` is the receiver itself, any other name is a member of
175+
/// it used unqualified. both lower to the block's receiver parameter
176+
fn implicit_receiver_name(&self, name: &ExprName) -> Option<ImplicitReceiverReference>;
176177

177178
/// the enum a *context-sensitively* resolved name must be qualified with —
178179
/// `Red` in a `Color` context lowers to `Color.Red`. `None` for every name
@@ -331,6 +332,11 @@ pub(crate) trait TypeInfo {
331332
/// variadic / positional-only last parameter)
332333
fn trailing_lambda_keyword(&self, callee: &Expr) -> Option<String>;
333334

335+
/// whether the trailing-lambda callee's callback declares an implicit
336+
/// receiver, which the block binds as a leading parameter — the body reads
337+
/// the receiver's members off it unqualified, and spells it `self`
338+
fn trailing_lambda_callback_has_receiver(&self, callee: &Expr) -> bool;
339+
334340
/// whether the trailing-lambda callee's callback parameter is `once` — the
335341
/// block runs exactly once, so its `return` propagates to the enclosing
336342
/// function rather than to the block. `false` when the marker is unreadable
@@ -546,7 +552,7 @@ impl TypeInfo for SemanticModel<'_> {
546552
SemanticModel::implicit_receiver_attribute(self, attribute)
547553
}
548554

549-
fn is_implicit_receiver_name(&self, name: &ExprName) -> bool {
555+
fn implicit_receiver_name(&self, name: &ExprName) -> Option<ImplicitReceiverReference> {
550556
SemanticModel::implicit_receiver_name(self, name)
551557
}
552558

@@ -830,6 +836,10 @@ impl TypeInfo for SemanticModel<'_> {
830836
SemanticModel::trailing_lambda_keyword(self, callee)
831837
}
832838

839+
fn trailing_lambda_callback_has_receiver(&self, callee: &Expr) -> bool {
840+
SemanticModel::trailing_lambda_callback_has_receiver(self, callee)
841+
}
842+
833843
fn trailing_lambda_callee_is_once(&self, callee: &Expr) -> bool {
834844
SemanticModel::trailing_lambda_callee_is_once(self, callee)
835845
}

‎crates/by_transforms/tests/implicit_receiver_runtime.rs‎

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@
66
//! plain function, `x.fn()` called through the receiver, an unapplied `x.fn`
77
//! bound as a `functools.partial`, and a trailing lambda block using the
88
//! receiver's members unqualified — and runs them on a real interpreter. A
9-
//! misplaced receiver argument or a member bound to the wrong object would fail
10-
//! here even though the type-level tests pass.
9+
//! misplaced receiver argument, a member bound to the wrong object, or a block
10+
//! whose parameters don't line up with the call would fail here even though the
11+
//! type-level tests pass.
1112
//!
1213
//! No third-party packages are needed, so any `python3` will do; if none is
1314
//! found the test skips rather than fails.
@@ -50,15 +51,16 @@ def suffixed(value: int, mark: str) -> str:
5051
5152
assert unapplied(suffixed) == "5!", "an unapplied reference binds the receiver"
5253
53-
# a trailing lambda block sees the receiver's members unqualified
54-
def apply(fn: str.() -> None) -> None:
55-
fn("abc")
54+
# a trailing lambda block sees the receiver's members unqualified, spells the
55+
# receiver itself `self`, and binds the callback's own argument as `it`
56+
def apply(fn: str.(int) -> None) -> None:
57+
fn("abc", 3)
5658
5759
seen: str = ""
5860
apply:
59-
seen = upper() + str(len(it))
61+
seen = upper() + str(it) + self
6062
61-
assert seen == "ABC3", "block members resolve against the receiver"
63+
assert seen == "ABC3abc", "a block's receiver, its members and `it` all bind"
6264
6365
print("ok")
6466
"##;

0 commit comments

Comments
 (0)