From 00ef8bb4c2920dd2f81be393c9adced15db3dc4d Mon Sep 17 00:00:00 2001 From: Dylan Baker Date: Tue, 23 Jul 2024 10:13:24 -0700 Subject: [PATCH] modules/codegen: Add support for bison/byacc/yacc --- docs/markdown/Codegen-module.md | 27 +++++++ mesonbuild/modules/codegen.py | 97 ++++++++++++++++++++++-- test cases/frameworks/8 flex/meson.build | 11 +-- 3 files changed, 122 insertions(+), 13 deletions(-) diff --git a/docs/markdown/Codegen-module.md b/docs/markdown/Codegen-module.md index 75e2f79b29f6..11187e492846 100644 --- a/docs/markdown/Codegen-module.md +++ b/docs/markdown/Codegen-module.md @@ -40,3 +40,30 @@ headers = [l1[1], l1[2]] # [header, table] l2 = codegen.lex('lexer.l', table : '@BASENAME@.tab.h') table = l2[1] ``` + +### yacc() + +```meson +codegen.yacc('parser.y') +``` + +This function wraps bison, yacc, byacc, and win_bison (on Windows), and attempts to abstract away the differences between them + +This requires an input file, which may be a string, File, or generated source. It additionally takes the following options keyword arguments: + +- `version`: Version constraints on the lexer +- `args`: An array of extra arguments to pass the lexer +- `plainname`: If set to true then `@PLAINNAME@` will be used as the source base, otherwise `@BASENAME@`. +- `source`: the name of the source output. If this is unset Meson will use `{base}.{ext}` with an extension of `cpp` if the input has an extension of `.yy` or `c` otherwise, with base being determined by the `plainname` argument. +- `header`: the name of the source output. If this is unset Meson will use `{base}.{ext}` with an extension of `hpp` if the input has an extension of `.yy` or `h` otherwise, with base being determined by the `plainname` argument. +- `locations`: The name of the locations file, if one is generated. Due to the way yacc works this must be duplicated in the file and in the command. + +The outputs will be in the form `source header [locations]`, which means those can be accessed by indexing the output of the `yacc` call: + +```meson +p1 = codegen.yacc('parser.y', header : '@BASENAME@.h', table : '@BASENAME@.tab.h') +headers = [l1[1], l1[2]] # [header, table] + +p2 = codegen.lex('parser.y', locations : 'locations.hpp') +table = l2[1] +``` diff --git a/mesonbuild/modules/codegen.py b/mesonbuild/modules/codegen.py index 58b43794a087..251ceffe0969 100644 --- a/mesonbuild/modules/codegen.py +++ b/mesonbuild/modules/codegen.py @@ -37,6 +37,15 @@ class LexKwargs(TypedDict): table: T.Optional[str] plainname: bool + class YaccKwargs(TypedDict): + + args: T.List[str] + version: T.List[str] + source: T.Optional[str] + header: T.Optional[str] + locations: T.Optional[str] + plainname: bool + @dataclasses.dataclass class Generator: @@ -57,17 +66,21 @@ class CodeGenModule(ExtensionModule): def __init__(self, interpreter: Interpreter) -> None: super().__init__(interpreter) - self._generators: T.Dict[Literal['lex'], Generator] = {} + self._generators: T.Dict[Literal['lex', 'yacc'], Generator] = {} self.methods.update({ 'lex': self.lex_method, + 'yacc': self.yacc_method, }) def __find_lex(self, state: ModuleState, version: T.List[str]) -> None: + if 'lex' in self._generators: + return + names: T.List[FileOrString] = [] assert state.environment.machines.host is not None, 'for mypy' if state.environment.machines.host.system == 'windows': names.append('win_flex') - names.extend(['flex', 'lex']) + names.extend(['flex', 'reflex', 'lex']) for name in names: bin = state.find_program(name, wanted=version, required=name == names[-1]) @@ -109,8 +122,7 @@ def __find_lex(self, state: ModuleState, version: T.List[str]) -> None: KwargInfo('plainname', bool, default=False), ) def lex_method(self, state: ModuleState, args: T.Tuple[T.Union[str, File, GeneratedList, CustomTarget, CustomTargetIndex]], kwargs: LexKwargs) -> ModuleReturnValue: - if 'lex' not in self._generators: - self.__find_lex(state, kwargs['version']) + self.__find_lex(state, kwargs['version']) input = state._interpreter.source_strings_to_files([args[0]])[0] if isinstance(input, File): @@ -130,8 +142,9 @@ def lex_method(self, state: ModuleState, args: T.Tuple[T.Union[str, File, Genera except KeyError: raise MesonException("Could not find a C++ compiler to search for FlexLexer.h") + base = '@PLAINNAME@' if kwargs['plainname'] else '@BASENAME@' if kwargs['source'] is None: - outputs = [f'@BASENAME@.{"cpp" if is_cpp else "c"}'] + outputs = [f'{base}.{"cpp" if is_cpp else "c"}'] else: outputs = [kwargs['source']] @@ -158,6 +171,80 @@ def lex_method(self, state: ModuleState, args: T.Tuple[T.Union[str, File, Genera return ModuleReturnValue(target, [target]) + def __find_yacc(self, state: ModuleState, version: T.List[str]) -> None: + if 'yacc' in self._generators: + return + + assert state.environment.machines.host is not None, 'for mypy' + names: T.List[FileOrString] + if state.environment.machines.host.system == 'windows': + names = ['win_bison', 'bison', 'yacc'] + else: + names = ['bison', 'byacc', 'yacc'] + + for name in names: + bin = state.find_program(names, wanted=version, required=name == names[-1]) + if bin.found(): + break + + args: T.List[str] = ['@INPUT@', '-o', '@OUTPUT0@'] + # TODO: Determine if "yacc" is "bison" or "byacc" + if bin.name == 'bison': + # TODO: add --color=always when appropriate + args.append('--defines=@OUTPUT1@') + else: + args.extend(['-H', '@OUTPUT1@']) + self._generators['yacc'] = Generator(bin, T.cast('ImmutableListProtocol[str]', args)) + + @typed_pos_args('codegen.yacc', (str, File, GeneratedList, CustomTarget, CustomTargetIndex)) + @typed_kwargs( + 'codegen.yacc', + KwargInfo('version', ContainerTypeInfo(list, str), default=[], listify=True), + KwargInfo('args', ContainerTypeInfo(list, str), default=[], listify=True), + KwargInfo('source', (str, NoneType)), + KwargInfo('header', (str, NoneType)), + KwargInfo('locations', (str, NoneType)), + KwargInfo('plainname', bool, default=False), + ) + def yacc_method(self, state: ModuleState, args: T.Tuple[T.Union[str, File, CustomTarget, CustomTargetIndex, GeneratedList]], kwargs: YaccKwargs) -> ModuleReturnValue: + self.__find_yacc(state, kwargs['version']) + + input = state._interpreter.source_strings_to_files([args[0]])[0] + if isinstance(input, File): + is_cpp = input.endswith(".yy") + name = os.path.splitext(input.fname)[0] + else: + is_cpp = input.get_outputs()[0].endswith('.yy') + name = os.path.splitext(input.get_outputs()[0])[0] + name = os.path.basename(name) + + command = self._generators['yacc'].command() + command.extend(kwargs['args']) + + source_ext = 'cpp' if is_cpp else 'c' + header_ext = 'hpp' if is_cpp else 'h' + + base = '@PLAINNAME@' if kwargs['plainname'] else '@BASENAME@' + outputs: T.List[str] = [] + outputs.append(f'{base}.{source_ext}' if kwargs['source'] is None else kwargs['source']) + outputs.append(f'{base}.{header_ext}' if kwargs['header'] is None else kwargs['header']) + if kwargs['locations'] is not None: + outputs.append(kwargs['locations']) + + target = CustomTarget( + f'codegen-yacc-{name}', + state.subdir, + state.subproject, + state.environment, + command, + [input], + outputs, + backend=state.backend, + description='Generating parser {} with yacc', + ) + + return ModuleReturnValue(target, [target]) + def initialize(interpreter: Interpreter) -> CodeGenModule: return CodeGenModule(interpreter) diff --git a/test cases/frameworks/8 flex/meson.build b/test cases/frameworks/8 flex/meson.build index dfc345426b0a..41666526037b 100644 --- a/test cases/frameworks/8 flex/meson.build +++ b/test cases/frameworks/8 flex/meson.build @@ -9,8 +9,8 @@ project('flex and bison', 'c') # TODO: handle win_flex/win_bison -flex = find_program('flex', 'lex', required: false) -bison = find_program('bison', required: false) +flex = find_program('flex', 'reflex', 'lex', required: false) +bison = find_program('bison', 'byacc', 'yacc', required: false) if not flex.found() error('MESON_SKIP_TEST flex not found.') @@ -22,12 +22,7 @@ endif codegen = import('unstable-codegen') lfiles = codegen.lex('lexer.l') - -pgen = generator(bison, -output : ['@BASENAME@.tab.c', '@BASENAME@.tab.h'], -arguments : ['@INPUT@', '--defines=@OUTPUT1@', '--output=@OUTPUT0@']) - -pfiles = pgen.process('parser.y') +pfiles = codegen.yacc('parser.y', header : '@BASENAME@.tab.h') e = executable( 'pgen',