# -*- coding: utf-8 -*- ########################################################################## ## # The Coq Proof Assistant / The Coq Development Team ## ## v # INRIA, CNRS and contributors - Copyright 1999-2018 ## ## `_. Unlike ``.. productionlist``\ s, this directive accepts notation syntax. Usage:: .. prodn:: token += production .. prodn:: token ::= production Example:: .. prodn:: term += let: @pattern := @term in @term .. prodn:: occ_switch ::= { {? + %| - } {* @num } } """ subdomain = "prodn" #annotation = "Grammar production" def _render_signature(self, signature, signode): raise NotImplementedError(self) SIG_ERROR = ("Invalid syntax in ``.. prodn::`` directive" + "\nExpected ``name ::= ...`` or ``name += ...``" + " (e.g. ``pattern += constr:(@ident)``)") def handle_signature(self, signature, signode): nsplits = 2 parts = signature.split(maxsplit=nsplits) if len(parts) != 3: raise ExtensionError(ProductionObject.SIG_ERROR) lhs, op, rhs = (part.strip() for part in parts) if op not in ["::=", "+="]: raise ExtensionError(ProductionObject.SIG_ERROR) self._render_annotation(signode) lhs_op = '{} {} '.format(lhs, op) lhs_node = nodes.literal(lhs_op, lhs_op) position = self.state_machine.get_source_and_line(self.lineno) rhs_node = parse_notation(rhs, *position) signode += addnodes.desc_name(signature, '', lhs_node, rhs_node) return ('token', lhs) if op == '::=' else None def _add_index_entry(self, name, target): pass def _target_id(self, name): # Use `name[1]` instead of ``nodes.make_id(name[1])`` to work around # https://github.com/sphinx-doc/sphinx/issues/4983 return 'grammar-token-{}'.format(name[1]) def _record_name(self, name, targetid): env = self.state.document.settings.env objects = env.domaindata['std']['objects'] self._warn_if_duplicate_name(objects, name) objects[name] = env.docname, targetid class ExceptionObject(NotationObject): """An error raised by a Coq command or tactic. This commonly appears nested in the ``.. tacn::`` that raises the exception. Example:: .. tacv:: assert @form by @tactic This tactic applies :n:`@tactic` to solve the subgoals generated by ``assert``. .. exn:: Proof is not complete Raised if :n:`@tactic` does not fully solve the goal. """ subdomain = "exn" index_suffix = "(err)" annotation = "Error" # Uses “exn” since “err” already is a CSS class added by “writer_aux”. # Generate names automatically def _name_from_signature(self, signature): return stringify_with_ellipses(signature) class WarningObject(NotationObject): """An warning raised by a Coq command or tactic.. Do not mistake this for ``.. warning::``; this directive is for warning messages produced by Coq. Example:: .. warn:: Ambiguous path When the coercion :token:`qualid` is added to the inheritance graph, non valid coercion paths are ignored. """ subdomain = "warn" index_suffix = "(warn)" annotation = "Warning" # Generate names automatically def _name_from_signature(self, signature): return stringify_with_ellipses(signature) def NotationRole(role, rawtext, text, lineno, inliner, options={}, content=[]): #pylint: disable=unused-argument, dangerous-default-value """Any text using the notation syntax (``@id``, ``{+, …}``, etc.). Use this to explain tactic equivalences. For example, you might write this:: :n:`generalize @term as @ident` is just like :n:`generalize @term`, but it names the introduced hypothesis :token:`ident`. Note that this example also uses ``:token:``. That's because ``ident`` is defined in the Coq manual as a grammar production, and ``:token:`` creates a link to that. When referring to a placeholder that happens to be a grammar production, ``:token:`…``` is typically preferable to ``:n:`@…```. """ notation = utils.unescape(text, 1) position = inliner.reporter.get_source_and_line(lineno) return [nodes.literal(rawtext, '', parse_notation(notation, *position, rawtext=rawtext))], [] def coq_code_role(role, rawtext, text, lineno, inliner, options={}, content=[]): #pylint: disable=dangerous-default-value """Coq code. Use this for Gallina and Ltac snippets:: :g:`apply plus_comm; reflexivity` :g:`Set Printing All.` :g:`forall (x: t), P(x)` """ options['language'] = 'Coq' return code_role(role, rawtext, text, lineno, inliner, options, content) ## Too heavy: ## Forked from code_role to use our custom tokenizer; this doesn't work for ## snippets though: for example CoqDoc swallows the parentheses around this: ## “(a: A) (b: B)” # set_classes(options) # classes = ['code', 'coq'] # code = utils.unescape(text, 1) # node = nodes.literal(rawtext, '', *highlight_using_coqdoc(code), classes=classes) # return [node], [] CoqCodeRole = coq_code_role class CoqtopDirective(Directive): r"""A reST directive to describe interactions with Coqtop. Usage:: .. coqtop:: options… Coq code to send to coqtop Example:: .. coqtop:: in reset undo Print nat. Definition a := 1. Here is a list of permissible options: - Display options - ``all``: Display input and output - ``in``: Display only input - ``out``: Display only output - ``none``: Display neither (useful for setup commands) - Behavior options - ``reset``: Send a ``Reset Initial`` command before running this block - ``undo``: Send an ``Undo n`` (``n`` = number of sentences) command after running all the commands in this block ``coqtop``\ 's state is preserved across consecutive ``.. coqtop::`` blocks of the same document (``coqrst`` creates a single ``coqtop`` process per reST source file). Use the ``reset`` option to reset Coq's state. """ has_content = True required_arguments = 0 optional_arguments = 1 final_argument_whitespace = True option_spec = { 'name': directives.unchanged } directive_name = "coqtop" def run(self): # Uses a ‘container’ instead of a ‘literal_block’ to disable # Pygments-based post-processing (we could also set rawsource to '') content = '\n'.join(self.content) args = self.arguments[0].split() if self.arguments else ['in'] if 'all' in args: args.extend(['in', 'out']) node = nodes.container(content, coqtop_options = list(set(args)), classes=['coqtop', 'literal-block']) self.add_name(node) return [node] class CoqdocDirective(Directive): """A reST directive to display Coqtop-formatted source code. Usage:: .. coqdoc:: Coq code to highlight Example:: .. coqdoc:: Definition test := 1. """ # TODO implement this as a Pygments highlighter? has_content = True required_arguments = 0 optional_arguments = 0 final_argument_whitespace = True option_spec = { 'name': directives.unchanged } directive_name = "coqdoc" def run(self): # Uses a ‘container’ instead of a ‘literal_block’ to disable # Pygments-based post-processing (we could also set rawsource to '') content = '\n'.join(self.content) node = nodes.inline(content, '', *highlight_using_coqdoc(content)) wrapper = nodes.container(content, node, classes=['coqdoc', 'literal-block']) self.add_name(wrapper) return [wrapper] class ExampleDirective(BaseAdmonition): """A reST directive for examples. This behaves like a generic admonition; see http://docutils.sourceforge.net/docs/ref/rst/directives.html#generic-admonition for more details. Optionally, any text immediately following the ``.. example::`` header is used as the example's title. Example:: .. example:: Adding a hint to a database The following adds ``plus_comm`` to the ``plu`` database: .. coqdoc:: Hint Resolve plus_comm : plu. """ node_class = nodes.admonition directive_name = "example" optional_arguments = 1 def run(self): # ‘BaseAdmonition’ checks whether ‘node_class’ is ‘nodes.admonition’, # and uses arguments[0] as the title in that case (in other cases, the # title is unset, and it is instead set in the HTML visitor). assert len(self.arguments) <= 1 self.arguments = [": ".join(['Example'] + self.arguments)] self.options['classes'] = ['admonition', 'note'] return super().run() class PreambleDirective(Directive): r"""A reST directive to include a TeX file. Mostly useful to let MathJax know about `\def`s and `\newcommand`s. The contents of the TeX file are wrapped in a math environment, as MathJax doesn't process LaTeX definitions otherwise. Usage:: .. preamble:: preamble.tex """ has_content = False required_arguments = 1 optional_arguments = 0 final_argument_whitespace = True option_spec = {} directive_name = "preamble" def run(self): document = self.state.document env = document.settings.env if not document.settings.file_insertion_enabled: msg = 'File insertion disabled' return [document.reporter.warning(msg, line=self.lineno)] rel_fname, abs_fname = env.relfn2path(self.arguments[0]) env.note_dependency(rel_fname) with open(abs_fname, encoding="utf-8") as ltx: latex = ltx.read() node = make_math_node(latex, env.docname, nowrap=False) node['classes'] = ["math-preamble"] set_source_info(self, node) return [node] class InferenceDirective(Directive): r"""A reST directive to format inference rules. This also serves as a small illustration of the way to create new Sphinx directives. Usage:: .. inference:: name newline-separated premises -------------------------- conclusion Example:: .. inference:: Prod-Pro \WTEG{T}{s} s \in \Sort \WTE{\Gamma::(x:T)}{U}{\Prop} ----------------------------- \WTEG{\forall~x:T,U}{\Prop} """ required_arguments = 1 optional_arguments = 0 has_content = True final_argument_whitespace = True directive_name = "inference" @staticmethod def prepare_latex_operand(op): # TODO: Could use a fancier inference class in LaTeX return '%\n\\hspace{3em}%\n'.join(op.strip().splitlines()) def prepare_latex(self, content): parts = re.split('^ *----+ *$', content, flags=re.MULTILINE) if len(parts) != 2: raise self.error('Expected two parts in ‘inference’ directive, separated by a rule (----).') top, bottom = tuple(InferenceDirective.prepare_latex_operand(p) for p in parts) return "%\n".join(("\\frac{", top, "}{", bottom, "}")) def run(self): self.assert_has_content() title = self.arguments[0] content = '\n'.join(self.content) latex = self.prepare_latex(content) docname = self.state.document.settings.env.docname math_node = make_math_node(latex, docname, nowrap=False) tid = nodes.make_id(title) target = nodes.target('', '', ids=['inference-' + tid]) self.state.document.note_explicit_target(target) term, desc = nodes.term('', title), nodes.description('', math_node) dli = nodes.definition_list_item('', term, desc) dl = nodes.definition_list(content, target, dli) set_source_info(self, dl) return [dl] class AnsiColorsParser(): """Parse ANSI-colored output from Coqtop into Sphinx nodes.""" # Coqtop's output crashes ansi.py, because it contains a bunch of extended codes # This class is a fork of the original ansi.py, released under a BSD license in sphinx-contribs COLOR_PATTERN = re.compile('\x1b\\[([^m]+)m') def __init__(self): self.new_nodes, self.pending_nodes = [], [] def _finalize_pending_nodes(self): self.new_nodes.extend(self.pending_nodes) self.pending_nodes = [] def _add_text(self, raw, beg, end): if beg < end: text = raw[beg:end] if self.pending_nodes: self.pending_nodes[-1].append(nodes.Text(text)) else: self.new_nodes.append(nodes.inline('', text)) def colorize_str(self, raw): """Parse raw (an ANSI-colored output string from Coqtop) into Sphinx nodes.""" last_end = 0 for match in AnsiColorsParser.COLOR_PATTERN.finditer(raw): self._add_text(raw, last_end, match.start()) last_end = match.end() classes = ansicolors.parse_ansi(match.group(1)) if 'ansi-reset' in classes: self._finalize_pending_nodes() else: node = nodes.inline() self.pending_nodes.append(node) node['classes'].extend(classes) self._add_text(raw, last_end, len(raw)) self._finalize_pending_nodes() return self.new_nodes class CoqtopBlocksTransform(Transform): """Filter handling the actual work for the coqtop directive Adds coqtop's responses, colorizes input and output, and merges consecutive coqtop directives for better visual rendition. """ default_priority = 10 @staticmethod def is_coqtop_block(node): return isinstance(node, nodes.Element) and 'coqtop_options' in node @staticmethod def split_sentences(source): """Split Coq sentences in source. Could be improved.""" return re.split(r"(?<=(?`_ and reference its tokens using ``:token:`…```. """ #pylint: disable=dangerous-default-value, unused-argument env = inliner.document.settings.env targetid = 'grammar-token-{}'.format(text) target = nodes.target('', '', ids=[targetid]) inliner.document.note_explicit_target(target) code = nodes.literal(rawtext, text, role=typ.lower()) node = nodes.inline(rawtext, '', target, code, classes=['inline-grammar-production']) set_role_source_info(inliner, lineno, node) env.domaindata['std']['objects']['token', text] = env.docname, targetid return [node], [] GrammarProductionRole.role_name = "production" class CoqDomain(Domain): """A domain to document Coq code. Sphinx has a notion of “domains”, used to tailor it to a specific language. Domains mostly consist in descriptions of the objects that we wish to describe (for Coq, this includes tactics, tactic notations, options, exceptions, etc.), as well as domain-specific roles and directives. Each domain is responsible for tracking its objects, and resolving references to them. In the case of Coq, this leads us to define Coq “subdomains”, which classify objects into categories in which names must be unique. For example, a tactic and a theorem may share a name, but two tactics cannot be named the same. """ name = 'coq' label = 'Coq' object_types = { # ObjType (= directive type) → (Local name, *xref-roles) 'cmd': ObjType('cmd', 'cmd'), 'cmdv': ObjType('cmdv', 'cmd'), 'tacn': ObjType('tacn', 'tacn'), 'tacv': ObjType('tacv', 'tacn'), 'opt': ObjType('opt', 'opt'), 'flag': ObjType('flag', 'flag'), 'table': ObjType('table', 'table'), 'thm': ObjType('thm', 'thm'), 'prodn': ObjType('prodn', 'prodn'), 'exn': ObjType('exn', 'exn'), 'warn': ObjType('warn', 'exn'), 'index': ObjType('index', 'index', searchprio=-1) } directives = { # Note that some directives live in the same semantic subdomain; ie # there's one directive per object type, but some object types map to # the same role. 'cmd': VernacObject, 'cmdv': VernacVariantObject, 'tacn': TacticNotationObject, 'tacv': TacticNotationVariantObject, 'opt': OptionObject, 'flag': FlagObject, 'table': TableObject, 'thm': GallinaObject, 'prodn' : ProductionObject, 'exn': ExceptionObject, 'warn': WarningObject, } roles = { # Each of these roles lives in a different semantic “subdomain” 'cmd': XRefRole(warn_dangling=True), 'tacn': XRefRole(warn_dangling=True), 'opt': XRefRole(warn_dangling=True), 'flag': XRefRole(warn_dangling=True), 'table': XRefRole(warn_dangling=True), 'thm': XRefRole(warn_dangling=True), 'prodn' : XRefRole(warn_dangling=True), 'exn': XRefRole(warn_dangling=True), 'warn': XRefRole(warn_dangling=True), # This one is special 'index': IndexXRefRole(), # These are used for highlighting 'n': NotationRole, 'g': CoqCodeRole } indices = [CoqVernacIndex, CoqTacticIndex, CoqOptionIndex, CoqGallinaIndex, CoqExceptionIndex] data_version = 1 initial_data = { # Collect everything under a key that we control, since Sphinx adds # others, such as “version” 'objects' : { # subdomain → name → docname, objtype, targetid 'cmd': {}, 'tacn': {}, 'opt': {}, 'flag': {}, 'table': {}, 'thm': {}, 'prodn' : {}, 'exn': {}, 'warn': {}, } } @staticmethod def find_index_by_name(targetid): for index in CoqDomain.indices: if index.name == targetid: return index def get_objects(self): # Used for searching and object inventories (intersphinx) for _, objects in self.data['objects'].items(): for name, (docname, objtype, targetid) in objects.items(): yield (name, name, objtype, docname, targetid, self.object_types[objtype].attrs['searchprio']) for index in self.indices: yield (index.name, index.localname, 'index', "coq-" + index.name, '', -1) def merge_domaindata(self, docnames, otherdata): DUP = "Duplicate declaration: '{}' also defined in '{}'.\n" for subdomain, their_objects in otherdata['objects'].items(): our_objects = self.data['objects'][subdomain] for name, (docname, objtype, targetid) in their_objects.items(): if docname in docnames: if name in our_objects: self.env.warn(docname, DUP.format(name, our_objects[name][0])) our_objects[name] = (docname, objtype, targetid) def resolve_xref(self, env, fromdocname, builder, role, targetname, node, contnode): # ‘target’ is the name that was written in the document # ‘role’ is where this xref comes from; it's exactly one of our subdomains if role == 'index': index = CoqDomain.find_index_by_name(targetname) if index: return make_refnode(builder, fromdocname, "coq-" + index.name, '', contnode, index.localname) else: resolved = self.data['objects'][role].get(targetname) if resolved: (todocname, _, targetid) = resolved return make_refnode(builder, fromdocname, todocname, targetid, contnode, targetname) def clear_doc(self, docname_to_clear): for subdomain_objects in self.data['objects'].values(): for name, (docname, _, _) in list(subdomain_objects.items()): if docname == docname_to_clear: del subdomain_objects[name] def is_coqtop_or_coqdoc_block(node): return (isinstance(node, nodes.Element) and ('coqtop' in node['classes'] or 'coqdoc' in node['classes'])) def simplify_source_code_blocks_for_latex(app, doctree, fromdocname): # pylint: disable=unused-argument """Simplify coqdoc and coqtop blocks. In HTML mode, this does nothing; in other formats, such as LaTeX, it replaces coqdoc and coqtop blocks by plain text sources, which will use pygments if available. This prevents the LaTeX builder from getting confused. """ is_html = app.builder.tags.has("html") for node in doctree.traverse(is_coqtop_or_coqdoc_block): if is_html: node.rawsource = '' # Prevent pygments from kicking in elif 'coqtop-hidden' in node['classes']: node.parent.remove(node) else: node.replace_self(nodes.literal_block(node.rawsource, node.rawsource, language="Coq")) COQ_ADDITIONAL_DIRECTIVES = [CoqtopDirective, CoqdocDirective, ExampleDirective, InferenceDirective, PreambleDirective] COQ_ADDITIONAL_ROLES = [GrammarProductionRole] def setup(app): """Register the Coq domain""" # A few sanity checks: subdomains = set(obj.subdomain for obj in CoqDomain.directives.values()) assert subdomains.issuperset(chain(*(idx.subdomains for idx in CoqDomain.indices))) assert subdomains.issubset(CoqDomain.roles.keys()) # Add domain, directives, and roles app.add_domain(CoqDomain) for role in COQ_ADDITIONAL_ROLES: app.add_role(role.role_name, role) for directive in COQ_ADDITIONAL_DIRECTIVES: app.add_directive(directive.directive_name, directive) app.add_transform(CoqtopBlocksTransform) app.connect('doctree-resolved', simplify_source_code_blocks_for_latex) app.connect('doctree-resolved', CoqtopBlocksTransform.merge_consecutive_coqtop_blocks) # Add extra styles app.add_stylesheet("fonts.css") app.add_stylesheet("ansi.css") app.add_stylesheet("coqdoc.css") app.add_javascript("notations.js") app.add_stylesheet("notations.css") app.add_stylesheet("pre-text.css") # Tell Sphinx about extra settings app.add_config_value("report_undocumented_coq_objects", None, 'env') # ``env_version`` is used by Sphinx to know when to invalidate # coqdomain-specific bits in its caches. It should be incremented when the # contents of ``env.domaindata['coq']`` change. See # `https://github.com/sphinx-doc/sphinx/issues/4460`. meta = { "version": "0.1", "env_version": 2, "parallel_read_safe": True } return meta