from __future__ import annotations import os from collections.abc import Callable, Sequence from typing import Any, TYPE_CHECKING import libclinic from libclinic import fail, warn from libclinic.function import Class from libclinic.block_parser import Block, BlockParser from libclinic.crenderdata import Include from libclinic.codegen import BlockPrinter, Destination from libclinic.parser import Parser, PythonParser from libclinic.dsl_parser import DSLParser if TYPE_CHECKING: from libclinic.clanguage import CLanguage from libclinic.function import ( Module, Function, ClassDict, ModuleDict) from libclinic.codegen import DestinationDict # maps strings to callables. # the callable should return an object # that implements the clinic parser # interface (__init__ and parse). # # example parsers: # "clinic", handles the Clinic DSL # "python", handles running Python code # parsers: dict[str, Callable[[Clinic], Parser]] = { 'clinic': DSLParser, 'python': PythonParser, } class Clinic: presets_text = """ preset block everything block methoddef_ifndef buffer 1 docstring_prototype suppress parser_prototype suppress cpp_if suppress cpp_endif suppress preset original everything block methoddef_ifndef buffer 1 docstring_prototype suppress parser_prototype suppress cpp_if suppress cpp_endif suppress preset file everything file methoddef_ifndef file 1 docstring_prototype suppress parser_prototype suppress impl_definition block preset buffer everything buffer methoddef_ifndef buffer 1 impl_definition block docstring_prototype suppress impl_prototype suppress parser_prototype suppress preset partial-buffer everything buffer methoddef_ifndef buffer 1 docstring_prototype block impl_prototype suppress methoddef_define block parser_prototype block impl_definition block """ def __init__( self, language: CLanguage, printer: BlockPrinter | None = None, *, filename: str, limited_capi: bool, verify: bool = True, ) -> None: # maps strings to Parser objects. # (instantiated from the "parsers" global.) self.parsers: dict[str, Parser] = {} self.language: CLanguage = language if printer: fail("Custom printers are broken right now") self.printer = printer or BlockPrinter(language) self.verify = verify self.limited_capi = limited_capi self.filename = filename self.modules: ModuleDict = {} self.classes: ClassDict = {} self.functions: list[Function] = [] # dict: include name => Include instance self.includes: dict[str, Include] = {} self.line_prefix = self.line_suffix = '' self.destinations: DestinationDict = {} self.add_destination("block", "buffer") self.add_destination("suppress", "suppress") self.add_destination("buffer", "buffer") if filename: self.add_destination("file", "file", "{dirname}/clinic/{basename}.h") d = self.get_destination_buffer self.destination_buffers = { 'cpp_if': d('file'), 'docstring_prototype': d('suppress'), 'docstring_definition': d('file'), 'methoddef_define': d('file'), 'impl_prototype': d('file'), 'parser_prototype': d('suppress'), 'parser_definition': d('file'), 'cpp_endif': d('file'), 'methoddef_ifndef': d('file', 1), 'impl_definition': d('block'), } DestBufferType = dict[str, list[str]] DestBufferList = list[DestBufferType] self.destination_buffers_stack: DestBufferList = [] self.ifndef_symbols: set[str] = set() self.presets: dict[str, dict[Any, Any]] = {} preset = None for line in self.presets_text.strip().split('\n'): line = line.strip() if not line: continue name, value, *options = line.split() if name == 'preset': self.presets[value] = preset = {} continue if len(options): index = int(options[0]) else: index = 0 buffer = self.get_destination_buffer(value, index) if name == 'everything': for name in self.destination_buffers: preset[name] = buffer continue assert name in self.destination_buffers preset[name] = buffer 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 add_destination( self, name: str, type: str, *args: str ) -> None: if name in self.destinations: fail(f"Destination already exists: {name!r}") self.destinations[name] = Destination(name, type, self, args) def get_destination(self, name: str) -> Destination: d = self.destinations.get(name) if not d: fail(f"Destination does not exist: {name!r}") return d def get_destination_buffer( self, name: str, item: int = 0 ) -> list[str]: d = self.get_destination(name) return d.buffers[item] def parse(self, input: str) -> str: printer = self.printer self.block_parser = BlockParser(input, self.language, verify=self.verify) for block in self.block_parser: dsl_name = block.dsl_name if dsl_name: if dsl_name not in self.parsers: assert dsl_name in parsers, f"No parser to handle {dsl_name!r} block." self.parsers[dsl_name] = parsers[dsl_name](self) parser = self.parsers[dsl_name] parser.parse(block) printer.print_block(block, limited_capi=self.limited_capi, header_includes=self.includes) # these are destinations not buffers for name, destination in self.destinations.items(): if destination.type == 'suppress': continue output = destination.dump() if output: block = Block("", dsl_name="clinic", output=output) if destination.type == 'buffer': block.input = "dump " + name + "\n" warn("Destination buffer " + repr(name) + " not empty at end of file, emptying.") printer.write("\n") printer.print_block(block, limited_capi=self.limited_capi, header_includes=self.includes) continue if destination.type == 'file': try: dirname = os.path.dirname(destination.filename) try: os.makedirs(dirname) except FileExistsError: if not os.path.isdir(dirname): fail(f"Can't write to destination " f"{destination.filename!r}; " f"can't make directory {dirname!r}!") if self.verify: with open(destination.filename) as f: parser_2 = BlockParser(f.read(), language=self.language) blocks = list(parser_2) if (len(blocks) != 1) or (blocks[0].input != 'preserve\n'): fail(f"Modified destination file " f"{destination.filename!r}; not overwriting!") except FileNotFoundError: pass block.input = 'preserve\n' printer_2 = BlockPrinter(self.language) printer_2.print_block(block, core_includes=True, limited_capi=self.limited_capi, header_includes=self.includes) libclinic.write_file(destination.filename, printer_2.f.getvalue()) continue return printer.f.getvalue() def _module_and_class( self, fields: Sequence[str] ) -> tuple[Module | Clinic, Class | None]: """ fields should be an iterable of field names. returns a tuple of (module, class). the module object could actually be self (a clinic object). this function is only ever used to find the parent of where a new class/module should go. """ parent: Clinic | Module | Class = self module: Clinic | Module = self cls: Class | None = None for idx, field in enumerate(fields): if not isinstance(parent, Class): if field in parent.modules: parent = module = parent.modules[field] continue if field in parent.classes: parent = cls = parent.classes[field] else: fullname = ".".join(fields[idx:]) fail(f"Parent class or module {fullname!r} does not exist.") return module, cls def __repr__(self) -> str: return ""