Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
157 changes: 157 additions & 0 deletions Lib/test/test_clinic.py
Original file line number Diff line number Diff line change
Expand Up @@ -626,6 +626,7 @@ def test_directive_output_invalid_command(self):
- 'impl_prototype'
- 'parser_prototype'
- 'parser_definition'
- 'vectorcall_definition'
- 'cpp_endif'
- 'methoddef_ifndef'
- 'impl_definition'
Expand Down Expand Up @@ -2496,6 +2497,162 @@ def test_duplicate_coexist(self):
"""
self.expect_failure(block, err, lineno=2)

def test_duplicate_vectorcall(self):
err = "Called @vectorcall twice"
block = """
module m
class Foo "FooObject *" ""
@vectorcall
@vectorcall
Foo.__init__
"""
self.expect_failure(block, err, lineno=3)

def test_vectorcall_on_regular_method(self):
err = "@vectorcall can only be used with __init__ and __new__ methods"
block = """
module m
class Foo "FooObject *" ""
@vectorcall
Foo.some_method
"""
self.expect_failure(block, err, lineno=3)

def test_vectorcall_on_module_function(self):
err = "@vectorcall can only be used with __init__ and __new__ methods"
block = """
module m
@vectorcall
m.fn
"""
self.expect_failure(block, err, lineno=2)

def test_vectorcall_on_init(self):
block = """
module m
class Foo "FooObject *" "Foo_Type"
@vectorcall
Foo.__init__
iterable: object = NULL
/
"""
func = self.parse_function(block, signatures_in_block=3,
function_index=2)
self.assertTrue(func.vectorcall)
self.assertFalse(func.vectorcall_exact_only)

def test_vectorcall_on_new(self):
block = """
module m
class Foo "FooObject *" "Foo_Type"
@classmethod
@vectorcall
Foo.__new__
x: object = NULL
/
"""
func = self.parse_function(block, signatures_in_block=3,
function_index=2)
self.assertTrue(func.vectorcall)
self.assertFalse(func.vectorcall_exact_only)

def test_vectorcall_exact_only(self):
block = """
module m
class Foo "FooObject *" "Foo_Type"
@vectorcall exact_only
Foo.__init__
iterable: object = NULL
/
"""
func = self.parse_function(block, signatures_in_block=3,
function_index=2)
self.assertTrue(func.vectorcall)
self.assertTrue(func.vectorcall_exact_only)

def test_vectorcall_init_with_kwargs(self):
block = """
module m
class Foo "FooObject *" "Foo_Type"
@vectorcall
Foo.__init__
source: object = NULL
encoding: str = NULL
errors: str = NULL
"""
func = self.parse_function(block, signatures_in_block=3,
function_index=2)
self.assertTrue(func.vectorcall)

def test_vectorcall_new_with_kwargs(self):
block = """
module m
class Foo "FooObject *" "Foo_Type"
@classmethod
@vectorcall
Foo.__new__
source: object = NULL
*
encoding: str = NULL
errors: str = NULL
"""
func = self.parse_function(block, signatures_in_block=3,
function_index=2)
self.assertTrue(func.vectorcall)

def test_vectorcall_init_no_args(self):
block = """
module m
class Foo "FooObject *" "Foo_Type"
@vectorcall
Foo.__init__
"""
func = self.parse_function(block, signatures_in_block=3,
function_index=2)
self.assertTrue(func.vectorcall)

def test_vectorcall_zero_arg(self):
block = """
module m
class Foo "FooObject *" "Foo_Type"
@classmethod
@vectorcall zero_arg=_PyFoo_GetEmpty()
Foo.__new__
x: object = NULL
/
"""
func = self.parse_function(block, signatures_in_block=3,
function_index=2)
self.assertTrue(func.vectorcall)
self.assertFalse(func.vectorcall_exact_only)
self.assertEqual(func.vectorcall_zero_arg, '_PyFoo_GetEmpty()')

def test_vectorcall_zero_arg_with_exact(self):
block = """
module m
class Foo "FooObject *" "Foo_Type"
@classmethod
@vectorcall exact_only zero_arg=get_cached()
Foo.__new__
x: object = NULL
/
"""
func = self.parse_function(block, signatures_in_block=3,
function_index=2)
self.assertTrue(func.vectorcall)
self.assertTrue(func.vectorcall_exact_only)
self.assertEqual(func.vectorcall_zero_arg, 'get_cached()')

def test_vectorcall_invalid_kwarg(self):
err = "unknown argument"
block = """
module m
class Foo "FooObject *" ""
@vectorcall bogus=True
Foo.__init__
"""
self.expect_failure(block, err, lineno=2)

def test_unused_param(self):
block = self.parse("""
module foo
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Add a ``@vectorcall`` decorator to Argument Clinic that can be used on
``__init__`` and ``__new__`` which generates :ref:`vectorcall` argument
parsing.
1 change: 1 addition & 0 deletions Tools/clinic/libclinic/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ def __init__(
'impl_prototype': d('file'),
'parser_prototype': d('suppress'),
'parser_definition': d('file'),
'vectorcall_definition': d('file'),
'cpp_endif': d('file'),
'methoddef_ifndef': d('file', 1),
'impl_definition': d('block'),
Expand Down
20 changes: 19 additions & 1 deletion Tools/clinic/libclinic/clanguage.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
from libclinic.function import (
Module, Class, Function, Parameter,
permute_optional_groups,
GETTER, SETTER, METHOD_INIT)
GETTER, SETTER, METHOD_INIT, METHOD_NEW)
from libclinic.converters import self_converter
from libclinic.parse_args import ParseArgsCodeGen
if TYPE_CHECKING:
Expand Down Expand Up @@ -478,6 +478,24 @@ def render_function(
template_dict['parser_parameters'] = ", ".join(data.impl_parameters[1:])
template_dict['impl_arguments'] = ", ".join(data.impl_arguments)

# Vectorcall impl arguments: replace self/type with the appropriate
# expression for the vectorcall calling convention.
if f.vectorcall and f.cls:
if f.kind is METHOD_INIT:
# For __init__: self is a locally-allocated PyObject*
vc_first = f"({f.cls.typedef})self"
elif f.kind is METHOD_NEW:
# For __new__: type is PyObject* in vectorcall, need cast
vc_first = "_PyType_CAST(type)"
else:
raise AssertionError(
f"Unhandled function kind for vectorcall: {f.kind!r}"
)
vc_impl_args = [vc_first] + data.impl_arguments[1:]
template_dict['vc_impl_arguments'] = ", ".join(vc_impl_args)
else:
template_dict['vc_impl_arguments'] = ""

template_dict['return_conversion'] = libclinic.format_escape("".join(data.return_conversion).rstrip())
template_dict['post_parsing'] = libclinic.format_escape("".join(data.post_parsing).rstrip())
template_dict['cleanup'] = libclinic.format_escape("".join(data.cleanup))
Expand Down
30 changes: 29 additions & 1 deletion Tools/clinic/libclinic/dsl_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,9 @@ def reset(self) -> None:
self.critical_section = False
self.target_critical_section = []
self.disable_fastcall = False
self.vectorcall = False
self.vectorcall_exact_only = False
self.vectorcall_zero_arg = ''
Comment on lines +305 to +307
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if we should collect these in a "vectorcall config dataclass". The stuff in this file is already so cluttered with tons of class members and local variables.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure it would help readability much for introducing a new to this bit of code pattern.

Would be really nice to refactor the decorator parsing -> argument functions (at_*), they are really repetative to me at the moment; would be nice to not have to do custom key=value parsing in the new at_vectorcall.

self.permit_long_summary = False
self.permit_long_docstring_body = False

Expand Down Expand Up @@ -466,6 +469,24 @@ def at_staticmethod(self) -> None:
fail("Can't set @staticmethod, function is not a normal callable")
self.kind = STATIC_METHOD

def at_vectorcall(self, *args: str) -> None:
if self.vectorcall:
fail("Called @vectorcall twice!")
self.vectorcall = True
for arg in args:
if '=' in arg:
key, value = arg.split('=', 1)
else:
key, value = arg, ''
if key == 'exact_only':
self.vectorcall_exact_only = True
elif key == 'zero_arg':
if not value:
fail("@vectorcall zero_arg requires a value")
self.vectorcall_zero_arg = value
else:
fail(f"@vectorcall: unknown argument {key!r}")

def at_coexist(self) -> None:
if self.coexist:
fail("Called @coexist twice!")
Expand Down Expand Up @@ -599,6 +620,10 @@ def normalize_function_kind(self, fullname: str) -> None:
elif name == '__init__':
self.kind = METHOD_INIT

# Validate @vectorcall usage.
if self.vectorcall and not self.kind.new_or_init:
fail("@vectorcall can only be used with __init__ and __new__ methods currently")

def resolve_return_converter(
self, full_name: str, forced_converter: str
) -> CReturnConverter:
Expand Down Expand Up @@ -723,7 +748,10 @@ def state_modulename_name(self, line: str) -> None:
critical_section=self.critical_section,
disable_fastcall=self.disable_fastcall,
target_critical_section=self.target_critical_section,
forced_text_signature=self.forced_text_signature
forced_text_signature=self.forced_text_signature,
vectorcall=self.vectorcall,
vectorcall_exact_only=self.vectorcall_exact_only,
vectorcall_zero_arg=self.vectorcall_zero_arg,
)
self.add_function(func)

Expand Down
3 changes: 3 additions & 0 deletions Tools/clinic/libclinic/function.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,9 @@ class Function:
critical_section: bool = False
disable_fastcall: bool = False
target_critical_section: list[str] = dc.field(default_factory=list)
vectorcall: bool = False
vectorcall_exact_only: bool = False
vectorcall_zero_arg: str = ''

def __post_init__(self) -> None:
self.parent = self.cls or self.module
Expand Down
Loading
Loading