11//! Implementation of the zip() builtin function.
22
33use crate :: {
4- args:: ArgValues ,
4+ args:: { ArgValues , KwargsValues } ,
55 bytecode:: VM ,
6- defer_drop_mut,
7- exception_private:: RunResult ,
8- heap:: HeapData ,
6+ defer_drop , defer_drop_mut,
7+ exception_private:: { ExcType , RunError , RunResult , SimpleException } ,
8+ heap:: { HeapData , HeapGuard } ,
99 resource:: ResourceTracker ,
10- types:: { List , MontyIter , allocate_tuple, tuple:: TupleVec } ,
10+ types:: { List , MontyIter , PyTrait , allocate_tuple, tuple:: TupleVec } ,
1111 value:: Value ,
1212} ;
1313
1414/// Implementation of the zip() builtin function.
1515///
1616/// Returns a list of tuples, where the i-th tuple contains the i-th element
1717/// from each of the argument iterables. Stops when the shortest iterable is exhausted.
18+ /// When `strict=True`, raises `ValueError` if any iterable has a different length.
1819/// Note: In Python this returns an iterator, but we return a list for simplicity.
1920pub fn builtin_zip ( vm : & mut VM < ' _ , ' _ , impl ResourceTracker > , args : ArgValues ) -> RunResult < Value > {
2021 let ( positional, kwargs) = args. into_parts ( ) ;
2122 defer_drop_mut ! ( positional, vm) ;
2223
23- // TODO: support kwargs (strict)
24- kwargs. not_supported_yet ( "zip" , vm. heap ) ?;
24+ let strict = extract_zip_strict ( kwargs, vm) ?;
2525
2626 if positional. len ( ) == 0 {
2727 // zip() with no arguments returns empty list
@@ -30,48 +30,113 @@ pub fn builtin_zip(vm: &mut VM<'_, '_, impl ResourceTracker>, args: ArgValues) -
3030 }
3131
3232 // Create iterators for each iterable
33- let mut iterators: Vec < MontyIter > = Vec :: with_capacity ( positional. len ( ) ) ;
33+ let iterators: Vec < MontyIter > = Vec :: with_capacity ( positional. len ( ) ) ;
34+ defer_drop_mut ! ( iterators, vm) ;
3435 for iterable in positional {
35- match MontyIter :: new ( iterable, vm) {
36- Ok ( iter) => iterators. push ( iter) ,
37- Err ( e) => {
38- // Clean up already-created iterators
39- for iter in iterators {
40- iter. drop_with_heap ( vm) ;
41- }
42- return Err ( e) ;
43- }
44- }
36+ iterators. push ( MontyIter :: new ( iterable, vm) ?) ;
4537 }
4638
47- let mut result: Vec < Value > = Vec :: new ( ) ;
39+ let mut result_guard = HeapGuard :: new ( Vec :: new ( ) , vm) ;
40+ let ( result, vm) = result_guard. as_parts_mut ( ) ;
4841
4942 // Zip until shortest iterator is exhausted
5043 ' outer: loop {
51- let mut tuple_items = TupleVec :: with_capacity ( iterators. len ( ) ) ;
44+ let mut items_guard = HeapGuard :: new ( TupleVec :: with_capacity ( iterators. len ( ) ) , vm) ;
45+ let ( tuple_items, vm) = items_guard. as_parts_mut ( ) ;
5246
53- for iter in & mut iterators {
47+ for ( i , iter) in iterators. iter_mut ( ) . enumerate ( ) {
5448 if let Some ( item) = iter. for_next ( vm) ? {
5549 tuple_items. push ( item) ;
5650 } else {
57- // This iterator is exhausted - drop partial tuple items and stop
58- for item in tuple_items {
59- item. drop_with_heap ( vm) ;
51+ // This iterator is exhausted - stop zipping
52+
53+ if strict {
54+ // In strict mode, if i > 0 then argument i+1 ran out before
55+ // the earlier ones, so it is "shorter."
56+ if i > 0 {
57+ return Err ( strict_length_error ( i + 1 , i, "shorter" ) ) ;
58+ }
59+ // i == 0: first iterator exhausted — verify every remaining
60+ // iterator is also exhausted. If any still yields a value,
61+ // that argument is "longer" than all preceding exhausted ones.
62+ // j is the 0-based index; iterators 0..j are all exhausted,
63+ // so j gives the count for the error message.
64+ for ( j, remaining) in iterators. iter_mut ( ) . enumerate ( ) . skip ( 1 ) {
65+ if let Some ( extra) = remaining. for_next ( vm) ? {
66+ extra. drop_with_heap ( vm) ;
67+ return Err ( strict_length_error ( j + 1 , j, "longer" ) ) ;
68+ }
69+ }
6070 }
71+
6172 break ' outer;
6273 }
6374 }
6475
6576 // Create tuple from collected items
77+ let ( tuple_items, vm) = items_guard. into_parts ( ) ;
6678 let tuple_val = allocate_tuple ( tuple_items, vm. heap ) ?;
6779 result. push ( tuple_val) ;
6880 }
6981
70- // Clean up iterators
71- for iter in iterators {
72- iter. drop_with_heap ( vm) ;
73- }
74-
82+ let ( result, vm) = result_guard. into_parts ( ) ;
7583 let heap_id = vm. heap . allocate ( HeapData :: List ( List :: new ( result) ) ) ?;
7684 Ok ( Value :: Ref ( heap_id) )
7785}
86+
87+ /// Extracts the `strict` keyword argument from `zip()`.
88+ ///
89+ /// Accepts any truthy/falsy value for `strict`, matching CPython behavior.
90+ /// Raises `TypeError` for unexpected keyword arguments.
91+ fn extract_zip_strict ( kwargs : KwargsValues , vm : & mut VM < ' _ , ' _ , impl ResourceTracker > ) -> RunResult < bool > {
92+ let mut strict = false ;
93+ let mut error: Option < RunError > = None ;
94+
95+ for ( key, value) in kwargs {
96+ defer_drop ! ( key, vm) ;
97+ defer_drop ! ( value, vm) ;
98+
99+ if error. is_some ( ) {
100+ continue ;
101+ }
102+
103+ let Some ( keyword_name) = key. as_either_str ( vm. heap ) else {
104+ error = Some ( SimpleException :: new_msg ( ExcType :: TypeError , "keywords must be strings" ) . into ( ) ) ;
105+ continue ;
106+ } ;
107+
108+ let key_str = keyword_name. as_str ( vm. interns ) ;
109+ match key_str {
110+ "strict" => {
111+ strict = value. py_bool ( vm) ;
112+ }
113+ _ => {
114+ error = Some ( ExcType :: type_error_unexpected_keyword ( "zip" , key_str) ) ;
115+ }
116+ }
117+ }
118+
119+ if let Some ( error) = error {
120+ Err ( error)
121+ } else {
122+ Ok ( strict)
123+ }
124+ }
125+
126+ /// Builds the `ValueError` for `zip(strict=True)` when iterables have different lengths.
127+ ///
128+ /// Matches CPython's error format:
129+ /// - `"zip() argument 2 is shorter than argument 1"` (singular)
130+ /// - `"zip() argument 4 is shorter than arguments 1-3"` (plural)
131+ fn strict_length_error ( exhausted_arg : usize , num_longer_args : usize , relation : & str ) -> RunError {
132+ let others = if num_longer_args == 1 {
133+ "argument 1" . to_owned ( )
134+ } else {
135+ format ! ( "arguments 1-{num_longer_args}" )
136+ } ;
137+ SimpleException :: new_msg (
138+ ExcType :: ValueError ,
139+ format ! ( "zip() argument {exhausted_arg} is {relation} than {others}" ) ,
140+ )
141+ . into ( )
142+ }
0 commit comments