6
6
"""
7
7
8
8
from typing import Union , List , Optional , Tuple , Callable
9
+ from typing_extensions import Type , ClassVar
9
10
10
- from mypy .nodes import Lvalue , Expression , TupleExpr , CallExpr , RefExpr , GeneratorExpr , ARG_POS
11
+ from mypy .nodes import (
12
+ Lvalue , Expression , TupleExpr , CallExpr , RefExpr , GeneratorExpr , ARG_POS , MemberExpr
13
+ )
11
14
from mypyc .ir .ops import (
12
- Value , BasicBlock , LoadInt , Branch , Register , AssignmentTarget
15
+ Value , BasicBlock , LoadInt , Branch , Register , AssignmentTarget , TupleGet ,
16
+ AssignmentTargetTuple , TupleSet , OpDescription
13
17
)
14
18
from mypyc .ir .rtypes import (
15
- RType , is_short_int_rprimitive , is_list_rprimitive , is_sequence_rprimitive
19
+ RType , is_short_int_rprimitive , is_list_rprimitive , is_sequence_rprimitive ,
20
+ RTuple , is_dict_rprimitive
21
+ )
22
+ from mypyc .primitives .dict_ops import (
23
+ dict_next_key_op , dict_next_value_op , dict_next_item_op , dict_check_size_op ,
24
+ dict_key_iter_op , dict_value_iter_op , dict_item_iter_op
16
25
)
17
26
from mypyc .primitives .int_ops import unsafe_short_add
18
27
from mypyc .primitives .list_ops import new_list_op , list_append_op , list_get_item_unsafe_op
@@ -170,6 +179,15 @@ def make_for_loop_generator(builder: IRBuilder,
170
179
for_list .init (expr_reg , target_type , reverse = False )
171
180
return for_list
172
181
182
+ if is_dict_rprimitive (rtyp ):
183
+ # Special case "for k in <dict>".
184
+ expr_reg = builder .accept (expr )
185
+ target_type = builder .get_dict_key_type (expr )
186
+
187
+ for_dict = ForDictionaryKeys (builder , index , body_block , loop_exit , line , nested )
188
+ for_dict .init (expr_reg , target_type )
189
+ return for_dict
190
+
173
191
if (isinstance (expr , CallExpr )
174
192
and isinstance (expr .callee , RefExpr )):
175
193
if (expr .callee .fullname == 'builtins.range'
@@ -233,6 +251,27 @@ def make_for_loop_generator(builder: IRBuilder,
233
251
for_list = ForSequence (builder , index , body_block , loop_exit , line , nested )
234
252
for_list .init (expr_reg , target_type , reverse = True )
235
253
return for_list
254
+ if (isinstance (expr , CallExpr )
255
+ and isinstance (expr .callee , MemberExpr )
256
+ and not expr .args ):
257
+ # Special cases for dictionary iterator methods, like dict.items().
258
+ rtype = builder .node_type (expr .callee .expr )
259
+ if (is_dict_rprimitive (rtype )
260
+ and expr .callee .name in ('keys' , 'values' , 'items' )):
261
+ expr_reg = builder .accept (expr .callee .expr )
262
+ for_dict_type = None # type: Optional[Type[ForGenerator]]
263
+ if expr .callee .name == 'keys' :
264
+ target_type = builder .get_dict_key_type (expr .callee .expr )
265
+ for_dict_type = ForDictionaryKeys
266
+ elif expr .callee .name == 'values' :
267
+ target_type = builder .get_dict_value_type (expr .callee .expr )
268
+ for_dict_type = ForDictionaryValues
269
+ else :
270
+ target_type = builder .get_dict_item_type (expr .callee .expr )
271
+ for_dict_type = ForDictionaryItems
272
+ for_dict_gen = for_dict_type (builder , index , body_block , loop_exit , line , nested )
273
+ for_dict_gen .init (expr_reg , target_type )
274
+ return for_dict_gen
236
275
237
276
# Default to a generic for loop.
238
277
expr_reg = builder .accept (expr )
@@ -292,6 +331,14 @@ def gen_step(self) -> None:
292
331
def gen_cleanup (self ) -> None :
293
332
"""Generate post-loop cleanup (if needed)."""
294
333
334
+ def load_len (self , expr : Union [Value , AssignmentTarget ]) -> Value :
335
+ """A helper to get collection length, used by several subclasses."""
336
+ return self .builder .builder .builtin_call (
337
+ [self .builder .read (expr , self .line )],
338
+ 'builtins.len' ,
339
+ self .line ,
340
+ )
341
+
295
342
296
343
class ForIterable (ForGenerator ):
297
344
"""Generate IR for a for loop over an arbitrary iterable (the normal case)."""
@@ -371,17 +418,11 @@ def init(self, expr_reg: Value, target_type: RType, reverse: bool) -> None:
371
418
if not reverse :
372
419
index_reg = builder .add (LoadInt (0 ))
373
420
else :
374
- index_reg = builder .binary_op (self .load_len (), builder .add (LoadInt (1 )), '-' , self .line )
421
+ index_reg = builder .binary_op (self .load_len (self .expr_target ),
422
+ builder .add (LoadInt (1 )), '-' , self .line )
375
423
self .index_target = builder .maybe_spill_assignable (index_reg )
376
424
self .target_type = target_type
377
425
378
- def load_len (self ) -> Value :
379
- return self .builder .builder .builtin_call (
380
- [self .builder .read (self .expr_target , self .line )],
381
- 'builtins.len' ,
382
- self .line ,
383
- )
384
-
385
426
def gen_condition (self ) -> None :
386
427
builder = self .builder
387
428
line = self .line
@@ -398,7 +439,7 @@ def gen_condition(self) -> None:
398
439
builder .activate_block (second_check )
399
440
# For compatibility with python semantics we recalculate the length
400
441
# at every iteration.
401
- len_reg = self .load_len ()
442
+ len_reg = self .load_len (self . expr_target )
402
443
comparison = builder .binary_op (builder .read (self .index_target , line ), len_reg , '<' , line )
403
444
builder .add_bool_branch (comparison , self .body_block , self .loop_exit )
404
445
@@ -430,6 +471,136 @@ def gen_step(self) -> None:
430
471
builder .add (LoadInt (step ))], line ), line )
431
472
432
473
474
+ class ForDictionaryCommon (ForGenerator ):
475
+ """Generate optimized IR for a for loop over dictionary keys/values.
476
+
477
+ The logic is pretty straightforward, we use PyDict_Next() API wrapped in
478
+ a tuple, so that we can modify only a single register. The layout of the tuple:
479
+ * f0: are there more items (bool)
480
+ * f1: current offset (int)
481
+ * f2: next key (object)
482
+ * f3: next value (object)
483
+ For more info see https://docs.python.org/3/c-api/dict.html#c.PyDict_Next.
484
+
485
+ Note that for subclasses we fall back to generic PyObject_GetIter() logic,
486
+ since they may override some iteration methods in subtly incompatible manner.
487
+ The fallback logic is implemented in CPy.h via dynamic type check.
488
+ """
489
+ dict_next_op = None # type: ClassVar[OpDescription]
490
+ dict_iter_op = None # type: ClassVar[OpDescription]
491
+
492
+ def need_cleanup (self ) -> bool :
493
+ # Technically, a dict subclass can raise an unrelated exception
494
+ # in __next__(), so we need this.
495
+ return True
496
+
497
+ def init (self , expr_reg : Value , target_type : RType ) -> None :
498
+ builder = self .builder
499
+ self .target_type = target_type
500
+
501
+ # We add some variables to environment class, so they can be read across yield.
502
+ self .expr_target = builder .maybe_spill (expr_reg )
503
+ offset_reg = builder .add (LoadInt (0 ))
504
+ self .offset_target = builder .maybe_spill_assignable (offset_reg )
505
+ self .size = builder .maybe_spill (self .load_len (self .expr_target ))
506
+
507
+ # For dict class (not a subclass) this is the dictionary itself.
508
+ iter_reg = builder .primitive_op (self .dict_iter_op , [expr_reg ], self .line )
509
+ self .iter_target = builder .maybe_spill (iter_reg )
510
+
511
+ def gen_condition (self ) -> None :
512
+ """Get next key/value pair, set new offset, and check if we should continue."""
513
+ builder = self .builder
514
+ line = self .line
515
+ self .next_tuple = self .builder .primitive_op (
516
+ self .dict_next_op , [builder .read (self .iter_target , line ),
517
+ builder .read (self .offset_target , line )], line )
518
+
519
+ # Do this here instead of in gen_step() to minimize variables in environment.
520
+ new_offset = builder .add (TupleGet (self .next_tuple , 1 , line ))
521
+ builder .assign (self .offset_target , new_offset , line )
522
+
523
+ should_continue = builder .add (TupleGet (self .next_tuple , 0 , line ))
524
+ builder .add (
525
+ Branch (should_continue , self .body_block , self .loop_exit , Branch .BOOL_EXPR )
526
+ )
527
+
528
+ def gen_step (self ) -> None :
529
+ """Check that dictionary didn't change size during iteration.
530
+
531
+ Raise RuntimeError if it is not the case to match CPython behavior.
532
+ """
533
+ builder = self .builder
534
+ line = self .line
535
+ # Technically, we don't need a new primitive for this, but it is simpler.
536
+ builder .primitive_op (dict_check_size_op ,
537
+ [builder .read (self .expr_target , line ),
538
+ builder .read (self .size , line )], line )
539
+
540
+ def gen_cleanup (self ) -> None :
541
+ # Same as for generic ForIterable.
542
+ self .builder .primitive_op (no_err_occurred_op , [], self .line )
543
+
544
+
545
+ class ForDictionaryKeys (ForDictionaryCommon ):
546
+ """Generate optimized IR for a for loop over dictionary keys."""
547
+ dict_next_op = dict_next_key_op
548
+ dict_iter_op = dict_key_iter_op
549
+
550
+ def begin_body (self ) -> None :
551
+ builder = self .builder
552
+ line = self .line
553
+
554
+ # Key is stored at the third place in the tuple.
555
+ key = builder .add (TupleGet (self .next_tuple , 2 , line ))
556
+ builder .assign (builder .get_assignment_target (self .index ),
557
+ builder .coerce (key , self .target_type , line ), line )
558
+
559
+
560
+ class ForDictionaryValues (ForDictionaryCommon ):
561
+ """Generate optimized IR for a for loop over dictionary values."""
562
+ dict_next_op = dict_next_value_op
563
+ dict_iter_op = dict_value_iter_op
564
+
565
+ def begin_body (self ) -> None :
566
+ builder = self .builder
567
+ line = self .line
568
+
569
+ # Value is stored at the third place in the tuple.
570
+ value = builder .add (TupleGet (self .next_tuple , 2 , line ))
571
+ builder .assign (builder .get_assignment_target (self .index ),
572
+ builder .coerce (value , self .target_type , line ), line )
573
+
574
+
575
+ class ForDictionaryItems (ForDictionaryCommon ):
576
+ """Generate optimized IR for a for loop over dictionary items."""
577
+ dict_next_op = dict_next_item_op
578
+ dict_iter_op = dict_item_iter_op
579
+
580
+ def begin_body (self ) -> None :
581
+ builder = self .builder
582
+ line = self .line
583
+
584
+ key = builder .add (TupleGet (self .next_tuple , 2 , line ))
585
+ value = builder .add (TupleGet (self .next_tuple , 3 , line ))
586
+
587
+ # Coerce just in case e.g. key is itself a tuple to be unpacked.
588
+ assert isinstance (self .target_type , RTuple )
589
+ key = builder .coerce (key , self .target_type .types [0 ], line )
590
+ value = builder .coerce (value , self .target_type .types [1 ], line )
591
+
592
+ target = builder .get_assignment_target (self .index )
593
+ if isinstance (target , AssignmentTargetTuple ):
594
+ # Simpler code for common case: for k, v in d.items().
595
+ if len (target .items ) != 2 :
596
+ builder .error ("Expected a pair for dict item iteration" , line )
597
+ builder .assign (target .items [0 ], key , line )
598
+ builder .assign (target .items [1 ], value , line )
599
+ else :
600
+ rvalue = builder .add (TupleSet ([key , value ], line ))
601
+ builder .assign (target , rvalue , line )
602
+
603
+
433
604
class ForRange (ForGenerator ):
434
605
"""Generate optimized IR for a for loop over an integer range."""
435
606
0 commit comments