cvc5-cvc5-1.2.0.src.options.mkoptions.py Maven / Gradle / Ivy
The newest version!
#!/usr/bin/env python
###############################################################################
# Top contributors (to current version):
# Gereon Kremer, Mathias Preiner, Alex Ozdemir
#
# This file is part of the cvc5 project.
#
# Copyright (c) 2009-2024 by the authors listed in the file AUTHORS
# in the top-level source directory and their institutional affiliations.
# All rights reserved. See the file COPYING in the top-level source
# directory for licensing information.
# #############################################################################
##
"""
Generate option handling code and documentation in one pass. The generated
files are only written to the destination file if the contents of the file
has changed (in order to avoid global re-compilation if only single option
files changed).
mkoptions.py +
base source directory of all toml files
build directory to write the generated sphinx docs
base destination directory for all generated files
+ one or more *_options.toml files
This script expects the following files (within ):
- /main/options_template.cpp
- /options/module_template.cpp
- /options/module_template.h
- /options/options_public_template.cpp
- /options/options_template.cpp
- /options/options_template.h
+ must be the list of all *.toml option configuration files.
This script generates the following files:
- /main/options.cpp
- /options/_options.cpp (for every toml file)
- /options/_options.h (for every toml file)
- /options/options_public.cpp
- /options/options.cpp
- /options/options.h
"""
import os
import re
import sys
import textwrap
try:
import tomllib
except ImportError:
import tomli as tomllib
### Allowed attributes for module/option
MODULE_ATTR_REQ = ['id', 'name']
MODULE_ATTR_ALL = MODULE_ATTR_REQ + ['option']
OPTION_ATTR_REQ = ['category', 'type']
OPTION_ATTR_ALL = OPTION_ATTR_REQ + [
'name', 'short', 'long', 'alias', 'default', 'alternate', 'mode',
'handler', 'predicates', 'includes', 'minimum', 'maximum', 'help',
'help_mode'
]
CATEGORY_VALUES = ['common', 'expert', 'regular', 'undocumented']
################################################################################
################################################################################
# utility functions
def wrap_line(s, indent, **kwargs):
"""Wrap and indent text and forward all other kwargs to textwrap.wrap()."""
return ('\n' + ' ' * indent).join(
textwrap.wrap(s, width=80 - indent, **kwargs))
def concat_format(s, objs, glue='\n'):
"""Helper method to render a string for a list of object"""
return glue.join([s.format(**o.__dict__) for o in objs])
def format_include(include):
"""Generate the #include directive for a given header name."""
if '<' in include:
return '#include {}'.format(include)
return '#include "{}"'.format(include)
def is_numeric_cpp_type(ctype):
"""Check if given type is a numeric type (double, int64_t or uint64_t)."""
return ctype in ['int64_t', 'uint64_t', 'double']
def die(msg):
"""Exit with the given error message."""
sys.exit('[error] {}'.format(msg))
def all_options(modules, sorted=False):
"""Helper to iterate all options from all modules."""
if sorted:
options = []
for m in modules:
options = options + [(m, o) for o in m.options]
options.sort(key=lambda t: t[1])
yield from options
else:
for module in modules:
if not module.options:
continue
for option in module.options:
yield module, option
def write_file(directory, name, content):
"""Write content to `directory/name`. If the file exists, only overwrite it
when the content would actually change."""
fname = os.path.join(directory, name)
try:
if os.path.isfile(fname):
with open(fname, 'r') as file:
if content == file.read():
return
with open(fname, 'w') as file:
file.write(content)
except IOError:
die("Could not write to '{}'".format(fname))
def read_tpl(directory, name):
"""Read a (custom) template file from `directory/name`. Expects placeholders
of the form `${varname}$` and turns them into `{varname}` while all other
curly braces are replaced by double curly braces. Thus, the result is
suitable for `.format()` with kwargs being used."""
fname = os.path.join(directory, name)
try:
with open(fname, 'r') as file:
res = file.read()
res = res.replace('{', '{{').replace('}', '}}')
return res.replace('${', '').replace('}$', '')
except IOError:
die("Could not find '{}'. Aborting.".format(fname))
################################################################################
################################################################################
# classes to represent modules and options
class Module(object):
"""Represents one options module from one _options.toml file."""
def __init__(self, d, filename):
self.__dict__ = {k: d.get(k, None) for k in MODULE_ATTR_ALL}
self.options = []
self.id = self.id.lower()
self.id_cap = self.id.upper()
self.id_capitalized = self.id.capitalize()
self.filename = os.path.splitext(os.path.split(filename)[-1])[0]
self.header = os.path.join('options', '{}.h'.format(self.filename))
class Option(object):
"""Represents on option."""
def __init__(self, d):
self.__dict__ = dict((k, None) for k in OPTION_ATTR_ALL)
self.includes = []
self.predicates = []
for (attr, val) in d.items():
assert attr in self.__dict__
if attr == 'alternate' or val:
self.__dict__[attr] = val
if self.type == 'bool' and self.alternate is None:
self.alternate = True
self.long_name = None
self.long_opt = None
if self.name:
self.name_capitalized = self.name[0].capitalize() + self.name[1:]
if self.long:
r = self.long.split('=', 1)
self.long_name = r[0]
if len(r) > 1:
self.long_opt = r[1]
self.fqdefault = self.default
if self.mode and self.type not in self.default:
self.fqdefault = '{}::{}'.format(self.type, self.default)
self.names = set()
if self.long_name:
self.names.add(self.long_name)
if self.alias:
self.names.update(self.alias)
if self.mode:
self.mode_name = { k: v[0]['name'] for k,v in self.mode.items() }
self.mode_help = { k: v[0].get('help', None) for k,v in self.mode.items() }
def __lt__(self, other):
if self.long_name and other.long_name:
return self.long_name < other.long_name
if self.long_name: return True
return False
def __str__(self):
return self.long_name if self.long_name else self.name
def enum_name(self):
return str(self).replace("-","_").upper()
################################################################################
################################################################################
# code generation functions
################################################################################
# for options/options.h
def generate_holder_fwd_decls(modules):
"""Render forward declaration of holder structs"""
return concat_format(' struct Holder{id_cap}; // include "{header}" if this is an incomplete type', modules)
def generate_holder_mem_decls(modules):
"""Render declarations of holder members of the Option class"""
return concat_format(
' std::unique_ptr d_{id};', modules)
def generate_holder_ref_decls(modules):
"""Render reference declarations for holder members of the Option class"""
return concat_format(''' const options::Holder{id_cap}& {id};
options::Holder{id_cap}& write_{id}();''', modules)
################################################################################
# for options/options.cpp
def generate_module_headers(modules):
"""Render includes for module headers"""
return concat_format('#include "{header}"', modules)
def generate_holder_mem_inits(modules):
"""Render initializations of holder members of the Option class"""
return concat_format(
' d_{id}(std::make_unique()),',
modules)
def generate_holder_ref_inits(modules):
"""Render initializations of holder references of the Option class"""
return concat_format(' {id}(*d_{id}),', modules)
def generate_write_functions(modules):
"""Render write functions for holders within the Option class"""
return concat_format(''' options::Holder{id_cap}& Options::write_{id}()
{{
return *d_{id};
}}
''', modules)
def generate_holder_mem_copy(modules):
"""Render copy operation of holder members of the Option class"""
return concat_format(' *d_{id} = *options.d_{id};', modules)
################################################################################
# for options/options_public.cpp
def generate_public_includes(modules):
"""Generates the list of includes for options_public.cpp."""
headers = set()
headers.add(format_include(""))
for _, option in all_options(modules):
headers.update([format_include(x) for x in option.includes])
return '\n'.join(headers)
def generate_option_enum_and_table(modules):
"""
Generate an enum class OptionEnum with one variant for each option.
Also, generate a map NAME_TO_ENUM from string names to enum variants.
This enum is used to branch (in C++) on an option string name.
First, you lookup the enum in the map.
Then, you switch-case on the enum, which generates a jump table.
When we measured, this was about 5x faster than a huge if-else chain.
It would probably be even faster with a better hash function.
"""
res = []
res.append('enum class OptionEnum {')
for module, option in all_options(modules, True):
if not option.long:
continue
res.append(' {n},'.format(n=option.enum_name()))
res.append('};')
res.append('const std::unordered_map NAME_TO_ENUM = {')
for module, option in all_options(modules, True):
if not option.long:
continue
for name in option.names:
res.append(' {{ \"{}\", OptionEnum::{} }},'
.format(name, option.enum_name()))
res.append('};')
return '\n '.join(res)
def generate_getnames_impl(modules):
"""Generates the implementation for options::getNames()."""
names = set()
for _, option in all_options(modules):
names.update(option.names)
res = ', '.join(map(lambda s: '"' + s + '"', sorted(names)))
return wrap_line(res, 4, break_on_hyphens=False)
def generate_get_impl(modules):
"""Generates the implementation for options::get()."""
res = []
res.append('auto it = NAME_TO_ENUM.find(name);')
res.append('if (it == NAME_TO_ENUM.end()) {')
res.append(' throw OptionException(\"Unrecognized option key or setting: \" + name);')
res.append('}')
res.append('switch (it->second) {')
for module, option in all_options(modules, True):
if not option.name or not option.long:
continue
ret = None
if option.type == 'bool':
ret = 'return options.{}.{} ? "true" : "false";'.format(
module.id, option.name)
elif option.type == 'std::string':
ret = 'return options.{}.{};'.format(module.id, option.name)
elif is_numeric_cpp_type(option.type):
ret = 'return std::to_string(options.{}.{});'.format(
module.id, option.name)
else:
ret = '{{ std::stringstream s; s << options.{}.{}; return s.str(); }}'.format(
module.id, option.name)
res.append(' case OptionEnum::{}: {}'.format(option.enum_name(), ret))
res.append(' default:')
res.append(' {')
res.append(' throw OptionException(\"Ungettable option key or setting: \" + name);')
res.append(' }')
res.append('}')
return '\n '.join(res)
def _set_handlers(option):
"""Render handler call for options::set()."""
if option.handler:
return 'opts.handler().{}(name, optionarg)'.format(option.handler)
elif option.mode:
return 'stringTo{}(optionarg)'.format(option.type)
return 'handlers::handleOption<{}>(name, optionarg)'.format(option.type)
def _set_predicates(module, option):
"""Render predicate calls for options::set()."""
res = []
if option.minimum:
res.append(
'opts.handler().checkMinimum(name, value, static_cast<{}>({}));'
.format(option.type, option.minimum))
if option.maximum:
res.append(
'opts.handler().checkMaximum(name, value, static_cast<{}>({}));'
.format(option.type, option.maximum))
res += [
'opts.handler().{}(name, value);'.format(x) for x in option.predicates
]
if module.id == 'printer':
res.append('ioutils::setDefault{}(value);'.format(option.name_capitalized))
return res
def generate_set_impl(modules):
"""Generates the implementation for options::set()."""
res = []
res.append('auto it = NAME_TO_ENUM.find(name);')
res.append('if (it == NAME_TO_ENUM.end()) {')
res.append(' throw OptionException(\"Unrecognized option key or setting: \" + name);')
res.append('}')
res.append('switch (it->second) {')
for module, option in all_options(modules, True):
if not option.long:
continue
res.append(' case OptionEnum::{}:'.format(option.enum_name()))
res.append(' {')
res.append(' auto value = {};'.format(_set_handlers(option)))
for pred in _set_predicates(module, option):
res.append(' {}'.format(pred))
if option.name:
res.append(' opts.write_{module}().{name} = value;'.format(
module=module.id, name=option.name))
res.append(' opts.write_{module}().{name}WasSetByUser = true;'.format(
module=module.id, name=option.name))
res.append(' break;')
res.append(' }')
res.append('}')
return '\n '.join(res)
def cpp_category(category):
assert category
return f'OptionInfo::Category::{category.upper()}'
def generate_getinfo_impl(modules):
"""Generates the implementation for options::getInfo()."""
res = []
res.append('auto it = NAME_TO_ENUM.find(name);')
res.append('if (it == NAME_TO_ENUM.end()) {')
res.append(' throw OptionException(\"Unrecognized option key or setting: \" + name);')
res.append('}')
res.append('switch (it->second) {')
for module, option in all_options(modules, True):
if not option.long:
continue
constr = None
fmt = {
'name': option.long_name,
'alias': '',
'type': option.type,
'value': 'opts.{}.{}'.format(module.id, option.name),
'setbyuser': 'opts.{}.{}WasSetByUser'.format(module.id, option.name),
'default': option.default if option.default else '{}()'.format(option.type),
'minimum': option.minimum if option.minimum else '{}',
'maximum': option.maximum if option.maximum else '{}',
'category': cpp_category(option.category)
}
if option.alias:
fmt['alias'] = ', '.join(map(lambda s: '"{}"'.format(s), option.alias))
if not option.name:
fmt['setbyuser'] = 'false'
constr = 'OptionInfo::VoidInfo{{}}'
elif option.type in ['bool', 'std::string']:
constr = 'OptionInfo::ValueInfo<{type}>{{{default}, {value}}}'
elif option.type == 'double' or is_numeric_cpp_type(option.type):
constr = 'OptionInfo::NumberInfo<{type}>{{{default}, {value}, {minimum}, {maximum}}}'
elif option.mode:
modes = { key: value[0]['name'] for key,value in option.mode.items() }
fmt['modes'] = ', '.join(['"{}"'.format(s) for s in sorted(modes.values())])
fmt['default'] = modes[fmt['default']]
constr = 'OptionInfo::ModeInfo{{"{default}", {value}, {{ {modes} }}}}'
else:
constr = 'OptionInfo::VoidInfo{{}}'
res.append(" case OptionEnum::{}:".format(option.enum_name()))
line = ' return OptionInfo{{"{name}", {{{alias}}}, {setbyuser}, {category}, ' + constr + '}};'
res.append(line.format(**fmt))
res.append("}")
return '\n '.join(res)
################################################################################
# for options/.h
def generate_module_includes(module):
includes = set()
for option in module.options:
if option.name is None:
continue
includes.update([format_include(x) for x in option.includes])
return '\n'.join(sorted(includes))
TPL_MODE_DECL = '''enum class {type}
{{
{values},
__MAX_VALUE = {maxvalue}
}};
std::ostream& operator<<(std::ostream& os, {type} mode);
{type} stringTo{type}(const std::string& optarg);
'''
def generate_module_mode_decl(module):
"""Generates the declarations of mode enums and utility functions."""
res = []
for option in module.options:
if not option.mode:
continue
values = list(option.mode.keys())
res.append(
TPL_MODE_DECL.format(type=option.type,
values=wrap_line(', '.join(values), 2),
maxvalue=values[-1]))
return '\n'.join(res)
def generate_module_holder_decl(module):
res = []
for option in module.options:
if option.name is None:
continue
if option.fqdefault:
res.append('{} {} = {};'.format(option.type, option.name, option.fqdefault))
else:
res.append('{} {};'.format(option.type, option.name))
res.append('bool {}WasSetByUser = false;'.format(option.name))
return '\n '.join(res)
def generate_module_long_name_decl(module):
res = []
for option in module.options:
if option.name is None:
continue
if option.long_name:
res.append('static constexpr const char* {} = "{}";'.format(
option.name, option.long_name))
return '\n '.join(res)
################################################################################
# for options/.cpp
TPL_MODE_STREAM_OPERATOR = '''std::ostream& operator<<(std::ostream& os, {type} mode)
{{
switch(mode)
{{
{cases}
default: Unreachable();
}}
return os;
}}'''
TPL_MODE_TO_STRING = '''{type} stringTo{type}(const std::string& optarg)
{{
{cases}
else if (optarg == "help")
{{
std::cerr << {help};
std::exit(1);
}}
throw OptionException(std::string("unknown option for --{long}: `") +
optarg + "'. Try --{long}=help.");
}}'''
def _module_mode_help(option):
"""Format help message for mode options."""
assert option.help_mode
assert option.mode
text = ['R"FOOBAR(']
text.append(' ' + wrap_line(option.help_mode, 2, break_on_hyphens=False))
text.append('Available {}s for --{} are:'.format(option.long_opt.lower(),
option.long_name))
for value, attrib in option.mode.items():
assert len(attrib) == 1
attrib = attrib[0]
if 'help' not in attrib:
continue
if value == option.default and attrib['name'] != "default":
text.append('+ {} (default)'.format(attrib['name']))
else:
text.append('+ {}'.format(attrib['name']))
text.append(' '
+ wrap_line(attrib['help'], 2, break_on_hyphens=False))
text.append(')FOOBAR"')
return '\n'.join(text)
def generate_module_mode_impl(module):
"""Generates the declarations of mode enums and utility functions."""
res = []
for option in module.options:
if not option.mode:
continue
cases = [
'case {type}::{enum}: return os << "{name}";'.format(
type=option.type, enum=enum, name=info[0]['name'])
for enum, info in option.mode.items()
]
res.append(
TPL_MODE_STREAM_OPERATOR.format(type=option.type,
cases='\n '.join(cases)))
# Generate str-to-enum handler
names = set()
cases = []
for value, attrib in option.mode.items():
assert len(attrib) == 1
name = attrib[0]['name']
if name in names:
die("multiple modes with the name '{}' for option '{}'".format(
name, option.long))
else:
names.add(name)
cases.append(
'if (optarg == "{name}") return {type}::{enum};'.format(
name=name, type=option.type, enum=value))
assert option.long
assert cases
res.append(
TPL_MODE_TO_STRING.format(type=option.type,
cases='\n else '.join(cases),
help=_module_mode_help(option),
long=option.long_name))
return '\n'.join(res)
################################################################################
# for main/options.cpp
def _add_cmdoption(option, name, opts, next_id):
fmt = {
'name': name,
'arg': 'no' if option.type == 'bool' else 'required',
'next_id': next_id
}
opts.append(
'{{ "{name}", {arg}_argument, nullptr, {next_id} }},'.format(**fmt))
def generate_parsing(modules):
"""Generates the implementation for main::parseInternal() and matching
options definitions suitable for getopt_long(). Returns a tuple with:
- short options description (passed as third argument to getopt_long)
- long options description (passed as fourth argument to getopt_long)
- handler code that turns getopt_long return value to a setOption call
"""
short = ""
opts = []
code = []
next_id = 256
for _, option in all_options(modules, False):
needs_impl = False
if option.short: # short option
needs_impl = True
code.append("case '{0}': // -{0}".format(option.short))
short += option.short
if option.type != 'bool':
short += ':'
if option.long: # long option
needs_impl = True
_add_cmdoption(option, option.long_name, opts, next_id)
code.append('case {}: // --{}'.format(next_id, option.long_name))
next_id += 1
if option.alias: # long option aliases
needs_impl = True
for alias in option.alias:
_add_cmdoption(option, alias, opts, next_id)
code.append('case {}: // --{}'.format(next_id, alias))
next_id += 1
if needs_impl:
# there is some way to call it, add call to solver.setOption()
if option.type == 'bool':
code.append(' solver.setOption("{}", "true"); break;'.format(
option.long_name))
else:
code.append(
' solver.setOption("{}", optionarg); break;'.format(
option.long_name))
if option.alternate:
assert option.type == 'bool'
# bool option that wants a --no-*
needs_impl = False
if option.long: # long option
needs_impl = True
_add_cmdoption(option, 'no-' + option.long_name, opts, next_id)
code.append('case {}: // --no-{}'.format(
next_id, option.long_name))
next_id += 1
if option.alias: # long option aliases
needs_impl = True
for alias in option.alias:
_add_cmdoption(option, 'no-' + alias, opts, next_id)
code.append('case {}: // --no-{}'.format(next_id, alias))
next_id += 1
code.append(' solver.setOption("{}", "false"); break;'.format(
option.long_name))
return short, '\n '.join(opts), '\n '.join(code)
def _cli_help_format_options(option):
"""
Format short and long options for the cmdline documentation
(--long | --alias | -short).
"""
opts = []
if option.long:
if option.long_opt:
opts.append('--{}={}'.format(option.long_name, option.long_opt))
else:
opts.append('--{}'.format(option.long_name))
if option.alias:
if option.long_opt:
opts.extend(
['--{}={}'.format(a, option.long_opt) for a in option.alias])
else:
opts.extend(['--{}'.format(a) for a in option.alias])
if option.short:
if option.long_opt:
opts.append('-{} {}'.format(option.short, option.long_opt))
else:
opts.append('-{}'.format(option.short))
return ' | '.join(opts)
def _cli_help_wrap(help_msg, opts):
"""Format cmdline documentation (--help) to be 80 chars wide."""
width_opt = 25
text = textwrap.wrap(help_msg, 80 - width_opt, break_on_hyphens=False)
if len(opts) > width_opt - 3:
lines = [' {}'.format(opts), ' ' * width_opt + text[0]]
else:
lines = [' {}{}'.format(opts.ljust(width_opt - 2), text[0])]
lines.extend([' ' * width_opt + l for l in text[1:]])
return lines
def generate_cli_help(modules):
"""Generate the output for --help."""
common = []
regular = []
others = []
for module in modules:
if not module.options:
continue
others.append('')
others.append('From the {} module:'.format(module.name))
for option in module.options:
if option.category == 'undocumented':
continue
msg = option.help
if option.category == 'expert':
msg += ' (EXPERTS only)'
opts = _cli_help_format_options(option)
if opts:
if option.alternate:
msg += ' [*]'
res = _cli_help_wrap(msg, opts)
if option.category == 'common':
common.extend(res)
else:
others.extend(res)
if option.category == 'regular':
regular.extend(res)
return '\n'.join(common), '\n'.join(others), '\n'.join(regular)
################################################################################
# sphinx command line documentation @ docs/options_generated.rst
def _sphinx_help_add(module, option, common, others):
"""Analyze an option and add it to either common or others."""
if option.category == 'common':
common.append(option)
else:
if module.name not in others:
others[module.name] = []
others[module.name].append(option)
def _sphinx_help_render_option(res, opt):
"""Render an option to be displayed with sphinx."""
names = []
if opt.short:
names.append(opt.short)
names.append(opt.long_name)
if opt.alias:
names.extend(opt.alias)
data = {
'names': ' | '.join(names),
'alternate': '',
'type': '',
'default': '',
}
if opt.alternate:
data['alternate'] = ' (also ``--no-*``)'
if opt.type == 'bool':
data['type'] = 'type ``bool``'
elif opt.type == 'std::string':
data['type'] = 'type ``string``'
elif is_numeric_cpp_type(opt.type):
data['type'] = 'type ``{}``'.format(opt.type)
if opt.minimum and opt.maximum:
data['type'] += ', ``{} <= {} <= {}``'.format(
opt.minimum, opt.long_opt, opt.maximum)
elif opt.minimum:
data['type'] += ', ``{} <= {}``'.format(opt.minimum, opt.long_opt)
elif opt.maximum:
data['type'] += ', ``{} <= {}``'.format(opt.long_opt, opt.maximum)
elif opt.mode:
data['type'] = '``' + ' | '.join(opt.mode_name.values()) + '``'
else:
data['type'] = 'custom ``{}``'.format(opt.type)
if opt.default:
if opt.mode:
data['default'] = ', default ``{}``'.format(
opt.mode_name[opt.default])
else:
data['default'] = ', default ``{}``'.format(opt.default)
desc = '``{names}`` [{type}{default}]{alternate}'.format(**data)
res.append('.. _lbl-option-{}:'.format(opt.long_name))
res.append('')
if opt.category == 'expert':
res.append('.. rst-class:: expert-option simple')
res.append('')
desc += '''
.. rst-class:: float-right
**[experts only]**
'''
res.append(desc)
res.append(' ' + opt.help.replace("*", "\\*"))
if opt.mode:
res.append(' ')
res.append(' ' + opt.help_mode)
res.append(' ')
for m in opt.mode.keys():
if opt.mode_help[m]:
res.append(' :``{}``: {}'.format(opt.mode_name[m], opt.mode_help[m]))
res.append(' ')
def generate_sphinx_help(modules):
"""Render the command line help for sphinx."""
common = []
others = {}
for module, option in all_options(modules, False):
if option.category == 'undocumented':
continue
if not option.long and not option.short:
continue
_sphinx_help_add(module, option, common, others)
res = []
res.append('Most Commonly-Used cvc5 Options')
res.append('===============================')
for opt in common:
_sphinx_help_render_option(res, opt)
res.append('')
res.append('Additional cvc5 Options')
res.append('=======================')
for module in others:
res.append('')
res.append('{} Module'.format(module))
res.append('-' * (len(module) + 8))
for opt in others[module]:
_sphinx_help_render_option(res, opt)
return '\n'.join(res)
################################################################################
# sphinx documentation for --output @ docs/output_tags_generated.rst
def generate_sphinx_output_tags(modules, src_dir, build_dir):
"""Render help for the --output option for sphinx."""
base = next(filter(lambda m: m.id == 'base', modules))
opt = next(filter(lambda o: o.long == 'output=TAG', base.options))
# The programoutput extension has weird semantics about the cwd:
# https://sphinxcontrib-programoutput.readthedocs.io/en/latest/#usage
cwd = '/' + os.path.relpath(build_dir, src_dir)
res = []
for name, info in opt.mode.items():
info = info[0]
if 'description' not in info:
continue
res.append(opt.mode_name[name])
res.append('~' * len(res[-1]))
res.append('')
res.append(info['description'])
if 'example-file' in info:
res.append('')
res.append('.. command-output:: bin/cvc5 -o {} ../test/regress/cli/{}'.format(info['name'], info['example-file']))
res.append(' :cwd: {}'.format(cwd))
res.append('')
res.append('')
return '\n'.join(res)
################################################################################
# for io_utils.h and io_utils.cpp
def __get_printer_options(modules):
for mod, opt in all_options(modules):
if mod.id == 'printer':
yield opt
def generate_iodecls(modules):
return concat_format(
'''
void setDefault{name_capitalized}({type} value);
void apply{name_capitalized}(std::ios_base& ios, {type} value) CVC5_EXPORT;
{type} get{name_capitalized}(std::ios_base& ios);''',
__get_printer_options(modules))
def generate_ioimpls(modules):
return concat_format(
'''
const static int s_ios{name_capitalized} = std::ios_base::xalloc();
static thread_local {type} s_{name}Default = {fqdefault};
void setDefault{name_capitalized}({type} value) {{ s_{name}Default = value; }}
void apply{name_capitalized}(std::ios_base& ios, {type} value) {{ setData(ios, s_ios{name_capitalized}, value); }}
{type} get{name_capitalized}(std::ios_base& ios) {{ return getData(ios, s_ios{name_capitalized}, s_{name}Default); }}
''', __get_printer_options(modules))
def generate_ioscope_members(modules):
return concat_format(' {type} d_{name};', __get_printer_options(modules))
def generate_ioscope_memberinit(modules):
return concat_format(' d_{name}(get{name_capitalized}(d_ios))',
__get_printer_options(modules),
glue=',\n')
def generate_ioscope_restore(modules):
return concat_format(' apply{name_capitalized}(d_ios, d_{name});',
__get_printer_options(modules))
################################################################################
# main code generation for individual modules
def codegen_module(module, dst_dir, tpls):
"""Generate code for one option module."""
data = {
'id_cap': module.id_cap,
'id': module.id,
# module header
'includes': generate_module_includes(module),
'modes_decl': generate_module_mode_decl(module),
'holder_decl': generate_module_holder_decl(module),
'long_name_decl': generate_module_long_name_decl(module),
# module source
'header': module.header,
'modes_impl': generate_module_mode_impl(module),
}
for tpl in tpls:
filename = tpl['output'].replace('module', module.filename)
write_file(dst_dir, filename, tpl['content'].format(**data))
################################################################################
# main code generation
def codegen_all_modules(modules, src_dir, build_dir, dst_dir, tpls):
"""Generate code for all option modules."""
short, cmdline_opts, parseinternal = generate_parsing(modules)
help_common, help_others, help_regular = generate_cli_help(modules)
if os.path.isdir('{}/docs/'.format(build_dir)):
write_file('{}/docs/'.format(build_dir), 'options_generated.rst',
generate_sphinx_help(modules))
write_file('{}/docs/'.format(build_dir), 'output_tags_generated.rst',
generate_sphinx_output_tags(modules, src_dir, build_dir))
data = {
# options/io_utils.h
'ioscope_members': generate_ioscope_members(modules),
'iodecls': generate_iodecls(modules),
# options/io_utils.cpp
'ioimpls': generate_ioimpls(modules),
'ioscope_memberinit': generate_ioscope_memberinit(modules),
'ioscope_restore': generate_ioscope_restore(modules),
# options/options.h
'holder_fwd_decls': generate_holder_fwd_decls(modules),
'holder_mem_decls': generate_holder_mem_decls(modules),
'holder_ref_decls': generate_holder_ref_decls(modules),
# options/options.cpp
'headers_module': generate_module_headers(modules),
'holder_mem_inits': generate_holder_mem_inits(modules),
'holder_ref_inits': generate_holder_ref_inits(modules),
'write_functions': generate_write_functions(modules),
'holder_mem_copy': generate_holder_mem_copy(modules),
# options/options_public.cpp
'options_includes': generate_public_includes(modules),
'getnames_impl': generate_getnames_impl(modules),
'option_enum_and_table': generate_option_enum_and_table(modules),
'get_impl': generate_get_impl(modules),
'set_impl': generate_set_impl(modules),
'getinfo_impl': generate_getinfo_impl(modules),
# main/options.cpp
'help_common': help_common,
'help_others': help_others,
'help_regular': help_regular,
'cmdoptions_long': cmdline_opts,
'cmdoptions_short': short,
'parseinternal_impl': parseinternal,
}
for tpl in tpls:
write_file(dst_dir, tpl['output'], tpl['content'].format(**data))
################################################################################
# sanity checking
class Checker:
"""Performs a variety of sanity checks on options and option modules, and
constructs `Module` and `Option` from dictionaries."""
def __init__(self):
self.__filename = None
self.__long_cache = {}
def perr(self, msg, *args, **kwargs):
"""Print an error and die."""
if 'option' in kwargs:
msg = "option '{}' {}".format(kwargs['option'], msg)
msg = 'parse error in {}: {}'.format(self.__filename, msg)
die(msg.format(*args, **kwargs))
def __check_module_attribs(self, req, valid, module):
"""Check the attributes of an option module."""
for k in req:
if k not in module:
self.perr("required module attribute '{}' not specified", k)
for k in module:
if k not in valid:
self.perr("invalid module attribute '{}' specified", k)
def __check_option_attribs(self, req, valid, option):
"""Check the attributes of an option."""
if 'name' in option:
name = option['name']
else:
name = option.get('long', '--')
for k in req:
if k not in option:
self.perr(
"required option attribute '{}' not specified for '{}'", k,
name)
for k in option:
if k not in valid:
self.perr("invalid option attribute '{}' specified for '{}'",
k, name)
def __check_option_long(self, option, long):
"""Check a long argument of an option (name and uniqueness)."""
if long.startswith('--'):
self.perr("remove '--' prefix from '{}'", long, option=option)
r = r'^[0-9a-zA-Z\-]+$'
if not re.match(r, long):
self.perr("long '{}' does not match '{}'", long, r, option=option)
if long in self.__long_cache:
file = self.__long_cache[long]
self.perr("long '{}' was already defined in '{}'",
long,
file,
option=option)
self.__long_cache[long] = self.__filename
def check_module(self, module, filename):
"""Check the given module and return a `Module` object."""
self.__filename = os.path.basename(filename)
self.__check_module_attribs(MODULE_ATTR_REQ, MODULE_ATTR_ALL, module)
return Module(module, filename)
def check_option(self, option):
"""Check the option module and return an `Option` object."""
self.__check_option_attribs(OPTION_ATTR_REQ, OPTION_ATTR_ALL, option)
o = Option(option)
if o.category not in CATEGORY_VALUES:
self.perr("has invalid category '{}'", o.category, option=o)
if o.mode and not o.help_mode:
self.perr('defines modes but no help_mode', option=o)
if o.mode and not o.default:
self.perr('mode option has no default', option=o)
if o.mode and o.default and o.default not in o.mode.keys():
self.perr("invalid default value '{}'", o.default, option=o)
if o.short and not o.long:
self.perr("has short '{}' but no long", o.short, option=o)
if o.category != 'undocumented' and not o.help:
self.perr("of type '{}' has no help text", o.category, option=o)
if o.alias and not o.long:
self.perr('has aliases but no long', option=o)
if o.alternate and o.type != 'bool':
self.perr('is alternate but not bool', option=o)
if o.name and o.default is None:
self.perr('has no default', option=o)
if o.long:
self.__check_option_long(o, o.long_name)
if o.alternate:
self.__check_option_long(o, 'no-' + o.long_name)
if o.type == 'bool' and '=' in o.long:
self.perr('bool options must not have an argument description', option=o)
if o.type != 'bool' and not '=' in o.long:
self.perr("needs argument description ('{}=...')",
o.long,
option=o)
if o.alias:
for alias in o.alias:
self.__check_option_long(o, alias)
if o.alternate:
self.__check_option_long(o, 'no-' + alias)
return o
################################################################################
# main entrypoint
def usage():
"""Print the command-line usage"""
print('mkoptions.py +')
print('')
print(' base source directory of all toml files')
print(' build directory to write the generated sphinx docs')
print(' base destination directory for all generated files')
print(' + one or more *_options.toml files')
print('')
def mkoptions_main():
if len(sys.argv) < 5:
usage()
die('missing arguments')
# Load command line arguments
_, src_dir, build_dir, dst_dir, *filenames = sys.argv
# Check if given directories exist.
for d in [src_dir, dst_dir]:
if not os.path.isdir(d):
usage()
die("'{}' is not a directory".format(d))
# Check if given configuration files exist.
for file in filenames:
if not os.path.exists(file):
die("configuration file '{}' does not exist".format(file))
module_tpls = [
{'input': 'options/module_template.h'},
{'input': 'options/module_template.cpp'},
]
global_tpls = [
{'input': 'options/io_utils_template.h'},
{'input': 'options/io_utils_template.cpp'},
{'input': 'options/options_template.h'},
{'input': 'options/options_template.cpp'},
{'input': 'options/options_public_template.cpp'},
{'input': 'main/options_template.cpp'},
]
# Load all template files
for tpl in module_tpls + global_tpls:
tpl['output'] = tpl['input'].replace('_template', '')
tpl['content'] = read_tpl(src_dir, tpl['input'])
# Parse and check toml files
checker = Checker()
modules = []
for filename in filenames:
with open(filename, "rb") as f:
data = tomllib.load(f)
module = checker.check_module(data, filename)
if 'option' in data:
module.options = sorted(
[checker.check_option(a) for a in data['option']])
modules.append(module)
# Generate code
for module in modules:
codegen_module(module, dst_dir, module_tpls)
codegen_all_modules(modules, src_dir, build_dir, dst_dir, global_tpls)
# Generate output file to signal cmake when this script was run last
open(os.path.join(dst_dir, 'options/options.stamp'), 'w').write('')
if __name__ == "__main__":
mkoptions_main()
sys.exit(0)