blob: 62a9475613028226f9e0171cabc25b400fe2d066 [file] [log] [blame]
# Copyright 2019 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Generates a Markdown file documenting the raw Emboss grammar."""
from __future__ import print_function
import re
import sys
from front_end import constraints
from front_end import module_ir
from front_end import tokenizer
# Keep the output to less than 80 columns, so that the preformatted sections are
# not cut off.
_MAX_OUTPUT_WIDTH = 80
_HEADER = """
This is the context-free grammar for Emboss. Terminal symbols are in `"quotes"`
or are named in `CamelCase`; nonterminal symbols are named in `snake_case`. The
term `<empty>` to the right of the `->` indicates an empty production (a rule
where the left-hand-side may be parsed from an empty string).
This listing is auto-generated from the grammar defined in `module_ir.py`.
Note that, unlike in many languages, comments are included in the grammar. This
is so that comments can be handled more easily by the autoformatter; comments
are ignored by the compiler. This is distinct from *documentation*, which is
included in the IR for use by documentation generators.
""".lstrip()
_BOILERPLATE_PRODUCTION_HEADER = """
The following productions are automatically generated to handle zero-or-more,
one-or-more, and zero-or-one repeated lists (`foo*`, `foo+`, and `foo?`
nonterminals) in LR(1). They are included for completeness, but may be ignored
if you just want to understand the grammar.
"""
_TOKENIZER_RULE_HEADER = """
The following regexes are used to tokenize input into the corresponding symbols.
Note that the `Indent`, `Dedent`, and `EndOfLine` symbols are generated using
separate logic.
"""
_KEYWORDS_HEADER = """
The following {} keywords are reserved, but not used, by Emboss. They may not
be used as field, type, or enum value names.
"""
def _sort_productions(productions, start_symbol):
"""Sorts the given productions in a human-friendly order."""
productions_by_lhs = {}
for p in productions:
if p.lhs not in productions_by_lhs:
productions_by_lhs[p.lhs] = set()
productions_by_lhs[p.lhs].add(p)
queue = [start_symbol]
previously_queued_symbols = set(queue)
main_production_list = []
# This sorts productions depth-first. I'm not sure if it is better to sort
# them breadth-first or depth-first, or with some hybrid.
while queue:
symbol = queue.pop(-1)
if symbol not in productions_by_lhs:
continue
for production in sorted(productions_by_lhs[symbol]):
main_production_list.append(production)
for symbol in production.rhs:
# Skip boilerplate productions for now, but include their base
# production.
if symbol and symbol[-1] in "*+?":
symbol = symbol[0:-1]
if symbol not in previously_queued_symbols:
queue.append(symbol)
previously_queued_symbols.add(symbol)
# It's not particularly important to put boilerplate productions in any
# particular order.
boilerplate_production_list = sorted(
set(productions) - set(main_production_list))
for production in boilerplate_production_list:
assert production.lhs[-1] in "*+?", "Found orphaned production {}".format(
production.lhs)
assert set(productions) == set(
main_production_list + boilerplate_production_list)
assert len(productions) == len(main_production_list) + len(
boilerplate_production_list)
return main_production_list, boilerplate_production_list
def _word_wrap_at_column(words, width):
"""Wraps words to the specified width, and returns a list of wrapped lines."""
result = []
in_progress = []
for word in words:
if len(" ".join(in_progress + [word])) > width:
result.append(" ".join(in_progress))
assert len(result[-1]) <= width
in_progress = []
in_progress.append(word)
result.append(" ".join(in_progress))
assert len(result[-1]) <= width
return result
def _format_productions(productions):
"""Formats a list of productions for inclusion in a Markdown document."""
max_lhs_len = max([len(production.lhs) for production in productions])
# TODO(bolms): This highlighting is close for now, but not actually right.
result = ["```shell\n"]
last_lhs = None
for production in productions:
if last_lhs == production.lhs:
lhs = ""
delimiter = " |"
else:
lhs = production.lhs
delimiter = "->"
leader = "{lhs:{width}} {delimiter}".format(
lhs=lhs,
width=max_lhs_len,
delimiter=delimiter)
for rhs_block in _word_wrap_at_column(
production.rhs or ["<empty>"], _MAX_OUTPUT_WIDTH - len(leader)):
result.append("{leader} {rhs}\n".format(leader=leader, rhs=rhs_block))
leader = " " * len(leader)
last_lhs = production.lhs
result.append("```\n")
return "".join(result)
def _normalize_literal_patterns(literals):
"""Normalizes a list of strings to a list of (regex, symbol) pairs."""
return [(re.sub(r"(\W)", r"\\\1", literal), '"' + literal + '"')
for literal in literals]
def _normalize_regex_patterns(regexes):
"""Normalizes a list of tokenizer regexes to a list of (regex, symbol)."""
# g3doc breaks up patterns containing '|' when they are inserted into a table,
# unless they're preceded by '\'. Note that other special characters,
# including '\', should *not* be escaped with '\'.
return [(re.sub(r"\|", r"\\|", r.regex.pattern), r.symbol) for r in regexes]
def _normalize_reserved_word_list(reserved_words):
"""Returns words that would be allowed as names if they were not reserved."""
interesting_reserved_words = []
for word in reserved_words:
tokens, errors = tokenizer.tokenize(word, "")
assert tokens and not errors, "Failed to tokenize " + word
if tokens[0].symbol in ["SnakeWord", "CamelWord", "ShoutyWord"]:
interesting_reserved_words.append(word)
return sorted(interesting_reserved_words)
def _format_token_rules(token_rules):
"""Formats a list of (pattern, symbol) pairs as a table."""
pattern_width = max([len(rule[0]) for rule in token_rules])
pattern_width += 2 # For the `` characters.
result = ["{pat_header:{width}} | Symbol\n"
"{empty:-<{width}} | {empty:-<30}\n".format(pat_header="Pattern",
width=pattern_width,
empty="")]
for rule in token_rules:
if rule[1]:
symbol_name = "`" + rule[1] + "`"
else:
symbol_name = "*no symbol emitted*"
result.append(
"{pattern:{width}} | {symbol}\n".format(pattern="`" + rule[0] + "`",
width=pattern_width,
symbol=symbol_name))
return "".join(result)
def _format_keyword_list(reserved_words):
"""formats a list of reserved words."""
lines = []
current_line = ""
for word in reserved_words:
if len(current_line) + len(word) + 2 > 80:
lines.append(current_line)
current_line = ""
current_line += "`{}` ".format(word)
return "".join([line[:-1] + "\n" for line in lines])
def generate_grammar_md():
"""Generates up-to-date text for grammar.md."""
main_productions, boilerplate_productions = _sort_productions(
module_ir.PRODUCTIONS, module_ir.START_SYMBOL)
result = [_HEADER, _format_productions(main_productions),
_BOILERPLATE_PRODUCTION_HEADER,
_format_productions(boilerplate_productions)]
main_tokens = _normalize_literal_patterns(tokenizer.LITERAL_TOKEN_PATTERNS)
main_tokens += _normalize_regex_patterns(tokenizer.REGEX_TOKEN_PATTERNS)
result.append(_TOKENIZER_RULE_HEADER)
result.append(_format_token_rules(main_tokens))
reserved_words = _normalize_reserved_word_list(
constraints.get_reserved_word_list())
result.append(_KEYWORDS_HEADER.format(len(reserved_words)))
result.append(_format_keyword_list(reserved_words))
return "".join(result)
def main(argv):
del argv # Unused.
print(generate_grammar_md(), end="")
return 0
if __name__ == "__main__":
sys.exit(main(sys.argv))