from __future__ import annotations import dataclasses as dc import io import os from typing import Final, TYPE_CHECKING import libclinic from libclinic import fail from libclinic.language import Language from libclinic.block_parser import Block if TYPE_CHECKING: from libclinic.app import Clinic TemplateDict = dict[str, str] class CRenderData: def __init__(self) -> None: # The C statements to declare variables. # Should be full lines with \n eol characters. self.declarations: list[str] = [] # The C statements required to initialize the variables before the parse call. # Should be full lines with \n eol characters. self.initializers: list[str] = [] # The C statements needed to dynamically modify the values # parsed by the parse call, before calling the impl. self.modifications: list[str] = [] # The entries for the "keywords" array for PyArg_ParseTuple. # Should be individual strings representing the names. self.keywords: list[str] = [] # The "format units" for PyArg_ParseTuple. # Should be individual strings that will get self.format_units: list[str] = [] # The varargs arguments for PyArg_ParseTuple. self.parse_arguments: list[str] = [] # The parameter declarations for the impl function. self.impl_parameters: list[str] = [] # The arguments to the impl function at the time it's called. self.impl_arguments: list[str] = [] # For return converters: the name of the variable that # should receive the value returned by the impl. self.return_value = "return_value" # For return converters: the code to convert the return # value from the parse function. This is also where # you should check the _return_value for errors, and # "goto exit" if there are any. self.return_conversion: list[str] = [] self.converter_retval = "_return_value" # The C statements required to do some operations # after the end of parsing but before cleaning up. # These operations may be, for example, memory deallocations which # can only be done without any error happening during argument parsing. self.post_parsing: list[str] = [] # The C statements required to clean up after the impl call. self.cleanup: list[str] = [] # The C statements to generate critical sections (per-object locking). self.lock: list[str] = [] self.unlock: list[str] = [] @dc.dataclass(slots=True, frozen=True) class Include: """ An include like: #include "pycore_long.h" // _Py_ID() """ # Example: "pycore_long.h". filename: str # Example: "_Py_ID()". reason: str # None means unconditional include. # Example: "#if defined(Py_BUILD_CORE) && !defined(Py_BUILD_CORE_MODULE)". condition: str | None def sort_key(self) -> tuple[str, str]: # order: '#if' comes before 'NO_CONDITION' return (self.condition or 'NO_CONDITION', self.filename) @dc.dataclass(slots=True) class BlockPrinter: language: Language f: io.StringIO = dc.field(default_factory=io.StringIO) # '#include "header.h" // reason': column of '//' comment INCLUDE_COMMENT_COLUMN: Final[int] = 35 def print_block( self, block: Block, *, header_includes: list[Include] | None = None, ) -> None: input = block.input output = block.output dsl_name = block.dsl_name write = self.f.write assert not ((dsl_name is None) ^ (output is None)), "you must specify dsl_name and output together, dsl_name " + repr(dsl_name) if not dsl_name: write(input) return write(self.language.start_line.format(dsl_name=dsl_name)) write("\n") body_prefix = self.language.body_prefix.format(dsl_name=dsl_name) if not body_prefix: write(input) else: for line in input.split('\n'): write(body_prefix) write(line) write("\n") write(self.language.stop_line.format(dsl_name=dsl_name)) write("\n") output = '' if header_includes: # Emit optional "#include" directives for C headers output += '\n' current_condition: str | None = None for include in header_includes: if include.condition != current_condition: if current_condition: output += '#endif\n' current_condition = include.condition if include.condition: output += f'{include.condition}\n' if current_condition: line = f'# include "{include.filename}"' else: line = f'#include "{include.filename}"' if include.reason: comment = f'// {include.reason}\n' line = line.ljust(self.INCLUDE_COMMENT_COLUMN - 1) + comment output += line if current_condition: output += '#endif\n' input = ''.join(block.input) output += ''.join(block.output) if output: if not output.endswith('\n'): output += '\n' write(output) arguments = "output={output} input={input}".format( output=libclinic.compute_checksum(output, 16), input=libclinic.compute_checksum(input, 16) ) write(self.language.checksum_line.format(dsl_name=dsl_name, arguments=arguments)) write("\n") def write(self, text: str) -> None: self.f.write(text) class BufferSeries: """ Behaves like a "defaultlist". When you ask for an index that doesn't exist yet, the object grows the list until that item exists. So o[n] will always work. Supports negative indices for actual items. e.g. o[-1] is an element immediately preceding o[0]. """ def __init__(self) -> None: self._start = 0 self._array: list[list[str]] = [] def __getitem__(self, i: int) -> list[str]: i -= self._start if i < 0: self._start += i prefix: list[list[str]] = [[] for x in range(-i)] self._array = prefix + self._array i = 0 while i >= len(self._array): self._array.append([]) return self._array[i] def clear(self) -> None: for ta in self._array: ta.clear() def dump(self) -> str: texts = ["".join(ta) for ta in self._array] self.clear() return "".join(texts) @dc.dataclass(slots=True, repr=False) class Destination: name: str type: str clinic: Clinic buffers: BufferSeries = dc.field(init=False, default_factory=BufferSeries) filename: str = dc.field(init=False) # set in __post_init__ args: dc.InitVar[tuple[str, ...]] = () def __post_init__(self, args: tuple[str, ...]) -> None: valid_types = ('buffer', 'file', 'suppress') if self.type not in valid_types: fail( f"Invalid destination type {self.type!r} for {self.name}, " f"must be {', '.join(valid_types)}" ) extra_arguments = 1 if self.type == "file" else 0 if len(args) < extra_arguments: fail(f"Not enough arguments for destination " f"{self.name!r} new {self.type!r}") if len(args) > extra_arguments: fail(f"Too many arguments for destination {self.name!r} new {self.type!r}") if self.type =='file': d = {} filename = self.clinic.filename d['path'] = filename dirname, basename = os.path.split(filename) if not dirname: dirname = '.' d['dirname'] = dirname d['basename'] = basename d['basename_root'], d['basename_extension'] = os.path.splitext(filename) self.filename = args[0].format_map(d) def __repr__(self) -> str: if self.type == 'file': type_repr = f"type='file' file={self.filename!r}" else: type_repr = f"type={self.type!r}" return f"" def clear(self) -> None: if self.type != 'buffer': fail(f"Can't clear destination {self.name!r}: it's not of type 'buffer'") self.buffers.clear() def dump(self) -> str: return self.buffers.dump() DestinationDict = dict[str, Destination] class CodeGen: def __init__(self, limited_capi: bool) -> None: self.limited_capi = limited_capi self._ifndef_symbols: set[str] = set() # dict: include name => Include instance self._includes: dict[str, Include] = {} def add_ifndef_symbol(self, name: str) -> bool: if name in self._ifndef_symbols: return False self._ifndef_symbols.add(name) return True def add_include(self, name: str, reason: str, *, condition: str | None = None) -> None: try: existing = self._includes[name] except KeyError: pass else: if existing.condition and not condition: # If the previous include has a condition and the new one is # unconditional, override the include. pass else: # Already included, do nothing. Only mention a single reason, # no need to list all of them. return self._includes[name] = Include(name, reason, condition) def get_includes(self) -> list[Include]: return sorted(self._includes.values(), key=Include.sort_key)