Skip to content

Commit 30efa34

Browse files
[mypyc] Initial optimization for f-string through a str.join() specializer (#10776)
This pull request adds a specializer for faster constructing f-string, which is translated into str.join() previously in mypy AST.
1 parent eb1c525 commit 30efa34

File tree

3 files changed

+117
-0
lines changed

3 files changed

+117
-0
lines changed

mypyc/irbuild/specialize.py

+42
Original file line numberDiff line numberDiff line change
@@ -460,3 +460,45 @@ def split_braces(format_str: str) -> List[str]:
460460
prev = ''
461461
ret_list.append(tmp_str)
462462
return ret_list
463+
464+
465+
@specialize_function('join', str_rprimitive)
466+
def translate_fstring(
467+
builder: IRBuilder, expr: CallExpr, callee: RefExpr) -> Optional[Value]:
468+
# Special case for f-string, which is translated into str.join() in mypy AST.
469+
# This specializer optimizes simplest f-strings which don't contain any
470+
# format operation.
471+
if (isinstance(callee, MemberExpr)
472+
and isinstance(callee.expr, StrExpr) and callee.expr.value == ''
473+
and expr.arg_kinds == [ARG_POS] and isinstance(expr.args[0], ListExpr)):
474+
for item in expr.args[0].items:
475+
if isinstance(item, StrExpr):
476+
continue
477+
elif isinstance(item, CallExpr):
478+
if (not isinstance(item.callee, MemberExpr)
479+
or item.callee.name != 'format'):
480+
return None
481+
elif (not isinstance(item.callee.expr, StrExpr)
482+
or item.callee.expr.value != '{:{}}'):
483+
return None
484+
485+
if not isinstance(item.args[1], StrExpr) or item.args[1].value != '':
486+
return None
487+
else:
488+
return None
489+
490+
result_list: List[Value] = [Integer(0, c_pyssize_t_rprimitive)]
491+
for item in expr.args[0].items:
492+
if isinstance(item, StrExpr) and item.value != '':
493+
result_list.append(builder.accept(item))
494+
elif isinstance(item, CallExpr):
495+
result_list.append(builder.call_c(str_op,
496+
[builder.accept(item.args[0])],
497+
expr.line))
498+
499+
if len(result_list) == 1:
500+
return builder.load_str("")
501+
502+
result_list[0] = Integer(len(result_list) - 1, c_pyssize_t_rprimitive)
503+
return builder.call_c(str_build_op, result_list, expr.line)
504+
return None

mypyc/test-data/irbuild-str.test

+53
Original file line numberDiff line numberDiff line change
@@ -191,3 +191,56 @@ L0:
191191
s3 = r13
192192
return 1
193193

194+
[case testFStrings]
195+
def f(var: str, num: int) -> None:
196+
s1 = f"Hi! I'm {var}. I am {num} years old."
197+
s2 = f'Hello {var:>{num}}'
198+
s3 = f''
199+
s4 = f'abc'
200+
[out]
201+
def f(var, num):
202+
var :: str
203+
num :: int
204+
r0, r1, r2 :: str
205+
r3 :: object
206+
r4, r5, r6, s1, r7, r8, r9, r10 :: str
207+
r11 :: object
208+
r12, r13, r14 :: str
209+
r15 :: object
210+
r16 :: str
211+
r17 :: list
212+
r18, r19, r20 :: ptr
213+
r21, s2, r22, s3, r23, s4 :: str
214+
L0:
215+
r0 = "Hi! I'm "
216+
r1 = PyObject_Str(var)
217+
r2 = '. I am '
218+
r3 = box(int, num)
219+
r4 = PyObject_Str(r3)
220+
r5 = ' years old.'
221+
r6 = CPyStr_Build(5, r0, r1, r2, r4, r5)
222+
s1 = r6
223+
r7 = ''
224+
r8 = 'Hello '
225+
r9 = '{:{}}'
226+
r10 = '>'
227+
r11 = box(int, num)
228+
r12 = PyObject_Str(r11)
229+
r13 = CPyStr_Build(2, r10, r12)
230+
r14 = 'format'
231+
r15 = CPyObject_CallMethodObjArgs(r9, r14, var, r13, 0)
232+
r16 = cast(str, r15)
233+
r17 = PyList_New(2)
234+
r18 = get_element_ptr r17 ob_item :: PyListObject
235+
r19 = load_mem r18 :: ptr*
236+
set_mem r19, r8 :: builtins.object*
237+
r20 = r19 + WORD_SIZE*1
238+
set_mem r20, r16 :: builtins.object*
239+
keep_alive r17
240+
r21 = PyUnicode_Join(r7, r17)
241+
s2 = r21
242+
r22 = ''
243+
s3 = r22
244+
r23 = 'abc'
245+
s4 = r23
246+
return 1

mypyc/test-data/run-strings.test

+22
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,21 @@ def test_fstring_basics() -> None:
192192
inf_num = float('inf')
193193
assert f'{nan_num}, {inf_num}' == 'nan, inf'
194194

195+
# F-strings would be translated into ''.join[string literals, format method call, ...] in mypy AST.
196+
# Currently we are using a str.join specializer for f-string speed up. We might not cover all cases
197+
# and the rest ones should fall back to a normal str.join method call.
198+
# TODO: Once we have a new pipeline for f-strings, this test case can be moved to testStringOps.
199+
def test_str_join() -> None:
200+
var = 'mypyc'
201+
num = 10
202+
assert ''.join(['a', 'b', '{}'.format(var), 'c']) == 'abmypycc'
203+
assert ''.join(['a', 'b', '{:{}}'.format(var, ''), 'c']) == 'abmypycc'
204+
assert ''.join(['a', 'b', '{:{}}'.format(var, '>10'), 'c']) == 'ab mypycc'
205+
assert ''.join(['a', 'b', '{:{}}'.format(var, '>{}'.format(num)), 'c']) == 'ab mypycc'
206+
assert var.join(['a', '{:{}}'.format(var, ''), 'b']) == 'amypycmypycmypycb'
207+
assert ','.join(['a', '{:{}}'.format(var, ''), 'b']) == 'a,mypyc,b'
208+
assert ''.join(['x', var]) == 'xmypyc'
209+
195210
class A:
196211
def __init__(self, name, age):
197212
self.name = name
@@ -356,6 +371,13 @@ def test_format_method_different_kind() -> None:
356371
assert "Test: {}{}".format(s3, s1) == "Test: 测试:Literal['😀']"
357372
assert "Test: {}{}".format(s3, s2) == "Test: 测试:Revealed type is"
358373

374+
def test_format_method_nested() -> None:
375+
var = 'mypyc'
376+
num = 10
377+
assert '{:{}}'.format(var, '') == 'mypyc'
378+
assert '{:{}}'.format(var, '>10') == ' mypyc'
379+
assert '{:{}}'.format(var, '>{}'.format(num)) == ' mypyc'
380+
359381
class Point:
360382
def __init__(self, x, y):
361383
self.x, self.y = x, y

0 commit comments

Comments
 (0)