Commit 652f061d authored by Stefan Behnel's avatar Stefan Behnel Committed by GitHub

Merge pull request #2949 from rjtobin/posonly_opt

Positional-only code generation optimization
parents 6e30c6ea 7b5ae8fc
...@@ -3661,6 +3661,7 @@ class DefNodeWrapper(FuncDefNode): ...@@ -3661,6 +3661,7 @@ class DefNodeWrapper(FuncDefNode):
positional_args = [] positional_args = []
required_kw_only_args = [] required_kw_only_args = []
optional_kw_only_args = [] optional_kw_only_args = []
num_pos_only_args = 0
for arg in args: for arg in args:
if arg.is_generic: if arg.is_generic:
if arg.default: if arg.default:
...@@ -3673,6 +3674,8 @@ class DefNodeWrapper(FuncDefNode): ...@@ -3673,6 +3674,8 @@ class DefNodeWrapper(FuncDefNode):
required_kw_only_args.append(arg) required_kw_only_args.append(arg)
elif not arg.is_self_arg and not arg.is_type_arg: elif not arg.is_self_arg and not arg.is_type_arg:
positional_args.append(arg) positional_args.append(arg)
if arg.pos_only:
num_pos_only_args += 1
# sort required kw-only args before optional ones to avoid special # sort required kw-only args before optional ones to avoid special
# cases in the unpacking code # cases in the unpacking code
...@@ -3691,10 +3694,11 @@ class DefNodeWrapper(FuncDefNode): ...@@ -3691,10 +3694,11 @@ class DefNodeWrapper(FuncDefNode):
code.putln('{') code.putln('{')
all_args = tuple(positional_args) + tuple(kw_only_args) all_args = tuple(positional_args) + tuple(kw_only_args)
non_posonly_args = [arg for arg in all_args if not arg.pos_only]
code.putln("static PyObject **%s[] = {%s};" % ( code.putln("static PyObject **%s[] = {%s};" % (
Naming.pykwdlist_cname, Naming.pykwdlist_cname,
','.join(['&%s' % code.intern_identifier(arg.name) ','.join(['&%s' % code.intern_identifier(arg.name)
for arg in all_args if not arg.pos_only] + ['0']))) for arg in non_posonly_args] + ['0'])))
# Before being converted and assigned to the target variables, # Before being converted and assigned to the target variables,
# borrowed references to all unpacked argument values are # borrowed references to all unpacked argument values are
...@@ -3706,13 +3710,41 @@ class DefNodeWrapper(FuncDefNode): ...@@ -3706,13 +3710,41 @@ class DefNodeWrapper(FuncDefNode):
# was passed for them. # was passed for them.
self.generate_argument_values_setup_code(all_args, code) self.generate_argument_values_setup_code(all_args, code)
# If all args are positional-only, we can raise an error
# straight away if we receive a non-empty kw-dict.
# This requires a PyDict_Size call. This call is wasteful
# for functions which do accept kw-args, so we do not generate
# the PyDict_Size call unless all args are positional-only.
accept_kwd_args = non_posonly_args or self.starstar_arg
if accept_kwd_args:
kw_unpacking_condition = Naming.kwds_cname
else:
kw_unpacking_condition = "%s && PyDict_Size(%s) > 0" % (
Naming.kwds_cname, Naming.kwds_cname)
# --- optimised code when we receive keyword arguments # --- optimised code when we receive keyword arguments
code.putln("if (%s(%s)) {" % ( code.putln("if (%s(%s)) {" % (
(self.num_required_kw_args > 0) and "likely" or "unlikely", (self.num_required_kw_args > 0) and "likely" or "unlikely",
Naming.kwds_cname)) kw_unpacking_condition))
self.generate_keyword_unpacking_code(
min_positional_args, max_positional_args, if accept_kwd_args:
has_fixed_positional_count, has_kw_only_args, all_args, argtuple_error_label, code) self.generate_keyword_unpacking_code(
min_positional_args, max_positional_args,
has_fixed_positional_count, has_kw_only_args, all_args, argtuple_error_label, code)
else:
# Here we do not accept kw-args but we are passed a non-empty kw-dict.
# We call ParseOptionalKeywords which will raise an appropriate error if
# the kw-args dict passed is non-empty (which it will be, since kw_unpacking_condition is true)
code.globalstate.use_utility_code(
UtilityCode.load_cached("ParseKeywords", "FunctionArguments.c"))
code.putln('if (likely(__Pyx_ParseOptionalKeywords(%s, %s, %s, %s, %s, "%s") < 0)) %s' % (
Naming.kwds_cname,
Naming.pykwdlist_cname,
self.starstar_arg and self.starstar_arg.entry.cname or '0',
'values',
0,
self.name,
code.error_goto(self.pos)))
# --- optimised code when we do not receive any keyword arguments # --- optimised code when we do not receive any keyword arguments
if (self.num_required_kw_args and min_positional_args > 0) or min_positional_args == max_positional_args: if (self.num_required_kw_args and min_positional_args > 0) or min_positional_args == max_positional_args:
...@@ -3877,18 +3909,42 @@ class DefNodeWrapper(FuncDefNode): ...@@ -3877,18 +3909,42 @@ class DefNodeWrapper(FuncDefNode):
def generate_keyword_unpacking_code(self, min_positional_args, max_positional_args, def generate_keyword_unpacking_code(self, min_positional_args, max_positional_args,
has_fixed_positional_count, has_fixed_positional_count,
has_kw_only_args, all_args, argtuple_error_label, code): has_kw_only_args, all_args, argtuple_error_label, code):
# First we count how many arguments must be passed as positional
num_required_posonly_args = num_pos_only_args = 0
for i, arg in enumerate(all_args):
if arg.pos_only:
num_pos_only_args += 1
if not arg.default:
num_required_posonly_args += 1
code.putln('Py_ssize_t kw_args;') code.putln('Py_ssize_t kw_args;')
code.putln('const Py_ssize_t pos_args = PyTuple_GET_SIZE(%s);' % Naming.args_cname) code.putln('const Py_ssize_t pos_args = PyTuple_GET_SIZE(%s);' % Naming.args_cname)
# copy the values from the args tuple and check that it's not too long # copy the values from the args tuple and check that it's not too long
code.putln('switch (pos_args) {') code.putln('switch (pos_args) {')
if self.star_arg: if self.star_arg:
code.putln('default:') code.putln('default:')
for i in range(max_positional_args-1, -1, -1):
for i in range(max_positional_args-1, num_required_posonly_args-1, -1):
code.put('case %2d: ' % (i+1)) code.put('case %2d: ' % (i+1))
code.putln("values[%d] = PyTuple_GET_ITEM(%s, %d);" % ( code.putln("values[%d] = PyTuple_GET_ITEM(%s, %d);" % (
i, Naming.args_cname, i)) i, Naming.args_cname, i))
code.putln('CYTHON_FALLTHROUGH;') code.putln('CYTHON_FALLTHROUGH;')
code.putln('case 0: break;') if num_required_posonly_args > 0:
code.put('case %2d: ' % num_required_posonly_args)
for i in range(num_required_posonly_args-1, -1, -1):
code.putln("values[%d] = PyTuple_GET_ITEM(%s, %d);" % (
i, Naming.args_cname, i))
code.putln('break;')
for i in range(num_required_posonly_args-2, -1, -1):
code.put('case %2d: ' % (i+1))
code.putln('CYTHON_FALLTHROUGH;')
code.put('case 0: ')
if num_required_posonly_args == 0:
code.putln('break;')
else:
# catch-all for not enough pos-only args passed
code.put_goto(argtuple_error_label)
if not self.star_arg: if not self.star_arg:
code.put('default: ') # more arguments than allowed code.put('default: ') # more arguments than allowed
code.put_goto(argtuple_error_label) code.put_goto(argtuple_error_label)
...@@ -3906,31 +3962,22 @@ class DefNodeWrapper(FuncDefNode): ...@@ -3906,31 +3962,22 @@ class DefNodeWrapper(FuncDefNode):
code.putln('kw_args = PyDict_Size(%s);' % Naming.kwds_cname) code.putln('kw_args = PyDict_Size(%s);' % Naming.kwds_cname)
if self.num_required_args or max_positional_args > 0: if self.num_required_args or max_positional_args > 0:
last_required_arg = -1 last_required_arg = -1
last_required_posonly_arg = -1
for i, arg in enumerate(all_args): for i, arg in enumerate(all_args):
if not arg.default: if not arg.default:
last_required_arg = i last_required_arg = i
if arg.pos_only and not arg.default:
last_required_posonly_arg = i
if last_required_arg < max_positional_args: if last_required_arg < max_positional_args:
last_required_arg = max_positional_args-1 last_required_arg = max_positional_args-1
if max_positional_args > 0: if max_positional_args > num_pos_only_args:
code.putln('switch (pos_args) {') code.putln('switch (pos_args) {')
for i, arg in enumerate(all_args[:last_required_arg+1]): for i, arg in enumerate(all_args[num_pos_only_args:last_required_arg+1], num_pos_only_args):
if max_positional_args > 0 and i <= max_positional_args: if max_positional_args > num_pos_only_args and i <= max_positional_args:
if i != 0: if i != num_pos_only_args:
code.putln('CYTHON_FALLTHROUGH;') code.putln('CYTHON_FALLTHROUGH;')
if self.star_arg and i == max_positional_args: if self.star_arg and i == max_positional_args:
code.putln('default:') code.putln('default:')
else: else:
code.putln('case %2d:' % i) code.putln('case %2d:' % i)
pystring_cname = code.intern_identifier(arg.name) pystring_cname = code.intern_identifier(arg.name)
if arg.pos_only:
if i == last_required_posonly_arg:
code.put_goto(argtuple_error_label)
elif i == last_required_arg:
code.putln('break;')
continue
if arg.default: if arg.default:
if arg.kw_only: if arg.kw_only:
# optional kw-only args are handled separately below # optional kw-only args are handled separately below
...@@ -3969,7 +4016,7 @@ class DefNodeWrapper(FuncDefNode): ...@@ -3969,7 +4016,7 @@ class DefNodeWrapper(FuncDefNode):
self.name, pystring_cname)) self.name, pystring_cname))
code.putln(code.error_goto(self.pos)) code.putln(code.error_goto(self.pos))
code.putln('}') code.putln('}')
if max_positional_args > 0: if max_positional_args > num_pos_only_args:
code.putln('}') code.putln('}')
if has_kw_only_args: if has_kw_only_args:
...@@ -3989,7 +4036,6 @@ class DefNodeWrapper(FuncDefNode): ...@@ -3989,7 +4036,6 @@ class DefNodeWrapper(FuncDefNode):
# ParseOptionalKeywords() needs to know how many of the arguments # ParseOptionalKeywords() needs to know how many of the arguments
# that could be passed as keywords have in fact been passed as # that could be passed as keywords have in fact been passed as
# positional args. # positional args.
num_pos_only_args = self.num_posonly_args
if num_pos_only_args > 0: if num_pos_only_args > 0:
# There are positional-only arguments which we don't want to count, # There are positional-only arguments which we don't want to count,
# since they cannot be keyword arguments. Subtract the number of # since they cannot be keyword arguments. Subtract the number of
...@@ -4033,14 +4079,17 @@ class DefNodeWrapper(FuncDefNode): ...@@ -4033,14 +4079,17 @@ class DefNodeWrapper(FuncDefNode):
def generate_optional_kwonly_args_unpacking_code(self, all_args, code): def generate_optional_kwonly_args_unpacking_code(self, all_args, code):
optional_args = [] optional_args = []
first_optional_arg = -1 first_optional_arg = -1
num_posonly_args = 0
for i, arg in enumerate(all_args): for i, arg in enumerate(all_args):
if arg.pos_only:
num_posonly_args += 1
if not arg.kw_only or not arg.default: if not arg.kw_only or not arg.default:
continue continue
if not optional_args: if not optional_args:
first_optional_arg = i first_optional_arg = i
optional_args.append(arg.name) optional_args.append(arg.name)
if self.num_posonly_args > 0: if num_posonly_args > 0:
posonly_correction = '-%d' % self.num_posonly_args posonly_correction = '-%d' % num_posonly_args
else: else:
posonly_correction = '' posonly_correction = ''
if optional_args: if optional_args:
......
...@@ -2,8 +2,6 @@ ...@@ -2,8 +2,6 @@
# mode: run # mode: run
# tag: posonly # tag: posonly
# TODO: remove posonly tag before merge
import cython import cython
import sys import sys
import pickle import pickle
...@@ -124,9 +122,9 @@ def test_use_positional_as_keyword2(a, /, b): ...@@ -124,9 +122,9 @@ def test_use_positional_as_keyword2(a, /, b):
def test_use_positional_as_keyword3(a, b, /): def test_use_positional_as_keyword3(a, b, /):
""" """
>>> test_use_positional_as_keyword3(1, 2) >>> test_use_positional_as_keyword3(1, 2)
>>> test_use_positional_as_keyword3(a=1, b=2) >>> test_use_positional_as_keyword3(a=1, b=2) # doctest:+ELLIPSIS
Traceback (most recent call last): Traceback (most recent call last):
TypeError: test_use_positional_as_keyword3() takes exactly 2 positional arguments (0 given) TypeError: test_use_positional_as_keyword3() got an unexpected keyword argument '...'
""" """
def test_positional_only_and_arg_invalid_calls(a, b, /, c): def test_positional_only_and_arg_invalid_calls(a, b, /, c):
...@@ -266,7 +264,7 @@ class TestPosonlyMethods(object): ...@@ -266,7 +264,7 @@ class TestPosonlyMethods(object):
Got type error Got type error
>>> TestPosonlyMethods().f(1, b=2) >>> TestPosonlyMethods().f(1, b=2)
Traceback (most recent call last): Traceback (most recent call last):
TypeError: f() takes exactly 3 positional arguments (2 given) TypeError: f() got an unexpected keyword argument 'b'
""" """
def f(self, a, b, /): def f(self, a, b, /):
return a, b return a, b
...@@ -365,9 +363,9 @@ def test_serialization1(a, b, /): ...@@ -365,9 +363,9 @@ def test_serialization1(a, b, /):
>>> unpickled_posonly = pickle.loads(pickled_posonly) >>> unpickled_posonly = pickle.loads(pickled_posonly)
>>> unpickled_posonly(1, 2) >>> unpickled_posonly(1, 2)
(1, 2) (1, 2)
>>> unpickled_posonly(a=1, b=2) >>> unpickled_posonly(a=1, b=2) # doctest: +ELLIPSIS
Traceback (most recent call last): Traceback (most recent call last):
TypeError: test_serialization1() takes exactly 2 positional arguments (0 given) TypeError: test_serialization1() got an unexpected keyword argument '...'
""" """
return (a, b) return (a, b)
...@@ -496,3 +494,65 @@ def f_call_one_optional_kwd(a,/,*,b=2): ...@@ -496,3 +494,65 @@ def f_call_one_optional_kwd(a,/,*,b=2):
(1, 3) (1, 3)
""" """
return (a,b) return (a,b)
def f_call_posonly_stararg(a,/,*args):
"""
>>> f_call_posonly_stararg(1)
(1, ())
>>> f_call_posonly_stararg(1, 2, 3, 4)
(1, (2, 3, 4))
"""
return (a,args)
def f_call_posonly_kwarg(a,/,**kw):
"""
>>> f_call_posonly_kwarg(1)
(1, {})
>>> all_args = f_call_posonly_kwarg(1, b=2, c=3, d=4)
>>> all_args == (1, {'b': 2, 'c': 3, 'd': 4}) or all_args
True
"""
return (a,kw)
def f_call_posonly_stararg_kwarg(a,/,*args,**kw):
"""
>>> f_call_posonly_stararg_kwarg(1)
(1, (), {})
>>> f_call_posonly_stararg_kwarg(1, 2)
(1, (2,), {})
>>> all_args = f_call_posonly_stararg_kwarg(1, b=3, c=4)
>>> all_args == (1, (), {'b': 3, 'c': 4}) or all_args
True
>>> all_args = f_call_posonly_stararg_kwarg(1, 2, b=3, c=4)
>>> all_args == (1, (2,), {'b': 3, 'c': 4}) or all_args
True
"""
return (a,args,kw)
def test_empty_kwargs(a, b, /):
"""
>>> test_empty_kwargs(1, 2)
(1, 2)
>>> test_empty_kwargs(1, 2, **{})
(1, 2)
>>> test_empty_kwargs(1, 2, **{'c': 3})
Traceback (most recent call last):
TypeError: test_empty_kwargs() got an unexpected keyword argument 'c'
"""
return (a,b)
cdef class TestExtensionClass:
"""
>>> t = TestExtensionClass()
>>> t.f(1,2)
(1, 2, 3)
>>> t.f(1,2,4)
(1, 2, 4)
>>> t.f(1, 2, c=4)
(1, 2, 4)
>>> t.f(1, 2, 5, c=6)
Traceback (most recent call last):
TypeError: f() got multiple values for keyword argument 'c'
"""
def f(self, a, b, /, c=3):
return (a,b,c)
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment