Skip to content

Commit 1573336

Browse files
[mypyc] Only construct dict setdefault collection when needed (#10668)
Closes mypyc/mypyc#830 Add dict_setdefault_spec_init_op.
1 parent 749dbf0 commit 1573336

File tree

7 files changed

+212
-7
lines changed

7 files changed

+212
-7
lines changed

mypyc/irbuild/specialize.py

Lines changed: 46 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,17 +14,22 @@
1414

1515
from typing import Callable, Optional, Dict, Tuple, List
1616

17-
from mypy.nodes import CallExpr, RefExpr, MemberExpr, TupleExpr, GeneratorExpr, ARG_POS
17+
from mypy.nodes import (
18+
CallExpr, RefExpr, MemberExpr, NameExpr, TupleExpr, GeneratorExpr,
19+
ListExpr, DictExpr, ARG_POS
20+
)
1821
from mypy.types import AnyType, TypeOfAny
1922

2023
from mypyc.ir.ops import (
2124
Value, Register, BasicBlock, Integer, RaiseStandardError, Unreachable
2225
)
2326
from mypyc.ir.rtypes import (
2427
RType, RTuple, str_rprimitive, list_rprimitive, dict_rprimitive, set_rprimitive,
25-
bool_rprimitive, is_dict_rprimitive
28+
bool_rprimitive, is_dict_rprimitive, c_int_rprimitive
29+
)
30+
from mypyc.primitives.dict_ops import (
31+
dict_keys_op, dict_values_op, dict_items_op, dict_setdefault_spec_init_op
2632
)
27-
from mypyc.primitives.dict_ops import dict_keys_op, dict_values_op, dict_items_op
2833
from mypyc.primitives.list_ops import new_list_set_item_op
2934
from mypyc.primitives.tuple_ops import new_tuple_set_item_op
3035
from mypyc.irbuild.builder import IRBuilder
@@ -313,3 +318,41 @@ def translate_isinstance(builder: IRBuilder, expr: CallExpr, callee: RefExpr) ->
313318
if irs is not None:
314319
return builder.builder.isinstance_helper(builder.accept(expr.args[0]), irs, expr.line)
315320
return None
321+
322+
323+
@specialize_function('setdefault', dict_rprimitive)
324+
def translate_dict_setdefault(
325+
builder: IRBuilder, expr: CallExpr, callee: RefExpr) -> Optional[Value]:
326+
if (len(expr.args) == 2
327+
and expr.arg_kinds == [ARG_POS, ARG_POS]
328+
and isinstance(callee, MemberExpr)):
329+
# Special case for dict.setdefault which would only construct default empty
330+
# collection when needed. The dict_setdefault_spec_init_op checks whether
331+
# the dict contains the key and would construct the empty collection only once.
332+
# For example, this specializer works for the following cases:
333+
# d.setdefault(key, set()).add(value)
334+
# d.setdefault(key, []).append(value)
335+
# d.setdefault(key, {})[inner_key] = inner_val
336+
arg = expr.args[1]
337+
if isinstance(arg, ListExpr):
338+
if len(arg.items):
339+
return None
340+
data_type = Integer(1, c_int_rprimitive, expr.line)
341+
elif isinstance(arg, DictExpr):
342+
if len(arg.items):
343+
return None
344+
data_type = Integer(2, c_int_rprimitive, expr.line)
345+
elif (isinstance(arg, CallExpr) and isinstance(arg.callee, NameExpr)
346+
and arg.callee.fullname == 'builtins.set'):
347+
if len(arg.args):
348+
return None
349+
data_type = Integer(3, c_int_rprimitive, expr.line)
350+
else:
351+
return None
352+
353+
callee_dict = builder.accept(callee.expr)
354+
key_val = builder.accept(expr.args[0])
355+
return builder.call_c(dict_setdefault_spec_init_op,
356+
[callee_dict, key_val, data_type],
357+
expr.line)
358+
return None

mypyc/lib-rt/CPy.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -344,6 +344,7 @@ PyObject *CPyDict_Get(PyObject *dict, PyObject *key, PyObject *fallback);
344344
PyObject *CPyDict_GetWithNone(PyObject *dict, PyObject *key);
345345
PyObject *CPyDict_SetDefault(PyObject *dict, PyObject *key, PyObject *value);
346346
PyObject *CPyDict_SetDefaultWithNone(PyObject *dict, PyObject *key);
347+
PyObject *CPyDict_SetDefaultWithEmptyDatatype(PyObject *dict, PyObject *key, int data_type);
347348
PyObject *CPyDict_Build(Py_ssize_t size, ...);
348349
int CPyDict_Update(PyObject *dict, PyObject *stuff);
349350
int CPyDict_UpdateInDisplay(PyObject *dict, PyObject *stuff);

mypyc/lib-rt/dict_ops.c

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,35 @@ PyObject *CPyDict_SetDefaultWithNone(PyObject *dict, PyObject *key) {
8080
return CPyDict_SetDefault(dict, key, Py_None);
8181
}
8282

83+
PyObject *CPyDict_SetDefaultWithEmptyDatatype(PyObject *dict, PyObject *key,
84+
int data_type) {
85+
PyObject *res = CPyDict_GetItem(dict, key);
86+
if (!res) {
87+
// CPyDict_GetItem() would generates an PyExc_KeyError
88+
// when key is not found.
89+
PyErr_Clear();
90+
91+
PyObject *new_obj;
92+
if (data_type == 1) {
93+
new_obj = PyList_New(0);
94+
} else if (data_type == 2) {
95+
new_obj = PyDict_New();
96+
} else if (data_type == 3) {
97+
new_obj = PySet_New(NULL);
98+
} else {
99+
return NULL;
100+
}
101+
102+
if (CPyDict_SetItem(dict, key, new_obj) == -1) {
103+
return NULL;
104+
} else {
105+
return new_obj;
106+
}
107+
} else {
108+
return res;
109+
}
110+
}
111+
83112
int CPyDict_SetItem(PyObject *dict, PyObject *key, PyObject *value) {
84113
if (PyDict_CheckExact(dict)) {
85114
return PyDict_SetItem(dict, key, value);

mypyc/primitives/dict_ops.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@
119119
error_kind=ERR_MAGIC)
120120

121121
# dict.setdefault(key, default)
122-
method_op(
122+
dict_setdefault_op = method_op(
123123
name='setdefault',
124124
arg_types=[dict_rprimitive, object_rprimitive, object_rprimitive],
125125
return_type=object_rprimitive,
@@ -135,6 +135,16 @@
135135
is_borrowed=True,
136136
error_kind=ERR_MAGIC)
137137

138+
# dict.setdefault(key, empty tuple/list/set)
139+
# The third argument marks the data type of the second argument.
140+
# 1: list 2: dict 3: set
141+
# Other number would lead to an error.
142+
dict_setdefault_spec_init_op = custom_op(
143+
arg_types=[dict_rprimitive, object_rprimitive, c_int_rprimitive],
144+
return_type=object_rprimitive,
145+
c_function_name='CPyDict_SetDefaultWithEmptyDatatype',
146+
error_kind=ERR_MAGIC)
147+
138148
# dict.keys()
139149
method_op(
140150
name='keys',

mypyc/test-data/fixtures/ir.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,7 @@ def values(self) -> Iterable[V]: pass
177177
def items(self) -> Iterable[Tuple[K, V]]: pass
178178
def clear(self) -> None: pass
179179
def copy(self) -> Dict[K, V]: pass
180-
def setdefault(self, k: K, v: Optional[V] = None) -> Optional[V]: pass
180+
def setdefault(self, key: K, val: V = ...) -> V: pass
181181

182182
class set(Generic[T]):
183183
def __init__(self, i: Optional[Iterable[T]] = None) -> None: pass

mypyc/test-data/irbuild-dict.test

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -340,6 +340,24 @@ L0:
340340
from typing import Dict
341341
def f(d: Dict[object, object]) -> object:
342342
return d.setdefault('a', 'b')
343+
344+
def f2(d: Dict[object, object], flag: bool) -> object:
345+
if flag:
346+
return d.setdefault('a', set())
347+
else:
348+
return d.setdefault('a', set('b'))
349+
350+
def f3(d: Dict[object, object], flag: bool) -> object:
351+
if flag:
352+
return d.setdefault('a', [])
353+
else:
354+
return d.setdefault('a', [1])
355+
356+
def f4(d: Dict[object, object], flag: bool) -> object:
357+
if flag:
358+
return d.setdefault('a', {})
359+
else:
360+
return d.setdefault('a', {'c': 1})
343361
[out]
344362
def f(d):
345363
d :: dict
@@ -350,3 +368,81 @@ L0:
350368
r1 = 'b'
351369
r2 = CPyDict_SetDefault(d, r0, r1)
352370
return r2
371+
def f2(d, flag):
372+
d :: dict
373+
flag :: bool
374+
r0 :: str
375+
r1 :: object
376+
r2, r3 :: str
377+
r4 :: set
378+
r5, r6 :: object
379+
L0:
380+
if flag goto L1 else goto L2 :: bool
381+
L1:
382+
r0 = 'a'
383+
r1 = CPyDict_SetDefaultWithEmptyDatatype(d, r0, 3)
384+
return r1
385+
L2:
386+
r2 = 'a'
387+
r3 = 'b'
388+
r4 = PySet_New(r3)
389+
r5 = CPyDict_SetDefault(d, r2, r4)
390+
return r5
391+
L3:
392+
r6 = box(None, 1)
393+
return r6
394+
def f3(d, flag):
395+
d :: dict
396+
flag :: bool
397+
r0 :: str
398+
r1 :: object
399+
r2 :: str
400+
r3 :: list
401+
r4 :: object
402+
r5, r6 :: ptr
403+
r7, r8 :: object
404+
L0:
405+
if flag goto L1 else goto L2 :: bool
406+
L1:
407+
r0 = 'a'
408+
r1 = CPyDict_SetDefaultWithEmptyDatatype(d, r0, 1)
409+
return r1
410+
L2:
411+
r2 = 'a'
412+
r3 = PyList_New(1)
413+
r4 = box(short_int, 2)
414+
r5 = get_element_ptr r3 ob_item :: PyListObject
415+
r6 = load_mem r5 :: ptr*
416+
set_mem r6, r4 :: builtins.object*
417+
keep_alive r3
418+
r7 = CPyDict_SetDefault(d, r2, r3)
419+
return r7
420+
L3:
421+
r8 = box(None, 1)
422+
return r8
423+
def f4(d, flag):
424+
d :: dict
425+
flag :: bool
426+
r0 :: str
427+
r1 :: object
428+
r2, r3 :: str
429+
r4 :: object
430+
r5 :: dict
431+
r6, r7 :: object
432+
L0:
433+
if flag goto L1 else goto L2 :: bool
434+
L1:
435+
r0 = 'a'
436+
r1 = CPyDict_SetDefaultWithEmptyDatatype(d, r0, 2)
437+
return r1
438+
L2:
439+
r2 = 'a'
440+
r3 = 'c'
441+
r4 = box(short_int, 2)
442+
r5 = CPyDict_Build(1, r3, r4)
443+
r6 = CPyDict_SetDefault(d, r2, r5)
444+
return r6
445+
L3:
446+
r7 = box(None, 1)
447+
return r7
448+

mypyc/test-data/run-dicts.test

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -196,7 +196,7 @@ else:
196196

197197
[case testDictMethods]
198198
from collections import defaultdict
199-
from typing import Dict, Optional
199+
from typing import Dict, Optional, List, Set
200200

201201
def test_dict_clear() -> None:
202202
d = {'a': 1, 'b': 2}
@@ -232,7 +232,7 @@ class MyDict(dict):
232232
return super().setdefault(k, v) + 10
233233

234234
def test_dict_setdefault() -> None:
235-
d: Dict[str, int] = {'a': 1, 'b': 2}
235+
d: Dict[str, Optional[int]] = {'a': 1, 'b': 2}
236236
assert d.setdefault('a', 2) == 1
237237
assert d.setdefault('b', 2) == 2
238238
assert d.setdefault('c', 3) == 3
@@ -254,6 +254,32 @@ def test_dict_subclass_setdefault() -> None:
254254
assert d.setdefault('e') == None
255255
assert d.setdefault('e', 100) == 110
256256

257+
def test_dict_empty_collection_setdefault() -> None:
258+
d1: Dict[str, List[int]] = {'a': [1, 2, 3]}
259+
assert d1.setdefault('a', []) == [1, 2, 3]
260+
assert d1.setdefault('b', []) == []
261+
assert 'b' in d1
262+
d1.setdefault('b', []).append(3)
263+
assert d1['b'] == [3]
264+
assert d1.setdefault('c', [1]) == [1]
265+
266+
d2: Dict[str, Dict[str, int]] = {'a': {'a': 1}}
267+
assert d2.setdefault('a', {}) == {'a': 1}
268+
assert d2.setdefault('b', {}) == {}
269+
assert 'b' in d2
270+
d2.setdefault('b', {})['aa'] = 2
271+
d2.setdefault('b', {})['bb'] = 3
272+
assert d2['b'] == {'aa': 2, 'bb': 3}
273+
assert d2.setdefault('c', {'cc': 1}) == {'cc': 1}
274+
275+
d3: Dict[str, Set[str]] = {'a': set('a')}
276+
assert d3.setdefault('a', set()) == {'a'}
277+
assert d3.setdefault('b', set()) == set()
278+
d3.setdefault('b', set()).add('b')
279+
d3.setdefault('b', set()).add('c')
280+
assert d3['b'] == {'b', 'c'}
281+
assert d3.setdefault('c', set('d')) == {'d'}
282+
257283
[case testDictToBool]
258284
from typing import Dict, List
259285

0 commit comments

Comments
 (0)