cpython/Tools/clinic/libclinic/clanguage.py

535 lines
20 KiB
Python

from __future__ import annotations
import itertools
import sys
import textwrap
from typing import TYPE_CHECKING, Literal, Final
from operator import attrgetter
from collections.abc import Iterable
import libclinic
from libclinic import (
unspecified, fail, Sentinels, VersionTuple)
from libclinic.codegen import CRenderData, TemplateDict, CodeGen
from libclinic.language import Language
from libclinic.function import (
Module, Class, Function, Parameter,
permute_optional_groups,
GETTER, SETTER, METHOD_INIT)
from libclinic.converters import self_converter
from libclinic.parse_args import ParseArgsCodeGen
if TYPE_CHECKING:
from libclinic.app import Clinic
def c_id(name: str) -> str:
if len(name) == 1 and ord(name) < 256:
if name.isalnum():
return f"_Py_LATIN1_CHR('{name}')"
else:
return f'_Py_LATIN1_CHR({ord(name)})'
else:
return f'&_Py_ID({name})'
class CLanguage(Language):
body_prefix = "#"
language = 'C'
start_line = "/*[{dsl_name} input]"
body_prefix = ""
stop_line = "[{dsl_name} start generated code]*/"
checksum_line = "/*[{dsl_name} end generated code: {arguments}]*/"
COMPILER_DEPRECATION_WARNING_PROTOTYPE: Final[str] = r"""
// Emit compiler warnings when we get to Python {major}.{minor}.
#if PY_VERSION_HEX >= 0x{major:02x}{minor:02x}00C0
# error {message}
#elif PY_VERSION_HEX >= 0x{major:02x}{minor:02x}00A0
# ifdef _MSC_VER
# pragma message ({message})
# else
# warning {message}
# endif
#endif
"""
DEPRECATION_WARNING_PROTOTYPE: Final[str] = r"""
if ({condition}) {{{{{errcheck}
if (PyErr_WarnEx(PyExc_DeprecationWarning,
{message}, 1))
{{{{
goto exit;
}}}}
}}}}
"""
def __init__(self, filename: str) -> None:
super().__init__(filename)
self.cpp = libclinic.cpp.Monitor(filename)
def parse_line(self, line: str) -> None:
self.cpp.writeline(line)
def render(
self,
clinic: Clinic,
signatures: Iterable[Module | Class | Function]
) -> str:
function = None
for o in signatures:
if isinstance(o, Function):
if function:
fail("You may specify at most one function per block.\nFound a block containing at least two:\n\t" + repr(function) + " and " + repr(o))
function = o
return self.render_function(clinic, function)
def compiler_deprecated_warning(
self,
func: Function,
parameters: list[Parameter],
) -> str | None:
minversion: VersionTuple | None = None
for p in parameters:
for version in p.deprecated_positional, p.deprecated_keyword:
if version and (not minversion or minversion > version):
minversion = version
if not minversion:
return None
# Format the preprocessor warning and error messages.
assert isinstance(self.cpp.filename, str)
message = f"Update the clinic input of {func.full_name!r}."
code = self.COMPILER_DEPRECATION_WARNING_PROTOTYPE.format(
major=minversion[0],
minor=minversion[1],
message=libclinic.c_repr(message),
)
return libclinic.normalize_snippet(code)
def deprecate_positional_use(
self,
func: Function,
params: dict[int, Parameter],
) -> str:
assert len(params) > 0
first_pos = next(iter(params))
last_pos = next(reversed(params))
# Format the deprecation message.
if len(params) == 1:
condition = f"nargs == {first_pos+1}"
amount = f"{first_pos+1} " if first_pos else ""
pl = "s"
else:
condition = f"nargs > {first_pos} && nargs <= {last_pos+1}"
amount = f"more than {first_pos} " if first_pos else ""
pl = "s" if first_pos != 1 else ""
message = (
f"Passing {amount}positional argument{pl} to "
f"{func.fulldisplayname}() is deprecated."
)
for (major, minor), group in itertools.groupby(
params.values(), key=attrgetter("deprecated_positional")
):
names = [repr(p.name) for p in group]
pstr = libclinic.pprint_words(names)
if len(names) == 1:
message += (
f" Parameter {pstr} will become a keyword-only parameter "
f"in Python {major}.{minor}."
)
else:
message += (
f" Parameters {pstr} will become keyword-only parameters "
f"in Python {major}.{minor}."
)
# Append deprecation warning to docstring.
docstring = textwrap.fill(f"Note: {message}")
func.docstring += f"\n\n{docstring}\n"
# Format and return the code block.
code = self.DEPRECATION_WARNING_PROTOTYPE.format(
condition=condition,
errcheck="",
message=libclinic.wrapped_c_string_literal(message, width=64,
subsequent_indent=20),
)
return libclinic.normalize_snippet(code, indent=4)
def deprecate_keyword_use(
self,
func: Function,
params: dict[int, Parameter],
argname_fmt: str | None = None,
*,
fastcall: bool,
codegen: CodeGen,
) -> str:
assert len(params) > 0
last_param = next(reversed(params.values()))
limited_capi = codegen.limited_capi
# Format the deprecation message.
containscheck = ""
conditions = []
for i, p in params.items():
if p.is_optional():
if argname_fmt:
conditions.append(f"nargs < {i+1} && {argname_fmt % i}")
elif fastcall:
conditions.append(f"nargs < {i+1} && PySequence_Contains(kwnames, {c_id(p.name)})")
containscheck = "PySequence_Contains"
codegen.add_include('pycore_runtime.h', '_Py_ID()')
else:
conditions.append(f"nargs < {i+1} && PyDict_Contains(kwargs, {c_id(p.name)})")
containscheck = "PyDict_Contains"
codegen.add_include('pycore_runtime.h', '_Py_ID()')
else:
conditions = [f"nargs < {i+1}"]
condition = ") || (".join(conditions)
if len(conditions) > 1:
condition = f"(({condition}))"
if last_param.is_optional():
if fastcall:
if limited_capi:
condition = f"kwnames && PyTuple_Size(kwnames) && {condition}"
else:
condition = f"kwnames && PyTuple_GET_SIZE(kwnames) && {condition}"
else:
if limited_capi:
condition = f"kwargs && PyDict_Size(kwargs) && {condition}"
else:
condition = f"kwargs && PyDict_GET_SIZE(kwargs) && {condition}"
names = [repr(p.name) for p in params.values()]
pstr = libclinic.pprint_words(names)
pl = 's' if len(params) != 1 else ''
message = (
f"Passing keyword argument{pl} {pstr} to "
f"{func.fulldisplayname}() is deprecated."
)
for (major, minor), group in itertools.groupby(
params.values(), key=attrgetter("deprecated_keyword")
):
names = [repr(p.name) for p in group]
pstr = libclinic.pprint_words(names)
pl = 's' if len(names) != 1 else ''
message += (
f" Parameter{pl} {pstr} will become positional-only "
f"in Python {major}.{minor}."
)
if containscheck:
errcheck = f"""
if (PyErr_Occurred()) {{{{ // {containscheck}() above can fail
goto exit;
}}}}"""
else:
errcheck = ""
if argname_fmt:
# Append deprecation warning to docstring.
docstring = textwrap.fill(f"Note: {message}")
func.docstring += f"\n\n{docstring}\n"
# Format and return the code block.
code = self.DEPRECATION_WARNING_PROTOTYPE.format(
condition=condition,
errcheck=errcheck,
message=libclinic.wrapped_c_string_literal(message, width=64,
subsequent_indent=20),
)
return libclinic.normalize_snippet(code, indent=4)
def output_templates(
self,
f: Function,
codegen: CodeGen,
) -> dict[str, str]:
args = ParseArgsCodeGen(f, codegen)
return args.parse_args(self)
@staticmethod
def group_to_variable_name(group: int) -> str:
adjective = "left_" if group < 0 else "right_"
return "group_" + adjective + str(abs(group))
def render_option_group_parsing(
self,
f: Function,
template_dict: TemplateDict,
limited_capi: bool,
) -> None:
# positional only, grouped, optional arguments!
# can be optional on the left or right.
# here's an example:
#
# [ [ [ A1 A2 ] B1 B2 B3 ] C1 C2 ] D1 D2 D3 [ E1 E2 E3 [ F1 F2 F3 ] ]
#
# Here group D are required, and all other groups are optional.
# (Group D's "group" is actually None.)
# We can figure out which sets of arguments we have based on
# how many arguments are in the tuple.
#
# Note that you need to count up on both sides. For example,
# you could have groups C+D, or C+D+E, or C+D+E+F.
#
# What if the number of arguments leads us to an ambiguous result?
# Clinic prefers groups on the left. So in the above example,
# five arguments would map to B+C, not C+D.
out = []
parameters = list(f.parameters.values())
if isinstance(parameters[0].converter, self_converter):
del parameters[0]
group: list[Parameter] | None = None
left = []
right = []
required: list[Parameter] = []
last: int | Literal[Sentinels.unspecified] = unspecified
for p in parameters:
group_id = p.group
if group_id != last:
last = group_id
group = []
if group_id < 0:
left.append(group)
elif group_id == 0:
group = required
else:
right.append(group)
assert group is not None
group.append(p)
count_min = sys.maxsize
count_max = -1
if limited_capi:
nargs = 'PyTuple_Size(args)'
else:
nargs = 'PyTuple_GET_SIZE(args)'
out.append(f"switch ({nargs}) {{\n")
for subset in permute_optional_groups(left, required, right):
count = len(subset)
count_min = min(count_min, count)
count_max = max(count_max, count)
if count == 0:
out.append(""" case 0:
break;
""")
continue
group_ids = {p.group for p in subset} # eliminate duplicates
d: dict[str, str | int] = {}
d['count'] = count
d['name'] = f.name
d['format_units'] = "".join(p.converter.format_unit for p in subset)
parse_arguments: list[str] = []
for p in subset:
p.converter.parse_argument(parse_arguments)
d['parse_arguments'] = ", ".join(parse_arguments)
group_ids.discard(0)
lines = "\n".join([
self.group_to_variable_name(g) + " = 1;"
for g in group_ids
])
s = """\
case {count}:
if (!PyArg_ParseTuple(args, "{format_units}:{name}", {parse_arguments})) {{
goto exit;
}}
{group_booleans}
break;
"""
s = libclinic.linear_format(s, group_booleans=lines)
s = s.format_map(d)
out.append(s)
out.append(" default:\n")
s = ' PyErr_SetString(PyExc_TypeError, "{} requires {} to {} arguments");\n'
out.append(s.format(f.full_name, count_min, count_max))
out.append(' goto exit;\n')
out.append("}")
template_dict['option_group_parsing'] = libclinic.format_escape("".join(out))
def render_function(
self,
clinic: Clinic,
f: Function | None
) -> str:
if f is None:
return ""
codegen = clinic.codegen
data = CRenderData()
assert f.parameters, "We should always have a 'self' at this point!"
parameters = f.render_parameters
converters = [p.converter for p in parameters]
templates = self.output_templates(f, codegen)
f_self = parameters[0]
selfless = parameters[1:]
assert isinstance(f_self.converter, self_converter), "No self parameter in " + repr(f.full_name) + "!"
if f.critical_section:
match len(f.target_critical_section):
case 0:
lock = 'Py_BEGIN_CRITICAL_SECTION({self_name});'
unlock = 'Py_END_CRITICAL_SECTION();'
case 1:
lock = 'Py_BEGIN_CRITICAL_SECTION({target_critical_section});'
unlock = 'Py_END_CRITICAL_SECTION();'
case _:
lock = 'Py_BEGIN_CRITICAL_SECTION2({target_critical_section});'
unlock = 'Py_END_CRITICAL_SECTION2();'
data.lock.append(lock)
data.unlock.append(unlock)
last_group = 0
first_optional = len(selfless)
positional = selfless and selfless[-1].is_positional_only()
has_option_groups = False
# offset i by -1 because first_optional needs to ignore self
for i, p in enumerate(parameters, -1):
c = p.converter
if (i != -1) and (p.default is not unspecified):
first_optional = min(first_optional, i)
# insert group variable
group = p.group
if last_group != group:
last_group = group
if group:
group_name = self.group_to_variable_name(group)
data.impl_arguments.append(group_name)
data.declarations.append("int " + group_name + " = 0;")
data.impl_parameters.append("int " + group_name)
has_option_groups = True
c.render(p, data)
if has_option_groups and (not positional):
fail("You cannot use optional groups ('[' and ']') "
"unless all parameters are positional-only ('/').")
# HACK
# when we're METH_O, but have a custom return converter,
# we use "impl_parameters" for the parsing function
# because that works better. but that means we must
# suppress actually declaring the impl's parameters
# as variables in the parsing function. but since it's
# METH_O, we have exactly one anyway, so we know exactly
# where it is.
if ("METH_O" in templates['methoddef_define'] and
'{impl_parameters}' in templates['parser_prototype']):
data.declarations.pop(0)
full_name = f.full_name
template_dict = {'full_name': full_name}
template_dict['name'] = f.displayname
if f.kind in {GETTER, SETTER}:
template_dict['getset_name'] = f.c_basename.upper()
template_dict['getset_basename'] = f.c_basename
if f.kind is GETTER:
template_dict['c_basename'] = f.c_basename + "_get"
elif f.kind is SETTER:
template_dict['c_basename'] = f.c_basename + "_set"
# Implicitly add the setter value parameter.
data.impl_parameters.append("PyObject *value")
data.impl_arguments.append("value")
else:
template_dict['methoddef_name'] = f.c_basename.upper() + "_METHODDEF"
template_dict['c_basename'] = f.c_basename
template_dict['docstring'] = libclinic.docstring_for_c_string(f.docstring)
template_dict['self_name'] = template_dict['self_type'] = template_dict['self_type_check'] = ''
template_dict['target_critical_section'] = ', '.join(f.target_critical_section)
for converter in converters:
converter.set_template_dict(template_dict)
if f.kind not in {SETTER, METHOD_INIT}:
f.return_converter.render(f, data)
template_dict['impl_return_type'] = f.return_converter.type
template_dict['declarations'] = libclinic.format_escape("\n".join(data.declarations))
template_dict['initializers'] = "\n\n".join(data.initializers)
template_dict['modifications'] = '\n\n'.join(data.modifications)
template_dict['keywords_c'] = ' '.join('"' + k + '",'
for k in data.keywords)
keywords = [k for k in data.keywords if k]
template_dict['keywords_py'] = ' '.join(c_id(k) + ','
for k in keywords)
template_dict['format_units'] = ''.join(data.format_units)
template_dict['parse_arguments'] = ', '.join(data.parse_arguments)
if data.parse_arguments:
template_dict['parse_arguments_comma'] = ',';
else:
template_dict['parse_arguments_comma'] = '';
template_dict['impl_parameters'] = ", ".join(data.impl_parameters)
template_dict['impl_arguments'] = ", ".join(data.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))
template_dict['return_value'] = data.return_value
template_dict['lock'] = "\n".join(data.lock)
template_dict['unlock'] = "\n".join(data.unlock)
# used by unpack tuple code generator
unpack_min = first_optional
unpack_max = len(selfless)
template_dict['unpack_min'] = str(unpack_min)
template_dict['unpack_max'] = str(unpack_max)
if has_option_groups:
self.render_option_group_parsing(f, template_dict,
limited_capi=codegen.limited_capi)
# buffers, not destination
for name, destination in clinic.destination_buffers.items():
template = templates[name]
if has_option_groups:
template = libclinic.linear_format(template,
option_group_parsing=template_dict['option_group_parsing'])
template = libclinic.linear_format(template,
declarations=template_dict['declarations'],
return_conversion=template_dict['return_conversion'],
initializers=template_dict['initializers'],
modifications=template_dict['modifications'],
post_parsing=template_dict['post_parsing'],
cleanup=template_dict['cleanup'],
lock=template_dict['lock'],
unlock=template_dict['unlock'],
)
# Only generate the "exit:" label
# if we have any gotos
label = "exit:" if "goto exit;" in template else ""
template = libclinic.linear_format(template, exit_label=label)
s = template.format_map(template_dict)
# mild hack:
# reflow long impl declarations
if name in {"impl_prototype", "impl_definition"}:
s = libclinic.wrap_declarations(s)
if clinic.line_prefix:
s = libclinic.indent_all_lines(s, clinic.line_prefix)
if clinic.line_suffix:
s = libclinic.suffix_all_lines(s, clinic.line_suffix)
destination.append(s)
return clinic.get_destination('block').dump()