Skip to content

Commit f768dc5

Browse files
youknowdotcodex
andauthored
Compose both argument arity bounds for flattened fields (#9528)
Count flattened required arguments, exclude phantom fields consistently, and saturate tuple lower-bound sums as well as upper bounds. Add pure metadata tests covering required, optional, default, nested, phantom and saturated bounds without constructing a VM. No binder error precedence or Lib changes. Source review and normal hooks passed; local unit tests remain unexecuted because the dependency cache is unavailable. Assisted-by: OpenAI Codex:model version not exposed Co-authored-by: Codex <codex@openai.com>
1 parent 504a08b commit f768dc5

2 files changed

Lines changed: 114 additions & 9 deletions

File tree

‎crates/derive-impl/src/from_args.rs‎

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -601,14 +601,21 @@ pub(crate) fn impl_from_args(input: DeriveInput) -> Result<TokenStream> {
601601
.iter()
602602
.map(ArgAttribute::try_from)
603603
.collect::<Result<Vec<_>>>()?;
604-
let (min_arity, max_arity) = compute_arity_bounds(&field_attrs);
605-
// A flattened field adds its own type's upper bound. An unbounded one
606-
// such as `PosArgs` is `usize::MAX`, so the sum saturates.
604+
// Phantom fields are absent from both argument extraction and signatures.
605+
let visible_attrs = struct_fields
606+
.iter()
607+
.filter(|field| !is_phantom(field))
608+
.map(ArgAttribute::try_from)
609+
.collect::<Result<Vec<_>>>()?;
610+
let (min_arity, max_arity) = compute_arity_bounds(&visible_attrs);
611+
// Flattened types contribute both bounds. Saturate each sum, including
612+
// custom required bounds and an unbounded `PosArgs` upper bound.
607613
let flattened = struct_fields
608614
.iter()
609615
.zip(&field_attrs)
610-
.filter(|(_, attr)| attr.kind == ParameterKind::Flatten)
611-
.map(|(field, _)| &field.ty);
616+
.filter(|(field, attr)| !is_phantom(field) && attr.kind == ParameterKind::Flatten)
617+
.map(|(field, _)| &field.ty)
618+
.collect::<Vec<_>>();
612619

613620
let mut params = Vec::new();
614621
for field in &struct_fields {
@@ -626,9 +633,14 @@ pub(crate) fn impl_from_args(input: DeriveInput) -> Result<TokenStream> {
626633
Some(&[#(#params),*]);
627634

628635
fn arity() -> ::std::ops::RangeInclusive<usize> {
629-
#min_arity..=#max_arity #(
630-
.saturating_add(*<#flattened as ::rustpython_vm::function::FromArgs>::arity().end())
636+
let min = #min_arity;
637+
let max = #max_arity;
638+
#(
639+
let (start, end) = <#flattened as ::rustpython_vm::function::FromArgs>::arity().into_inner();
640+
let min = min.saturating_add(start);
641+
let max = max.saturating_add(end);
631642
)*
643+
min..=max
632644
}
633645

634646
fn from_args(

‎crates/vm/src/function/argument.rs‎

Lines changed: 95 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1034,8 +1034,9 @@ macro_rules! tuple_from_py_func_args {
10341034
let mut max = 0;
10351035
$(
10361036
let (start, end) = $T::arity().into_inner();
1037-
min += start;
1038-
// An unbounded `end` is `usize::MAX`, so the sum saturates.
1037+
// Both custom required bounds and unbounded upper bounds
1038+
// can reach `usize::MAX`; neither composition may overflow.
1039+
min = usize::saturating_add(min, start);
10391040
max = usize::saturating_add(max, end);
10401041
)+
10411042
min..=max
@@ -1059,3 +1060,95 @@ tuple_from_py_func_args!(A, B, C, D, E);
10591060
tuple_from_py_func_args!(A, B, C, D, E, F);
10601061
tuple_from_py_func_args!(A, B, C, D, E, F, G);
10611062
tuple_from_py_func_args!(A, B, C, D, E, F, G, H);
1063+
1064+
#[cfg(test)]
1065+
mod arity_composition_tests {
1066+
use super::*;
1067+
1068+
struct Bounds<const MIN: usize, const MAX: usize>;
1069+
1070+
impl<const MIN: usize, const MAX: usize> FromArgs for Bounds<MIN, MAX> {
1071+
fn arity() -> RangeInclusive<usize> {
1072+
MIN..=MAX
1073+
}
1074+
1075+
fn from_args(_vm: &VirtualMachine, _args: &mut FuncArgs) -> Result<Self, ArgumentError> {
1076+
unreachable!("metadata-only fixture; no interpreter is constructed")
1077+
}
1078+
}
1079+
1080+
#[allow(dead_code)]
1081+
#[derive(FromArgs)]
1082+
struct Mixed {
1083+
#[pyarg(flatten)]
1084+
prefix: Bounds<2, 3>,
1085+
#[pyarg(positional)]
1086+
required: usize,
1087+
#[pyarg(positional, optional)]
1088+
optional: OptionalArg<usize>,
1089+
#[pyarg(positional, default = 0)]
1090+
defaulted: usize,
1091+
}
1092+
1093+
#[allow(dead_code)]
1094+
#[derive(FromArgs)]
1095+
struct Nested(#[pyarg(flatten)] (Bounds<1, 2>, Bounds<0, 1>));
1096+
1097+
#[allow(dead_code)]
1098+
#[derive(FromArgs)]
1099+
struct OptionalOnly {
1100+
#[pyarg(flatten)]
1101+
prefix: Bounds<0, 2>,
1102+
#[pyarg(positional, optional)]
1103+
optional: OptionalArg<usize>,
1104+
}
1105+
1106+
#[allow(dead_code)]
1107+
#[derive(FromArgs)]
1108+
struct Phantoms {
1109+
_phantom: core::marker::PhantomData<()>,
1110+
#[pyarg(flatten)]
1111+
_phantom_flattened: core::marker::PhantomData<()>,
1112+
#[pyarg(positional)]
1113+
required: usize,
1114+
}
1115+
1116+
#[allow(dead_code)]
1117+
#[derive(FromArgs)]
1118+
struct Saturated {
1119+
#[pyarg(flatten)]
1120+
prefix: Bounds<{ usize::MAX }, { usize::MAX }>,
1121+
#[pyarg(positional)]
1122+
required: usize,
1123+
}
1124+
1125+
#[test]
1126+
fn derived_bounds_include_required_flattened_arguments() {
1127+
assert_eq!(Mixed::arity(), 3..=6);
1128+
assert_eq!(Nested::arity(), 1..=3);
1129+
assert_eq!(Phantoms::arity(), 1..=1);
1130+
}
1131+
1132+
#[test]
1133+
fn optional_only_flattening_preserves_zero_minimum() {
1134+
assert_eq!(OptionalOnly::arity(), 0..=3);
1135+
assert_eq!(<(Bounds<0, 2>, Bounds<0, 3>)>::arity(), 0..=5);
1136+
assert_eq!(
1137+
<(Bounds<2, 3>, (Bounds<1, 2>, Bounds<0, 1>))>::arity(),
1138+
3..=6
1139+
);
1140+
}
1141+
1142+
#[test]
1143+
fn both_tuple_and_derived_bounds_saturate() {
1144+
assert_eq!(
1145+
<(Bounds<1, 1>, Bounds<0, { usize::MAX }>)>::arity(),
1146+
1..=usize::MAX
1147+
);
1148+
assert_eq!(Saturated::arity(), usize::MAX..=usize::MAX);
1149+
assert_eq!(
1150+
<(Bounds<{ usize::MAX }, { usize::MAX }>, Bounds<1, 1>)>::arity(),
1151+
usize::MAX..=usize::MAX,
1152+
);
1153+
}
1154+
}

0 commit comments

Comments
 (0)