1from __future__ import annotations 2import itertools 3import sys 4import textwrap 5from typing import TYPE_CHECKING, Literal, Final 6from operator import attrgetter 7from collections.abc import Iterable 8 9import libclinic 10from libclinic import ( 11 unspecified, fail, Sentinels, VersionTuple) 12from libclinic.codegen import CRenderData, TemplateDict, CodeGen 13from libclinic.language import Language 14from libclinic.function import ( 15 Module, Class, Function, Parameter, 16 permute_optional_groups, 17 GETTER, SETTER, METHOD_INIT) 18from libclinic.converters import self_converter 19from libclinic.parse_args import ParseArgsCodeGen 20if TYPE_CHECKING: 21 from libclinic.app import Clinic 22 23 24def c_id(name: str) -> str: 25 if len(name) == 1 and ord(name) < 256: 26 if name.isalnum(): 27 return f"_Py_LATIN1_CHR('{name}')" 28 else: 29 return f'_Py_LATIN1_CHR({ord(name)})' 30 else: 31 return f'&_Py_ID({name})' 32 33 34class CLanguage(Language): 35 36 body_prefix = "#" 37 language = 'C' 38 start_line = "/*[{dsl_name} input]" 39 body_prefix = "" 40 stop_line = "[{dsl_name} start generated code]*/" 41 checksum_line = "/*[{dsl_name} end generated code: {arguments}]*/" 42 43 COMPILER_DEPRECATION_WARNING_PROTOTYPE: Final[str] = r""" 44 // Emit compiler warnings when we get to Python {major}.{minor}. 45 #if PY_VERSION_HEX >= 0x{major:02x}{minor:02x}00C0 46 # error {message} 47 #elif PY_VERSION_HEX >= 0x{major:02x}{minor:02x}00A0 48 # ifdef _MSC_VER 49 # pragma message ({message}) 50 # else 51 # warning {message} 52 # endif 53 #endif 54 """ 55 DEPRECATION_WARNING_PROTOTYPE: Final[str] = r""" 56 if ({condition}) {{{{{errcheck} 57 if (PyErr_WarnEx(PyExc_DeprecationWarning, 58 {message}, 1)) 59 {{{{ 60 goto exit; 61 }}}} 62 }}}} 63 """ 64 65 def __init__(self, filename: str) -> None: 66 super().__init__(filename) 67 self.cpp = libclinic.cpp.Monitor(filename) 68 69 def parse_line(self, line: str) -> None: 70 self.cpp.writeline(line) 71 72 def render( 73 self, 74 clinic: Clinic, 75 signatures: Iterable[Module | Class | Function] 76 ) -> str: 77 function = None 78 for o in signatures: 79 if isinstance(o, Function): 80 if function: 81 fail("You may specify at most one function per block.\nFound a block containing at least two:\n\t" + repr(function) + " and " + repr(o)) 82 function = o 83 return self.render_function(clinic, function) 84 85 def compiler_deprecated_warning( 86 self, 87 func: Function, 88 parameters: list[Parameter], 89 ) -> str | None: 90 minversion: VersionTuple | None = None 91 for p in parameters: 92 for version in p.deprecated_positional, p.deprecated_keyword: 93 if version and (not minversion or minversion > version): 94 minversion = version 95 if not minversion: 96 return None 97 98 # Format the preprocessor warning and error messages. 99 assert isinstance(self.cpp.filename, str) 100 message = f"Update the clinic input of {func.full_name!r}." 101 code = self.COMPILER_DEPRECATION_WARNING_PROTOTYPE.format( 102 major=minversion[0], 103 minor=minversion[1], 104 message=libclinic.c_repr(message), 105 ) 106 return libclinic.normalize_snippet(code) 107 108 def deprecate_positional_use( 109 self, 110 func: Function, 111 params: dict[int, Parameter], 112 ) -> str: 113 assert len(params) > 0 114 first_pos = next(iter(params)) 115 last_pos = next(reversed(params)) 116 117 # Format the deprecation message. 118 if len(params) == 1: 119 condition = f"nargs == {first_pos+1}" 120 amount = f"{first_pos+1} " if first_pos else "" 121 pl = "s" 122 else: 123 condition = f"nargs > {first_pos} && nargs <= {last_pos+1}" 124 amount = f"more than {first_pos} " if first_pos else "" 125 pl = "s" if first_pos != 1 else "" 126 message = ( 127 f"Passing {amount}positional argument{pl} to " 128 f"{func.fulldisplayname}() is deprecated." 129 ) 130 131 for (major, minor), group in itertools.groupby( 132 params.values(), key=attrgetter("deprecated_positional") 133 ): 134 names = [repr(p.name) for p in group] 135 pstr = libclinic.pprint_words(names) 136 if len(names) == 1: 137 message += ( 138 f" Parameter {pstr} will become a keyword-only parameter " 139 f"in Python {major}.{minor}." 140 ) 141 else: 142 message += ( 143 f" Parameters {pstr} will become keyword-only parameters " 144 f"in Python {major}.{minor}." 145 ) 146 147 # Append deprecation warning to docstring. 148 docstring = textwrap.fill(f"Note: {message}") 149 func.docstring += f"\n\n{docstring}\n" 150 # Format and return the code block. 151 code = self.DEPRECATION_WARNING_PROTOTYPE.format( 152 condition=condition, 153 errcheck="", 154 message=libclinic.wrapped_c_string_literal(message, width=64, 155 subsequent_indent=20), 156 ) 157 return libclinic.normalize_snippet(code, indent=4) 158 159 def deprecate_keyword_use( 160 self, 161 func: Function, 162 params: dict[int, Parameter], 163 argname_fmt: str | None = None, 164 *, 165 fastcall: bool, 166 codegen: CodeGen, 167 ) -> str: 168 assert len(params) > 0 169 last_param = next(reversed(params.values())) 170 limited_capi = codegen.limited_capi 171 172 # Format the deprecation message. 173 containscheck = "" 174 conditions = [] 175 for i, p in params.items(): 176 if p.is_optional(): 177 if argname_fmt: 178 conditions.append(f"nargs < {i+1} && {argname_fmt % i}") 179 elif fastcall: 180 conditions.append(f"nargs < {i+1} && PySequence_Contains(kwnames, {c_id(p.name)})") 181 containscheck = "PySequence_Contains" 182 codegen.add_include('pycore_runtime.h', '_Py_ID()') 183 else: 184 conditions.append(f"nargs < {i+1} && PyDict_Contains(kwargs, {c_id(p.name)})") 185 containscheck = "PyDict_Contains" 186 codegen.add_include('pycore_runtime.h', '_Py_ID()') 187 else: 188 conditions = [f"nargs < {i+1}"] 189 condition = ") || (".join(conditions) 190 if len(conditions) > 1: 191 condition = f"(({condition}))" 192 if last_param.is_optional(): 193 if fastcall: 194 if limited_capi: 195 condition = f"kwnames && PyTuple_Size(kwnames) && {condition}" 196 else: 197 condition = f"kwnames && PyTuple_GET_SIZE(kwnames) && {condition}" 198 else: 199 if limited_capi: 200 condition = f"kwargs && PyDict_Size(kwargs) && {condition}" 201 else: 202 condition = f"kwargs && PyDict_GET_SIZE(kwargs) && {condition}" 203 names = [repr(p.name) for p in params.values()] 204 pstr = libclinic.pprint_words(names) 205 pl = 's' if len(params) != 1 else '' 206 message = ( 207 f"Passing keyword argument{pl} {pstr} to " 208 f"{func.fulldisplayname}() is deprecated." 209 ) 210 211 for (major, minor), group in itertools.groupby( 212 params.values(), key=attrgetter("deprecated_keyword") 213 ): 214 names = [repr(p.name) for p in group] 215 pstr = libclinic.pprint_words(names) 216 pl = 's' if len(names) != 1 else '' 217 message += ( 218 f" Parameter{pl} {pstr} will become positional-only " 219 f"in Python {major}.{minor}." 220 ) 221 222 if containscheck: 223 errcheck = f""" 224 if (PyErr_Occurred()) {{{{ // {containscheck}() above can fail 225 goto exit; 226 }}}}""" 227 else: 228 errcheck = "" 229 if argname_fmt: 230 # Append deprecation warning to docstring. 231 docstring = textwrap.fill(f"Note: {message}") 232 func.docstring += f"\n\n{docstring}\n" 233 # Format and return the code block. 234 code = self.DEPRECATION_WARNING_PROTOTYPE.format( 235 condition=condition, 236 errcheck=errcheck, 237 message=libclinic.wrapped_c_string_literal(message, width=64, 238 subsequent_indent=20), 239 ) 240 return libclinic.normalize_snippet(code, indent=4) 241 242 def output_templates( 243 self, 244 f: Function, 245 codegen: CodeGen, 246 ) -> dict[str, str]: 247 args = ParseArgsCodeGen(f, codegen) 248 return args.parse_args(self) 249 250 @staticmethod 251 def group_to_variable_name(group: int) -> str: 252 adjective = "left_" if group < 0 else "right_" 253 return "group_" + adjective + str(abs(group)) 254 255 def render_option_group_parsing( 256 self, 257 f: Function, 258 template_dict: TemplateDict, 259 limited_capi: bool, 260 ) -> None: 261 # positional only, grouped, optional arguments! 262 # can be optional on the left or right. 263 # here's an example: 264 # 265 # [ [ [ A1 A2 ] B1 B2 B3 ] C1 C2 ] D1 D2 D3 [ E1 E2 E3 [ F1 F2 F3 ] ] 266 # 267 # Here group D are required, and all other groups are optional. 268 # (Group D's "group" is actually None.) 269 # We can figure out which sets of arguments we have based on 270 # how many arguments are in the tuple. 271 # 272 # Note that you need to count up on both sides. For example, 273 # you could have groups C+D, or C+D+E, or C+D+E+F. 274 # 275 # What if the number of arguments leads us to an ambiguous result? 276 # Clinic prefers groups on the left. So in the above example, 277 # five arguments would map to B+C, not C+D. 278 279 out = [] 280 parameters = list(f.parameters.values()) 281 if isinstance(parameters[0].converter, self_converter): 282 del parameters[0] 283 284 group: list[Parameter] | None = None 285 left = [] 286 right = [] 287 required: list[Parameter] = [] 288 last: int | Literal[Sentinels.unspecified] = unspecified 289 290 for p in parameters: 291 group_id = p.group 292 if group_id != last: 293 last = group_id 294 group = [] 295 if group_id < 0: 296 left.append(group) 297 elif group_id == 0: 298 group = required 299 else: 300 right.append(group) 301 assert group is not None 302 group.append(p) 303 304 count_min = sys.maxsize 305 count_max = -1 306 307 if limited_capi: 308 nargs = 'PyTuple_Size(args)' 309 else: 310 nargs = 'PyTuple_GET_SIZE(args)' 311 out.append(f"switch ({nargs}) {{\n") 312 for subset in permute_optional_groups(left, required, right): 313 count = len(subset) 314 count_min = min(count_min, count) 315 count_max = max(count_max, count) 316 317 if count == 0: 318 out.append(""" case 0: 319 break; 320""") 321 continue 322 323 group_ids = {p.group for p in subset} # eliminate duplicates 324 d: dict[str, str | int] = {} 325 d['count'] = count 326 d['name'] = f.name 327 d['format_units'] = "".join(p.converter.format_unit for p in subset) 328 329 parse_arguments: list[str] = [] 330 for p in subset: 331 p.converter.parse_argument(parse_arguments) 332 d['parse_arguments'] = ", ".join(parse_arguments) 333 334 group_ids.discard(0) 335 lines = "\n".join([ 336 self.group_to_variable_name(g) + " = 1;" 337 for g in group_ids 338 ]) 339 340 s = """\ 341 case {count}: 342 if (!PyArg_ParseTuple(args, "{format_units}:{name}", {parse_arguments})) {{ 343 goto exit; 344 }} 345 {group_booleans} 346 break; 347""" 348 s = libclinic.linear_format(s, group_booleans=lines) 349 s = s.format_map(d) 350 out.append(s) 351 352 out.append(" default:\n") 353 s = ' PyErr_SetString(PyExc_TypeError, "{} requires {} to {} arguments");\n' 354 out.append(s.format(f.full_name, count_min, count_max)) 355 out.append(' goto exit;\n') 356 out.append("}") 357 358 template_dict['option_group_parsing'] = libclinic.format_escape("".join(out)) 359 360 def render_function( 361 self, 362 clinic: Clinic, 363 f: Function | None 364 ) -> str: 365 if f is None: 366 return "" 367 368 codegen = clinic.codegen 369 data = CRenderData() 370 371 assert f.parameters, "We should always have a 'self' at this point!" 372 parameters = f.render_parameters 373 converters = [p.converter for p in parameters] 374 375 templates = self.output_templates(f, codegen) 376 377 f_self = parameters[0] 378 selfless = parameters[1:] 379 assert isinstance(f_self.converter, self_converter), "No self parameter in " + repr(f.full_name) + "!" 380 381 if f.critical_section: 382 match len(f.target_critical_section): 383 case 0: 384 lock = 'Py_BEGIN_CRITICAL_SECTION({self_name});' 385 unlock = 'Py_END_CRITICAL_SECTION();' 386 case 1: 387 lock = 'Py_BEGIN_CRITICAL_SECTION({target_critical_section});' 388 unlock = 'Py_END_CRITICAL_SECTION();' 389 case _: 390 lock = 'Py_BEGIN_CRITICAL_SECTION2({target_critical_section});' 391 unlock = 'Py_END_CRITICAL_SECTION2();' 392 data.lock.append(lock) 393 data.unlock.append(unlock) 394 395 last_group = 0 396 first_optional = len(selfless) 397 positional = selfless and selfless[-1].is_positional_only() 398 has_option_groups = False 399 400 # offset i by -1 because first_optional needs to ignore self 401 for i, p in enumerate(parameters, -1): 402 c = p.converter 403 404 if (i != -1) and (p.default is not unspecified): 405 first_optional = min(first_optional, i) 406 407 if p.is_vararg(): 408 data.cleanup.append(f"Py_XDECREF({c.parser_name});") 409 410 # insert group variable 411 group = p.group 412 if last_group != group: 413 last_group = group 414 if group: 415 group_name = self.group_to_variable_name(group) 416 data.impl_arguments.append(group_name) 417 data.declarations.append("int " + group_name + " = 0;") 418 data.impl_parameters.append("int " + group_name) 419 has_option_groups = True 420 421 c.render(p, data) 422 423 if has_option_groups and (not positional): 424 fail("You cannot use optional groups ('[' and ']') " 425 "unless all parameters are positional-only ('/').") 426 427 # HACK 428 # when we're METH_O, but have a custom return converter, 429 # we use "impl_parameters" for the parsing function 430 # because that works better. but that means we must 431 # suppress actually declaring the impl's parameters 432 # as variables in the parsing function. but since it's 433 # METH_O, we have exactly one anyway, so we know exactly 434 # where it is. 435 if ("METH_O" in templates['methoddef_define'] and 436 '{impl_parameters}' in templates['parser_prototype']): 437 data.declarations.pop(0) 438 439 full_name = f.full_name 440 template_dict = {'full_name': full_name} 441 template_dict['name'] = f.displayname 442 if f.kind in {GETTER, SETTER}: 443 template_dict['getset_name'] = f.c_basename.upper() 444 template_dict['getset_basename'] = f.c_basename 445 if f.kind is GETTER: 446 template_dict['c_basename'] = f.c_basename + "_get" 447 elif f.kind is SETTER: 448 template_dict['c_basename'] = f.c_basename + "_set" 449 # Implicitly add the setter value parameter. 450 data.impl_parameters.append("PyObject *value") 451 data.impl_arguments.append("value") 452 else: 453 template_dict['methoddef_name'] = f.c_basename.upper() + "_METHODDEF" 454 template_dict['c_basename'] = f.c_basename 455 456 template_dict['docstring'] = libclinic.docstring_for_c_string(f.docstring) 457 template_dict['self_name'] = template_dict['self_type'] = template_dict['self_type_check'] = '' 458 template_dict['target_critical_section'] = ', '.join(f.target_critical_section) 459 for converter in converters: 460 converter.set_template_dict(template_dict) 461 462 if f.kind not in {SETTER, METHOD_INIT}: 463 f.return_converter.render(f, data) 464 template_dict['impl_return_type'] = f.return_converter.type 465 466 template_dict['declarations'] = libclinic.format_escape("\n".join(data.declarations)) 467 template_dict['initializers'] = "\n\n".join(data.initializers) 468 template_dict['modifications'] = '\n\n'.join(data.modifications) 469 template_dict['keywords_c'] = ' '.join('"' + k + '",' 470 for k in data.keywords) 471 keywords = [k for k in data.keywords if k] 472 template_dict['keywords_py'] = ' '.join(c_id(k) + ',' 473 for k in keywords) 474 template_dict['format_units'] = ''.join(data.format_units) 475 template_dict['parse_arguments'] = ', '.join(data.parse_arguments) 476 if data.parse_arguments: 477 template_dict['parse_arguments_comma'] = ','; 478 else: 479 template_dict['parse_arguments_comma'] = ''; 480 template_dict['impl_parameters'] = ", ".join(data.impl_parameters) 481 template_dict['impl_arguments'] = ", ".join(data.impl_arguments) 482 483 template_dict['return_conversion'] = libclinic.format_escape("".join(data.return_conversion).rstrip()) 484 template_dict['post_parsing'] = libclinic.format_escape("".join(data.post_parsing).rstrip()) 485 template_dict['cleanup'] = libclinic.format_escape("".join(data.cleanup)) 486 487 template_dict['return_value'] = data.return_value 488 template_dict['lock'] = "\n".join(data.lock) 489 template_dict['unlock'] = "\n".join(data.unlock) 490 491 # used by unpack tuple code generator 492 unpack_min = first_optional 493 unpack_max = len(selfless) 494 template_dict['unpack_min'] = str(unpack_min) 495 template_dict['unpack_max'] = str(unpack_max) 496 497 if has_option_groups: 498 self.render_option_group_parsing(f, template_dict, 499 limited_capi=codegen.limited_capi) 500 501 # buffers, not destination 502 for name, destination in clinic.destination_buffers.items(): 503 template = templates[name] 504 if has_option_groups: 505 template = libclinic.linear_format(template, 506 option_group_parsing=template_dict['option_group_parsing']) 507 template = libclinic.linear_format(template, 508 declarations=template_dict['declarations'], 509 return_conversion=template_dict['return_conversion'], 510 initializers=template_dict['initializers'], 511 modifications=template_dict['modifications'], 512 post_parsing=template_dict['post_parsing'], 513 cleanup=template_dict['cleanup'], 514 lock=template_dict['lock'], 515 unlock=template_dict['unlock'], 516 ) 517 518 # Only generate the "exit:" label 519 # if we have any gotos 520 label = "exit:" if "goto exit;" in template else "" 521 template = libclinic.linear_format(template, exit_label=label) 522 523 s = template.format_map(template_dict) 524 525 # mild hack: 526 # reflow long impl declarations 527 if name in {"impl_prototype", "impl_definition"}: 528 s = libclinic.wrap_declarations(s) 529 530 if clinic.line_prefix: 531 s = libclinic.indent_all_lines(s, clinic.line_prefix) 532 if clinic.line_suffix: 533 s = libclinic.suffix_all_lines(s, clinic.line_suffix) 534 535 destination.append(s) 536 537 return clinic.get_destination('block').dump() 538