blob: bd27c8a6397bdd37a4b92e49ef50d4b2a3f7cbba [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.
"""module_ir contains code for generating module-level IRs from parse trees.
The primary export is build_ir(), which takes a parse tree (as returned by a
parser from lr1.py), and returns a module-level intermediate representation
("module IR").
This module also notably exports PRODUCTIONS and START_SYMBOL, which should be
fed to lr1.Grammar in order to create a parser for the Emboss language.
"""
import re
import sys
from compiler.util import ir_data
from compiler.util import ir_data_utils
from compiler.util import name_conversion
from compiler.util import parser_types
# Intermediate types; should not be found in the final IR.
class _List(object):
"""A list with source location information."""
__slots__ = ("list", "source_location")
def __init__(self, l):
assert isinstance(l, list), "_List object must wrap list, not '%r'" % l
self.list = l
self.source_location = ir_data.Location()
class _ExpressionTail(object):
"""A fragment of an expression with an operator and right-hand side.
_ExpressionTail is the tail of an expression, consisting of an operator and
the right-hand argument to the operator; for example, in the expression (6+8),
the _ExpressionTail would be "+8".
This is used as a temporary object while converting the right-recursive
"expression" and "times-expression" productions into left-associative
Expressions.
Attributes:
operator: An ir_data.Word of the operator's name.
expression: The expression on the right side of the operator.
source_location: The source location of the operation fragment.
"""
__slots__ = ("operator", "expression", "source_location")
def __init__(self, operator, expression):
self.operator = operator
self.expression = expression
self.source_location = ir_data.Location()
class _FieldWithType(object):
"""A field with zero or more types defined inline with that field."""
__slots__ = ("field", "subtypes", "source_location")
def __init__(self, field, subtypes=None):
self.field = field
self.subtypes = subtypes or []
self.source_location = ir_data.Location()
def build_ir(parse_tree, used_productions=None):
r"""Builds a module-level intermediate representation from a valid parse tree.
The parse tree is precisely dictated by the exact productions in the grammar
used by the parser, with no semantic information. _really_build_ir transforms
this "raw" form into a stable, cooked representation, thereby isolating
subsequent steps from the exact details of the grammar.
(Probably incomplete) list of transformations:
* ParseResult and Token nodes are replaced with Module, Attribute, Struct,
Type, etc. objects.
* Purely syntactic tokens ('"["', '"struct"', etc.) are discarded.
* Repeated elements are transformed from tree form to list form:
a*
/ \
b a*
/ \
c a*
/ \
d a*
(where b, c, and d are nodes of type "a") becomes [b, c, d].
* The values of numeric constants (Number, etc. tokens) are parsed.
* Different classes of names (snake_names, CamelNames, ShoutyNames) are
folded into a single "Name" type, since they are guaranteed to appear in
the correct places in the parse tree.
Arguments:
parse_tree: A parse tree. Each leaf node should be a parser_types.Token
object, and each non-leaf node should have a 'symbol' attribute specifying
which grammar symbol it represents, and a 'children' attribute containing
a list of child nodes. This is the format returned by the parsers
produced by the lr1 module, when run against tokens from the tokenizer
module.
used_productions: If specified, used_productions.add() will be called with
each production actually used in parsing. This can be useful when
developing the grammar and writing tests; in particular, it can be used to
figure out which productions are *not* used when parsing a particular
file.
Returns:
A module-level intermediate representation (module IR) for an Emboss module
(source file). This IR will not have symbols resolved; that must be done on
a forest of module IRs so that names from other modules can be resolved.
"""
# TODO(b/140259131): Refactor _really_build_ir to be less recursive/use an
# explicit stack.
old_recursion_limit = sys.getrecursionlimit()
sys.setrecursionlimit(16 * 1024) # ~8000 top-level entities in one module.
try:
result = _really_build_ir(parse_tree, used_productions)
finally:
sys.setrecursionlimit(old_recursion_limit)
return result
def _really_build_ir(parse_tree, used_productions):
"""Real implementation of build_ir()."""
if used_productions is None:
used_productions = set()
if hasattr(parse_tree, "children"):
parsed_children = [
_really_build_ir(child, used_productions) for child in parse_tree.children
]
used_productions.add(parse_tree.production)
result = _handlers[parse_tree.production](*parsed_children)
if parse_tree.source_location is not None:
if result.source_location:
ir_data_utils.update(result.source_location, parse_tree.source_location)
else:
result.source_location = ir_data_utils.copy(parse_tree.source_location)
return result
else:
# For leaf nodes, the temporary "IR" is just the token. Higher-level rules
# will translate it to a real IR.
assert isinstance(parse_tree, parser_types.Token), str(parse_tree)
return parse_tree
# Map of productions to their handlers.
_handlers = {}
_anonymous_name_counter = 0
def _get_anonymous_field_name():
global _anonymous_name_counter
_anonymous_name_counter += 1
return "emboss_reserved_anonymous_field_{}".format(_anonymous_name_counter)
def _handles(production_text):
"""_handles marks a function as the handler for a particular production."""
production = parser_types.Production.parse(production_text)
def handles(f):
_handlers[production] = f
return f
return handles
def _make_prelude_import(position):
"""Helper function to construct a synthetic ir_data.Import for the prelude."""
location = parser_types.make_location(position, position)
return ir_data.Import(
file_name=ir_data.String(text="", source_location=location),
local_name=ir_data.Word(text="", source_location=location),
source_location=location,
)
def _text_to_operator(text):
"""Converts an operator's textual name to its corresponding enum."""
operations = {
"+": ir_data.FunctionMapping.ADDITION,
"-": ir_data.FunctionMapping.SUBTRACTION,
"*": ir_data.FunctionMapping.MULTIPLICATION,
"==": ir_data.FunctionMapping.EQUALITY,
"!=": ir_data.FunctionMapping.INEQUALITY,
"&&": ir_data.FunctionMapping.AND,
"||": ir_data.FunctionMapping.OR,
">": ir_data.FunctionMapping.GREATER,
">=": ir_data.FunctionMapping.GREATER_OR_EQUAL,
"<": ir_data.FunctionMapping.LESS,
"<=": ir_data.FunctionMapping.LESS_OR_EQUAL,
}
return operations[text]
def _text_to_function(text):
"""Converts a function's textual name to its corresponding enum."""
functions = {
"$max": ir_data.FunctionMapping.MAXIMUM,
"$present": ir_data.FunctionMapping.PRESENCE,
"$upper_bound": ir_data.FunctionMapping.UPPER_BOUND,
"$lower_bound": ir_data.FunctionMapping.LOWER_BOUND,
}
return functions[text]
################################################################################
# Grammar & parse tree to IR translation.
#
# From here to (almost) the end of the file are functions which recursively
# build an IR. The @_handles annotations indicate the exact grammar
# production(s) handled by each function. The handler function should take
# exactly one argument for each symbol in the production's RHS.
#
# The actual Emboss grammar is extracted directly from the @_handles
# annotations, so this is also the grammar definition. For convenience, the
# grammar can be viewed separately in g3doc/grammar.md.
#
# At the end, symbols whose names end in "*", "+", or "?" are extracted from the
# grammar, and appropriate productions are added for zero-or-more, one-or-more,
# or zero-or-one lists, respectively. (This is analogous to the *, +, and ?
# operators in regex.) It is necessary for this to happen here (and not in
# lr1.py) because the generated productions must be associated with
# IR-generation functions.
# A module file is a list of documentation, then imports, then top-level
# attributes, then type definitions. Any section may be missing.
# TODO(bolms): Should Emboss disallow completely empty files?
@_handles(
"module -> comment-line* doc-line* import-line* attribute-line*"
" type-definition*"
)
def _file(leading_newlines, docs, imports, attributes, type_definitions):
"""Assembles the top-level IR for a module."""
del leading_newlines # Unused.
# Figure out the best synthetic source_location for the synthesized prelude
# import.
if imports.list:
position = imports.list[0].source_location.start
elif docs.list:
position = docs.list[0].source_location.end
elif attributes.list:
position = attributes.list[0].source_location.start
elif type_definitions.list:
position = type_definitions.list[0].source_location.start
else:
position = 1, 1
# If the source file is completely empty, build_ir won't automatically
# populate the source_location attribute for the module.
if (
not docs.list
and not imports.list
and not attributes.list
and not type_definitions.list
):
module_source_location = parser_types.make_location((1, 1), (1, 1))
else:
module_source_location = None
return ir_data.Module(
documentation=docs.list,
foreign_import=[_make_prelude_import(position)] + imports.list,
attribute=attributes.list,
type=type_definitions.list,
source_location=module_source_location,
)
@_handles("import-line ->" ' "import" string-constant "as" snake-word Comment? eol')
def _import(import_, file_name, as_, local_name, comment, eol):
del import_, as_, comment, eol # Unused
return ir_data.Import(file_name=file_name, local_name=local_name)
@_handles("doc-line -> doc Comment? eol")
def _doc_line(doc, comment, eol):
del comment, eol # Unused.
return doc
@_handles("doc -> Documentation")
def _doc(documentation):
# As a special case, an empty documentation string may omit the trailing
# space.
if documentation.text == "--":
doc_text = "-- "
else:
doc_text = documentation.text
assert doc_text[0:3] == "-- ", "Documentation token '{}' in unknown format.".format(
documentation.text
)
return ir_data.Documentation(text=doc_text[3:])
# A attribute-line is just a attribute on its own line.
@_handles("attribute-line -> attribute Comment? eol")
def _attribute_line(attr, comment, eol):
del comment, eol # Unused.
return attr
# A attribute is [name = value].
@_handles(
'attribute -> "[" attribute-context? "$default"?'
' snake-word ":" attribute-value "]"'
)
def _attribute(
open_bracket,
context_specifier,
default_specifier,
name,
colon,
attribute_value,
close_bracket,
):
del open_bracket, colon, close_bracket # Unused.
if context_specifier.list:
return ir_data.Attribute(
name=name,
value=attribute_value,
is_default=bool(default_specifier.list),
back_end=context_specifier.list[0],
)
else:
return ir_data.Attribute(
name=name, value=attribute_value, is_default=bool(default_specifier.list)
)
@_handles('attribute-context -> "(" snake-word ")"')
def _attribute_context(open_paren, context_name, close_paren):
del open_paren, close_paren # Unused.
return context_name
@_handles("attribute-value -> expression")
def _attribute_value_expression(expression):
return ir_data.AttributeValue(expression=expression)
@_handles("attribute-value -> string-constant")
def _attribute_value_string(string):
return ir_data.AttributeValue(string_constant=string)
@_handles("boolean-constant -> BooleanConstant")
def _boolean_constant(boolean):
return ir_data.BooleanConstant(value=(boolean.text == "true"))
@_handles("string-constant -> String")
def _string_constant(string):
"""Turns a String token into an ir_data.String, with proper unescaping.
Arguments:
string: A String token.
Returns:
An ir_data.String with the "text" field set to the unescaped value of
string.text.
"""
# TODO(bolms): If/when this logic becomes more complex (e.g., to handle \NNN
# or \xNN escapes), extract this into a separate module with separate tests.
assert string.text[0] == '"'
assert string.text[-1] == '"'
assert len(string.text) >= 2
result = []
for substring in re.split(r"(\\.)", string.text[1:-1]):
if substring and substring[0] == "\\":
assert len(substring) == 2
result.append({"\\": "\\", '"': '"', "n": "\n"}[substring[1]])
else:
result.append(substring)
return ir_data.String(text="".join(result))
# In Emboss, '&&' and '||' may not be mixed without parentheses. These are all
# fine:
#
# x && y && z
# x || y || z
# (x || y) && z
# x || (y && z)
#
# These are syntax errors:
#
# x || y && z
# x && y || z
#
# This is accomplished by making && and || separate-but-equal in the precedence
# hierarchy. Instead of the more traditional:
#
# logical-expression -> or-expression
# or-expression -> and-expression or-expression-right*
# or-expression-right -> '||' and-expression
# and-expression -> equality-expression and-expression-right*
# and-expression-right -> '&&' equality-expression
#
# Or, using yacc-style precedence specifiers:
#
# %left "||"
# %left "&&"
# expression -> expression
# | expression '||' expression
# | expression '&&' expression
#
# Emboss uses a slightly more complex grammar, in which '&&' and '||' are
# parallel, but unmixable:
#
# logical-expression -> and-expression
# | or-expression
# | equality-expression
# or-expression -> equality-expression or-expression-right+
# or-expression-right -> '||' equality-expression
# and-expression -> equality-expression and-expression-right+
# and-expression-right -> '&&' equality-expression
#
# In either case, explicit parenthesization is handled elsewhere in the grammar.
@_handles("logical-expression -> and-expression")
@_handles("logical-expression -> or-expression")
@_handles("logical-expression -> comparison-expression")
@_handles("choice-expression -> logical-expression")
@_handles("expression -> choice-expression")
def _expression(expression):
return expression
# The `logical-expression`s here means that ?: can't be chained without
# parentheses. `x < 0 ? -1 : (x == 0 ? 0 : 1)` is OK, but `x < 0 ? -1 : x == 0
# ? 0 : 1` is not. Parentheses are also needed in the middle: `x <= 0 ? x < 0 ?
# -1 : 0 : 1` is not syntactically valid.
@_handles(
'choice-expression -> logical-expression "?" logical-expression'
' ":" logical-expression'
)
def _choice_expression(condition, question, if_true, colon, if_false):
location = parser_types.make_location(
condition.source_location.start, if_false.source_location.end
)
operator_location = parser_types.make_location(
question.source_location.start, colon.source_location.end
)
# The function_name is a bit weird, but should suffice for any error messages
# that might need it.
return ir_data.Expression(
function=ir_data.Function(
function=ir_data.FunctionMapping.CHOICE,
args=[condition, if_true, if_false],
function_name=ir_data.Word(text="?:", source_location=operator_location),
source_location=location,
)
)
@_handles("comparison-expression -> additive-expression")
def _no_op_comparative_expression(expression):
return expression
@_handles(
"comparison-expression ->"
" additive-expression inequality-operator additive-expression"
)
def _comparative_expression(left, operator, right):
location = parser_types.make_location(
left.source_location.start, right.source_location.end
)
return ir_data.Expression(
function=ir_data.Function(
function=_text_to_operator(operator.text),
args=[left, right],
function_name=operator,
source_location=location,
)
)
@_handles("additive-expression -> times-expression additive-expression-right*")
@_handles("times-expression -> negation-expression times-expression-right*")
@_handles("and-expression -> comparison-expression and-expression-right+")
@_handles("or-expression -> comparison-expression or-expression-right+")
def _binary_operator_expression(expression, expression_right):
"""Builds the IR for a chain of equal-precedence left-associative operations.
_binary_operator_expression transforms a right-recursive list of expression
tails into a left-associative Expression tree. For example, given the
arguments:
6, (Tail("+", 7), Tail("-", 8), Tail("+", 10))
_expression produces a structure like:
Expression(Expression(Expression(6, "+", 7), "-", 8), "+", 10)
This transformation is necessary because strict LR(1) grammars do not allow
left recursion.
Note that this method is used for several productions; each of those
productions handles a different precedence level, but are identical in form.
Arguments:
expression: An ir_data.Expression which is the head of the (expr, operator,
expr, operator, expr, ...) list.
expression_right: A list of _ExpressionTails corresponding to the (operator,
expr, operator, expr, ...) list that comes after expression.
Returns:
An ir_data.Expression with the correct recursive structure to represent a
list of left-associative operations.
"""
e = expression
for right in expression_right.list:
location = parser_types.make_location(
e.source_location.start, right.source_location.end
)
e = ir_data.Expression(
function=ir_data.Function(
function=_text_to_operator(right.operator.text),
args=[e, right.expression],
function_name=right.operator,
source_location=location,
),
source_location=location,
)
return e
@_handles(
"comparison-expression ->" " additive-expression equality-expression-right+"
)
@_handles(
"comparison-expression ->" " additive-expression less-expression-right-list"
)
@_handles(
"comparison-expression ->" " additive-expression greater-expression-right-list"
)
def _chained_comparison_expression(expression, expression_right):
"""Builds the IR for a chain of comparisons, like a == b == c.
Like _binary_operator_expression, _chained_comparison_expression transforms a
right-recursive list of expression tails into a left-associative Expression
tree. Unlike _binary_operator_expression, extra AND nodes are added. For
example, the following expression:
0 <= b <= 64
must be translated to the conceptually-equivalent expression:
0 <= b && b <= 64
(The middle subexpression is duplicated -- this would be a problem in a
programming language like C where expressions like `x++` have side effects,
but side effects do not make sense in a data definition language like Emboss.)
_chained_comparison_expression receives a left-hand head expression and a list
of tails, like:
6, (Tail("<=", b), Tail("<=", 64))
which it translates to a structure like:
Expression(Expression(6, "<=", b), "&&", Expression(b, "<=", 64))
The Emboss grammar is constructed such that sequences of "<", "<=", and "=="
comparisons may be chained, and sequences of ">", ">=", and "==" can be
chained, but greater and less-than comparisons may not; e.g., "b < 64 > a" is
not allowed.
Arguments:
expression: An ir_data.Expression which is the head of the (expr, operator,
expr, operator, expr, ...) list.
expression_right: A list of _ExpressionTails corresponding to the (operator,
expr, operator, expr, ...) list that comes after expression.
Returns:
An ir_data.Expression with the correct recursive structure to represent a
chain of left-associative comparison operations.
"""
sequence = [expression]
for right in expression_right.list:
sequence.append(right.operator)
sequence.append(right.expression)
comparisons = []
for i in range(0, len(sequence) - 1, 2):
left, operator, right = sequence[i : i + 3]
location = parser_types.make_location(
left.source_location.start, right.source_location.end
)
comparisons.append(
ir_data.Expression(
function=ir_data.Function(
function=_text_to_operator(operator.text),
args=[left, right],
function_name=operator,
source_location=location,
),
source_location=location,
)
)
e = comparisons[0]
for comparison in comparisons[1:]:
location = parser_types.make_location(
e.source_location.start, comparison.source_location.end
)
e = ir_data.Expression(
function=ir_data.Function(
function=ir_data.FunctionMapping.AND,
args=[e, comparison],
function_name=ir_data.Word(
text="&&",
source_location=comparison.function.args[0].source_location,
),
source_location=location,
),
source_location=location,
)
return e
# _chained_comparison_expression, above, handles three types of chains: `a == b
# == c`, `a < b <= c`, and `a > b >= c`.
#
# This requires a bit of subtlety in the productions for
# `x-expression-right-list`, because the `==` operator may be freely mixed into
# greater-than or less-than chains, like `a < b == c <= d` or `a > b == c >= d`,
# but greater-than and less-than may not be mixed; i.e., `a < b >= c` is
# disallowed.
#
# In order to keep the grammar unambiguous -- that is, in order to ensure that
# every valid input can only be parsed in exactly one way -- the languages
# defined by `equality-expression-right*`, `greater-expression-right-list`, and
# `less-expression-right-list` cannot overlap.
#
# `equality-expression-right*`, by definition, only contains `== n` elements.
# By forcing `greater-expression-right-list` to contain at least one
# `greater-expression-right`, we can ensure that a chain like `== n == m` cannot
# be parsed as a `greater-expression-right-list`. Similar logic applies in the
# less-than case.
#
# There is another potential source of ambiguity here: if
# `greater-expression-right-list` were
#
# greater-expression-right-list ->
# equality-or-greater-expression-right* greater-expression-right
# equality-or-greater-expression-right*
#
# then a sequence like '> b > c > d' could be parsed as any of:
#
# () (> b) ((> c) (> d))
# ((> b)) (> c) ((> d))
# ((> b) (> c)) (> d) ()
#
# By using `equality-expression-right*` for the first symbol, only the first
# parse is possible.
@_handles(
"greater-expression-right-list ->"
" equality-expression-right* greater-expression-right"
" equality-or-greater-expression-right*"
)
@_handles(
"less-expression-right-list ->"
" equality-expression-right* less-expression-right"
" equality-or-less-expression-right*"
)
def _chained_comparison_tails(start, middle, end):
return _List(start.list + [middle] + end.list)
@_handles("equality-or-greater-expression-right -> equality-expression-right")
@_handles("equality-or-greater-expression-right -> greater-expression-right")
@_handles("equality-or-less-expression-right -> equality-expression-right")
@_handles("equality-or-less-expression-right -> less-expression-right")
def _equality_or_less_or_greater(right):
return right
@_handles("and-expression-right -> and-operator comparison-expression")
@_handles("or-expression-right -> or-operator comparison-expression")
@_handles("additive-expression-right -> additive-operator times-expression")
@_handles("equality-expression-right -> equality-operator additive-expression")
@_handles("greater-expression-right -> greater-operator additive-expression")
@_handles("less-expression-right -> less-operator additive-expression")
@_handles("times-expression-right ->" " multiplicative-operator negation-expression")
def _expression_right_production(operator, expression):
return _ExpressionTail(operator, expression)
# This supports a single layer of unary plus/minus, so "+5" and "-value" are
# allowed, but "+-5" or "-+-something" are not.
@_handles("negation-expression -> additive-operator bottom-expression")
def _negation_expression_with_operator(operator, expression):
phantom_zero_location = ir_data.Location(
start=operator.source_location.start, end=operator.source_location.start
)
return ir_data.Expression(
function=ir_data.Function(
function=_text_to_operator(operator.text),
args=[
ir_data.Expression(
constant=ir_data.NumericConstant(
value="0", source_location=phantom_zero_location
),
source_location=phantom_zero_location,
),
expression,
],
function_name=operator,
source_location=ir_data.Location(
start=operator.source_location.start, end=expression.source_location.end
),
)
)
@_handles("negation-expression -> bottom-expression")
def _negation_expression(expression):
return expression
@_handles('bottom-expression -> "(" expression ")"')
def _bottom_expression_parentheses(open_paren, expression, close_paren):
del open_paren, close_paren # Unused.
return expression
@_handles('bottom-expression -> function-name "(" argument-list ")"')
def _bottom_expression_function(function, open_paren, arguments, close_paren):
del open_paren # Unused.
return ir_data.Expression(
function=ir_data.Function(
function=_text_to_function(function.text),
args=arguments.list,
function_name=function,
source_location=ir_data.Location(
start=function.source_location.start,
end=close_paren.source_location.end,
),
)
)
@_handles('comma-then-expression -> "," expression')
def _comma_then_expression(comma, expression):
del comma # Unused.
return expression
@_handles("argument-list -> expression comma-then-expression*")
def _argument_list(head, tail):
tail.list.insert(0, head)
return tail
@_handles("argument-list ->")
def _empty_argument_list():
return _List([])
@_handles("bottom-expression -> numeric-constant")
def _bottom_expression_from_numeric_constant(constant):
return ir_data.Expression(constant=constant)
@_handles("bottom-expression -> constant-reference")
def _bottom_expression_from_constant_reference(reference):
return ir_data.Expression(constant_reference=reference)
@_handles("bottom-expression -> builtin-reference")
def _bottom_expression_from_builtin(reference):
return ir_data.Expression(builtin_reference=reference)
@_handles("bottom-expression -> boolean-constant")
def _bottom_expression_from_boolean_constant(boolean):
return ir_data.Expression(boolean_constant=boolean)
@_handles("bottom-expression -> field-reference")
def _bottom_expression_from_reference(reference):
return reference
@_handles("field-reference -> snake-reference field-reference-tail*")
def _indirect_field_reference(field_reference, field_references):
if field_references.source_location.HasField("end"):
end_location = field_references.source_location.end
else:
end_location = field_reference.source_location.end
return ir_data.Expression(
field_reference=ir_data.FieldReference(
path=[field_reference] + field_references.list,
source_location=parser_types.make_location(
field_reference.source_location.start, end_location
),
)
)
# If "Type.field" ever becomes syntactically valid, it will be necessary to
# check that enum values are compile-time constants.
@_handles('field-reference-tail -> "." snake-reference')
def _field_reference_tail(dot, reference):
del dot # Unused.
return reference
@_handles("numeric-constant -> Number")
def _numeric_constant(number):
# All types of numeric constant tokenize to the same symbol, because they are
# interchangeable in source code.
if number.text[0:2] == "0b":
n = int(number.text.replace("_", "")[2:], 2)
elif number.text[0:2] == "0x":
n = int(number.text.replace("_", "")[2:], 16)
else:
n = int(number.text.replace("_", ""), 10)
return ir_data.NumericConstant(value=str(n))
@_handles("type-definition -> struct")
@_handles("type-definition -> bits")
@_handles("type-definition -> enum")
@_handles("type-definition -> external")
def _type_definition(type_definition):
return type_definition
# struct StructureName:
# ... fields ...
# bits BitName:
# ... fields ...
@_handles(
'struct -> "struct" type-name delimited-parameter-definition-list?'
' ":" Comment? eol struct-body'
)
@_handles(
'bits -> "bits" type-name delimited-parameter-definition-list? ":"'
" Comment? eol bits-body"
)
def _structure(struct, name, parameters, colon, comment, newline, struct_body):
"""Composes the top-level IR for an Emboss structure."""
del colon, comment, newline # Unused.
ir_data_utils.builder(struct_body.structure).source_location.start.CopyFrom(
struct.source_location.start
)
ir_data_utils.builder(struct_body.structure).source_location.end.CopyFrom(
struct_body.source_location.end
)
if struct_body.name:
ir_data_utils.update(struct_body.name, name)
else:
struct_body.name = ir_data_utils.copy(name)
if parameters.list:
struct_body.runtime_parameter.extend(parameters.list[0].list)
return struct_body
@_handles(
"delimited-parameter-definition-list ->" ' "(" parameter-definition-list ")"'
)
def _delimited_parameter_definition_list(open_paren, parameters, close_paren):
del open_paren, close_paren # Unused
return parameters
@_handles('parameter-definition -> snake-name ":" type')
def _parameter_definition(name, double_colon, parameter_type):
del double_colon # Unused
return ir_data.RuntimeParameter(name=name, physical_type_alias=parameter_type)
@_handles('parameter-definition-list-tail -> "," parameter-definition')
def _parameter_definition_list_tail(comma, parameter):
del comma # Unused.
return parameter
@_handles(
"parameter-definition-list -> parameter-definition"
" parameter-definition-list-tail*"
)
def _parameter_definition_list(head, tail):
tail.list.insert(0, head)
return tail
@_handles("parameter-definition-list ->")
def _empty_parameter_definition_list():
return _List([])
# The body of a struct: basically, the part after the first line.
@_handles(
"struct-body -> Indent doc-line* attribute-line*"
" type-definition* struct-field-block Dedent"
)
def _struct_body(indent, docs, attributes, types, fields, dedent):
del indent, dedent # Unused.
return _structure_body(
docs, attributes, types, fields, ir_data.AddressableUnit.BYTE
)
def _structure_body(docs, attributes, types, fields, addressable_unit):
"""Constructs the body of a structure (bits or struct) definition."""
return ir_data.TypeDefinition(
structure=ir_data.Structure(field=[field.field for field in fields.list]),
documentation=docs.list,
attribute=attributes.list,
subtype=types.list
+ [subtype for field in fields.list for subtype in field.subtypes],
addressable_unit=addressable_unit,
)
@_handles("struct-field-block ->")
@_handles("bits-field-block ->")
@_handles("anonymous-bits-field-block ->")
def _empty_field_block():
return _List([])
@_handles(
"struct-field-block ->" " conditional-struct-field-block struct-field-block"
)
@_handles("bits-field-block ->" " conditional-bits-field-block bits-field-block")
@_handles(
"anonymous-bits-field-block -> conditional-anonymous-bits-field-block"
" anonymous-bits-field-block"
)
def _conditional_block_plus_field_block(conditional_block, block):
return _List(conditional_block.list + block.list)
@_handles("struct-field-block ->" " unconditional-struct-field struct-field-block")
@_handles("bits-field-block ->" " unconditional-bits-field bits-field-block")
@_handles(
"anonymous-bits-field-block ->"
" unconditional-anonymous-bits-field anonymous-bits-field-block"
)
def _unconditional_block_plus_field_block(field, block):
"""Prepends an unconditional field to block."""
ir_data_utils.builder(field.field).existence_condition.source_location.CopyFrom(
field.source_location
)
ir_data_utils.builder(
field.field
).existence_condition.boolean_constant.source_location.CopyFrom(
field.source_location
)
ir_data_utils.builder(field.field).existence_condition.boolean_constant.value = True
return _List([field] + block.list)
# Struct "fields" are regular fields, inline enums, bits, or structs, anonymous
# inline bits, or virtual fields.
@_handles("unconditional-struct-field -> field")
@_handles("unconditional-struct-field -> inline-enum-field-definition")
@_handles("unconditional-struct-field -> inline-bits-field-definition")
@_handles("unconditional-struct-field -> inline-struct-field-definition")
@_handles("unconditional-struct-field -> anonymous-bits-field-definition")
@_handles("unconditional-struct-field -> virtual-field")
# Bits fields are "regular" fields, inline enums or bits, or virtual fields.
#
# Inline structs and anonymous inline bits are not allowed inside of bits:
# anonymous inline bits are pointless, and inline structs do not make sense,
# since a struct cannot be a part of a bits.
#
# Anonymous inline bits may not include virtual fields; instead, the virtual
# field should be a direct part of the enclosing structure.
@_handles("unconditional-anonymous-bits-field -> field")
@_handles("unconditional-anonymous-bits-field -> inline-enum-field-definition")
@_handles("unconditional-anonymous-bits-field -> inline-bits-field-definition")
@_handles("unconditional-bits-field -> unconditional-anonymous-bits-field")
@_handles("unconditional-bits-field -> virtual-field")
def _unconditional_field(field):
"""Handles the unifying grammar production for a struct or bits field."""
return field
# TODO(bolms): Add 'elif' and 'else' support.
# TODO(bolms): Should nested 'if' blocks be allowed?
@_handles(
"conditional-struct-field-block ->"
' "if" expression ":" Comment? eol'
" Indent unconditional-struct-field+ Dedent"
)
@_handles(
"conditional-bits-field-block ->"
' "if" expression ":" Comment? eol'
" Indent unconditional-bits-field+ Dedent"
)
@_handles(
"conditional-anonymous-bits-field-block ->"
' "if" expression ":" Comment? eol'
" Indent unconditional-anonymous-bits-field+ Dedent"
)
def _conditional_field_block(
if_keyword, expression, colon, comment, newline, indent, fields, dedent
):
"""Applies an existence_condition to each element of fields."""
del if_keyword, newline, colon, comment, indent, dedent # Unused.
for field in fields.list:
condition = ir_data_utils.builder(field.field).existence_condition
condition.CopyFrom(expression)
condition.source_location.is_disjoint_from_parent = True
return fields
# The body of a bit field definition: basically, the part after the first line.
@_handles(
"bits-body -> Indent doc-line* attribute-line*"
" type-definition* bits-field-block Dedent"
)
def _bits_body(indent, docs, attributes, types, fields, dedent):
del indent, dedent # Unused.
return _structure_body(docs, attributes, types, fields, ir_data.AddressableUnit.BIT)
# Inline bits (defined as part of a field) are more restricted than standalone
# bits.
@_handles(
"anonymous-bits-body ->"
" Indent attribute-line* anonymous-bits-field-block Dedent"
)
def _anonymous_bits_body(indent, attributes, fields, dedent):
del indent, dedent # Unused.
return _structure_body(
_List([]), attributes, _List([]), fields, ir_data.AddressableUnit.BIT
)
# A field is:
# range type name (abbr) [attr: value] [attr2: value] -- doc
# -- doc
# -- doc
# [attr3: value]
# [attr4: value]
@_handles(
"field ->"
" field-location type snake-name abbreviation? attribute* doc?"
" Comment? eol field-body?"
)
def _field(
location,
field_type,
name,
abbreviation,
attributes,
doc,
comment,
newline,
field_body,
):
"""Constructs an ir_data.Field from the given components."""
del comment # Unused
field_ir = ir_data.Field(
location=location,
type=field_type,
name=name,
attribute=attributes.list,
documentation=doc.list,
)
field = ir_data_utils.builder(field_ir)
if field_body.list:
field.attribute.extend(field_body.list[0].attribute)
field.documentation.extend(field_body.list[0].documentation)
if abbreviation.list:
field.abbreviation.CopyFrom(abbreviation.list[0])
field.source_location.start.CopyFrom(location.source_location.start)
if field_body.source_location.HasField("end"):
field.source_location.end.CopyFrom(field_body.source_location.end)
else:
field.source_location.end.CopyFrom(newline.source_location.end)
return _FieldWithType(field=field_ir)
# A "virtual field" is:
# let name = value
# -- doc
# -- doc
# [attr1: value]
# [attr2: value]
@_handles(
"virtual-field ->" ' "let" snake-name "=" expression Comment? eol field-body?'
)
def _virtual_field(let, name, equals, value, comment, newline, field_body):
"""Constructs an ir_data.Field from the given components."""
del equals, comment # Unused
field_ir = ir_data.Field(read_transform=value, name=name)
field = ir_data_utils.builder(field_ir)
if field_body.list:
field.attribute.extend(field_body.list[0].attribute)
field.documentation.extend(field_body.list[0].documentation)
field.source_location.start.CopyFrom(let.source_location.start)
if field_body.source_location.HasField("end"):
field.source_location.end.CopyFrom(field_body.source_location.end)
else:
field.source_location.end.CopyFrom(newline.source_location.end)
return _FieldWithType(field=field_ir)
# An inline enum is:
# range "enum" name (abbr):
# -- doc
# -- doc
# [attr3: value]
# [attr4: value]
# NAME = 10
# NAME2 = 20
@_handles(
"inline-enum-field-definition ->"
' field-location "enum" snake-name abbreviation? ":" Comment? eol'
" enum-body"
)
def _inline_enum_field(
location, enum, name, abbreviation, colon, comment, newline, enum_body
):
"""Constructs an ir_data.Field for an inline enum field."""
del enum, colon, comment, newline # Unused.
return _inline_type_field(location, name, abbreviation, enum_body)
@_handles(
"inline-struct-field-definition ->"
' field-location "struct" snake-name abbreviation? ":" Comment? eol'
" struct-body"
)
def _inline_struct_field(
location, struct, name, abbreviation, colon, comment, newline, struct_body
):
del struct, colon, comment, newline # Unused.
return _inline_type_field(location, name, abbreviation, struct_body)
@_handles(
"inline-bits-field-definition ->"
' field-location "bits" snake-name abbreviation? ":" Comment? eol'
" bits-body"
)
def _inline_bits_field(
location, bits, name, abbreviation, colon, comment, newline, bits_body
):
del bits, colon, comment, newline # Unused.
return _inline_type_field(location, name, abbreviation, bits_body)
def _inline_type_field(location, name, abbreviation, body):
"""Shared implementation of _inline_enum_field and _anonymous_bit_field."""
field_ir = ir_data.Field(
location=location,
name=name,
attribute=body.attribute,
documentation=body.documentation,
)
field = ir_data_utils.builder(field_ir)
# All attributes should be attached to the field, not the type definition: if
# the user wants to use type attributes, they should create a separate type
# definition and reference it.
del body.attribute[:]
type_name = ir_data_utils.copy(name)
ir_data_utils.builder(type_name).name.text = name_conversion.snake_to_camel(
type_name.name.text
)
field.type.atomic_type.reference.source_name.extend([type_name.name])
field.type.atomic_type.reference.source_location.CopyFrom(type_name.source_location)
field.type.atomic_type.reference.is_local_name = True
field.type.atomic_type.source_location.CopyFrom(type_name.source_location)
field.type.source_location.CopyFrom(type_name.source_location)
if abbreviation.list:
field.abbreviation.CopyFrom(abbreviation.list[0])
field.source_location.start.CopyFrom(location.source_location.start)
ir_data_utils.builder(body.source_location).start.CopyFrom(
location.source_location.start
)
if body.HasField("enumeration"):
ir_data_utils.builder(body.enumeration).source_location.CopyFrom(
body.source_location
)
else:
assert body.HasField("structure")
ir_data_utils.builder(body.structure).source_location.CopyFrom(
body.source_location
)
ir_data_utils.builder(body).name.CopyFrom(type_name)
field.source_location.end.CopyFrom(body.source_location.end)
subtypes = [body] + list(body.subtype)
del body.subtype[:]
return _FieldWithType(field=field_ir, subtypes=subtypes)
@_handles(
"anonymous-bits-field-definition ->"
' field-location "bits" ":" Comment? eol anonymous-bits-body'
)
def _anonymous_bit_field(location, bits_keyword, colon, comment, newline, bits_body):
"""Constructs an ir_data.Field for an anonymous bit field."""
del colon, comment, newline # Unused.
name = ir_data.NameDefinition(
name=ir_data.Word(
text=_get_anonymous_field_name(),
source_location=bits_keyword.source_location,
),
source_location=bits_keyword.source_location,
is_anonymous=True,
)
return _inline_type_field(location, name, _List([]), bits_body)
@_handles("field-body -> Indent doc-line* attribute-line* Dedent")
def _field_body(indent, docs, attributes, dedent):
del indent, dedent # Unused.
return ir_data.Field(documentation=docs.list, attribute=attributes.list)
# A parenthetically-denoted abbreviation.
@_handles('abbreviation -> "(" snake-word ")"')
def _abbreviation(open_paren, word, close_paren):
del open_paren, close_paren # Unused.
return word
# enum EnumName:
# ... values ...
@_handles('enum -> "enum" type-name ":" Comment? eol enum-body')
def _enum(enum, name, colon, comment, newline, enum_body):
del colon, comment, newline # Unused.
ir_data_utils.builder(enum_body.enumeration).source_location.start.CopyFrom(
enum.source_location.start
)
ir_data_utils.builder(enum_body.enumeration).source_location.end.CopyFrom(
enum_body.source_location.end
)
ir_data_utils.builder(enum_body).name.CopyFrom(name)
return enum_body
# [enum Foo:]
# name = value
# name = value
@_handles("enum-body -> Indent doc-line* attribute-line* enum-value+ Dedent")
def _enum_body(indent, docs, attributes, values, dedent):
del indent, dedent # Unused.
return ir_data.TypeDefinition(
enumeration=ir_data.Enum(value=values.list),
documentation=docs.list,
attribute=attributes.list,
addressable_unit=ir_data.AddressableUnit.BIT,
)
# name = value
@_handles(
"enum-value -> "
' constant-name "=" expression attribute* doc? Comment? eol enum-value-body?'
)
def _enum_value(
name, equals, expression, attribute, documentation, comment, newline, body
):
del equals, comment, newline # Unused.
result = ir_data.EnumValue(
name=name,
value=expression,
documentation=documentation.list,
attribute=attribute.list,
)
if body.list:
result.documentation.extend(body.list[0].documentation)
result.attribute.extend(body.list[0].attribute)
return result
@_handles("enum-value-body -> Indent doc-line* attribute-line* Dedent")
def _enum_value_body(indent, docs, attributes, dedent):
del indent, dedent # Unused.
return ir_data.EnumValue(documentation=docs.list, attribute=attributes.list)
# An external is just a declaration that a type exists and has certain
# attributes.
@_handles('external -> "external" type-name ":" Comment? eol external-body')
def _external(external, name, colon, comment, newline, external_body):
del colon, comment, newline # Unused.
ir_data_utils.builder(external_body.source_location).start.CopyFrom(
external.source_location.start
)
if external_body.name:
ir_data_utils.update(external_body.name, name)
else:
external_body.name = ir_data_utils.copy(name)
return external_body
# This syntax implicitly requires either a documentation line or a attribute
# line, or it won't parse (because no Indent/Dedent tokens will be emitted).
@_handles("external-body -> Indent doc-line* attribute-line* Dedent")
def _external_body(indent, docs, attributes, dedent):
return ir_data.TypeDefinition(
external=ir_data.External(
# Set source_location here, since it won't be set automatically.
source_location=ir_data.Location(
start=indent.source_location.start, end=dedent.source_location.end
)
),
documentation=docs.list,
attribute=attributes.list,
)
@_handles('field-location -> expression "[" "+" expression "]"')
def _field_location(start, open_bracket, plus, size, close_bracket):
del open_bracket, plus, close_bracket # Unused.
return ir_data.FieldLocation(start=start, size=size)
@_handles('delimited-argument-list -> "(" argument-list ")"')
def _type_argument_list(open_paren, arguments, close_paren):
del open_paren, close_paren # Unused
return arguments
# A type is "TypeName" or "TypeName[length]" or "TypeName[length][length]", etc.
# An array type may have an empty length ("Type[]"). This is only valid for the
# outermost length (the last set of brackets), but that must be checked
# elsewhere.
@_handles(
"type -> type-reference delimited-argument-list? type-size-specifier?"
" array-length-specifier*"
)
def _type(reference, parameters, size, array_spec):
"""Builds the IR for a type specifier."""
base_type_source_location_end = reference.source_location.end
atomic_type_source_location_end = reference.source_location.end
if parameters.list:
base_type_source_location_end = parameters.source_location.end
atomic_type_source_location_end = parameters.source_location.end
if size.list:
base_type_source_location_end = size.source_location.end
base_type_location = parser_types.make_location(
reference.source_location.start, base_type_source_location_end
)
atomic_type_location = parser_types.make_location(
reference.source_location.start, atomic_type_source_location_end
)
t = ir_data.Type(
atomic_type=ir_data.AtomicType(
reference=ir_data_utils.copy(reference),
source_location=atomic_type_location,
runtime_parameter=parameters.list[0].list if parameters.list else [],
),
size_in_bits=size.list[0] if size.list else None,
source_location=base_type_location,
)
for length in array_spec.list:
location = parser_types.make_location(
t.source_location.start, length.source_location.end
)
if isinstance(length, ir_data.Expression):
t = ir_data.Type(
array_type=ir_data.ArrayType(
base_type=t, element_count=length, source_location=location
),
source_location=location,
)
elif isinstance(length, ir_data.Empty):
t = ir_data.Type(
array_type=ir_data.ArrayType(
base_type=t, automatic=length, source_location=location
),
source_location=location,
)
else:
assert False, "Shouldn't be here."
return t
# TODO(bolms): Should symbolic names or expressions be allowed? E.g.,
# UInt:FIELD_SIZE or UInt:(16 + 16)?
@_handles('type-size-specifier -> ":" numeric-constant')
def _type_size_specifier(colon, numeric_constant):
"""handles the ":32" part of a type specifier like "UInt:32"."""
del colon
return ir_data.Expression(constant=numeric_constant)
# The distinctions between different formats of NameDefinitions, Words, and
# References are enforced during parsing, but not propagated to the IR.
@_handles("type-name -> type-word")
@_handles("snake-name -> snake-word")
@_handles("constant-name -> constant-word")
def _name(word):
return ir_data.NameDefinition(name=word)
@_handles("type-word -> CamelWord")
@_handles("snake-word -> SnakeWord")
@_handles('builtin-field-word -> "$size_in_bits"')
@_handles('builtin-field-word -> "$size_in_bytes"')
@_handles('builtin-field-word -> "$max_size_in_bits"')
@_handles('builtin-field-word -> "$max_size_in_bytes"')
@_handles('builtin-field-word -> "$min_size_in_bits"')
@_handles('builtin-field-word -> "$min_size_in_bytes"')
@_handles('builtin-word -> "$is_statically_sized"')
@_handles('builtin-word -> "$static_size_in_bits"')
@_handles('builtin-word -> "$next"')
@_handles("constant-word -> ShoutyWord")
@_handles('and-operator -> "&&"')
@_handles('or-operator -> "||"')
@_handles('less-operator -> "<="')
@_handles('less-operator -> "<"')
@_handles('greater-operator -> ">="')
@_handles('greater-operator -> ">"')
@_handles('equality-operator -> "=="')
@_handles('inequality-operator -> "!="')
@_handles('additive-operator -> "+"')
@_handles('additive-operator -> "-"')
@_handles('multiplicative-operator -> "*"')
@_handles('function-name -> "$max"')
@_handles('function-name -> "$present"')
@_handles('function-name -> "$upper_bound"')
@_handles('function-name -> "$lower_bound"')
def _word(word):
return ir_data.Word(text=word.text)
@_handles("type-reference -> type-reference-tail")
@_handles("constant-reference -> constant-reference-tail")
def _un_module_qualified_type_reference(reference):
return reference
@_handles("constant-reference-tail -> constant-word")
@_handles("type-reference-tail -> type-word")
@_handles("snake-reference -> snake-word")
@_handles("snake-reference -> builtin-field-word")
def _reference(word):
return ir_data.Reference(source_name=[word])
@_handles("builtin-reference -> builtin-word")
def _builtin_reference(word):
return ir_data.Reference(
source_name=[word],
canonical_name=ir_data.CanonicalName(object_path=[word.text]),
)
# Because constant-references ("Enum.NAME") are used in the same contexts as
# field-references ("field.subfield"), module-qualified constant references
# ("module.Enum.VALUE") have to take snake-reference, not snake-word, on the
# left side of the dot. Otherwise, when a "snake_word" is followed by a "." in
# an expression context, the LR(1) parser cannot determine whether to reduce the
# snake-word to snake-reference (to eventually become field-reference), or to
# shift the dot onto the stack (to eventually become constant-reference). By
# using snake-reference as the head of both, the parser can always reduce, then
# shift the dot, then determine whether to proceed with constant-reference if it
# sees "snake_name.TypeName" or field-reference if it sees
# "snake_name.snake_name".
@_handles('constant-reference -> snake-reference "." constant-reference-tail')
def _module_qualified_constant_reference(new_head, dot, reference):
del dot # Unused.
new_source_name = list(new_head.source_name) + list(reference.source_name)
del reference.source_name[:]
reference.source_name.extend(new_source_name)
return reference
@_handles('constant-reference-tail -> type-word "." constant-reference-tail')
# module.Type.SubType.name is a reference to something that *must* be a
# constant.
@_handles('constant-reference-tail -> type-word "." snake-reference')
@_handles('type-reference-tail -> type-word "." type-reference-tail')
@_handles('type-reference -> snake-word "." type-reference-tail')
def _qualified_reference(word, dot, reference):
"""Adds a name. or Type. qualification to the head of a reference."""
del dot # Unused.
new_source_name = [word] + list(reference.source_name)
del reference.source_name[:]
reference.source_name.extend(new_source_name)
return reference
# Arrays are properly translated to IR in _type().
@_handles('array-length-specifier -> "[" expression "]"')
def _array_length_specifier(open_bracket, length, close_bracket):
del open_bracket, close_bracket # Unused.
return length
# An array specifier can end with empty brackets ("arr[3][]"), in which case the
# array's size is inferred from the size of its enclosing field.
@_handles('array-length-specifier -> "[" "]"')
def _auto_array_length_specifier(open_bracket, close_bracket):
# Note that the Void's source_location is the space between the brackets (if
# any).
return ir_data.Empty(
source_location=ir_data.Location(
start=open_bracket.source_location.end,
end=close_bracket.source_location.start,
)
)
@_handles('eol -> "\\n" comment-line*')
def _eol(eol, comments):
del comments # Unused
return eol
@_handles('comment-line -> Comment? "\\n"')
def _comment_line(comment, eol):
del comment # Unused
return eol
def _finalize_grammar():
"""_Finalize adds productions for foo*, foo+, and foo? symbols."""
star_symbols = set()
plus_symbols = set()
option_symbols = set()
for production in _handlers:
for symbol in production.rhs:
if symbol[-1] == "*":
star_symbols.add(symbol[:-1])
elif symbol[-1] == "+":
# symbol+ relies on the rule for symbol*
star_symbols.add(symbol[:-1])
plus_symbols.add(symbol[:-1])
elif symbol[-1] == "?":
option_symbols.add(symbol[:-1])
for symbol in star_symbols:
_handles("{s}* -> {s} {s}*".format(s=symbol))(lambda e, r: _List([e] + r.list))
_handles("{s}* ->".format(s=symbol))(lambda: _List([]))
for symbol in plus_symbols:
_handles("{s}+ -> {s} {s}*".format(s=symbol))(lambda e, r: _List([e] + r.list))
for symbol in option_symbols:
_handles("{s}? -> {s}".format(s=symbol))(lambda e: _List([e]))
_handles("{s}? ->".format(s=symbol))(lambda: _List([]))
_finalize_grammar()
# End of grammar.
################################################################################
# These export the grammar used by module_ir so that parser_generator can build
# a parser for the same language.
START_SYMBOL = "module"
EXPRESSION_START_SYMBOL = "expression"
PRODUCTIONS = list(_handlers.keys())