Skip to content

Commit e27d0b0

Browse files
committed
added implicit receivers
emit one positional-only marker for a receiver callable any `/` the arguments emit — explicit, or implied by a bare positional followed by a named parameter — already closes off the receiver, so the protocol form was rendering two and producing invalid python fix implicit receiver review findings - a scope that binds the name shadows an outer receiver callable, as it would shadow an ordinary load; `x.fn()` no longer resolves past a local binding and silently lowers to a call of it - reject a trailing lambda block that rebinds `it` and uses its receiver's members: the lowering reads them from `it` - guard the receiver-callable parse arm with the recursion limit its sibling arms use - widen the synthesized protocol receiver parameter name past any parameter the callable declares itself - name the receiver in a missing-argument diagnostic render and traverse the implicit receiver everywhere codegen dropped the receiver when unparsing a callable type, and `any_over_expr` never visited it. adds formatter, parser and codegen round-trip coverage for the surface syntax
1 parent 9592449 commit e27d0b0

39 files changed

Lines changed: 1589 additions & 76 deletions

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

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -37,13 +37,13 @@ use super::{
3737
annotation, anon_named_tuple, auto_quote, callable, character_type, checked_cast, coalesce,
3838
coalesce_chain, compat, context_params, decl_site_variance, decorator_keyword, dedent_string,
3939
dynamic_keyword, empty_declarations, extension, float_const, force_unwrap, frameworks,
40-
generic_call, generics, grapheme_string, identity_swap, implicit_typing, inferred_annotation,
41-
init_method, just_float, kw_subscript, literal_types, local_once, main_function, modifiers,
42-
mutable_defaults, none_chain, optional_type, overload, parametric_is, postfix_await, propagate,
43-
properties, protocol_type, reified_generic, repeated_underscore, sentinel, some_ctor,
44-
soundness, string_tag, super_keyword, symbolic_type_op, top_star, trailing_lambda, tuple_index,
45-
type_is, type_reification, typed_dict_literal, typed_lambda, typeof_keyword, unpack,
46-
use_site_variance,
40+
generic_call, generics, grapheme_string, identity_swap, implicit_receiver, implicit_typing,
41+
inferred_annotation, init_method, just_float, kw_subscript, literal_types, local_once,
42+
main_function, modifiers, mutable_defaults, none_chain, optional_type, overload, parametric_is,
43+
postfix_await, propagate, properties, protocol_type, reified_generic, repeated_underscore,
44+
sentinel, some_ctor, soundness, string_tag, super_keyword, symbolic_type_op, top_star,
45+
trailing_lambda, tuple_index, type_is, type_reification, typed_dict_literal, typed_lambda,
46+
typeof_keyword, unpack, use_site_variance,
4747
};
4848
use crate::Config;
4949
use crate::type_info::TypeInfo;
@@ -490,6 +490,7 @@ pub(crate) fn run_against_source<'a>(
490490
let context_params_pass = context_params::ContextParamsPass::new(source_ref);
491491
let extension_block_pass = extension::ExtensionBlockPass::new(source_ref);
492492
let extension_call_pass = extension::ExtensionCallPass;
493+
let implicit_receiver_pass = implicit_receiver::ImplicitReceiverPass;
493494
let frameworks_pass = frameworks::FrameworksPass::new(source_ref);
494495
let variance_pass = decl_site_variance::VarianceStripPass::new(source_ref);
495496
let anon_named_tuple_pass =
@@ -594,6 +595,11 @@ pub(crate) fn run_against_source<'a>(
594595
// type-directed rewrite of attribute accesses ty resolved to
595596
// extension members (`xs.second()` → `__by_ext__list__second(xs)`)
596597
&extension_call_pass,
598+
// implicit receivers: `x.fn()` → `fn(x)` for a receiver callable in
599+
// scope, and a trailing lambda block's unqualified receiver members →
600+
// `it.<name>`. same shape as the extension rewrite above, which wins
601+
// when both could apply
602+
&implicit_receiver_pass,
597603
&dynamic_keyword_pass,
598604
// import-only companion to the ty-side implicit `Character` resolution;
599605
// emits no text edits, so ordering among the type passes is free

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

