Skip to content

Commit

Permalink
modules/codegen: Add support for bison/byacc/yacc
Browse files Browse the repository at this point in the history
  • Loading branch information
dcbaker committed Aug 8, 2024
1 parent 217c21d commit 00ef8bb
Show file tree
Hide file tree
Showing 3 changed files with 122 additions and 13 deletions.
27 changes: 27 additions & 0 deletions docs/markdown/Codegen-module.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,30 @@ headers = [l1[1], l1[2]] # [header, table]
l2 = codegen.lex('lexer.l', table : '@[email protected]')
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 : '@[email protected]', table : '@[email protected]')
headers = [l1[1], l1[2]] # [header, table]
p2 = codegen.lex('parser.y', locations : 'locations.hpp')
table = l2[1]
```
97 changes: 92 additions & 5 deletions mesonbuild/modules/codegen.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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])
Expand Down Expand Up @@ -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):
Expand All @@ -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']]

Expand All @@ -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)
11 changes: 3 additions & 8 deletions test cases/frameworks/8 flex/meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -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.')
Expand All @@ -22,12 +22,7 @@ endif

codegen = import('unstable-codegen')
lfiles = codegen.lex('lexer.l')

pgen = generator(bison,
output : ['@[email protected]', '@[email protected]'],
arguments : ['@INPUT@', '--defines=@OUTPUT1@', '--output=@OUTPUT0@'])

pfiles = pgen.process('parser.y')
pfiles = codegen.yacc('parser.y', header : '@[email protected]')

e = executable(
'pgen',
Expand Down

0 comments on commit 00ef8bb

Please sign in to comment.