Lines changed: 79 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -311,12 +311,17 @@ impl<'src> CallableSyntax<'src> {
311311
/// `args` / `slash` / `star` are passed separately rather than read off a
312312
/// node so a protocol method member can hand over the parameters that
313313
/// follow its receiver, which is a name rather than a type.
314+
///
315+
/// `implicit_receiver` is the rendered leading parameter of a basedpython
316+
/// implicit receiver (`int.() -> str`), which is a type rather than a name and
317+
/// so is spelled by a synthesized parameter.
314318
pub(crate) fn render_protocol_params(
315319
&mut self,
316320
args: &[Expr],
317321
explicit_slash: Option<usize>,
318322
star: Option<usize>,
319323
receiver: &str,
324+
implicit_receiver: Option<String>,
320325
) -> String {
321326
let mut parts: Vec<String> = vec![receiver.to_owned()];
322327
// implicit `/` after the last bare positional (no label) when followed
@@ -339,6 +344,15 @@ impl<'src> CallableSyntax<'src> {
339344
})
340345
};
341346
let slash = explicit_slash.or(implicit_slash);
347+
// an implicit receiver leads the parameter list, positional-only. any `/`
348+
// the arguments themselves emit comes after the receiver and so already
349+
// closes it off — a second one is a `SyntaxError`
350+
if let Some(implicit_receiver) = implicit_receiver {
351+
parts.push(implicit_receiver);
352+
if slash.is_none() {
353+
parts.push("/".to_owned());
354+
}
355+
}
342356
let mut star_emitted = false;
343357
for (i, arg) in args.iter().enumerate() {
344358
if Some(i) == slash {
@@ -485,27 +499,40 @@ impl<'src> CallableSyntax<'src> {
485499
let (prefix, paramspec) = self.paramspec_tail(ct)?;
486500
let ret_str = self.rewrite_or_leaf(&ct.returns);
487501
let ps = self.rewrite_or_leaf(paramspec);
488-
if prefix.is_empty() {
502+
// an implicit receiver is the callable's leading positional
503+
// parameter, so it joins the `Concatenate` prefix
504+
let mut prefix_str: Vec<String> = ct
505+
.receiver
506+
.iter()
507+
.map(|receiver| self.rewrite_or_leaf(receiver))
508+
.collect();
509+
prefix_str.extend(prefix.iter().map(|a| self.rewrite_or_leaf(a)));
510+
if prefix_str.is_empty() {
489511
Some(format!("Callable[{ps}, {ret_str}]"))
490512
} else {
491513
self.needs_concatenate_import = true;
492-
let prefix_str = prefix
493-
.iter()
494-
.map(|a| self.rewrite_or_leaf(a))
495-
.collect::<Vec<_>>()
496-
.join(", ");
497514
Some(format!(
498-
"Callable[Concatenate[{prefix_str}, {ps}], {ret_str}]"
515+
"Callable[Concatenate[{}, {ps}], {ret_str}]",
516+
prefix_str.join(", ")
499517
))
500518
}
501519
}
502520
Expr::CallableType(ct) if self.is_non_denotable(ct) => {
503521
self.needs_protocol_import = true;
522+
let implicit_receiver = ct.receiver.as_ref().map(|receiver| {
523+
let rendered = self.rewrite_or_leaf(receiver);
524+
format!(
525+
"{}: {}",
526+
receiver_parameter_name(ct),
527+
quote_forward_ref(&rendered)
528+
)
529+
});
504530
let params = self.render_protocol_params(
505531
&ct.args,
506532
ct.parameter_slash.map(|i| i as usize),
507533
ct.parameter_star.map(|i| i as usize),
508534
"self",
535+
implicit_receiver,
509536
);
510537
let returns = quote_forward_ref(&self.rewrite_or_leaf(&ct.returns));
511538
let shape = ProtocolShape { params, returns };
@@ -514,6 +541,9 @@ impl<'src> CallableSyntax<'src> {
514541
// `(...) -> R` — a single bare ellipsis parameter list is python's
515542
// "any arguments" callable: `Callable[..., R]`, not the
516543
// single-`...`-argument `Callable[[...], R]`
544+
// a receiver in front of a gradual parameter list is absorbed by it:
545+
// `Callable[..., R]` already accepts the receiver-first call, and
546+
// `Concatenate[T, ...]` is not spellable on every supported version
517547
Expr::CallableType(ExprCallableType { args, returns, .. })
518548
if matches!(args.as_slice(), [Expr::EllipsisLiteral(_)]) =>
519549
{
@@ -522,13 +552,19 @@ impl<'src> CallableSyntax<'src> {
522552
Some(format!("Callable[..., {ret_str}]"))
523553
}
524554

525-
Expr::CallableType(ExprCallableType { args, returns, .. }) => {
555+
Expr::CallableType(ExprCallableType {
556+
receiver,
557+
args,
558+
returns,
559+
..
560+
}) => {
526561
self.needs_import = true;
527-
let args_str = args
562+
let mut rendered: Vec<String> = receiver
528563
.iter()
529-
.map(|a| self.rewrite_or_leaf(a))
530-
.collect::<Vec<_>>()
531-
.join(", ");
564+
.map(|receiver| self.rewrite_or_leaf(receiver))
565+
.collect();
566+
rendered.extend(args.iter().map(|a| self.rewrite_or_leaf(a)));
567+
let args_str = rendered.join(", ");
532568
let ret_str = self.rewrite_or_leaf(returns);
533569
Some(format!("Callable[[{args_str}], {ret_str}]"))
534570
}
@@ -691,6 +727,37 @@ fn quote_forward_ref(ty: &str) -> String {
691727
format!("\"{}\"", ty.replace('\\', "\\\\").replace('"', "\\\""))
692728
}
693729

730+
/// The synthesized name of a receiver parameter in a protocol `__call__`. The
731+
/// receiver is unnamed in the surface syntax, so any name works — but a callable
732+
/// may itself declare a parameter called `_receiver`, and two parameters of the
733+
/// same name is a `SyntaxError` in the emitted class rather than a parse error
734+
/// the final verification would catch. Widen until it is free.
735+
fn receiver_parameter_name(ct: &ExprCallableType) -> String {
736+
let mut name = "_receiver".to_owned();
737+
while declared_parameter_names(ct).any(|declared| declared == name) {
738+
name.push('_');
739+
}
740+
name
741+
}
742+
743+
/// The parameter names a callable's arguments spell, in any of the named forms
744+
/// (`name: T`, `*args: T`, `**kwargs: T`)
745+
fn declared_parameter_names(ct: &ExprCallableType) -> impl Iterator<Item = &str> {
746+
ct.args.iter().filter_map(|arg| {
747+
let Expr::Named(named) = arg else {
748+
return None;
749+
};
750+
let target = match named.target.as_ref() {
751+
Expr::Starred(starred) => match starred.value.as_ref() {
752+
Expr::Starred(inner) => inner.value.as_ref(),
753+
value => value,
754+
},
755+
target => target,
756+
};
757+
target.as_name_expr().map(|name| name.id.as_str())
758+
})
759+
}
760+
694761
/// whether `expr` is a `Name` / `Attribute` referring to the given identifier
695762
fn is_named(expr: &Expr, ident: &str) -> bool {
696763
match expr {

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -388,7 +388,7 @@ pub(crate) fn hoist_backing_functions(
388388
/// does the postfix spine of `expr` contain an optional (`?.`) segment? the
389389
/// receiver of an extension rewrite must not, because the rewrite hoists the
390390
/// member access out of the chain's short-circuit
391-
fn spine_has_optional(expr: &Expr) -> bool {
391+
pub(super) fn spine_has_optional(expr: &Expr) -> bool {
392392
match expr {
393393
Expr::Attribute(attr) => attr.optional || spine_has_optional(&attr.value),
394394
Expr::Subscript(subscript) => spine_has_optional(&subscript.value),
@@ -399,7 +399,7 @@ fn spine_has_optional(expr: &Expr) -> bool {
399399

400400
/// the source span of a call's arguments (positional and keyword), without
401401
/// the parentheses. `None` when the call has no arguments
402-
fn arguments_span(arguments: &ast::Arguments) -> Option<TextRange> {
402+
pub(super) fn arguments_span(arguments: &ast::Arguments) -> Option<TextRange> {
403403
let starts = arguments
404404
.args
405405
.iter()

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -904,6 +904,9 @@ fn rename_in_expr(expr: &Expr, renames: &HashMap<String, String>, edits: &mut Ve
904904
// an arrow callable `(**P) -> None` lowers to `Callable[P, None]` via a template edit
905905
// that passes its operand source through, so a rename on the inner name still lands
906906
Expr::CallableType(c) => {
907+
if let Some(receiver) = &c.receiver {
908+
rename_in_expr(receiver, renames, edits);
909+
}
907910
c.args
908911
.iter()
909912
.for_each(|a| rename_in_expr(a, renames, edits));

0 commit comments

Comments
 (0)